diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index 0d14bbb..e13db90 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -1,4 +1,4 @@ -import { intro, isCancel, outro, select, text } from "@clack/prompts" +import { confirm, intro, isCancel, outro, select, text } from "@clack/prompts" import color from "picocolors" import { getUserPkgManager } from "@/utils/get-user-pkg-manager.js" @@ -8,6 +8,7 @@ export interface CliResults { dialect?: "postgres" | undefined provider?: "neon" | "postgres" | "vercel-postgres" | "planetscale" | undefined noInstall?: boolean + useCloudflareApi?: boolean } export type Dialect = CliResults["dialect"] @@ -73,6 +74,18 @@ export async function runCli(): Promise { } } + const useCloudflareApi = await confirm({ + message: "Will your API run on Cloudflare Workers (Wrangler)?", + initialValue: true, + }) + + if (isCancel(useCloudflareApi)) { + outro("Setup cancelled.") + return undefined + } + + const shouldUseCloudflareApi = Boolean(useCloudflareApi) + let noInstall = noInstallFlag if (!noInstall) { const pkgManager = getUserPkgManager() @@ -98,5 +111,6 @@ export async function runCli(): Promise { dialect, provider, noInstall, + useCloudflareApi: shouldUseCloudflareApi, } } diff --git a/cli/src/helpers/configure-wrangler-dev.ts b/cli/src/helpers/configure-wrangler-dev.ts new file mode 100644 index 0000000..83e744f --- /dev/null +++ b/cli/src/helpers/configure-wrangler-dev.ts @@ -0,0 +1,69 @@ +import fs from "fs-extra" +import path from "path" +import sortPackageJson from "sort-package-json" +import { type PackageJson } from "type-fest" + +import { type PackageManager } from "@/utils/get-user-pkg-manager.js" + +const MPROCS_VERSION = "^0.6.3" + +type ConfigureWranglerDevOptions = { + projectDir: string + pkgManager: PackageManager +} + +const getScriptCommand = (pkgManager: PackageManager, script: string) => { + switch (pkgManager) { + case "npm": + return `npm run ${script}` + case "pnpm": + return `pnpm run ${script}` + case "bun": + return `bun run ${script}` + case "yarn": + return `yarn ${script}` + default: + return `npm run ${script}` + } +} + +export const configureWranglerDev = ({ projectDir, pkgManager }: ConfigureWranglerDevOptions) => { + const packageJsonPath = path.join(projectDir, "package.json") + const packageJson = fs.readJSONSync(packageJsonPath) as PackageJson + + const scripts = packageJson.scripts ?? {} + + if (!scripts["dev:next"]) { + scripts["dev:next"] = "next dev" + } + + scripts["dev:worker"] = "wrangler dev" + scripts.dev = "mprocs" + + packageJson.scripts = scripts + + const devDependencies = packageJson.devDependencies ?? {} + if (!devDependencies.mprocs) { + devDependencies.mprocs = MPROCS_VERSION + } + + packageJson.devDependencies = devDependencies + + const sortedPackageJson = sortPackageJson(packageJson) + + fs.writeJSONSync(packageJsonPath, sortedPackageJson, { + spaces: 2, + }) + + const mprocsFileContent = [ + "procs:", + " next:", + ` shell: \"${getScriptCommand(pkgManager, "dev:next")}\"`, + " worker:", + ` shell: \"${getScriptCommand(pkgManager, "dev:worker")}\"`, + "", + ].join("\n") + + const mprocsPath = path.join(projectDir, "mprocs.yaml") + fs.writeFileSync(mprocsPath, `${mprocsFileContent}\n`) +} diff --git a/cli/src/helpers/scaffold-project.ts b/cli/src/helpers/scaffold-project.ts index 0630e86..9e9db4a 100644 --- a/cli/src/helpers/scaffold-project.ts +++ b/cli/src/helpers/scaffold-project.ts @@ -4,6 +4,7 @@ import { getUserPkgManager } from "@/utils/get-user-pkg-manager.js" import path from "path" import { installBaseTemplate } from "./install-base-template.js" import { installPackages } from "./install-packages.js" +import { configureWranglerDev } from "./configure-wrangler-dev.js" interface ScaffoldProjectOptions { projectName: string @@ -11,9 +12,16 @@ interface ScaffoldProjectOptions { dialect: Dialect installers: InstallerMap databaseProvider: Provider + useCloudflareApi: boolean } -export const scaffoldProject = async ({ databaseProvider, projectName, installers, orm }: ScaffoldProjectOptions) => { +export const scaffoldProject = async ({ + databaseProvider, + projectName, + installers, + orm, + useCloudflareApi, +}: ScaffoldProjectOptions) => { const projectDir = path.resolve(process.cwd(), projectName) const pkgManager = getUserPkgManager() @@ -24,6 +32,7 @@ export const scaffoldProject = async ({ databaseProvider, projectName, installer installers, projectName, databaseProvider, + useCloudflareApi, }) installPackages({ @@ -33,7 +42,15 @@ export const scaffoldProject = async ({ databaseProvider, projectName, installer installers, projectName, databaseProvider, + useCloudflareApi, }) + if (useCloudflareApi) { + configureWranglerDev({ + projectDir, + pkgManager, + }) + } + return projectDir } diff --git a/cli/src/index.ts b/cli/src/index.ts index a9c288c..2314313 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -16,7 +16,7 @@ const main = async () => { return } - const { projectName, orm, dialect, provider } = results + const { projectName, orm, dialect, provider, useCloudflareApi } = results const installers = buildInstallerMap(orm, provider) @@ -26,6 +26,7 @@ const main = async () => { databaseProvider: provider ?? "neon", installers, projectName, + useCloudflareApi: useCloudflareApi ?? false, }) const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index 1d3091a..7cd3af6 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -40,6 +40,7 @@ export interface InstallerOptions { appRouter?: boolean projectName: string databaseProvider: Provider + useCloudflareApi?: boolean } export type Installer = (opts: InstallerOptions) => void diff --git a/cli/template/base-assets/base-package.json b/cli/template/base-assets/base-package.json index 158f8d0..55dd0cb 100644 --- a/cli/template/base-assets/base-package.json +++ b/cli/template/base-assets/base-package.json @@ -9,19 +9,40 @@ "lint": "next lint" }, "dependencies": { - "@tailwindcss/postcss": "^4.0.0", - "@tanstack/react-query": "^5.51.23", + "@tailwindcss/postcss": "^4.0.15", + "@tanstack/react-query": "^5.69.0", "clsx": "^2.1.1", +<<<<<<< HEAD + "drizzle-orm": "^0.41.0", + "hono": "^4.7.5", + "jstack": "^1.0.8", + "next": "^15.2.3", +======= "hono": "^4.7.0", "jstack": "^1.1.1", "next": "^15.1.6", +>>>>>>> 4c241093d6beb4a9ffcb2863439af87355f70327 "react": "^19.0.0", "react-dom": "^19.0.0", - "superjson": "^2.2.1", - "tailwind-merge": "^2.5.2", - "zod": "^3.24.1" + "superjson": "^2.2.2", + "tailwind-merge": "^3.0.2", + "zod": "^3.24.2" }, "devDependencies": { +<<<<<<< HEAD + "dotenv": "^16.4.7", + "@cloudflare/workers-types": "^4.20250321.0", + "@types/node": "^22.13.12", + "@types/react": "^19.0.12", + "@types/react-dom": "^19.0.4", + "drizzle-kit": "^0.30.5", + "eslint": "^9.23.0", + "eslint-config-next": "^15.2.3", + "postcss": "^8.5.3", + "tailwindcss": "^4.0.15", + "typescript": "^5.8.2", + "wrangler": "^4.4.0" +======= "dotenv": "^16.4.5", "@cloudflare/workers-types": "^4.20250214.0", "@types/node": "^22.10.6", @@ -33,5 +54,6 @@ "tailwindcss": "^4.0.0", "typescript": "^5", "wrangler": "^4.0.0" +>>>>>>> 4c241093d6beb4a9ffcb2863439af87355f70327 } } diff --git a/cli/template/base/wrangler.json b/cli/template/base/wrangler.json new file mode 100644 index 0000000..d931670 --- /dev/null +++ b/cli/template/base/wrangler.json @@ -0,0 +1,30 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "jstack-app", + "main": "src/server/index.ts", + "compatibility_date": "2025-03-22", + "compatibility_flags": [ + "nodejs_compat", + "nodejs_als", + "no_handle_cross_request_promise_resolution" + ], + // Add these in if you want to use a custom domain + // "routes": [ + // { + // "pattern": "sub.domain.com", + // "custom_domain": true + // } + // ], + // "workers_dev": false, + // "preview_urls": false, + "observability": { + "enabled": true, + "head_sampling_rate": 1 + }, + "placement": { + "mode": "smart" + }, + "dev": { + "port": 8080 + } +}