From d2674270a403a689ed81bccdfac70d235d390ef8 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 5 Jul 2025 15:51:26 +0530 Subject: [PATCH] add cloudflare workers support for all frontends (#366) --- .changeset/fuzzy-falcons-rhyme.md | 5 + apps/cli/package.json | 1 + apps/cli/src/constants.ts | 13 +- .../helpers/project-generation/add-addons.ts | 5 +- .../project-generation/add-deployment.ts | 113 ++++++++++++++++ .../project-generation/command-handlers.ts | 96 ++++++++++---- .../project-generation/create-project.ts | 6 + .../detect-project-config.ts | 1 + .../cli/src/helpers/project-generation/git.ts | 2 +- .../project-generation/post-installation.ts | 2 +- .../project-generation/template-manager.ts | 44 +++++++ apps/cli/src/helpers/setup/addons-setup.ts | 7 + apps/cli/src/helpers/setup/vite-pwa-setup.ts | 73 +++++++++++ .../cli/src/helpers/setup/web-deploy-setup.ts | 89 +++++++++++++ .../src/helpers/setup/workers-nuxt-setup.ts | 112 ++++++++++++++++ .../src/helpers/setup/workers-svelte-setup.ts | 74 +++++++++++ .../src/helpers/setup/workers-vite-setup.ts | 76 +++++++++++ apps/cli/src/index.ts | 10 +- apps/cli/src/prompts/addons.ts | 2 +- apps/cli/src/prompts/config-prompts.ts | 13 ++ apps/cli/src/prompts/runtime.ts | 2 +- apps/cli/src/prompts/web-deploy.ts | 122 ++++++++++++++++++ apps/cli/src/types.ts | 9 ++ apps/cli/src/utils/bts-config.ts | 4 +- apps/cli/src/utils/display-config.ts | 6 + .../utils/generate-reproducible-command.ts | 1 + apps/cli/src/utils/ts-morph.ts | 31 +++++ apps/cli/src/validation.ts | 23 ++++ .../deploy/web/nuxt/wrangler.jsonc.hbs | 51 ++++++++ .../deploy/web/react/next/open-next.config.ts | 6 + .../deploy/web/react/next/wrangler.jsonc.hbs | 22 ++++ .../web/react/react-router/wrangler.jsonc.hbs | 8 ++ .../react/tanstack-router/wrangler.jsonc.hbs | 8 ++ .../deploy/web/solid/wrangler.jsonc.hbs | 8 ++ .../deploy/web/svelte/wrangler.jsonc.hbs | 51 ++++++++ .../react/react-router/vite.config.ts.hbs | 23 +--- .../react/tanstack-router/vite.config.ts.hbs | 27 +--- .../react/tanstack-start/package.json.hbs | 12 +- .../frontend/react/web-base/_gitignore | 5 + apps/cli/templates/frontend/solid/_gitignore | 3 + .../templates/frontend/solid/package.json.hbs | 4 +- .../templates/frontend/solid/src/main.tsx.hbs | 5 +- .../frontend/solid/vite.config.js.hbs | 39 ------ .../frontend/solid/vite.config.ts.hbs | 18 +++ .../frontend/svelte/package.json.hbs | 2 +- ...{svelte.config.js => svelte.config.js.hbs} | 0 apps/web/public/analytics-data.json | 2 +- apps/web/public/schema.json | 11 +- apps/web/scripts/generate-schema.ts | 54 +++++--- .../app/(home)/_components/stack-builder.tsx | 31 +++++ apps/web/src/lib/constant.ts | 21 ++- apps/web/src/lib/stack-url-state.ts | 4 + bun.lock | 15 +++ 53 files changed, 1213 insertions(+), 159 deletions(-) create mode 100644 .changeset/fuzzy-falcons-rhyme.md create mode 100644 apps/cli/src/helpers/project-generation/add-deployment.ts create mode 100644 apps/cli/src/helpers/setup/vite-pwa-setup.ts create mode 100644 apps/cli/src/helpers/setup/web-deploy-setup.ts create mode 100644 apps/cli/src/helpers/setup/workers-nuxt-setup.ts create mode 100644 apps/cli/src/helpers/setup/workers-svelte-setup.ts create mode 100644 apps/cli/src/helpers/setup/workers-vite-setup.ts create mode 100644 apps/cli/src/prompts/web-deploy.ts create mode 100644 apps/cli/src/utils/ts-morph.ts create mode 100644 apps/cli/templates/deploy/web/nuxt/wrangler.jsonc.hbs create mode 100644 apps/cli/templates/deploy/web/react/next/open-next.config.ts create mode 100644 apps/cli/templates/deploy/web/react/next/wrangler.jsonc.hbs create mode 100644 apps/cli/templates/deploy/web/react/react-router/wrangler.jsonc.hbs create mode 100644 apps/cli/templates/deploy/web/react/tanstack-router/wrangler.jsonc.hbs create mode 100644 apps/cli/templates/deploy/web/solid/wrangler.jsonc.hbs create mode 100644 apps/cli/templates/deploy/web/svelte/wrangler.jsonc.hbs delete mode 100644 apps/cli/templates/frontend/solid/vite.config.js.hbs create mode 100644 apps/cli/templates/frontend/solid/vite.config.ts.hbs rename apps/cli/templates/frontend/svelte/{svelte.config.js => svelte.config.js.hbs} (100%) diff --git a/.changeset/fuzzy-falcons-rhyme.md b/.changeset/fuzzy-falcons-rhyme.md new file mode 100644 index 0000000..3ca0608 --- /dev/null +++ b/.changeset/fuzzy-falcons-rhyme.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add cloudflare workers deployment support for next, solid, tanstack-router, react-router, nuxt diff --git a/apps/cli/package.json b/apps/cli/package.json index 6897764..79c9fe8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -64,6 +64,7 @@ "picocolors": "^1.1.1", "posthog-node": "^5.1.1", "trpc-cli": "^0.9.2", + "ts-morph": "^26.0.0", "zod": "^3.25.67" }, "devDependencies": { diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index cec798a..b53997f 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -24,6 +24,7 @@ export const DEFAULT_CONFIG: ProjectConfig = { backend: "hono", runtime: "bun", api: "trpc", + webDeploy: "none", }; export const dependencyVersionMap = { @@ -45,8 +46,8 @@ export const dependencyVersionMap = { mongoose: "^8.14.0", - "vite-plugin-pwa": "^0.21.2", - "@vite-pwa/assets-generator": "^0.2.6", + "vite-plugin-pwa": "^1.0.1", + "@vite-pwa/assets-generator": "^1.0.0", "@tauri-apps/cli": "^2.4.0", @@ -107,13 +108,17 @@ export const dependencyVersionMap = { "@tanstack/solid-query": "^5.75.0", "@tanstack/solid-query-devtools": "^5.75.0", - wrangler: "^4.20.0", + wrangler: "^4.23.0", + "@cloudflare/vite-plugin": "^1.9.0", + "@opennextjs/cloudflare": "^1.3.0", + "nitro-cloudflare-dev": "^0.2.2", + "@sveltejs/adapter-cloudflare": "^7.0.4", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; export const ADDON_COMPATIBILITY = { - pwa: ["tanstack-router", "react-router", "solid"], + pwa: ["tanstack-router", "react-router", "solid", "next"], tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"], biome: [], husky: [], diff --git a/apps/cli/src/helpers/project-generation/add-addons.ts b/apps/cli/src/helpers/project-generation/add-addons.ts index 623be49..d60d64d 100644 --- a/apps/cli/src/helpers/project-generation/add-addons.ts +++ b/apps/cli/src/helpers/project-generation/add-addons.ts @@ -18,7 +18,7 @@ function exitWithError(message: string): never { } export async function addAddonsToProject( - input: AddInput & { addons: Addons[] }, + input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean }, ): Promise { try { const projectDir = input.projectDir || process.cwd(); @@ -55,6 +55,7 @@ export async function addAddonsToProject( install: input.install || false, dbSetup: detectedConfig.dbSetup || "none", api: detectedConfig.api || "none", + webDeploy: detectedConfig.webDeploy || "none", }; for (const addon of input.addons) { @@ -88,7 +89,7 @@ export async function addAddonsToProject( projectDir, packageManager: config.packageManager, }); - } else { + } else if (!input.suppressInstallMessage) { log.info( pc.yellow( `Run ${pc.bold( diff --git a/apps/cli/src/helpers/project-generation/add-deployment.ts b/apps/cli/src/helpers/project-generation/add-deployment.ts new file mode 100644 index 0000000..aabc9f3 --- /dev/null +++ b/apps/cli/src/helpers/project-generation/add-deployment.ts @@ -0,0 +1,113 @@ +import path from "node:path"; +import { cancel, log } from "@clack/prompts"; +import pc from "picocolors"; +import type { AddInput, ProjectConfig, WebDeploy } from "../../types"; +import { updateBtsConfig } from "../../utils/bts-config"; +import { setupWebDeploy } from "../setup/web-deploy-setup"; +import { + detectProjectConfig, + isBetterTStackProject, +} from "./detect-project-config"; +import { installDependencies } from "./install-dependencies"; +import { setupDeploymentTemplates } from "./template-manager"; + +function exitWithError(message: string): never { + cancel(pc.red(message)); + process.exit(1); +} + +export async function addDeploymentToProject( + input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean }, +): Promise { + try { + const projectDir = input.projectDir || process.cwd(); + + const isBetterTStack = await isBetterTStackProject(projectDir); + if (!isBetterTStack) { + exitWithError( + "This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.", + ); + } + + const detectedConfig = await detectProjectConfig(projectDir); + if (!detectedConfig) { + exitWithError( + "Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.", + ); + } + + if (detectedConfig.webDeploy === input.webDeploy) { + exitWithError( + `${input.webDeploy} deployment is already configured for this project.`, + ); + } + + if (input.webDeploy === "workers") { + const compatibleFrontends = [ + "tanstack-router", + "react-router", + "solid", + "next", + "svelte", + ]; + const hasCompatible = detectedConfig.frontend?.some((f) => + compatibleFrontends.includes(f), + ); + if (!hasCompatible) { + exitWithError( + "Cloudflare Workers deployment requires a compatible web frontend (tanstack-router, react-router, solid, next, or svelte).", + ); + } + } + + const config: ProjectConfig = { + projectName: detectedConfig.projectName || path.basename(projectDir), + projectDir, + relativePath: ".", + database: detectedConfig.database || "none", + orm: detectedConfig.orm || "none", + backend: detectedConfig.backend || "none", + runtime: detectedConfig.runtime || "none", + frontend: detectedConfig.frontend || [], + addons: detectedConfig.addons || [], + examples: detectedConfig.examples || [], + auth: detectedConfig.auth || false, + git: false, + packageManager: + input.packageManager || detectedConfig.packageManager || "npm", + install: input.install || false, + dbSetup: detectedConfig.dbSetup || "none", + api: detectedConfig.api || "none", + webDeploy: input.webDeploy, + }; + + log.info( + pc.green( + `Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`, + ), + ); + + await setupDeploymentTemplates(projectDir, config); + await setupWebDeploy(config); + + await updateBtsConfig(projectDir, { webDeploy: input.webDeploy }); + + if (config.install) { + await installDependencies({ + projectDir, + packageManager: config.packageManager, + }); + } else if (!input.suppressInstallMessage) { + log.info( + pc.yellow( + `Run ${pc.bold( + `${config.packageManager} install`, + )} to install dependencies`, + ), + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + exitWithError(`Error adding deployment: ${message}`); + } +} diff --git a/apps/cli/src/helpers/project-generation/command-handlers.ts b/apps/cli/src/helpers/project-generation/command-handlers.ts index 74a9887..fc36fcb 100644 --- a/apps/cli/src/helpers/project-generation/command-handlers.ts +++ b/apps/cli/src/helpers/project-generation/command-handlers.ts @@ -6,6 +6,7 @@ import { DEFAULT_CONFIG } from "../../constants"; import { getAddonsToAdd } from "../../prompts/addons"; import { gatherConfig } from "../../prompts/config-prompts"; import { getProjectName } from "../../prompts/project-name"; +import { getDeploymentToAdd } from "../../prompts/web-deploy"; import type { AddInput, CreateInput, ProjectConfig } from "../../types"; import { trackProjectCreation } from "../../utils/analytics"; import { displayConfig } from "../../utils/display-config"; @@ -17,8 +18,10 @@ import { import { renderTitle } from "../../utils/render-title"; import { getProvidedFlags, processAndValidateFlags } from "../../validation"; import { addAddonsToProject } from "./add-addons"; +import { addDeploymentToProject } from "./add-deployment"; import { createProject } from "./create-project"; import { detectProjectConfig } from "./detect-project-config"; +import { installDependencies } from "./install-dependencies"; export async function createProjectHandler( input: CreateInput & { projectName?: string }, @@ -135,45 +138,84 @@ export async function createProjectHandler( export async function addAddonsHandler(input: AddInput): Promise { try { + const projectDir = input.projectDir || process.cwd(); + const detectedConfig = await detectProjectConfig(projectDir); + + if (!detectedConfig) { + cancel( + pc.red( + "Could not detect project configuration. Please ensure this is a valid Better-T Stack project.", + ), + ); + process.exit(1); + } + if (!input.addons || input.addons.length === 0) { - const projectDir = input.projectDir || process.cwd(); - const detectedConfig = await detectProjectConfig(projectDir); - - if (!detectedConfig) { - cancel( - pc.red( - "Could not detect project configuration. Please ensure this is a valid Better-T Stack project.", - ), - ); - process.exit(1); - } - const addonsPrompt = await getAddonsToAdd( detectedConfig.frontend || [], detectedConfig.addons || [], ); - if (addonsPrompt.length === 0) { - outro( - pc.yellow( - "No addons to add or all compatible addons are already present.", - ), - ); - return; + if (addonsPrompt.length > 0) { + input.addons = addonsPrompt; } - - input.addons = addonsPrompt; } - if (!input.addons || input.addons.length === 0) { - outro(pc.yellow("No addons specified to add.")); + if (!input.webDeploy) { + const deploymentPrompt = await getDeploymentToAdd( + detectedConfig.frontend || [], + detectedConfig.webDeploy, + ); + + if (deploymentPrompt !== "none") { + input.webDeploy = deploymentPrompt; + } + } + + const packageManager = + input.packageManager || detectedConfig.packageManager || "npm"; + + let somethingAdded = false; + + if (input.addons && input.addons.length > 0) { + await addAddonsToProject({ + ...input, + install: false, + suppressInstallMessage: true, + addons: input.addons, + }); + somethingAdded = true; + } + + if (input.webDeploy && input.webDeploy !== "none") { + await addDeploymentToProject({ + ...input, + install: false, + suppressInstallMessage: true, + webDeploy: input.webDeploy, + }); + somethingAdded = true; + } + + if (!somethingAdded) { + outro(pc.yellow("No addons or deployment configurations to add.")); return; } - await addAddonsToProject({ - ...input, - addons: input.addons, - }); + if (input.install) { + await installDependencies({ + projectDir, + packageManager, + }); + } else { + log.info( + pc.yellow( + `Run ${pc.bold(`${packageManager} install`)} to install dependencies`, + ), + ); + } + + outro(pc.green("Add command completed successfully!")); } catch (error) { console.error(error); process.exit(1); diff --git a/apps/cli/src/helpers/project-generation/create-project.ts b/apps/cli/src/helpers/project-generation/create-project.ts index c4d37b4..de8e644 100644 --- a/apps/cli/src/helpers/project-generation/create-project.ts +++ b/apps/cli/src/helpers/project-generation/create-project.ts @@ -13,6 +13,7 @@ import { generateCloudflareWorkerTypes, setupRuntime, } from "../setup/runtime-setup"; +import { setupWebDeploy } from "../setup/web-deploy-setup"; import { createReadme } from "./create-readme"; import { setupEnvironmentVariables } from "./env-setup"; import { initializeGit } from "./git"; @@ -26,6 +27,7 @@ import { setupAuthTemplate, setupBackendFramework, setupDbOrmTemplates, + setupDeploymentTemplates, setupExamplesTemplate, setupFrontendTemplates, } from "./template-manager"; @@ -49,6 +51,8 @@ export async function createProject(options: ProjectConfig) { } await setupAddonsTemplate(projectDir, options); + await setupDeploymentTemplates(projectDir, options); + await setupApi(options); if (!isConvex) { @@ -70,6 +74,8 @@ export async function createProject(options: ProjectConfig) { await handleExtras(projectDir, options); + await setupWebDeploy(options); + await setupEnvironmentVariables(options); await updatePackageConfigurations(projectDir, options); await createReadme(projectDir, options); diff --git a/apps/cli/src/helpers/project-generation/detect-project-config.ts b/apps/cli/src/helpers/project-generation/detect-project-config.ts index 78d9e63..f4ac423 100644 --- a/apps/cli/src/helpers/project-generation/detect-project-config.ts +++ b/apps/cli/src/helpers/project-generation/detect-project-config.ts @@ -23,6 +23,7 @@ export async function detectProjectConfig( packageManager: btsConfig.packageManager, dbSetup: btsConfig.dbSetup, api: btsConfig.api, + webDeploy: btsConfig.webDeploy, }; } diff --git a/apps/cli/src/helpers/project-generation/git.ts b/apps/cli/src/helpers/project-generation/git.ts index 21ceccf..b7365a3 100644 --- a/apps/cli/src/helpers/project-generation/git.ts +++ b/apps/cli/src/helpers/project-generation/git.ts @@ -30,5 +30,5 @@ export async function initializeGit( } await $({ cwd: projectDir })`git add -A`; - await $({ cwd: projectDir })`git commit -m ${"Initial commit"}`; + await $({ cwd: projectDir })`git commit -m ${"initial commit"}`; } diff --git a/apps/cli/src/helpers/project-generation/post-installation.ts b/apps/cli/src/helpers/project-generation/post-installation.ts index 98feaba..65c9783 100644 --- a/apps/cli/src/helpers/project-generation/post-installation.ts +++ b/apps/cli/src/helpers/project-generation/post-installation.ts @@ -287,7 +287,7 @@ function getTauriInstructions(runCmd?: string): string { function getPwaInstructions(): string { return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow( "NOTE:", - )} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`; + )} There is a known compatibility issue between VitePWA \nand React Router v7.See: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`; } function getStarlightInstructions(runCmd?: string): string { diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 7780d31..d97deb5 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -832,3 +832,47 @@ export async function handleExtras( } } } + +export async function setupDeploymentTemplates( + projectDir: string, + context: ProjectConfig, +): Promise { + if (context.webDeploy === "none") { + return; + } + + if (context.webDeploy === "workers") { + const webAppDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(webAppDir))) { + return; + } + + const frontends = context.frontend; + + const templateMap: Record = { + "tanstack-router": "react/tanstack-router", + "react-router": "react/react-router", + solid: "solid", + next: "react/next", + nuxt: "nuxt", + svelte: "svelte", + }; + + for (const f of frontends) { + if (templateMap[f]) { + const deployTemplateSrc = path.join( + PKG_ROOT, + `templates/deploy/web/${templateMap[f]}`, + ); + if (await fs.pathExists(deployTemplateSrc)) { + await processAndCopyFiles( + "**/*", + deployTemplateSrc, + webAppDir, + context, + ); + } + } + } + } +} diff --git a/apps/cli/src/helpers/setup/addons-setup.ts b/apps/cli/src/helpers/setup/addons-setup.ts index cbbaa40..2a7acd2 100644 --- a/apps/cli/src/helpers/setup/addons-setup.ts +++ b/apps/cli/src/helpers/setup/addons-setup.ts @@ -6,6 +6,7 @@ import type { Frontend, ProjectConfig } from "../../types"; import { addPackageDependency } from "../../utils/add-package-deps"; import { setupStarlight } from "./starlight-setup"; import { setupTauri } from "./tauri-setup"; +import { addPwaToViteConfig } from "./vite-pwa-setup"; export async function setupAddons(config: ProjectConfig, isAddCommand = false) { const { addons, frontend, projectDir } = config; @@ -149,4 +150,10 @@ async function setupPwa(projectDir: string, frontends: Frontend[]) { await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 }); } + + const viteConfigTs = path.join(clientPackageDir, "vite.config.ts"); + + if (await fs.pathExists(viteConfigTs)) { + await addPwaToViteConfig(viteConfigTs, path.basename(projectDir)); + } } diff --git a/apps/cli/src/helpers/setup/vite-pwa-setup.ts b/apps/cli/src/helpers/setup/vite-pwa-setup.ts new file mode 100644 index 0000000..91aab65 --- /dev/null +++ b/apps/cli/src/helpers/setup/vite-pwa-setup.ts @@ -0,0 +1,73 @@ +import { + type CallExpression, + Node, + type ObjectLiteralExpression, + SyntaxKind, +} from "ts-morph"; +import { ensureArrayProperty, tsProject } from "../../utils/ts-morph"; + +export async function addPwaToViteConfig( + viteConfigPath: string, + projectName: string, +): Promise { + const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath); + if (!sourceFile) { + throw new Error("vite config not found"); + } + + const hasImport = sourceFile + .getImportDeclarations() + .some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa"); + + if (!hasImport) { + sourceFile.insertImportDeclaration(0, { + namedImports: ["VitePWA"], + moduleSpecifier: "vite-plugin-pwa", + }); + } + + const defineCall = sourceFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((expr) => { + const expression = expr.getExpression(); + return ( + Node.isIdentifier(expression) && expression.getText() === "defineConfig" + ); + }); + + if (!defineCall) { + throw new Error("Could not find defineConfig call in vite config"); + } + + const callExpr = defineCall as CallExpression; + const configObject = callExpr.getArguments()[0] as + | ObjectLiteralExpression + | undefined; + if (!configObject) { + throw new Error("defineConfig argument is not an object literal"); + } + + const pluginsArray = ensureArrayProperty(configObject, "plugins"); + + const alreadyPresent = pluginsArray + .getElements() + .some((el) => el.getText().startsWith("VitePWA(")); + + if (!alreadyPresent) { + pluginsArray.addElement( + `VitePWA({ + registerType: "autoUpdate", + manifest: { + name: "${projectName}", + short_name: "${projectName}", + description: "${projectName} - PWA Application", + theme_color: "#0c0c0c", + }, + pwaAssets: { disabled: false, config: true }, + devOptions: { enabled: true }, +})`, + ); + } + + await tsProject.save(); +} diff --git a/apps/cli/src/helpers/setup/web-deploy-setup.ts b/apps/cli/src/helpers/setup/web-deploy-setup.ts new file mode 100644 index 0000000..65129ac --- /dev/null +++ b/apps/cli/src/helpers/setup/web-deploy-setup.ts @@ -0,0 +1,89 @@ +import path from "node:path"; +import fs from "fs-extra"; +import type { PackageManager, ProjectConfig } from "../../types"; +import { addPackageDependency } from "../../utils/add-package-deps"; +import { setupNuxtWorkersDeploy } from "./workers-nuxt-setup"; +import { setupSvelteWorkersDeploy } from "./workers-svelte-setup"; +import { setupWorkersVitePlugin } from "./workers-vite-setup"; + +export async function setupWebDeploy(config: ProjectConfig): Promise { + const { webDeploy, frontend, projectDir } = config; + const { packageManager } = config; + + if (webDeploy === "none") return; + + if (webDeploy !== "workers") return; + + const isNext = frontend.includes("next"); + const isNuxt = frontend.includes("nuxt"); + const isSvelte = frontend.includes("svelte"); + const isTanstackRouter = frontend.includes("tanstack-router"); + const isReactRouter = frontend.includes("react-router"); + const isSolid = frontend.includes("solid"); + + if (isNext) { + await setupNextWorkersDeploy(projectDir, packageManager); + } else if (isNuxt) { + await setupNuxtWorkersDeploy(projectDir, packageManager); + } else if (isSvelte) { + await setupSvelteWorkersDeploy(projectDir, packageManager); + } else if (isTanstackRouter || isReactRouter || isSolid) { + await setupWorkersWebDeploy(projectDir, packageManager); + } +} + +async function setupWorkersWebDeploy( + projectDir: string, + pkgManager: PackageManager, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + + if (!(await fs.pathExists(webAppDir))) { + return; + } + + const packageJsonPath = path.join(webAppDir, "package.json"); + if (await fs.pathExists(packageJsonPath)) { + const packageJson = await fs.readJson(packageJsonPath); + + packageJson.scripts = { + ...packageJson.scripts, + "wrangler:dev": "wrangler dev --port=3001", + deploy: `${pkgManager} run build && wrangler deploy`, + }; + + await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); + } + + await setupWorkersVitePlugin(projectDir); +} + +async function setupNextWorkersDeploy( + projectDir: string, + _packageManager: PackageManager, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(webAppDir))) return; + + await addPackageDependency({ + dependencies: ["@opennextjs/cloudflare"], + devDependencies: ["wrangler"], + projectDir: webAppDir, + }); + + const packageJsonPath = path.join(webAppDir, "package.json"); + if (await fs.pathExists(packageJsonPath)) { + const pkg = await fs.readJson(packageJsonPath); + + pkg.scripts = { + ...pkg.scripts, + preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview", + deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload", + "cf-typegen": + "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts", + }; + + await fs.writeJson(packageJsonPath, pkg, { spaces: 2 }); + } +} diff --git a/apps/cli/src/helpers/setup/workers-nuxt-setup.ts b/apps/cli/src/helpers/setup/workers-nuxt-setup.ts new file mode 100644 index 0000000..74690bc --- /dev/null +++ b/apps/cli/src/helpers/setup/workers-nuxt-setup.ts @@ -0,0 +1,112 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { + type ArrayLiteralExpression, + type CallExpression, + Node, + type ObjectLiteralExpression, + type PropertyAssignment, + SyntaxKind, +} from "ts-morph"; +import type { PackageManager } from "../../types"; +import { addPackageDependency } from "../../utils/add-package-deps"; +import { tsProject } from "../../utils/ts-morph"; + +export async function setupNuxtWorkersDeploy( + projectDir: string, + packageManager: PackageManager, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(webAppDir))) return; + + await addPackageDependency({ + devDependencies: ["nitro-cloudflare-dev", "wrangler"], + projectDir: webAppDir, + }); + + const pkgPath = path.join(webAppDir, "package.json"); + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + + pkg.scripts = { + ...pkg.scripts, + deploy: `${packageManager} run build && wrangler deploy`, + "cf-typegen": "wrangler types", + }; + await fs.writeJson(pkgPath, pkg, { spaces: 2 }); + } + + const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts"); + if (!(await fs.pathExists(nuxtConfigPath))) return; + + const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath); + if (!sourceFile) return; + + const defineCall = sourceFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((expr) => { + const expression = expr.getExpression(); + return ( + Node.isIdentifier(expression) && + expression.getText() === "defineNuxtConfig" + ); + }) as CallExpression | undefined; + + if (!defineCall) return; + + const configObj = defineCall.getArguments()[0] as + | ObjectLiteralExpression + | undefined; + if (!configObj) return; + + const today = new Date().toISOString().slice(0, 10); + + const compatProp = configObj.getProperty("compatibilityDate"); + if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) { + (compatProp as PropertyAssignment).setInitializer(`'${today}'`); + } else { + configObj.addPropertyAssignment({ + name: "compatibilityDate", + initializer: `'${today}'`, + }); + } + + const nitroInitializer = `{ + preset: "cloudflare_module", + cloudflare: { + deployConfig: true, + nodeCompat: true + } + }`; + const nitroProp = configObj.getProperty("nitro"); + if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) { + (nitroProp as PropertyAssignment).setInitializer(nitroInitializer); + } else { + configObj.addPropertyAssignment({ + name: "nitro", + initializer: nitroInitializer, + }); + } + + const modulesProp = configObj.getProperty("modules"); + if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) { + const arrayExpr = modulesProp.getFirstDescendantByKind( + SyntaxKind.ArrayLiteralExpression, + ) as ArrayLiteralExpression | undefined; + if (arrayExpr) { + const alreadyHas = arrayExpr + .getElements() + .some( + (el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev", + ); + if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'"); + } + } else { + configObj.addPropertyAssignment({ + name: "modules", + initializer: "['nitro-cloudflare-dev']", + }); + } + + await tsProject.save(); +} diff --git a/apps/cli/src/helpers/setup/workers-svelte-setup.ts b/apps/cli/src/helpers/setup/workers-svelte-setup.ts new file mode 100644 index 0000000..19e7f4e --- /dev/null +++ b/apps/cli/src/helpers/setup/workers-svelte-setup.ts @@ -0,0 +1,74 @@ +import path from "node:path"; +import fs from "fs-extra"; +import type { ImportDeclaration } from "ts-morph"; +import type { PackageManager } from "../../types"; +import { addPackageDependency } from "../../utils/add-package-deps"; +import { tsProject } from "../../utils/ts-morph"; + +export async function setupSvelteWorkersDeploy( + projectDir: string, + packageManager: PackageManager, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(webAppDir))) return; + + await addPackageDependency({ + devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"], + projectDir: webAppDir, + }); + + const pkgPath = path.join(webAppDir, "package.json"); + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + pkg.scripts = { + ...pkg.scripts, + deploy: `${packageManager} run build && wrangler deploy`, + "cf-typegen": "wrangler types ./src/worker-configuration.d.ts", + }; + await fs.writeJson(pkgPath, pkg, { spaces: 2 }); + } + + const possibleConfigFiles = [ + path.join(webAppDir, "svelte.config.js"), + path.join(webAppDir, "svelte.config.ts"), + ]; + + const existingConfigPath = ( + await Promise.all( + possibleConfigFiles.map(async (p) => ((await fs.pathExists(p)) ? p : "")), + ) + ).find((p) => p); + + if (existingConfigPath) { + const sourceFile = + tsProject.addSourceFileAtPathIfExists(existingConfigPath); + if (!sourceFile) return; + + const adapterImport = sourceFile + .getImportDeclarations() + .find((imp: ImportDeclaration) => + ["@sveltejs/adapter-auto", "@sveltejs/adapter-node"].includes( + imp.getModuleSpecifierValue(), + ), + ); + + if (adapterImport) { + adapterImport.setModuleSpecifier("@sveltejs/adapter-cloudflare"); + } else { + const alreadyHasCloudflare = sourceFile + .getImportDeclarations() + .some( + (imp) => + imp.getModuleSpecifierValue() === "@sveltejs/adapter-cloudflare", + ); + if (!alreadyHasCloudflare) { + sourceFile.insertImportDeclaration(0, { + defaultImport: "adapter", + moduleSpecifier: "@sveltejs/adapter-cloudflare", + }); + } + } + + await tsProject.save(); + } +} diff --git a/apps/cli/src/helpers/setup/workers-vite-setup.ts b/apps/cli/src/helpers/setup/workers-vite-setup.ts new file mode 100644 index 0000000..725eb75 --- /dev/null +++ b/apps/cli/src/helpers/setup/workers-vite-setup.ts @@ -0,0 +1,76 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { + type CallExpression, + Node, + type ObjectLiteralExpression, + SyntaxKind, +} from "ts-morph"; +import { addPackageDependency } from "../../utils/add-package-deps"; +import { ensureArrayProperty, tsProject } from "../../utils/ts-morph"; + +export async function setupWorkersVitePlugin( + projectDir: string, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + const viteConfigPath = path.join(webAppDir, "vite.config.ts"); + + if (!(await fs.pathExists(viteConfigPath))) { + throw new Error("vite.config.ts not found in web app directory"); + } + + await addPackageDependency({ + devDependencies: ["@cloudflare/vite-plugin", "wrangler"], + projectDir: webAppDir, + }); + + const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath); + if (!sourceFile) { + throw new Error("vite.config.ts not found in web app directory"); + } + + const hasCloudflareImport = sourceFile + .getImportDeclarations() + .some((imp) => imp.getModuleSpecifierValue() === "@cloudflare/vite-plugin"); + + if (!hasCloudflareImport) { + sourceFile.insertImportDeclaration(0, { + namedImports: ["cloudflare"], + moduleSpecifier: "@cloudflare/vite-plugin", + }); + } + + const defineCall = sourceFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((expr) => { + const expression = expr.getExpression(); + return ( + Node.isIdentifier(expression) && expression.getText() === "defineConfig" + ); + }); + + if (!defineCall) { + throw new Error("Could not find defineConfig call in vite config"); + } + + const callExpr = defineCall as CallExpression; + const configObject = callExpr.getArguments()[0] as + | ObjectLiteralExpression + | undefined; + + if (!configObject) { + throw new Error("defineConfig argument is not an object literal"); + } + + const pluginsArray = ensureArrayProperty(configObject, "plugins"); + + const hasCloudflarePlugin = pluginsArray + .getElements() + .some((el) => el.getText().includes("cloudflare(")); + + if (!hasCloudflarePlugin) { + pluginsArray.addElement("cloudflare()"); + } + + await tsProject.save(); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 843ba7f..09967d4 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -18,6 +18,7 @@ import { PackageManagerSchema, ProjectNameSchema, RuntimeSchema, + WebDeploySchema, } from "./types"; import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; import { openUrl } from "./utils/open-url"; @@ -55,6 +56,7 @@ const router = t.router({ backend: BackendSchema.optional(), runtime: RuntimeSchema.optional(), api: APISchema.optional(), + webDeploy: WebDeploySchema.optional(), }) .optional() .default({}), @@ -70,19 +72,23 @@ const router = t.router({ }), add: t.procedure .meta({ - description: "Add addons to an existing Better-T Stack project", + description: + "Add addons or deployment configurations to an existing Better-T Stack project", }) .input( z.tuple([ z .object({ addons: z.array(AddonsSchema).optional().default([]), + webDeploy: WebDeploySchema.optional(), projectDir: z.string().optional(), install: z .boolean() .optional() .default(false) - .describe("Install dependencies after adding addons"), + .describe( + "Install dependencies after adding addons or deployment", + ), packageManager: PackageManagerSchema.optional(), }) .optional() diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 006bb83..6cc6623 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -136,7 +136,7 @@ export async function getAddonsToAdd( const response = await multiselect({ message: "Select addons", options: options, - required: true, + required: false, }); if (isCancel(response)) { diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 5d5978d..34f287d 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -12,6 +12,7 @@ import type { PackageManager, ProjectConfig, Runtime, + WebDeploy, } from "../types"; import { getAddonsChoice } from "./addons"; import { getApiChoice } from "./api"; @@ -26,6 +27,7 @@ import { getinstallChoice } from "./install"; import { getORMChoice } from "./orm"; import { getPackageManagerChoice } from "./package-manager"; import { getRuntimeChoice } from "./runtime"; +import { getDeploymentChoice } from "./web-deploy"; type PromptGroupResults = { frontend: Frontend[]; @@ -41,6 +43,7 @@ type PromptGroupResults = { git: boolean; packageManager: PackageManager; install: boolean; + webDeploy: WebDeploy; }; export async function gatherConfig( @@ -87,6 +90,13 @@ export async function gatherConfig( results.backend, results.runtime, ), + webDeploy: ({ results }) => + getDeploymentChoice( + flags.webDeploy, + results.runtime, + results.backend, + results.frontend, + ), git: () => getGitChoice(flags.git), packageManager: () => getPackageManagerChoice(flags.packageManager), install: () => getinstallChoice(flags.install), @@ -107,6 +117,7 @@ export async function gatherConfig( result.auth = false; result.dbSetup = "none"; result.examples = ["todo"]; + result.webDeploy = "none"; } if (result.backend === "none") { @@ -117,6 +128,7 @@ export async function gatherConfig( result.auth = false; result.dbSetup = "none"; result.examples = []; + result.webDeploy = "none"; } return { @@ -136,5 +148,6 @@ export async function gatherConfig( install: result.install, dbSetup: result.dbSetup, api: result.api, + webDeploy: result.webDeploy, }; } diff --git a/apps/cli/src/prompts/runtime.ts b/apps/cli/src/prompts/runtime.ts index 03f2470..2a73ab9 100644 --- a/apps/cli/src/prompts/runtime.ts +++ b/apps/cli/src/prompts/runtime.ts @@ -37,7 +37,7 @@ export async function getRuntimeChoice( if (backend === "hono") { runtimeOptions.push({ value: "workers", - label: "Cloudflare Workers (beta)", + label: "Cloudflare Workers", hint: "Edge runtime on Cloudflare's global network", }); } diff --git a/apps/cli/src/prompts/web-deploy.ts b/apps/cli/src/prompts/web-deploy.ts new file mode 100644 index 0000000..9b4e8fd --- /dev/null +++ b/apps/cli/src/prompts/web-deploy.ts @@ -0,0 +1,122 @@ +import { cancel, isCancel, select } from "@clack/prompts"; +import pc from "picocolors"; +import { DEFAULT_CONFIG } from "../constants"; +import type { Backend, Frontend, Runtime, WebDeploy } from "../types"; + +const WORKERS_COMPATIBLE_FRONTENDS: Frontend[] = [ + "tanstack-router", + "react-router", + "solid", + "next", + "nuxt", + "svelte", +]; + +type DeploymentOption = { + value: WebDeploy; + label: string; + hint: string; +}; + +function getDeploymentDisplay(deployment: WebDeploy): { + label: string; + hint: string; +} { + if (deployment === "workers") { + return { + label: "Cloudflare Workers", + hint: "Deploy to Cloudflare Workers using Wrangler", + }; + } + return { + label: deployment, + hint: `Add ${deployment} deployment`, + }; +} + +export async function getDeploymentChoice( + deployment?: WebDeploy, + _runtime?: Runtime, + _backend?: Backend, + frontend: Frontend[] = [], +): Promise { + if (deployment !== undefined) return deployment; + + const hasCompatibleFrontend = frontend.some((f) => + WORKERS_COMPATIBLE_FRONTENDS.includes(f), + ); + + if (!hasCompatibleFrontend) { + return "none"; + } + + const options: DeploymentOption[] = [ + { + value: "workers", + label: "Cloudflare Workers", + hint: "Deploy to Cloudflare Workers using Wrangler", + }, + { value: "none", label: "None", hint: "Manual setup" }, + ]; + + const response = await select({ + message: "Select web deployment", + options, + initialValue: DEFAULT_CONFIG.webDeploy, + }); + + if (isCancel(response)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + return response; +} + +export async function getDeploymentToAdd( + frontend: Frontend[], + existingDeployment?: WebDeploy, +): Promise { + const options: DeploymentOption[] = []; + + if ( + frontend.some((f) => WORKERS_COMPATIBLE_FRONTENDS.includes(f)) && + existingDeployment !== "workers" + ) { + const { label, hint } = getDeploymentDisplay("workers"); + options.push({ + value: "workers", + label, + hint, + }); + } + + if (existingDeployment && existingDeployment !== "none") { + return "none"; + } + + if (options.length > 0) { + options.push({ + value: "none", + label: "None", + hint: "Skip deployment setup", + }); + } + + if (options.length === 0) { + return "none"; + } + + const response = await select({ + message: "Select web deployment", + options, + initialValue: DEFAULT_CONFIG.webDeploy, + }); + + if (isCancel(response)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + return response; +} diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 6a561fa..6ba1bc2 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -92,6 +92,11 @@ export const ProjectNameSchema = z .describe("Project name or path"); export type ProjectName = z.infer; +export const WebDeploySchema = z + .enum(["workers", "none"]) + .describe("Web deployment"); +export type WebDeploy = z.infer; + export type CreateInput = { projectName?: string; yes?: boolean; @@ -108,10 +113,12 @@ export type CreateInput = { backend?: Backend; runtime?: Runtime; api?: API; + webDeploy?: WebDeploy; }; export type AddInput = { addons?: Addons[]; + webDeploy?: WebDeploy; projectDir?: string; install?: boolean; packageManager?: PackageManager; @@ -138,6 +145,7 @@ export interface ProjectConfig { install: boolean; dbSetup: DatabaseSetup; api: API; + webDeploy: WebDeploy; } export interface BetterTStackConfig { @@ -154,6 +162,7 @@ export interface BetterTStackConfig { packageManager: PackageManager; dbSetup: DatabaseSetup; api: API; + webDeploy: WebDeploy; } export type AvailablePackageManagers = "npm" | "pnpm" | "bun"; diff --git a/apps/cli/src/utils/bts-config.ts b/apps/cli/src/utils/bts-config.ts index e312e6d..df167e9 100644 --- a/apps/cli/src/utils/bts-config.ts +++ b/apps/cli/src/utils/bts-config.ts @@ -23,6 +23,7 @@ export async function writeBtsConfig( packageManager: projectConfig.packageManager, dbSetup: projectConfig.dbSetup, api: projectConfig.api, + webDeploy: projectConfig.webDeploy, }; const baseContent = { @@ -40,6 +41,7 @@ export async function writeBtsConfig( packageManager: btsConfig.packageManager, dbSetup: btsConfig.dbSetup, api: btsConfig.api, + webDeploy: btsConfig.webDeploy, }; let configContent = JSON.stringify(baseContent); @@ -91,7 +93,7 @@ export async function readBtsConfig( export async function updateBtsConfig( projectDir: string, - updates: Partial>, + updates: Partial>, ): Promise { try { const configPath = path.join(projectDir, BTS_CONFIG_FILE); diff --git a/apps/cli/src/utils/display-config.ts b/apps/cli/src/utils/display-config.ts index 76a2dbf..e4103da 100644 --- a/apps/cli/src/utils/display-config.ts +++ b/apps/cli/src/utils/display-config.ts @@ -101,6 +101,12 @@ export function displayConfig(config: Partial) { ); } + if (config.webDeploy !== undefined) { + configDisplay.push( + `${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`, + ); + } + if (configDisplay.length === 0) { return pc.yellow("No configuration selected."); } diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 83f546b..36d06e7 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -29,6 +29,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string { } flags.push(`--db-setup ${config.dbSetup}`); + flags.push(`--web-deploy ${config.webDeploy}`); flags.push(config.git ? "--git" : "--no-git"); flags.push(`--package-manager ${config.packageManager}`); flags.push(config.install ? "--install" : "--no-install"); diff --git a/apps/cli/src/utils/ts-morph.ts b/apps/cli/src/utils/ts-morph.ts new file mode 100644 index 0000000..136e74a --- /dev/null +++ b/apps/cli/src/utils/ts-morph.ts @@ -0,0 +1,31 @@ +import { + type ArrayLiteralExpression, + IndentationText, + type ObjectLiteralExpression, + Project, + QuoteKind, + SyntaxKind, +} from "ts-morph"; + +export const tsProject = new Project({ + useInMemoryFileSystem: false, + skipAddingFilesFromTsConfig: true, + manipulationSettings: { + quoteKind: QuoteKind.Single, + indentationText: IndentationText.TwoSpaces, + }, +}); + +export function ensureArrayProperty( + obj: ObjectLiteralExpression, + name: string, +): ArrayLiteralExpression { + return (obj + .getProperty(name) + ?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? + obj + .addPropertyAssignment({ name, initializer: "[]" }) + .getFirstDescendantByKindOrThrow( + SyntaxKind.ArrayLiteralExpression, + )) as ArrayLiteralExpression; +} diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index d7942a5..6028a6e 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -14,6 +14,7 @@ import { type ProjectConfig, ProjectNameSchema, type Runtime, + type WebDeploy, } from "./types"; export function processAndValidateFlags( @@ -82,6 +83,10 @@ export function processAndValidateFlags( config.packageManager = options.packageManager as PackageManager; } + if (options.webDeploy) { + config.webDeploy = options.webDeploy as WebDeploy; + } + if (projectName) { const result = ProjectNameSchema.safeParse(path.basename(projectName)); if (!result.success) { @@ -446,6 +451,24 @@ export function processAndValidateFlags( process.exit(1); } + if ( + config.webDeploy === "workers" && + config.frontend && + config.frontend.length > 0 + ) { + const incompatibleFrontends = config.frontend.filter( + (f) => f === "tanstack-start", + ); + if (incompatibleFrontends.length > 0) { + consola.fatal( + `The following frontends are not compatible with '--web-deploy workers': ${incompatibleFrontends.join( + ", ", + )}. Please choose a different frontend or remove '--web-deploy workers'.`, + ); + process.exit(1); + } + } + return config; } diff --git a/apps/cli/templates/deploy/web/nuxt/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/nuxt/wrangler.jsonc.hbs new file mode 100644 index 0000000..84cb91b --- /dev/null +++ b/apps/cli/templates/deploy/web/nuxt/wrangler.jsonc.hbs @@ -0,0 +1,51 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "main": "./.output/server/index.mjs", + "compatibility_date": "2025-07-01", + "assets": { + "binding": "ASSETS", + "directory": "./.output/public/" + }, + "observability": { + "enabled": true + } + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" }, + + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" }, + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" }, + + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] +} diff --git a/apps/cli/templates/deploy/web/react/next/open-next.config.ts b/apps/cli/templates/deploy/web/react/next/open-next.config.ts new file mode 100644 index 0000000..f0b0147 --- /dev/null +++ b/apps/cli/templates/deploy/web/react/next/open-next.config.ts @@ -0,0 +1,6 @@ +import { defineCloudflareConfig } from "@opennextjs/cloudflare/config"; +// import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"; + +export default defineCloudflareConfig({ + // incrementalCache: r2IncrementalCache, +}); diff --git a/apps/cli/templates/deploy/web/react/next/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/react/next/wrangler.jsonc.hbs new file mode 100644 index 0000000..5f9b58b --- /dev/null +++ b/apps/cli/templates/deploy/web/react/next/wrangler.jsonc.hbs @@ -0,0 +1,22 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "main": ".open-next/worker.js", + "name": "{{projectName}}", + "compatibility_date": "2025-07-05", + "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], + "assets": { + "directory": ".open-next/assets", + "binding": "ASSETS" + }, + // "r2_buckets": [ + // // Use R2 incremental cache + // // See https://opennext.js.org/cloudflare/caching + // { + // "binding": "NEXT_INC_CACHE_R2_BUCKET", + // // Create the bucket before deploying + // // You can change the bucket name if you want + // // See https://developers.cloudflare.com/workers/wrangler/commands/#r2-bucket-create + // "bucket_name": "cache" + // } + // ] +} diff --git a/apps/cli/templates/deploy/web/react/react-router/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/react/react-router/wrangler.jsonc.hbs new file mode 100644 index 0000000..15cb97a --- /dev/null +++ b/apps/cli/templates/deploy/web/react/react-router/wrangler.jsonc.hbs @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "compatibility_date": "2025-04-03", + "assets": { + "not_found_handling": "single-page-application" + } +} diff --git a/apps/cli/templates/deploy/web/react/tanstack-router/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/react/tanstack-router/wrangler.jsonc.hbs new file mode 100644 index 0000000..15cb97a --- /dev/null +++ b/apps/cli/templates/deploy/web/react/tanstack-router/wrangler.jsonc.hbs @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "compatibility_date": "2025-04-03", + "assets": { + "not_found_handling": "single-page-application" + } +} diff --git a/apps/cli/templates/deploy/web/solid/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/solid/wrangler.jsonc.hbs new file mode 100644 index 0000000..b36eef8 --- /dev/null +++ b/apps/cli/templates/deploy/web/solid/wrangler.jsonc.hbs @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "compatibility_date": "2025-04-03", + "assets": { + "not_found_handling": "single-page-application" + } +} \ No newline at end of file diff --git a/apps/cli/templates/deploy/web/svelte/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/svelte/wrangler.jsonc.hbs new file mode 100644 index 0000000..0eac96f --- /dev/null +++ b/apps/cli/templates/deploy/web/svelte/wrangler.jsonc.hbs @@ -0,0 +1,51 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_date": "2025-07-05", + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare" + }, + "observability": { + "enabled": true + } + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" }, + + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" }, + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" }, + + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] +} diff --git a/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs b/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs index 9f15eaa..1e3cad6 100644 --- a/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs +++ b/apps/cli/templates/frontend/react/react-router/vite.config.ts.hbs @@ -1,6 +1,3 @@ -{{#if (includes addons "pwa")}} -import { VitePWA } from "vite-plugin-pwa"; -{{/if}} import { reactRouter } from "@react-router/dev/vite"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; @@ -11,23 +8,5 @@ export default defineConfig({ tailwindcss(), reactRouter(), tsconfigPaths(), - {{#if (includes addons "pwa")}} - VitePWA({ - registerType: "autoUpdate", - manifest: { - name: "{{projectName}}", - short_name: "{{projectName}}", - description: "{{projectName}} - PWA Application", - theme_color: "#0c0c0c", - }, - pwaAssets: { - disabled: false, - config: true, - }, - devOptions: { - enabled: true, - }, - }), - {{/if}} ], -}); +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs b/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs index 24eadde..f23a1b3 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs +++ b/apps/cli/templates/frontend/react/tanstack-router/vite.config.ts.hbs @@ -1,8 +1,5 @@ -{{#if (includes addons "pwa")}} -import { VitePWA } from "vite-plugin-pwa"; -{{/if}} import tailwindcss from "@tailwindcss/vite"; -import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import path from "node:path"; import { defineConfig } from "vite"; @@ -10,30 +7,12 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [ tailwindcss(), - TanStackRouterVite({}), + tanstackRouter({}), react(), - {{#if (includes addons "pwa")}} - VitePWA({ - registerType: "autoUpdate", - manifest: { - name: "{{projectName}}", - short_name: "{{projectName}}", - description: "{{projectName}} - PWA Application", - theme_color: "#0c0c0c", - }, - pwaAssets: { - disabled: false, - config: true, - }, - devOptions: { - enabled: true, - }, - }), - {{/if}} ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, -}); +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/react/tanstack-start/package.json.hbs b/apps/cli/templates/frontend/react/tanstack-start/package.json.hbs index 6951869..23a751a 100644 --- a/apps/cli/templates/frontend/react/tanstack-start/package.json.hbs +++ b/apps/cli/templates/frontend/react/tanstack-start/package.json.hbs @@ -17,14 +17,14 @@ "@tanstack/react-start": "^1.121.0-alpha.27", "@tanstack/router-plugin": "^1.121.0", "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.473.0", + "clsx": "^2.1.1", + "lucide-react": "^0.525.0", "next-themes": "^0.4.6", "react": "19.0.0", "react-dom": "19.0.0", "sonner": "^2.0.3", "tailwindcss": "^4.1.3", - "tailwind-merge": "^2.6.0", + "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.2.5", "vite-tsconfig-paths": "^5.1.4", "zod": "^3.25.16" @@ -38,7 +38,7 @@ "@vitejs/plugin-react": "^4.5.2", "jsdom": "^26.0.0", "typescript": "^5.7.2", - "vite": "^6.3.5", - "web-vitals": "^4.2.4" + "vite": "^7.0.2", + "web-vitals": "^5.0.3" } -} +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/react/web-base/_gitignore b/apps/cli/templates/frontend/react/web-base/_gitignore index a26bdf7..7a1d535 100644 --- a/apps/cli/templates/frontend/react/web-base/_gitignore +++ b/apps/cli/templates/frontend/react/web-base/_gitignore @@ -50,3 +50,8 @@ next-env.d.ts # Other dev-dist + +.wrangler +.dev.vars* + +.open-next diff --git a/apps/cli/templates/frontend/solid/_gitignore b/apps/cli/templates/frontend/solid/_gitignore index 2749f5b..4efc4c0 100644 --- a/apps/cli/templates/frontend/solid/_gitignore +++ b/apps/cli/templates/frontend/solid/_gitignore @@ -5,3 +5,6 @@ dist-ssr *.local .env .env.* + +.wrangler +.dev.vars* \ No newline at end of file diff --git a/apps/cli/templates/frontend/solid/package.json.hbs b/apps/cli/templates/frontend/solid/package.json.hbs index 4e686f1..67c0406 100644 --- a/apps/cli/templates/frontend/solid/package.json.hbs +++ b/apps/cli/templates/frontend/solid/package.json.hbs @@ -4,7 +4,7 @@ "type": "module", "scripts": { "dev": "vite --port 3001", - "build": "vite build && tsc", + "build": "vite build", "serve": "vite preview", "test": "vitest run" }, @@ -21,7 +21,7 @@ }, "devDependencies": { "typescript": "^5.7.2", - "vite": "^6.0.11", + "vite": "^7.0.2", "vite-plugin-solid": "^2.11.2" } } diff --git a/apps/cli/templates/frontend/solid/src/main.tsx.hbs b/apps/cli/templates/frontend/solid/src/main.tsx.hbs index cfed685..95946b5 100644 --- a/apps/cli/templates/frontend/solid/src/main.tsx.hbs +++ b/apps/cli/templates/frontend/solid/src/main.tsx.hbs @@ -4,7 +4,7 @@ import { routeTree } from "./routeTree.gen"; import "./styles.css"; {{#if (eq api "orpc")}} import { QueryClientProvider } from "@tanstack/solid-query"; -import { queryClient } from "./utils/orpc"; +import { orpc, queryClient } from "./utils/orpc"; {{/if}} const router = createRouter({ @@ -12,6 +12,9 @@ const router = createRouter({ defaultPreload: "intent", scrollRestoration: true, defaultPreloadStaleTime: 0, + {{#if (eq api "orpc")}} + context: { orpc, queryClient }, + {{/if}} }); declare module "@tanstack/solid-router" { diff --git a/apps/cli/templates/frontend/solid/vite.config.js.hbs b/apps/cli/templates/frontend/solid/vite.config.js.hbs deleted file mode 100644 index c677aa4..0000000 --- a/apps/cli/templates/frontend/solid/vite.config.js.hbs +++ /dev/null @@ -1,39 +0,0 @@ -import { defineConfig } from "vite"; -import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; -import solidPlugin from "vite-plugin-solid"; -import tailwindcss from "@tailwindcss/vite"; -import path from "node:path"; -{{#if (includes addons "pwa")}} -import { VitePWA } from "vite-plugin-pwa"; -{{/if}} - -export default defineConfig({ - plugins: [ - TanStackRouterVite({ target: "solid", autoCodeSplitting: true }), - solidPlugin(), - tailwindcss(), - {{#if (includes addons "pwa")}} - VitePWA({ - registerType: "autoUpdate", - manifest: { - name: "{{projectName}}", - short_name: "{{projectName}}", - description: "{{projectName}} - PWA Application", - theme_color: "#0c0c0c", - }, - pwaAssets: { - disabled: false, - config: true, - }, - devOptions: { - enabled: true, - }, - }), - {{/if}} - ], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); diff --git a/apps/cli/templates/frontend/solid/vite.config.ts.hbs b/apps/cli/templates/frontend/solid/vite.config.ts.hbs new file mode 100644 index 0000000..dbfa674 --- /dev/null +++ b/apps/cli/templates/frontend/solid/vite.config.ts.hbs @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import solidPlugin from "vite-plugin-solid"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; + +export default defineConfig({ + plugins: [ + tanstackRouter({ target: "solid", autoCodeSplitting: true }), + solidPlugin(), + tailwindcss(), + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/svelte/package.json.hbs b/apps/cli/templates/frontend/svelte/package.json.hbs index 3a125f1..91bc320 100644 --- a/apps/cli/templates/frontend/svelte/package.json.hbs +++ b/apps/cli/templates/frontend/svelte/package.json.hbs @@ -21,7 +21,7 @@ "tailwindcss": "^4.1.4", "typescript": "^5.8.3", "@tanstack/svelte-query-devtools": "^5.74.6", - "vite": "^6.3.3" + "vite": "^7.0.2" }, "dependencies": { "@tanstack/svelte-form": "^1.7.0", diff --git a/apps/cli/templates/frontend/svelte/svelte.config.js b/apps/cli/templates/frontend/svelte/svelte.config.js.hbs similarity index 100% rename from apps/cli/templates/frontend/svelte/svelte.config.js rename to apps/cli/templates/frontend/svelte/svelte.config.js.hbs diff --git a/apps/web/public/analytics-data.json b/apps/web/public/analytics-data.json index 0c26a7b..f7155c1 100644 --- a/apps/web/public/analytics-data.json +++ b/apps/web/public/analytics-data.json @@ -140013,6 +140013,6 @@ } ], "lastUpdated": "Jul 1, 2025, 03:45 AM", - "generatedAt": "2025-07-01T04:23:00.971Z", + "generatedAt": "2025-07-05T10:12:56.694Z", "totalRecords": 5637 } \ No newline at end of file diff --git a/apps/web/public/schema.json b/apps/web/public/schema.json index 1a94b8e..6ef3489 100644 --- a/apps/web/public/schema.json +++ b/apps/web/public/schema.json @@ -144,6 +144,14 @@ "none" ], "description": "API type" + }, + "webDeploy": { + "type": "string", + "enum": [ + "workers", + "none" + ], + "description": "Web deployment" } }, "required": [ @@ -159,7 +167,8 @@ "auth", "packageManager", "dbSetup", - "api" + "api", + "webDeploy" ], "additionalProperties": false } \ No newline at end of file diff --git a/apps/web/scripts/generate-schema.ts b/apps/web/scripts/generate-schema.ts index 1f9ada0..e31b472 100644 --- a/apps/web/scripts/generate-schema.ts +++ b/apps/web/scripts/generate-schema.ts @@ -11,6 +11,7 @@ import { ORMSchema, PackageManagerSchema, RuntimeSchema, + WebDeploySchema, } from "../../cli/src/types"; const DATABASE_VALUES = DatabaseSchema.options; @@ -23,91 +24,97 @@ const EXAMPLES_VALUES = ExamplesSchema.options; const PACKAGE_MANAGER_VALUES = PackageManagerSchema.options; const DATABASE_SETUP_VALUES = DatabaseSetupSchema.options; const API_VALUES = APISchema.options; +const WEB_DEPLOY_VALUES = WebDeploySchema.options; -const schema = { +const configSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "https://better-t-stack.dev/schema.json", title: "Better-T-Stack Configuration", description: "Configuration file for Better-T-Stack projects", - type: "object" as const, + type: "object", properties: { $schema: { - type: "string" as const, + type: "string", description: "JSON Schema reference for validation", }, version: { - type: "string" as const, + type: "string", description: "CLI version used to create this project", pattern: "^\\d+\\.\\d+\\.\\d+$", }, createdAt: { - type: "string" as const, - format: "date-time" as const, + type: "string", + format: "date-time", description: "Timestamp when the project was created", }, database: { - type: "string" as const, + type: "string", enum: DATABASE_VALUES, description: DatabaseSchema.description, }, orm: { - type: "string" as const, + type: "string", enum: ORM_VALUES, description: ORMSchema.description, }, backend: { - type: "string" as const, + type: "string", enum: BACKEND_VALUES, description: BackendSchema.description, }, runtime: { - type: "string" as const, + type: "string", enum: RUNTIME_VALUES, description: RuntimeSchema.description, }, frontend: { - type: "array" as const, + type: "array", items: { - type: "string" as const, + type: "string", enum: FRONTEND_VALUES, }, description: FrontendSchema.description, }, addons: { - type: "array" as const, + type: "array", items: { - type: "string" as const, + type: "string", enum: ADDONS_VALUES, }, description: AddonsSchema.description, }, examples: { - type: "array" as const, + type: "array", items: { - type: "string" as const, + type: "string", enum: EXAMPLES_VALUES, }, description: ExamplesSchema.description, }, auth: { - type: "boolean" as const, + type: "boolean", description: "Whether authentication is enabled", }, packageManager: { - type: "string" as const, + type: "string", enum: PACKAGE_MANAGER_VALUES, description: PackageManagerSchema.description, }, dbSetup: { - type: "string" as const, + type: "string", enum: DATABASE_SETUP_VALUES, description: DatabaseSetupSchema.description, }, api: { - type: "string" as const, + type: "string", enum: API_VALUES, description: APISchema.description, }, + webDeploy: { + type: "string", + enum: WEB_DEPLOY_VALUES, + description: WebDeploySchema.description, + }, }, required: [ "version", @@ -123,6 +130,7 @@ const schema = { "packageManager", "dbSetup", "api", + "webDeploy", ], additionalProperties: false, }; @@ -130,7 +138,11 @@ const schema = { async function generateSchema() { const schemaPath = path.join(process.cwd(), "public", "schema.json"); await fs.ensureDir(path.dirname(schemaPath)); - await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8"); + await fs.writeFile( + schemaPath, + JSON.stringify(configSchema, null, 2), + "utf-8", + ); console.log("✅ Generated schema.json from shared types package"); } diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 899c224..e591b10 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -73,6 +73,7 @@ const CATEGORY_ORDER: Array = [ "database", "orm", "dbSetup", + "webDeploy", "auth", "packageManager", "addons", @@ -124,6 +125,7 @@ const getBadgeColors = (category: string): string => { case "packageManager": return "border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700/30 dark:bg-orange-900/30 dark:text-orange-300"; case "git": + case "webDeploy": case "install": return "border-gray-300 bg-gray-100 text-gray-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"; default: @@ -800,6 +802,28 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { if (nextStack.examples.length !== originalExamplesLength) changed = true; } + + // Web deploy compatibility: Workers not supported with TanStack Start + if ( + nextStack.webDeploy === "workers" && + nextStack.webFrontend.includes("tanstack-start") + ) { + notes.webDeploy.notes.push( + "Cloudflare Workers deployment is not supported with TanStack Start. It will be set to 'None'.", + ); + notes.webFrontend.notes.push( + "TanStack Start is not compatible with Cloudflare Workers deployment.", + ); + notes.webDeploy.hasIssue = true; + notes.webFrontend.hasIssue = true; + nextStack.webDeploy = "none"; + changed = true; + changes.push({ + category: "webDeploy", + message: + "Web deployment set to 'None' (Workers not compatible with TanStack Start)", + }); + } } } @@ -899,6 +923,13 @@ const generateCommand = (stackState: StackState): string => { } } + if ( + stackState.webDeploy && + !checkDefault("webDeploy", stackState.webDeploy) + ) { + flags.push(`--web-deploy ${stackState.webDeploy}`); + } + if (!checkDefault("install", stackState.install)) { if (stackState.install === "false" && DEFAULT_STACK.install === "true") { flags.push("--no-install"); diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index ecd755e..643bd4b 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -134,7 +134,7 @@ export const TECH_OPTIONS = { }, { id: "workers", - name: "Cloudflare Workers (beta)", + name: "Cloudflare Workers", description: "Serverless runtime for the edge", icon: "/icon/workers.svg", color: "from-orange-400 to-orange-600", @@ -320,6 +320,23 @@ export const TECH_OPTIONS = { default: true, }, ], + webDeploy: [ + { + id: "workers", + name: "Cloudflare Workers", + description: "Deploy to Cloudflare Workers", + icon: "/icon/workers.svg", + color: "from-orange-400 to-orange-600", + }, + { + id: "none", + name: "No Deployment", + description: "Skip deployment configuration", + icon: "", + color: "from-gray-400 to-gray-600", + default: true, + }, + ], auth: [ { id: "true", @@ -594,6 +611,7 @@ export type StackState = { git: string; install: string; api: string; + webDeploy: string; }; export const DEFAULT_STACK: StackState = { @@ -612,6 +630,7 @@ export const DEFAULT_STACK: StackState = { git: "true", install: "true", api: "trpc", + webDeploy: "none", }; export const isStackDefault = ( diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index 2dc12d3..f11a373 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -51,6 +51,9 @@ export const stackParsers = { "true", "false", ]).withDefault(DEFAULT_STACK.install), + webDeploy: parseAsStringEnum( + getValidIds("webDeploy"), + ).withDefault(DEFAULT_STACK.webDeploy), }; export const stackUrlKeys: UrlKeys = { @@ -69,6 +72,7 @@ export const stackUrlKeys: UrlKeys = { examples: "ex", git: "git", install: "i", + webDeploy: "wd", }; export const stackQueryStatesOptions = { diff --git a/bun.lock b/bun.lock index 25982e8..99381dd 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,7 @@ "picocolors": "^1.1.1", "posthog-node": "^5.1.1", "trpc-cli": "^0.9.2", + "ts-morph": "^26.0.0", "zod": "^3.25.67", }, "devDependencies": { @@ -287,6 +288,10 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], @@ -553,6 +558,8 @@ "@trpc/server": ["@trpc/server@11.4.2", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-THyq/V5bSFDHeWEAk6LqHF0IVTGk6voGwWsFEipzRRKOWWMIZINCsKZ4cISG6kWO2X9jBfMWv/S2o9hnC0zQ0w=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], @@ -781,6 +788,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -1475,6 +1484,8 @@ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1735,6 +1746,8 @@ "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "tsdown": ["tsdown@0.12.8", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^1.1.0", "hookable": "^5.5.3", "rolldown": "1.0.0-beta.15", "rolldown-plugin-dts": "^0.13.11", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unconfig": "^7.3.2" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-niHeVcFCNjvVZYVGTeoM4BF+/DWxP8pFH2tUs71sEKYdcKtJIbkSdEmtxByaRZeMgwVbVgPb8nv9i9okVwFLAA=="], @@ -1895,6 +1908,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@ts-morph/common/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "create-better-t-stack/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],