add workers support for tanstack start (#369)

This commit is contained in:
Aman Varshney
2025-07-06 16:32:13 +05:30
committed by GitHub
parent 79479e01f5
commit 0ae1347e9d
11 changed files with 252 additions and 254 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
add workers support for tanstack start

View File

@@ -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 = { const config: ProjectConfig = {
projectName: detectedConfig.projectName || path.basename(projectDir), projectName: detectedConfig.projectName || path.basename(projectDir),
projectDir, projectDir,

View File

@@ -851,6 +851,7 @@ export async function setupDeploymentTemplates(
const templateMap: Record<string, string> = { const templateMap: Record<string, string> = {
"tanstack-router": "react/tanstack-router", "tanstack-router": "react/tanstack-router",
"tanstack-start": "react/tanstack-start",
"react-router": "react/react-router", "react-router": "react/react-router",
solid: "solid", solid: "solid",
next: "react/next", next: "react/next",

View File

@@ -4,6 +4,7 @@ import type { PackageManager, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps"; import { addPackageDependency } from "../../utils/add-package-deps";
import { setupNuxtWorkersDeploy } from "./workers-nuxt-setup"; import { setupNuxtWorkersDeploy } from "./workers-nuxt-setup";
import { setupSvelteWorkersDeploy } from "./workers-svelte-setup"; import { setupSvelteWorkersDeploy } from "./workers-svelte-setup";
import { setupTanstackStartWorkersDeploy } from "./workers-tanstack-start-setup";
import { setupWorkersVitePlugin } from "./workers-vite-setup"; import { setupWorkersVitePlugin } from "./workers-vite-setup";
export async function setupWebDeploy(config: ProjectConfig): Promise<void> { export async function setupWebDeploy(config: ProjectConfig): Promise<void> {
@@ -18,6 +19,7 @@ export async function setupWebDeploy(config: ProjectConfig): Promise<void> {
const isNuxt = frontend.includes("nuxt"); const isNuxt = frontend.includes("nuxt");
const isSvelte = frontend.includes("svelte"); const isSvelte = frontend.includes("svelte");
const isTanstackRouter = frontend.includes("tanstack-router"); const isTanstackRouter = frontend.includes("tanstack-router");
const isTanstackStart = frontend.includes("tanstack-start");
const isReactRouter = frontend.includes("react-router"); const isReactRouter = frontend.includes("react-router");
const isSolid = frontend.includes("solid"); const isSolid = frontend.includes("solid");
@@ -27,6 +29,8 @@ export async function setupWebDeploy(config: ProjectConfig): Promise<void> {
await setupNuxtWorkersDeploy(projectDir, packageManager); await setupNuxtWorkersDeploy(projectDir, packageManager);
} else if (isSvelte) { } else if (isSvelte) {
await setupSvelteWorkersDeploy(projectDir, packageManager); await setupSvelteWorkersDeploy(projectDir, packageManager);
} else if (isTanstackStart) {
await setupTanstackStartWorkersDeploy(projectDir, packageManager);
} else if (isTanstackRouter || isReactRouter || isSolid) { } else if (isTanstackRouter || isReactRouter || isSolid) {
await setupWorkersWebDeploy(projectDir, packageManager); await setupWorkersWebDeploy(projectDir, packageManager);
} }

View File

@@ -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<void> {
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();
}

View File

@@ -3,15 +3,6 @@ import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants"; import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend, Runtime, WebDeploy } from "../types"; import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
const WORKERS_COMPATIBLE_FRONTENDS: Frontend[] = [
"tanstack-router",
"react-router",
"solid",
"next",
"nuxt",
"svelte",
];
type DeploymentOption = { type DeploymentOption = {
value: WebDeploy; value: WebDeploy;
label: string; label: string;
@@ -38,18 +29,10 @@ export async function getDeploymentChoice(
deployment?: WebDeploy, deployment?: WebDeploy,
_runtime?: Runtime, _runtime?: Runtime,
_backend?: Backend, _backend?: Backend,
frontend: Frontend[] = [], _frontend: Frontend[] = [],
): Promise<WebDeploy> { ): Promise<WebDeploy> {
if (deployment !== undefined) return deployment; if (deployment !== undefined) return deployment;
const hasCompatibleFrontend = frontend.some((f) =>
WORKERS_COMPATIBLE_FRONTENDS.includes(f),
);
if (!hasCompatibleFrontend) {
return "none";
}
const options: DeploymentOption[] = [ const options: DeploymentOption[] = [
{ {
value: "workers", value: "workers",
@@ -74,15 +57,12 @@ export async function getDeploymentChoice(
} }
export async function getDeploymentToAdd( export async function getDeploymentToAdd(
frontend: Frontend[], _frontend: Frontend[],
existingDeployment?: WebDeploy, existingDeployment?: WebDeploy,
): Promise<WebDeploy> { ): Promise<WebDeploy> {
const options: DeploymentOption[] = []; const options: DeploymentOption[] = [];
if ( if (existingDeployment !== "workers") {
frontend.some((f) => WORKERS_COMPATIBLE_FRONTENDS.includes(f)) &&
existingDeployment !== "workers"
) {
const { label, hint } = getDeploymentDisplay("workers"); const { label, hint } = getDeploymentDisplay("workers");
options.push({ options.push({
value: "workers", value: "workers",

View File

@@ -451,24 +451,6 @@ export function processAndValidateFlags(
process.exit(1); 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; return config;
} }

View File

@@ -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": "<Your KV ID>",
// },
// ],
}

View File

@@ -19,6 +19,8 @@
.vinxi .vinxi
.output .output
.react-router/ .react-router/
.tanstack/
.nitro/
# Deployment # Deployment
.vercel .vercel

View File

@@ -1,174 +1,143 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://better-t-stack.dev/schema.json", "$id": "https://better-t-stack.dev/schema.json",
"title": "Better-T-Stack Configuration", "title": "Better-T-Stack Configuration",
"description": "Configuration file for Better-T-Stack projects", "description": "Configuration file for Better-T-Stack projects",
"type": "object", "type": "object",
"properties": { "properties": {
"$schema": { "$schema": {
"type": "string", "type": "string",
"description": "JSON Schema reference for validation" "description": "JSON Schema reference for validation"
}, },
"version": { "version": {
"type": "string", "type": "string",
"description": "CLI version used to create this project", "description": "CLI version used to create this project",
"pattern": "^\\d+\\.\\d+\\.\\d+$" "pattern": "^\\d+\\.\\d+\\.\\d+$"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
"description": "Timestamp when the project was created" "description": "Timestamp when the project was created"
}, },
"database": { "database": {
"type": "string", "type": "string",
"enum": [ "enum": ["none", "sqlite", "postgres", "mysql", "mongodb"],
"none", "description": "Database type"
"sqlite", },
"postgres", "orm": {
"mysql", "type": "string",
"mongodb" "enum": ["drizzle", "prisma", "mongoose", "none"],
], "description": "ORM type"
"description": "Database type" },
}, "backend": {
"orm": { "type": "string",
"type": "string", "enum": [
"enum": [ "hono",
"drizzle", "express",
"prisma", "fastify",
"mongoose", "next",
"none" "elysia",
], "convex",
"description": "ORM type" "none"
}, ],
"backend": { "description": "Backend framework"
"type": "string", },
"enum": [ "runtime": {
"hono", "type": "string",
"express", "enum": ["bun", "node", "workers", "none"],
"fastify", "description": "Runtime environment (workers only available with hono backend and drizzle orm)"
"next", },
"elysia", "frontend": {
"convex", "type": "array",
"none" "items": {
], "type": "string",
"description": "Backend framework" "enum": [
}, "tanstack-router",
"runtime": { "react-router",
"type": "string", "tanstack-start",
"enum": [ "next",
"bun", "nuxt",
"node", "native-nativewind",
"workers", "native-unistyles",
"none" "svelte",
], "solid",
"description": "Runtime environment (workers only available with hono backend and drizzle orm)" "none"
}, ]
"frontend": { },
"type": "array", "description": "Frontend framework"
"items": { },
"type": "string", "addons": {
"enum": [ "type": "array",
"tanstack-router", "items": {
"react-router", "type": "string",
"tanstack-start", "enum": [
"next", "pwa",
"nuxt", "tauri",
"native-nativewind", "starlight",
"native-unistyles", "biome",
"svelte", "husky",
"solid", "turborepo",
"none" "none"
] ]
}, },
"description": "Frontend framework" "description": "Additional addons"
}, },
"addons": { "examples": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"enum": [ "enum": ["todo", "ai", "none"]
"pwa", },
"tauri", "description": "Example templates to include"
"starlight", },
"biome", "auth": {
"husky", "type": "boolean",
"turborepo", "description": "Whether authentication is enabled"
"none" },
] "packageManager": {
}, "type": "string",
"description": "Additional addons" "enum": ["npm", "pnpm", "bun"],
}, "description": "Package manager"
"examples": { },
"type": "array", "dbSetup": {
"items": { "type": "string",
"type": "string", "enum": [
"enum": [ "turso",
"todo", "neon",
"ai", "prisma-postgres",
"none" "mongodb-atlas",
] "supabase",
}, "d1",
"description": "Example templates to include" "none"
}, ],
"auth": { "description": "Database hosting setup"
"type": "boolean", },
"description": "Whether authentication is enabled" "api": {
}, "type": "string",
"packageManager": { "enum": ["trpc", "orpc", "none"],
"type": "string", "description": "API type"
"enum": [ },
"npm", "webDeploy": {
"pnpm", "type": "string",
"bun" "enum": ["workers", "none"],
], "description": "Web deployment"
"description": "Package manager" }
}, },
"dbSetup": { "required": [
"type": "string", "version",
"enum": [ "createdAt",
"turso", "database",
"neon", "orm",
"prisma-postgres", "backend",
"mongodb-atlas", "runtime",
"supabase", "frontend",
"d1", "addons",
"none" "examples",
], "auth",
"description": "Database hosting setup" "packageManager",
}, "dbSetup",
"api": { "api",
"type": "string", "webDeploy"
"enum": [ ],
"trpc", "additionalProperties": false
"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
}

View File

@@ -802,28 +802,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
if (nextStack.examples.length !== originalExamplesLength) if (nextStack.examples.length !== originalExamplesLength)
changed = true; 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)",
});
}
} }
} }