From 0ae1347e9df57e598f64306ea9cde9b896c49fa7 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 6 Jul 2025 16:32:13 +0530 Subject: [PATCH] add workers support for tanstack start (#369) --- .changeset/upset-pears-argue.md | 5 + .../project-generation/add-deployment.ts | 18 - .../project-generation/template-manager.ts | 1 + .../cli/src/helpers/setup/web-deploy-setup.ts | 4 + .../setup/workers-tanstack-start-setup.ts | 75 +++++ apps/cli/src/prompts/web-deploy.ts | 26 +- apps/cli/src/validation.ts | 18 - .../react/tanstack-start/wrangler.jsonc.hbs | 20 ++ .../frontend/react/web-base/_gitignore | 2 + apps/web/public/schema.json | 315 ++++++++---------- .../app/(home)/_components/stack-builder.tsx | 22 -- 11 files changed, 252 insertions(+), 254 deletions(-) create mode 100644 .changeset/upset-pears-argue.md create mode 100644 apps/cli/src/helpers/setup/workers-tanstack-start-setup.ts create mode 100644 apps/cli/templates/deploy/web/react/tanstack-start/wrangler.jsonc.hbs diff --git a/.changeset/upset-pears-argue.md b/.changeset/upset-pears-argue.md new file mode 100644 index 0000000..5779ed6 --- /dev/null +++ b/.changeset/upset-pears-argue.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add workers support for tanstack start diff --git a/apps/cli/src/helpers/project-generation/add-deployment.ts b/apps/cli/src/helpers/project-generation/add-deployment.ts index aabc9f3..c189c7e 100644 --- a/apps/cli/src/helpers/project-generation/add-deployment.ts +++ b/apps/cli/src/helpers/project-generation/add-deployment.ts @@ -42,24 +42,6 @@ export async function addDeploymentToProject( ); } - 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, diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index d97deb5..0617aff 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -851,6 +851,7 @@ export async function setupDeploymentTemplates( const templateMap: Record = { "tanstack-router": "react/tanstack-router", + "tanstack-start": "react/tanstack-start", "react-router": "react/react-router", solid: "solid", next: "react/next", diff --git a/apps/cli/src/helpers/setup/web-deploy-setup.ts b/apps/cli/src/helpers/setup/web-deploy-setup.ts index 65129ac..267cfba 100644 --- a/apps/cli/src/helpers/setup/web-deploy-setup.ts +++ b/apps/cli/src/helpers/setup/web-deploy-setup.ts @@ -4,6 +4,7 @@ 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 { setupTanstackStartWorkersDeploy } from "./workers-tanstack-start-setup"; import { setupWorkersVitePlugin } from "./workers-vite-setup"; export async function setupWebDeploy(config: ProjectConfig): Promise { @@ -18,6 +19,7 @@ export async function setupWebDeploy(config: ProjectConfig): Promise { const isNuxt = frontend.includes("nuxt"); const isSvelte = frontend.includes("svelte"); const isTanstackRouter = frontend.includes("tanstack-router"); + const isTanstackStart = frontend.includes("tanstack-start"); const isReactRouter = frontend.includes("react-router"); const isSolid = frontend.includes("solid"); @@ -27,6 +29,8 @@ export async function setupWebDeploy(config: ProjectConfig): Promise { await setupNuxtWorkersDeploy(projectDir, packageManager); } else if (isSvelte) { await setupSvelteWorkersDeploy(projectDir, packageManager); + } else if (isTanstackStart) { + await setupTanstackStartWorkersDeploy(projectDir, packageManager); } else if (isTanstackRouter || isReactRouter || isSolid) { await setupWorkersWebDeploy(projectDir, packageManager); } diff --git a/apps/cli/src/helpers/setup/workers-tanstack-start-setup.ts b/apps/cli/src/helpers/setup/workers-tanstack-start-setup.ts new file mode 100644 index 0000000..4d00993 --- /dev/null +++ b/apps/cli/src/helpers/setup/workers-tanstack-start-setup.ts @@ -0,0 +1,75 @@ +import path from "node:path"; +import fs from "fs-extra"; +import { + type CallExpression, + Node, + type ObjectLiteralExpression, + SyntaxKind, +} from "ts-morph"; +import type { PackageManager } from "../../types"; +import { addPackageDependency } from "../../utils/add-package-deps"; +import { ensureArrayProperty, tsProject } from "../../utils/ts-morph"; + +export async function setupTanstackStartWorkersDeploy( + projectDir: string, + packageManager: PackageManager, +): Promise { + const webAppDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(webAppDir))) return; + + await addPackageDependency({ + devDependencies: ["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 --env-interface Env", + }; + await fs.writeJson(pkgPath, pkg, { spaces: 2 }); + } + + const viteConfigPath = path.join(webAppDir, "vite.config.ts"); + if (!(await fs.pathExists(viteConfigPath))) return; + + const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath); + if (!sourceFile) return; + + const defineCall = sourceFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((expr) => { + const expression = expr.getExpression(); + return ( + Node.isIdentifier(expression) && expression.getText() === "defineConfig" + ); + }) as CallExpression | undefined; + + if (!defineCall) return; + + const configObj = defineCall.getArguments()[0] as + | ObjectLiteralExpression + | undefined; + if (!configObj) return; + + const pluginsArray = ensureArrayProperty(configObj, "plugins"); + + const tanstackPluginIndex = pluginsArray + .getElements() + .findIndex((el) => el.getText().includes("tanstackStart(")); + + const tanstackPluginText = 'tanstackStart({ target: "cloudflare-module" })'; + + if (tanstackPluginIndex === -1) { + pluginsArray.addElement(tanstackPluginText); + } else { + pluginsArray + .getElements() + [tanstackPluginIndex].replaceWithText(tanstackPluginText); + } + + await tsProject.save(); +} diff --git a/apps/cli/src/prompts/web-deploy.ts b/apps/cli/src/prompts/web-deploy.ts index 9b4e8fd..8d31ce0 100644 --- a/apps/cli/src/prompts/web-deploy.ts +++ b/apps/cli/src/prompts/web-deploy.ts @@ -3,15 +3,6 @@ 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; @@ -38,18 +29,10 @@ export async function getDeploymentChoice( deployment?: WebDeploy, _runtime?: Runtime, _backend?: Backend, - frontend: Frontend[] = [], + _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", @@ -74,15 +57,12 @@ export async function getDeploymentChoice( } export async function getDeploymentToAdd( - frontend: Frontend[], + _frontend: Frontend[], existingDeployment?: WebDeploy, ): Promise { const options: DeploymentOption[] = []; - if ( - frontend.some((f) => WORKERS_COMPATIBLE_FRONTENDS.includes(f)) && - existingDeployment !== "workers" - ) { + if (existingDeployment !== "workers") { const { label, hint } = getDeploymentDisplay("workers"); options.push({ value: "workers", diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 6028a6e..1d04deb 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -451,24 +451,6 @@ 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/react/tanstack-start/wrangler.jsonc.hbs b/apps/cli/templates/deploy/web/react/tanstack-start/wrangler.jsonc.hbs new file mode 100644 index 0000000..0398327 --- /dev/null +++ b/apps/cli/templates/deploy/web/react/tanstack-start/wrangler.jsonc.hbs @@ -0,0 +1,20 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "{{projectName}}", + "main": ".output/server/index.mjs", + "compatibility_date": "2025-07-05", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": ".output/public", + }, + "observability": { + "enabled": true, + }, + // "kv_namespaces": [ + // { + // "binding": "CACHE", + // "id": "", + // }, + // ], + } + \ 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 7a1d535..bea4be6 100644 --- a/apps/cli/templates/frontend/react/web-base/_gitignore +++ b/apps/cli/templates/frontend/react/web-base/_gitignore @@ -19,6 +19,8 @@ .vinxi .output .react-router/ +.tanstack/ +.nitro/ # Deployment .vercel diff --git a/apps/web/public/schema.json b/apps/web/public/schema.json index 6ef3489..a36d901 100644 --- a/apps/web/public/schema.json +++ b/apps/web/public/schema.json @@ -1,174 +1,143 @@ { - "$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", - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema reference for validation" - }, - "version": { - "type": "string", - "description": "CLI version used to create this project", - "pattern": "^\\d+\\.\\d+\\.\\d+$" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Timestamp when the project was created" - }, - "database": { - "type": "string", - "enum": [ - "none", - "sqlite", - "postgres", - "mysql", - "mongodb" - ], - "description": "Database type" - }, - "orm": { - "type": "string", - "enum": [ - "drizzle", - "prisma", - "mongoose", - "none" - ], - "description": "ORM type" - }, - "backend": { - "type": "string", - "enum": [ - "hono", - "express", - "fastify", - "next", - "elysia", - "convex", - "none" - ], - "description": "Backend framework" - }, - "runtime": { - "type": "string", - "enum": [ - "bun", - "node", - "workers", - "none" - ], - "description": "Runtime environment (workers only available with hono backend and drizzle orm)" - }, - "frontend": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "native-nativewind", - "native-unistyles", - "svelte", - "solid", - "none" - ] - }, - "description": "Frontend framework" - }, - "addons": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "pwa", - "tauri", - "starlight", - "biome", - "husky", - "turborepo", - "none" - ] - }, - "description": "Additional addons" - }, - "examples": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "todo", - "ai", - "none" - ] - }, - "description": "Example templates to include" - }, - "auth": { - "type": "boolean", - "description": "Whether authentication is enabled" - }, - "packageManager": { - "type": "string", - "enum": [ - "npm", - "pnpm", - "bun" - ], - "description": "Package manager" - }, - "dbSetup": { - "type": "string", - "enum": [ - "turso", - "neon", - "prisma-postgres", - "mongodb-atlas", - "supabase", - "d1", - "none" - ], - "description": "Database hosting setup" - }, - "api": { - "type": "string", - "enum": [ - "trpc", - "orpc", - "none" - ], - "description": "API type" - }, - "webDeploy": { - "type": "string", - "enum": [ - "workers", - "none" - ], - "description": "Web deployment" - } - }, - "required": [ - "version", - "createdAt", - "database", - "orm", - "backend", - "runtime", - "frontend", - "addons", - "examples", - "auth", - "packageManager", - "dbSetup", - "api", - "webDeploy" - ], - "additionalProperties": false -} \ No newline at end of file + "$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", + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for validation" + }, + "version": { + "type": "string", + "description": "CLI version used to create this project", + "pattern": "^\\d+\\.\\d+\\.\\d+$" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the project was created" + }, + "database": { + "type": "string", + "enum": ["none", "sqlite", "postgres", "mysql", "mongodb"], + "description": "Database type" + }, + "orm": { + "type": "string", + "enum": ["drizzle", "prisma", "mongoose", "none"], + "description": "ORM type" + }, + "backend": { + "type": "string", + "enum": [ + "hono", + "express", + "fastify", + "next", + "elysia", + "convex", + "none" + ], + "description": "Backend framework" + }, + "runtime": { + "type": "string", + "enum": ["bun", "node", "workers", "none"], + "description": "Runtime environment (workers only available with hono backend and drizzle orm)" + }, + "frontend": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "native-nativewind", + "native-unistyles", + "svelte", + "solid", + "none" + ] + }, + "description": "Frontend framework" + }, + "addons": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "pwa", + "tauri", + "starlight", + "biome", + "husky", + "turborepo", + "none" + ] + }, + "description": "Additional addons" + }, + "examples": { + "type": "array", + "items": { + "type": "string", + "enum": ["todo", "ai", "none"] + }, + "description": "Example templates to include" + }, + "auth": { + "type": "boolean", + "description": "Whether authentication is enabled" + }, + "packageManager": { + "type": "string", + "enum": ["npm", "pnpm", "bun"], + "description": "Package manager" + }, + "dbSetup": { + "type": "string", + "enum": [ + "turso", + "neon", + "prisma-postgres", + "mongodb-atlas", + "supabase", + "d1", + "none" + ], + "description": "Database hosting setup" + }, + "api": { + "type": "string", + "enum": ["trpc", "orpc", "none"], + "description": "API type" + }, + "webDeploy": { + "type": "string", + "enum": ["workers", "none"], + "description": "Web deployment" + } + }, + "required": [ + "version", + "createdAt", + "database", + "orm", + "backend", + "runtime", + "frontend", + "addons", + "examples", + "auth", + "packageManager", + "dbSetup", + "api", + "webDeploy" + ], + "additionalProperties": false +} diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index e591b10..2f76136 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -802,28 +802,6 @@ 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)", - }); - } } }