Skip to content

Commit d267427

Browse files
add cloudflare workers support for all frontends (#366)
1 parent 6499f8c commit d267427

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1212
-158
lines changed

.changeset/fuzzy-falcons-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-better-t-stack": minor
3+
---
4+
5+
add cloudflare workers deployment support for next, solid, tanstack-router, react-router, nuxt

apps/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"picocolors": "^1.1.1",
6565
"posthog-node": "^5.1.1",
6666
"trpc-cli": "^0.9.2",
67+
"ts-morph": "^26.0.0",
6768
"zod": "^3.25.67"
6869
},
6970
"devDependencies": {

apps/cli/src/constants.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
2424
backend: "hono",
2525
runtime: "bun",
2626
api: "trpc",
27+
webDeploy: "none",
2728
};
2829

2930
export const dependencyVersionMap = {
@@ -45,8 +46,8 @@ export const dependencyVersionMap = {
4546

4647
mongoose: "^8.14.0",
4748

48-
"vite-plugin-pwa": "^0.21.2",
49-
"@vite-pwa/assets-generator": "^0.2.6",
49+
"vite-plugin-pwa": "^1.0.1",
50+
"@vite-pwa/assets-generator": "^1.0.0",
5051

5152
"@tauri-apps/cli": "^2.4.0",
5253

@@ -107,13 +108,17 @@ export const dependencyVersionMap = {
107108
"@tanstack/solid-query": "^5.75.0",
108109
"@tanstack/solid-query-devtools": "^5.75.0",
109110

110-
wrangler: "^4.20.0",
111+
wrangler: "^4.23.0",
112+
"@cloudflare/vite-plugin": "^1.9.0",
113+
"@opennextjs/cloudflare": "^1.3.0",
114+
"nitro-cloudflare-dev": "^0.2.2",
115+
"@sveltejs/adapter-cloudflare": "^7.0.4",
111116
} as const;
112117

113118
export type AvailableDependencies = keyof typeof dependencyVersionMap;
114119

115120
export const ADDON_COMPATIBILITY = {
116-
pwa: ["tanstack-router", "react-router", "solid"],
121+
pwa: ["tanstack-router", "react-router", "solid", "next"],
117122
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
118123
biome: [],
119124
husky: [],

apps/cli/src/helpers/project-generation/add-addons.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function exitWithError(message: string): never {
1818
}
1919

2020
export async function addAddonsToProject(
21-
input: AddInput & { addons: Addons[] },
21+
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
2222
): Promise<void> {
2323
try {
2424
const projectDir = input.projectDir || process.cwd();
@@ -55,6 +55,7 @@ export async function addAddonsToProject(
5555
install: input.install || false,
5656
dbSetup: detectedConfig.dbSetup || "none",
5757
api: detectedConfig.api || "none",
58+
webDeploy: detectedConfig.webDeploy || "none",
5859
};
5960

6061
for (const addon of input.addons) {
@@ -88,7 +89,7 @@ export async function addAddonsToProject(
8889
projectDir,
8990
packageManager: config.packageManager,
9091
});
91-
} else {
92+
} else if (!input.suppressInstallMessage) {
9293
log.info(
9394
pc.yellow(
9495
`Run ${pc.bold(
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import path from "node:path";
2+
import { cancel, log } from "@clack/prompts";
3+
import pc from "picocolors";
4+
import type { AddInput, ProjectConfig, WebDeploy } from "../../types";
5+
import { updateBtsConfig } from "../../utils/bts-config";
6+
import { setupWebDeploy } from "../setup/web-deploy-setup";
7+
import {
8+
detectProjectConfig,
9+
isBetterTStackProject,
10+
} from "./detect-project-config";
11+
import { installDependencies } from "./install-dependencies";
12+
import { setupDeploymentTemplates } from "./template-manager";
13+
14+
function exitWithError(message: string): never {
15+
cancel(pc.red(message));
16+
process.exit(1);
17+
}
18+
19+
export async function addDeploymentToProject(
20+
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
21+
): Promise<void> {
22+
try {
23+
const projectDir = input.projectDir || process.cwd();
24+
25+
const isBetterTStack = await isBetterTStackProject(projectDir);
26+
if (!isBetterTStack) {
27+
exitWithError(
28+
"This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.",
29+
);
30+
}
31+
32+
const detectedConfig = await detectProjectConfig(projectDir);
33+
if (!detectedConfig) {
34+
exitWithError(
35+
"Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.",
36+
);
37+
}
38+
39+
if (detectedConfig.webDeploy === input.webDeploy) {
40+
exitWithError(
41+
`${input.webDeploy} deployment is already configured for this project.`,
42+
);
43+
}
44+
45+
if (input.webDeploy === "workers") {
46+
const compatibleFrontends = [
47+
"tanstack-router",
48+
"react-router",
49+
"solid",
50+
"next",
51+
"svelte",
52+
];
53+
const hasCompatible = detectedConfig.frontend?.some((f) =>
54+
compatibleFrontends.includes(f),
55+
);
56+
if (!hasCompatible) {
57+
exitWithError(
58+
"Cloudflare Workers deployment requires a compatible web frontend (tanstack-router, react-router, solid, next, or svelte).",
59+
);
60+
}
61+
}
62+
63+
const config: ProjectConfig = {
64+
projectName: detectedConfig.projectName || path.basename(projectDir),
65+
projectDir,
66+
relativePath: ".",
67+
database: detectedConfig.database || "none",
68+
orm: detectedConfig.orm || "none",
69+
backend: detectedConfig.backend || "none",
70+
runtime: detectedConfig.runtime || "none",
71+
frontend: detectedConfig.frontend || [],
72+
addons: detectedConfig.addons || [],
73+
examples: detectedConfig.examples || [],
74+
auth: detectedConfig.auth || false,
75+
git: false,
76+
packageManager:
77+
input.packageManager || detectedConfig.packageManager || "npm",
78+
install: input.install || false,
79+
dbSetup: detectedConfig.dbSetup || "none",
80+
api: detectedConfig.api || "none",
81+
webDeploy: input.webDeploy,
82+
};
83+
84+
log.info(
85+
pc.green(
86+
`Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`,
87+
),
88+
);
89+
90+
await setupDeploymentTemplates(projectDir, config);
91+
await setupWebDeploy(config);
92+
93+
await updateBtsConfig(projectDir, { webDeploy: input.webDeploy });
94+
95+
if (config.install) {
96+
await installDependencies({
97+
projectDir,
98+
packageManager: config.packageManager,
99+
});
100+
} else if (!input.suppressInstallMessage) {
101+
log.info(
102+
pc.yellow(
103+
`Run ${pc.bold(
104+
`${config.packageManager} install`,
105+
)} to install dependencies`,
106+
),
107+
);
108+
}
109+
} catch (error) {
110+
const message = error instanceof Error ? error.message : String(error);
111+
exitWithError(`Error adding deployment: ${message}`);
112+
}
113+
}

apps/cli/src/helpers/project-generation/command-handlers.ts

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DEFAULT_CONFIG } from "../../constants";
66
import { getAddonsToAdd } from "../../prompts/addons";
77
import { gatherConfig } from "../../prompts/config-prompts";
88
import { getProjectName } from "../../prompts/project-name";
9+
import { getDeploymentToAdd } from "../../prompts/web-deploy";
910
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
1011
import { trackProjectCreation } from "../../utils/analytics";
1112
import { displayConfig } from "../../utils/display-config";
@@ -17,8 +18,10 @@ import {
1718
import { renderTitle } from "../../utils/render-title";
1819
import { getProvidedFlags, processAndValidateFlags } from "../../validation";
1920
import { addAddonsToProject } from "./add-addons";
21+
import { addDeploymentToProject } from "./add-deployment";
2022
import { createProject } from "./create-project";
2123
import { detectProjectConfig } from "./detect-project-config";
24+
import { installDependencies } from "./install-dependencies";
2225

2326
export async function createProjectHandler(
2427
input: CreateInput & { projectName?: string },
@@ -135,45 +138,84 @@ export async function createProjectHandler(
135138

136139
export async function addAddonsHandler(input: AddInput): Promise<void> {
137140
try {
138-
if (!input.addons || input.addons.length === 0) {
139-
const projectDir = input.projectDir || process.cwd();
140-
const detectedConfig = await detectProjectConfig(projectDir);
141-
142-
if (!detectedConfig) {
143-
cancel(
144-
pc.red(
145-
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
146-
),
147-
);
148-
process.exit(1);
149-
}
141+
const projectDir = input.projectDir || process.cwd();
142+
const detectedConfig = await detectProjectConfig(projectDir);
143+
144+
if (!detectedConfig) {
145+
cancel(
146+
pc.red(
147+
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
148+
),
149+
);
150+
process.exit(1);
151+
}
150152

153+
if (!input.addons || input.addons.length === 0) {
151154
const addonsPrompt = await getAddonsToAdd(
152155
detectedConfig.frontend || [],
153156
detectedConfig.addons || [],
154157
);
155158

156-
if (addonsPrompt.length === 0) {
157-
outro(
158-
pc.yellow(
159-
"No addons to add or all compatible addons are already present.",
160-
),
161-
);
162-
return;
159+
if (addonsPrompt.length > 0) {
160+
input.addons = addonsPrompt;
163161
}
162+
}
163+
164+
if (!input.webDeploy) {
165+
const deploymentPrompt = await getDeploymentToAdd(
166+
detectedConfig.frontend || [],
167+
detectedConfig.webDeploy,
168+
);
164169

165-
input.addons = addonsPrompt;
170+
if (deploymentPrompt !== "none") {
171+
input.webDeploy = deploymentPrompt;
172+
}
166173
}
167174

168-
if (!input.addons || input.addons.length === 0) {
169-
outro(pc.yellow("No addons specified to add."));
175+
const packageManager =
176+
input.packageManager || detectedConfig.packageManager || "npm";
177+
178+
let somethingAdded = false;
179+
180+
if (input.addons && input.addons.length > 0) {
181+
await addAddonsToProject({
182+
...input,
183+
install: false,
184+
suppressInstallMessage: true,
185+
addons: input.addons,
186+
});
187+
somethingAdded = true;
188+
}
189+
190+
if (input.webDeploy && input.webDeploy !== "none") {
191+
await addDeploymentToProject({
192+
...input,
193+
install: false,
194+
suppressInstallMessage: true,
195+
webDeploy: input.webDeploy,
196+
});
197+
somethingAdded = true;
198+
}
199+
200+
if (!somethingAdded) {
201+
outro(pc.yellow("No addons or deployment configurations to add."));
170202
return;
171203
}
172204

173-
await addAddonsToProject({
174-
...input,
175-
addons: input.addons,
176-
});
205+
if (input.install) {
206+
await installDependencies({
207+
projectDir,
208+
packageManager,
209+
});
210+
} else {
211+
log.info(
212+
pc.yellow(
213+
`Run ${pc.bold(`${packageManager} install`)} to install dependencies`,
214+
),
215+
);
216+
}
217+
218+
outro(pc.green("Add command completed successfully!"));
177219
} catch (error) {
178220
console.error(error);
179221
process.exit(1);

apps/cli/src/helpers/project-generation/create-project.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
generateCloudflareWorkerTypes,
1414
setupRuntime,
1515
} from "../setup/runtime-setup";
16+
import { setupWebDeploy } from "../setup/web-deploy-setup";
1617
import { createReadme } from "./create-readme";
1718
import { setupEnvironmentVariables } from "./env-setup";
1819
import { initializeGit } from "./git";
@@ -26,6 +27,7 @@ import {
2627
setupAuthTemplate,
2728
setupBackendFramework,
2829
setupDbOrmTemplates,
30+
setupDeploymentTemplates,
2931
setupExamplesTemplate,
3032
setupFrontendTemplates,
3133
} from "./template-manager";
@@ -49,6 +51,8 @@ export async function createProject(options: ProjectConfig) {
4951
}
5052
await setupAddonsTemplate(projectDir, options);
5153

54+
await setupDeploymentTemplates(projectDir, options);
55+
5256
await setupApi(options);
5357

5458
if (!isConvex) {
@@ -70,6 +74,8 @@ export async function createProject(options: ProjectConfig) {
7074

7175
await handleExtras(projectDir, options);
7276

77+
await setupWebDeploy(options);
78+
7379
await setupEnvironmentVariables(options);
7480
await updatePackageConfigurations(projectDir, options);
7581
await createReadme(projectDir, options);

apps/cli/src/helpers/project-generation/detect-project-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function detectProjectConfig(
2323
packageManager: btsConfig.packageManager,
2424
dbSetup: btsConfig.dbSetup,
2525
api: btsConfig.api,
26+
webDeploy: btsConfig.webDeploy,
2627
};
2728
}
2829

apps/cli/src/helpers/project-generation/git.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ export async function initializeGit(
3030
}
3131

3232
await $({ cwd: projectDir })`git add -A`;
33-
await $({ cwd: projectDir })`git commit -m ${"Initial commit"}`;
33+
await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
3434
}

apps/cli/src/helpers/project-generation/post-installation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ function getTauriInstructions(runCmd?: string): string {
287287
function getPwaInstructions(): string {
288288
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
289289
"NOTE:",
290-
)} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
290+
)} There is a known compatibility issue between VitePWA \nand React Router v7.See: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
291291
}
292292

293293
function getStarlightInstructions(runCmd?: string): string {

0 commit comments

Comments
 (0)