create-better-t-stack 2.33.8 → 2.33.9-canary.20c8e952

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/index.d.ts +17 -4
  4. package/dist/index.js +1 -1
  5. package/dist/{src-jDxvJPRx.js → src-CpB-qAME.js} +1473 -582
  6. package/package.json +7 -4
  7. package/templates/addons/biome/biome.json.hbs +1 -0
  8. package/templates/addons/ruler/.ruler/mcp.json.hbs +1 -1
  9. package/templates/addons/ultracite/biome.json.hbs +1 -0
  10. package/templates/auth/server/base/src/lib/auth.ts.hbs +37 -4
  11. package/templates/backend/server/server-base/_gitignore +1 -0
  12. package/templates/backend/server/server-base/tsconfig.json.hbs +1 -1
  13. package/templates/base/_gitignore +2 -0
  14. package/templates/deploy/alchemy/alchemy.run.ts.hbs +200 -0
  15. package/templates/deploy/alchemy/env.d.ts.hbs +20 -0
  16. package/templates/deploy/{web → wrangler/web}/nuxt/wrangler.jsonc.hbs +1 -1
  17. package/templates/deploy/{web → wrangler/web}/react/next/wrangler.jsonc.hbs +1 -1
  18. package/templates/deploy/{web → wrangler/web}/react/react-router/wrangler.jsonc.hbs +1 -1
  19. package/templates/deploy/{web → wrangler/web}/react/tanstack-router/wrangler.jsonc.hbs +1 -1
  20. package/templates/deploy/{web → wrangler/web}/react/tanstack-start/wrangler.jsonc.hbs +1 -1
  21. package/templates/deploy/{web → wrangler/web}/solid/wrangler.jsonc.hbs +1 -1
  22. package/templates/deploy/{web → wrangler/web}/svelte/wrangler.jsonc.hbs +1 -1
  23. package/templates/frontend/nuxt/_gitignore +3 -0
  24. package/templates/frontend/nuxt/tsconfig.json.hbs +4 -4
  25. package/templates/frontend/react/web-base/_gitignore +1 -0
  26. package/templates/frontend/react/web-base/src/components/header.tsx.hbs +0 -1
  27. package/templates/frontend/solid/_gitignore +1 -0
  28. package/templates/frontend/solid/package.json.hbs +0 -1
  29. package/templates/frontend/svelte/_gitignore +1 -0
  30. package/templates/frontend/svelte/package.json.hbs +11 -13
  31. /package/templates/{runtime/workers/apps → deploy/wrangler}/server/wrangler.jsonc.hbs +0 -0
  32. /package/templates/deploy/{web → wrangler/web}/react/next/open-next.config.ts +0 -0
@@ -10,9 +10,10 @@ import { fileURLToPath } from "node:url";
10
10
  import gradient from "gradient-string";
11
11
  import * as JSONC from "jsonc-parser";
12
12
  import { $, execa } from "execa";
13
- import { globby } from "globby";
13
+ import { glob } from "tinyglobby";
14
14
  import handlebars from "handlebars";
15
15
  import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
16
+ import { Biome } from "@biomejs/js-api/nodejs";
16
17
  import os from "node:os";
17
18
 
18
19
  //#region src/utils/get-package-manager.ts
@@ -28,9 +29,8 @@ const getUserPkgManager = () => {
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const distPath = path.dirname(__filename);
30
31
  const PKG_ROOT = path.join(distPath, "../");
31
- const DEFAULT_CONFIG = {
32
+ const DEFAULT_CONFIG_BASE = {
32
33
  projectName: "my-better-t-app",
33
- projectDir: path.resolve(process.cwd(), "my-better-t-app"),
34
34
  relativePath: "my-better-t-app",
35
35
  frontend: ["tanstack-router"],
36
36
  database: "sqlite",
@@ -39,14 +39,25 @@ const DEFAULT_CONFIG = {
39
39
  addons: ["turborepo"],
40
40
  examples: [],
41
41
  git: true,
42
- packageManager: getUserPkgManager(),
43
42
  install: true,
44
43
  dbSetup: "none",
45
44
  backend: "hono",
46
45
  runtime: "bun",
47
46
  api: "trpc",
48
- webDeploy: "none"
47
+ webDeploy: "none",
48
+ serverDeploy: "none"
49
49
  };
50
+ function getDefaultConfig() {
51
+ return {
52
+ ...DEFAULT_CONFIG_BASE,
53
+ projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
54
+ packageManager: getUserPkgManager(),
55
+ frontend: [...DEFAULT_CONFIG_BASE.frontend],
56
+ addons: [...DEFAULT_CONFIG_BASE.addons],
57
+ examples: [...DEFAULT_CONFIG_BASE.examples]
58
+ };
59
+ }
60
+ const DEFAULT_CONFIG = getDefaultConfig();
50
61
  const dependencyVersionMap = {
51
62
  "better-auth": "^1.3.4",
52
63
  "@better-auth/expo": "^1.3.4",
@@ -103,18 +114,24 @@ const dependencyVersionMap = {
103
114
  "convex-svelte": "^0.0.11",
104
115
  "convex-nuxt": "0.1.5",
105
116
  "convex-vue": "^0.1.5",
106
- "@tanstack/svelte-query": "^5.74.4",
117
+ "@tanstack/svelte-query": "^5.85.3",
118
+ "@tanstack/svelte-query-devtools": "^5.85.3",
107
119
  "@tanstack/vue-query-devtools": "^5.83.0",
108
120
  "@tanstack/vue-query": "^5.83.0",
109
121
  "@tanstack/react-query-devtools": "^5.80.5",
110
122
  "@tanstack/react-query": "^5.80.5",
111
123
  "@tanstack/solid-query": "^5.75.0",
112
124
  "@tanstack/solid-query-devtools": "^5.75.0",
125
+ "@tanstack/solid-router-devtools": "^1.131.25",
113
126
  wrangler: "^4.23.0",
114
127
  "@cloudflare/vite-plugin": "^1.9.0",
115
128
  "@opennextjs/cloudflare": "^1.3.0",
116
129
  "nitro-cloudflare-dev": "^0.2.2",
117
- "@sveltejs/adapter-cloudflare": "^7.0.4"
130
+ "@sveltejs/adapter-cloudflare": "^7.2.1",
131
+ "@cloudflare/workers-types": "^4.20250813.0",
132
+ alchemy: "^0.62.1",
133
+ nitropack: "^2.12.4",
134
+ dotenv: "^17.2.1"
118
135
  };
119
136
  const ADDON_COMPATIBILITY = {
120
137
  pwa: [
@@ -128,7 +145,8 @@ const ADDON_COMPATIBILITY = {
128
145
  "react-router",
129
146
  "nuxt",
130
147
  "svelte",
131
- "solid"
148
+ "solid",
149
+ "next"
132
150
  ],
133
151
  biome: [],
134
152
  husky: [],
@@ -233,7 +251,16 @@ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(
233
251
  ];
234
252
  return !invalidChars.some((char) => name.includes(char));
235
253
  }, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
236
- const WebDeploySchema = z.enum(["workers", "none"]).describe("Web deployment");
254
+ const WebDeploySchema = z.enum([
255
+ "wrangler",
256
+ "alchemy",
257
+ "none"
258
+ ]).describe("Web deployment");
259
+ const ServerDeploySchema = z.enum([
260
+ "wrangler",
261
+ "alchemy",
262
+ "none"
263
+ ]).describe("Server deployment");
237
264
  const DirectoryConflictSchema = z.enum([
238
265
  "merge",
239
266
  "overwrite",
@@ -544,6 +571,23 @@ function isExampleAIAllowed(backend, frontends = []) {
544
571
  function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
545
572
  if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
546
573
  }
574
+ function validateServerDeployRequiresBackend(serverDeploy, backend) {
575
+ if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
576
+ }
577
+ function validateAddonsAgainstFrontends(addons = [], frontends = []) {
578
+ for (const addon of addons) {
579
+ if (addon === "none") continue;
580
+ const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
581
+ if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
582
+ }
583
+ }
584
+ function validateExamplesCompatibility(examples, backend, database, frontend) {
585
+ const examplesArr = examples ?? [];
586
+ if (examplesArr.length === 0 || examplesArr.includes("none")) return;
587
+ if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
588
+ if (examplesArr.includes("ai") && backend === "elysia") exitWithError("The 'ai' example is not compatible with the Elysia backend.");
589
+ if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
590
+ }
547
591
 
548
592
  //#endregion
549
593
  //#region src/prompts/api.ts
@@ -1004,16 +1048,97 @@ async function getRuntimeChoice(runtime, backend) {
1004
1048
  return response;
1005
1049
  }
1006
1050
 
1051
+ //#endregion
1052
+ //#region src/prompts/server-deploy.ts
1053
+ function getDeploymentDisplay$1(deployment) {
1054
+ if (deployment === "wrangler") return {
1055
+ label: "Wrangler",
1056
+ hint: "Deploy to Cloudflare Workers using Wrangler"
1057
+ };
1058
+ if (deployment === "alchemy") return {
1059
+ label: "Alchemy",
1060
+ hint: "Deploy to Cloudflare Workers using Alchemy"
1061
+ };
1062
+ return {
1063
+ label: deployment,
1064
+ hint: `Add ${deployment} deployment`
1065
+ };
1066
+ }
1067
+ async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
1068
+ if (deployment !== void 0) return deployment;
1069
+ if (backend === "none" || backend === "convex") return "none";
1070
+ const options = [];
1071
+ if (runtime === "workers") ["alchemy", "wrangler"].forEach((deploy) => {
1072
+ const { label, hint } = getDeploymentDisplay$1(deploy);
1073
+ options.unshift({
1074
+ value: deploy,
1075
+ label,
1076
+ hint
1077
+ });
1078
+ });
1079
+ else options.push({
1080
+ value: "none",
1081
+ label: "None",
1082
+ hint: "Manual setup"
1083
+ });
1084
+ const response = await select({
1085
+ message: "Select server deployment",
1086
+ options,
1087
+ initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
1088
+ });
1089
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1090
+ return response;
1091
+ }
1092
+ async function getServerDeploymentToAdd(runtime, existingDeployment) {
1093
+ const options = [];
1094
+ if (runtime === "workers") {
1095
+ if (existingDeployment !== "wrangler") {
1096
+ const { label, hint } = getDeploymentDisplay$1("wrangler");
1097
+ options.push({
1098
+ value: "wrangler",
1099
+ label,
1100
+ hint
1101
+ });
1102
+ }
1103
+ if (existingDeployment !== "alchemy") {
1104
+ const { label, hint } = getDeploymentDisplay$1("alchemy");
1105
+ options.push({
1106
+ value: "alchemy",
1107
+ label,
1108
+ hint
1109
+ });
1110
+ }
1111
+ }
1112
+ if (existingDeployment && existingDeployment !== "none") return "none";
1113
+ if (options.length > 0) options.push({
1114
+ value: "none",
1115
+ label: "None",
1116
+ hint: "Skip deployment setup"
1117
+ });
1118
+ if (options.length === 0) return "none";
1119
+ const response = await select({
1120
+ message: "Select server deployment",
1121
+ options,
1122
+ initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
1123
+ });
1124
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1125
+ return response;
1126
+ }
1127
+
1007
1128
  //#endregion
1008
1129
  //#region src/prompts/web-deploy.ts
1009
1130
  function hasWebFrontend(frontends) {
1010
1131
  return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
1011
1132
  }
1012
1133
  function getDeploymentDisplay(deployment) {
1013
- if (deployment === "workers") return {
1014
- label: "Cloudflare Workers",
1134
+ if (deployment === "wrangler") return {
1135
+ label: "Wrangler",
1015
1136
  hint: "Deploy to Cloudflare Workers using Wrangler"
1016
1137
  };
1138
+ if (deployment === "alchemy") return {
1139
+ label: "Alchemy",
1140
+ hint: "Deploy to Cloudflare Workers using Alchemy"
1141
+ };
1017
1142
  return {
1018
1143
  label: deployment,
1019
1144
  hint: `Add ${deployment} deployment`
@@ -1022,15 +1147,18 @@ function getDeploymentDisplay(deployment) {
1022
1147
  async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
1023
1148
  if (deployment !== void 0) return deployment;
1024
1149
  if (!hasWebFrontend(frontend)) return "none";
1025
- const options = [{
1026
- value: "workers",
1027
- label: "Cloudflare Workers",
1028
- hint: "Deploy to Cloudflare Workers using Wrangler"
1029
- }, {
1030
- value: "none",
1031
- label: "None",
1032
- hint: "Manual setup"
1033
- }];
1150
+ const options = [
1151
+ "wrangler",
1152
+ "alchemy",
1153
+ "none"
1154
+ ].map((deploy) => {
1155
+ const { label, hint } = getDeploymentDisplay(deploy);
1156
+ return {
1157
+ value: deploy,
1158
+ label,
1159
+ hint
1160
+ };
1161
+ });
1034
1162
  const response = await select({
1035
1163
  message: "Select web deployment",
1036
1164
  options,
@@ -1042,10 +1170,18 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
1042
1170
  async function getDeploymentToAdd(frontend, existingDeployment) {
1043
1171
  if (!hasWebFrontend(frontend)) return "none";
1044
1172
  const options = [];
1045
- if (existingDeployment !== "workers") {
1046
- const { label, hint } = getDeploymentDisplay("workers");
1173
+ if (existingDeployment !== "wrangler") {
1174
+ const { label, hint } = getDeploymentDisplay("wrangler");
1175
+ options.push({
1176
+ value: "wrangler",
1177
+ label,
1178
+ hint
1179
+ });
1180
+ }
1181
+ if (existingDeployment !== "alchemy") {
1182
+ const { label, hint } = getDeploymentDisplay("alchemy");
1047
1183
  options.push({
1048
- value: "workers",
1184
+ value: "alchemy",
1049
1185
  label,
1050
1186
  hint
1051
1187
  });
@@ -1081,6 +1217,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1081
1217
  examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
1082
1218
  dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
1083
1219
  webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
1220
+ serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
1084
1221
  git: () => getGitChoice(flags.git),
1085
1222
  packageManager: () => getPackageManagerChoice(flags.packageManager),
1086
1223
  install: () => getinstallChoice(flags.install)
@@ -1120,7 +1257,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1120
1257
  install: result.install,
1121
1258
  dbSetup: result.dbSetup,
1122
1259
  api: result.api,
1123
- webDeploy: result.webDeploy
1260
+ webDeploy: result.webDeploy,
1261
+ serverDeploy: result.serverDeploy
1124
1262
  };
1125
1263
  }
1126
1264
 
@@ -1152,7 +1290,7 @@ async function getProjectName(initialName) {
1152
1290
  let projectPath = "";
1153
1291
  let defaultName = DEFAULT_CONFIG.projectName;
1154
1292
  let counter = 1;
1155
- while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0) {
1293
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
1156
1294
  defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
1157
1295
  counter++;
1158
1296
  }
@@ -1199,7 +1337,7 @@ const getLatestCLIVersion = () => {
1199
1337
  */
1200
1338
  function isTelemetryEnabled() {
1201
1339
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
1202
- const BTS_TELEMETRY = "1";
1340
+ const BTS_TELEMETRY = "0";
1203
1341
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
1204
1342
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
1205
1343
  return true;
@@ -1207,11 +1345,16 @@ function isTelemetryEnabled() {
1207
1345
 
1208
1346
  //#endregion
1209
1347
  //#region src/utils/analytics.ts
1210
- const POSTHOG_API_KEY = "phc_8ZUxEwwfKMajJLvxz1daGd931dYbQrwKNficBmsdIrs";
1211
- const POSTHOG_HOST = "https://us.i.posthog.com";
1348
+ const POSTHOG_API_KEY = "random";
1349
+ const POSTHOG_HOST = "random";
1350
+ function generateSessionId() {
1351
+ const rand = Math.random().toString(36).slice(2);
1352
+ const now = Date.now().toString(36);
1353
+ return `cli_${now}${rand}`;
1354
+ }
1212
1355
  async function trackProjectCreation(config, disableAnalytics = false) {
1213
1356
  if (!isTelemetryEnabled() || disableAnalytics) return;
1214
- const sessionId = `cli_${crypto.randomUUID().replace(/-/g, "")}`;
1357
+ const sessionId = generateSessionId();
1215
1358
  const { projectName, projectDir, relativePath,...safeConfig } = config;
1216
1359
  const payload = {
1217
1360
  api_key: POSTHOG_API_KEY,
@@ -1219,8 +1362,8 @@ async function trackProjectCreation(config, disableAnalytics = false) {
1219
1362
  properties: {
1220
1363
  ...safeConfig,
1221
1364
  cli_version: getLatestCLIVersion(),
1222
- node_version: process.version,
1223
- platform: process.platform,
1365
+ node_version: typeof process !== "undefined" ? process.version : "",
1366
+ platform: typeof process !== "undefined" ? process.platform : "",
1224
1367
  $ip: null
1225
1368
  },
1226
1369
  distinct_id: sessionId
@@ -1274,6 +1417,7 @@ function displayConfig(config) {
1274
1417
  }
1275
1418
  if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
1276
1419
  if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
1420
+ if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
1277
1421
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
1278
1422
  return configDisplay.join("\n");
1279
1423
  }
@@ -1296,6 +1440,7 @@ function generateReproducibleCommand(config) {
1296
1440
  else flags.push("--examples none");
1297
1441
  flags.push(`--db-setup ${config.dbSetup}`);
1298
1442
  flags.push(`--web-deploy ${config.webDeploy}`);
1443
+ flags.push(`--server-deploy ${config.serverDeploy}`);
1299
1444
  flags.push(config.git ? "--git" : "--no-git");
1300
1445
  flags.push(`--package-manager ${config.packageManager}`);
1301
1446
  flags.push(config.install ? "--install" : "--no-install");
@@ -1313,8 +1458,8 @@ function generateReproducibleCommand(config) {
1313
1458
  async function handleDirectoryConflict(currentPathInput, silent = false) {
1314
1459
  while (true) {
1315
1460
  const resolvedPath = path.resolve(process.cwd(), currentPathInput);
1316
- const dirExists = fs.pathExistsSync(resolvedPath);
1317
- const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
1461
+ const dirExists = await fs.pathExists(resolvedPath);
1462
+ const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
1318
1463
  if (!dirIsNotEmpty) return {
1319
1464
  finalPathInput: currentPathInput,
1320
1465
  shouldClearDirectory: false
@@ -1440,7 +1585,7 @@ const renderTitle = () => {
1440
1585
  };
1441
1586
 
1442
1587
  //#endregion
1443
- //#region src/validation.ts
1588
+ //#region src/utils/config-processing.ts
1444
1589
  function processArrayOption(options) {
1445
1590
  if (!options || options.length === 0) return [];
1446
1591
  if (options.includes("none")) return [];
@@ -1451,22 +1596,10 @@ function deriveProjectName(projectName, projectDirectory) {
1451
1596
  if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
1452
1597
  return "";
1453
1598
  }
1454
- function validateProjectName(name) {
1455
- const result = ProjectNameSchema.safeParse(name);
1456
- if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1457
- }
1458
- function processAndValidateFlags(options, providedFlags, projectName) {
1599
+ function processFlags(options, projectName) {
1459
1600
  const config = {};
1460
- if (options.api) {
1461
- config.api = options.api;
1462
- if (options.api === "none") {
1463
- if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1464
- }
1465
- }
1601
+ if (options.api) config.api = options.api;
1466
1602
  if (options.backend) config.backend = options.backend;
1467
- if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
1468
- if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1469
- }
1470
1603
  if (options.database) config.database = options.database;
1471
1604
  if (options.orm) config.orm = options.orm;
1472
1605
  if (options.auth !== void 0) config.auth = options.auth;
@@ -1476,70 +1609,182 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1476
1609
  if (options.dbSetup) config.dbSetup = options.dbSetup;
1477
1610
  if (options.packageManager) config.packageManager = options.packageManager;
1478
1611
  if (options.webDeploy) config.webDeploy = options.webDeploy;
1612
+ if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
1479
1613
  const derivedName = deriveProjectName(projectName, options.projectDirectory);
1480
- if (derivedName) {
1481
- const nameToValidate = projectName ? path.basename(projectName) : derivedName;
1482
- validateProjectName(nameToValidate);
1483
- config.projectName = projectName || derivedName;
1484
- }
1485
- if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
1486
- if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
1487
- config.frontend = [];
1488
- } else {
1489
- const validOptions = processArrayOption(options.frontend);
1490
- ensureSingleWebAndNative(validOptions);
1491
- config.frontend = validOptions;
1492
- }
1493
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
1494
- if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
1495
- if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
1496
- config.addons = [];
1497
- } else config.addons = processArrayOption(options.addons);
1498
- if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
1499
- if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
1500
- config.examples = [];
1501
- } else {
1502
- config.examples = processArrayOption(options.examples);
1503
- if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
1614
+ if (derivedName) config.projectName = projectName || derivedName;
1615
+ if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
1616
+ if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
1617
+ if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
1618
+ return config;
1619
+ }
1620
+ function getProvidedFlags(options) {
1621
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1622
+ }
1623
+ function validateNoneExclusivity(options, optionName) {
1624
+ if (!options || options.length === 0) return;
1625
+ if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
1626
+ }
1627
+ function validateArrayOptions(options) {
1628
+ validateNoneExclusivity(options.frontend, "frontend options");
1629
+ validateNoneExclusivity(options.addons, "addons");
1630
+ validateNoneExclusivity(options.examples, "examples");
1631
+ }
1632
+
1633
+ //#endregion
1634
+ //#region src/utils/config-validation.ts
1635
+ function validateDatabaseOrmAuth(cfg, flags) {
1636
+ const db = cfg.database;
1637
+ const orm = cfg.orm;
1638
+ const has = (k) => flags ? flags.has(k) : true;
1639
+ if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1640
+ if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1641
+ if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1642
+ if (has("database") && has("orm") && db && db !== "none" && orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1643
+ if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1644
+ if (has("auth") && has("database") && cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1645
+ if (cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1646
+ if (orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1647
+ }
1648
+ function validateDatabaseSetup(config, providedFlags) {
1649
+ const { dbSetup, database, runtime } = config;
1650
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1651
+ const setupValidations = {
1652
+ turso: {
1653
+ database: "sqlite",
1654
+ errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
1655
+ },
1656
+ neon: {
1657
+ database: "postgres",
1658
+ errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1659
+ },
1660
+ "prisma-postgres": {
1661
+ database: "postgres",
1662
+ errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1663
+ },
1664
+ "mongodb-atlas": {
1665
+ database: "mongodb",
1666
+ errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
1667
+ },
1668
+ supabase: {
1669
+ database: "postgres",
1670
+ errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1671
+ },
1672
+ d1: {
1673
+ database: "sqlite",
1674
+ runtime: "workers",
1675
+ errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
1676
+ },
1677
+ docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
1678
+ none: { errorMessage: "" }
1679
+ };
1680
+ if (dbSetup && dbSetup !== "none") {
1681
+ const validation = setupValidations[dbSetup];
1682
+ if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
1683
+ if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
1684
+ if (dbSetup === "docker") {
1685
+ if (database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1686
+ if (runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1687
+ }
1688
+ }
1689
+ }
1690
+ function validateBackendConstraints(config, providedFlags, options) {
1691
+ const { backend } = config;
1692
+ if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none") {
1693
+ if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.");
1504
1694
  }
1505
- if (config.backend === "convex" || config.backend === "none") {
1506
- const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
1507
- if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1508
- if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1695
+ if (backend === "convex" || backend === "none") {
1696
+ const incompatibleFlags = incompatibleFlagsForBackend(backend, providedFlags, options);
1697
+ if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1698
+ if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1509
1699
  const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
1510
1700
  if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1511
1701
  }
1512
1702
  coerceBackendPresets(config);
1513
1703
  }
1514
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1515
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1516
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1517
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1518
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1519
- if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1520
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1521
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") exitWithError("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1522
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1523
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1524
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1525
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1526
- if (config.dbSetup === "d1") {
1527
- if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
1528
- if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1529
- }
1530
- if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
1531
- if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1532
- }
1704
+ }
1705
+ function validateFrontendConstraints(config, providedFlags) {
1706
+ const { frontend } = config;
1707
+ if (frontend && frontend.length > 0) {
1708
+ ensureSingleWebAndNative(frontend);
1709
+ if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
1710
+ const hasWebFrontendFlag = frontend.some((f) => isWebFrontend(f));
1711
+ validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
1712
+ }
1713
+ }
1714
+ function validateApiConstraints(config, options) {
1715
+ if (config.api === "none") {
1716
+ if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1533
1717
  }
1534
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1535
- if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1718
+ }
1719
+ function validateFullConfig(config, providedFlags, options) {
1720
+ validateDatabaseOrmAuth(config, providedFlags);
1721
+ validateDatabaseSetup(config, providedFlags);
1722
+ validateBackendConstraints(config, providedFlags, options);
1723
+ validateFrontendConstraints(config, providedFlags);
1724
+ validateApiConstraints(config, options);
1725
+ validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
1536
1726
  validateWorkersCompatibility(providedFlags, options, config);
1537
- const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
1538
- validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
1727
+ if (config.addons && config.addons.length > 0) {
1728
+ validateAddonsAgainstFrontends(config.addons, config.frontend);
1729
+ config.addons = [...new Set(config.addons)];
1730
+ }
1731
+ validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
1732
+ }
1733
+ function validateConfigForProgrammaticUse(config) {
1734
+ try {
1735
+ validateDatabaseOrmAuth(config);
1736
+ if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
1737
+ validateApiFrontendCompatibility(config.api, config.frontend);
1738
+ if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
1739
+ validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
1740
+ } catch (error) {
1741
+ if (error instanceof Error) throw error;
1742
+ throw new Error(String(error));
1743
+ }
1744
+ }
1745
+
1746
+ //#endregion
1747
+ //#region src/utils/project-name-validation.ts
1748
+ function validateProjectName(name) {
1749
+ const result = ProjectNameSchema.safeParse(name);
1750
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1751
+ }
1752
+ function validateProjectNameThrow(name) {
1753
+ const result = ProjectNameSchema.safeParse(name);
1754
+ if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
1755
+ }
1756
+ function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
1757
+ const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
1758
+ if (!derivedName) return "";
1759
+ const nameToValidate = projectName ? path.basename(projectName) : derivedName;
1760
+ if (throwOnError) validateProjectNameThrow(nameToValidate);
1761
+ else validateProjectName(nameToValidate);
1762
+ return projectName || derivedName;
1763
+ }
1764
+
1765
+ //#endregion
1766
+ //#region src/validation.ts
1767
+ function processAndValidateFlags(options, providedFlags, projectName) {
1768
+ try {
1769
+ validateArrayOptions(options);
1770
+ } catch (error) {
1771
+ exitWithError(error instanceof Error ? error.message : String(error));
1772
+ }
1773
+ const config = processFlags(options, projectName);
1774
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
1775
+ if (validatedProjectName) config.projectName = validatedProjectName;
1776
+ validateFullConfig(config, providedFlags, options);
1539
1777
  return config;
1540
1778
  }
1541
- function getProvidedFlags(options) {
1542
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1779
+ function processProvidedFlagsWithoutValidation(options, projectName) {
1780
+ const config = processFlags(options, projectName);
1781
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
1782
+ if (validatedProjectName) config.projectName = validatedProjectName;
1783
+ return config;
1784
+ }
1785
+ function validateConfigCompatibility(config, providedFlags, options) {
1786
+ if (options && providedFlags) validateFullConfig(config, providedFlags, options);
1787
+ else validateConfigForProgrammaticUse(config);
1543
1788
  }
1544
1789
 
1545
1790
  //#endregion
@@ -1560,7 +1805,8 @@ async function writeBtsConfig(projectConfig) {
1560
1805
  packageManager: projectConfig.packageManager,
1561
1806
  dbSetup: projectConfig.dbSetup,
1562
1807
  api: projectConfig.api,
1563
- webDeploy: projectConfig.webDeploy
1808
+ webDeploy: projectConfig.webDeploy,
1809
+ serverDeploy: projectConfig.serverDeploy
1564
1810
  };
1565
1811
  const baseContent = {
1566
1812
  $schema: "https://r2.better-t-stack.dev/schema.json",
@@ -1577,7 +1823,8 @@ async function writeBtsConfig(projectConfig) {
1577
1823
  packageManager: btsConfig.packageManager,
1578
1824
  dbSetup: btsConfig.dbSetup,
1579
1825
  api: btsConfig.api,
1580
- webDeploy: btsConfig.webDeploy
1826
+ webDeploy: btsConfig.webDeploy,
1827
+ serverDeploy: btsConfig.serverDeploy
1581
1828
  };
1582
1829
  let configContent = JSON.stringify(baseContent);
1583
1830
  const formatResult = JSONC.format(configContent, void 0, {
@@ -1670,7 +1917,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
1670
1917
  }
1671
1918
 
1672
1919
  //#endregion
1673
- //#region src/helpers/setup/fumadocs-setup.ts
1920
+ //#region src/helpers/addons/fumadocs-setup.ts
1674
1921
  const TEMPLATES = {
1675
1922
  "next-mdx": {
1676
1923
  label: "Next.js: Fumadocs MDX",
@@ -1757,9 +2004,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
1757
2004
  handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
1758
2005
 
1759
2006
  //#endregion
1760
- //#region src/helpers/project-generation/template-manager.ts
2007
+ //#region src/helpers/core/template-manager.ts
1761
2008
  async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
1762
- const sourceFiles = await globby(sourcePattern, {
2009
+ const sourceFiles = await glob(sourcePattern, {
1763
2010
  cwd: baseSourceDir,
1764
2011
  dot: true,
1765
2012
  onlyFiles: true,
@@ -2073,10 +2320,6 @@ async function handleExtras(projectDir, context) {
2073
2320
  const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
2074
2321
  if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
2075
2322
  }
2076
- if (context.runtime === "workers") {
2077
- const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers");
2078
- if (await fs.pathExists(runtimeWorkersDir)) await processAndCopyFiles("**/*", runtimeWorkersDir, projectDir, context, false);
2079
- }
2080
2323
  }
2081
2324
  async function setupDockerComposeTemplates(projectDir, context) {
2082
2325
  if (context.dbSetup !== "docker" || context.database === "none") return;
@@ -2085,29 +2328,58 @@ async function setupDockerComposeTemplates(projectDir, context) {
2085
2328
  if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
2086
2329
  }
2087
2330
  async function setupDeploymentTemplates(projectDir, context) {
2088
- if (context.webDeploy === "none") return;
2089
- if (context.webDeploy === "workers") {
2331
+ if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
2332
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2333
+ if (await fs.pathExists(alchemyTemplateSrc)) {
2334
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
2335
+ const serverAppDir = path.join(projectDir, "apps/server");
2336
+ if (await fs.pathExists(serverAppDir)) await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2337
+ }
2338
+ } else {
2339
+ if (context.webDeploy === "alchemy") {
2340
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2341
+ const webAppDir = path.join(projectDir, "apps/web");
2342
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2343
+ }
2344
+ if (context.serverDeploy === "alchemy") {
2345
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2346
+ const serverAppDir = path.join(projectDir, "apps/server");
2347
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2348
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2349
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2350
+ }
2351
+ }
2352
+ }
2353
+ if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
2090
2354
  const webAppDir = path.join(projectDir, "apps/web");
2091
- if (!await fs.pathExists(webAppDir)) return;
2092
- const frontends = context.frontend;
2093
- const templateMap = {
2094
- "tanstack-router": "react/tanstack-router",
2095
- "tanstack-start": "react/tanstack-start",
2096
- "react-router": "react/react-router",
2097
- solid: "solid",
2098
- next: "react/next",
2099
- nuxt: "nuxt",
2100
- svelte: "svelte"
2101
- };
2102
- for (const f of frontends) if (templateMap[f]) {
2103
- const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/web/${templateMap[f]}`);
2104
- if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2355
+ if (await fs.pathExists(webAppDir)) {
2356
+ const frontends = context.frontend;
2357
+ const templateMap = {
2358
+ "tanstack-router": "react/tanstack-router",
2359
+ "tanstack-start": "react/tanstack-start",
2360
+ "react-router": "react/react-router",
2361
+ solid: "solid",
2362
+ next: "react/next",
2363
+ nuxt: "nuxt",
2364
+ svelte: "svelte"
2365
+ };
2366
+ for (const f of frontends) if (templateMap[f]) {
2367
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
2368
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2369
+ }
2370
+ }
2371
+ }
2372
+ if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
2373
+ const serverAppDir = path.join(projectDir, "apps/server");
2374
+ if (await fs.pathExists(serverAppDir)) {
2375
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
2376
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
2105
2377
  }
2106
2378
  }
2107
2379
  }
2108
2380
 
2109
2381
  //#endregion
2110
- //#region src/helpers/setup/ruler-setup.ts
2382
+ //#region src/helpers/addons/ruler-setup.ts
2111
2383
  async function setupVibeRules(config) {
2112
2384
  const { packageManager, projectDir } = config;
2113
2385
  try {
@@ -2189,7 +2461,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2189
2461
  }
2190
2462
 
2191
2463
  //#endregion
2192
- //#region src/helpers/setup/starlight-setup.ts
2464
+ //#region src/helpers/addons/starlight-setup.ts
2193
2465
  async function setupStarlight(config) {
2194
2466
  const { packageManager, projectDir } = config;
2195
2467
  const s = spinner();
@@ -2221,7 +2493,7 @@ async function setupStarlight(config) {
2221
2493
  }
2222
2494
 
2223
2495
  //#endregion
2224
- //#region src/helpers/setup/tauri-setup.ts
2496
+ //#region src/helpers/addons/tauri-setup.ts
2225
2497
  async function setupTauri(config) {
2226
2498
  const { packageManager, frontend, projectDir } = config;
2227
2499
  const s = spinner();
@@ -2258,8 +2530,8 @@ async function setupTauri(config) {
2258
2530
  `--window-title=${path.basename(projectDir)}`,
2259
2531
  `--frontend-dist=${frontendDist}`,
2260
2532
  `--dev-url=${devUrl}`,
2261
- `--before-dev-command=\"${packageManager} run dev\"`,
2262
- `--before-build-command=\"${packageManager} run build\"`
2533
+ `--before-dev-command="${packageManager} run dev"`,
2534
+ `--before-build-command="${packageManager} run build"`
2263
2535
  ];
2264
2536
  const tauriArgsString = tauriArgs.join(" ");
2265
2537
  const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
@@ -2277,7 +2549,7 @@ async function setupTauri(config) {
2277
2549
  }
2278
2550
 
2279
2551
  //#endregion
2280
- //#region src/helpers/setup/ultracite-setup.ts
2552
+ //#region src/helpers/addons/ultracite-setup.ts
2281
2553
  const EDITORS = {
2282
2554
  vscode: {
2283
2555
  label: "VSCode / Cursor / Windsurf",
@@ -2346,7 +2618,7 @@ async function setupUltracite(config, hasHusky) {
2346
2618
  ];
2347
2619
  if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2348
2620
  if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2349
- if (hasHusky) ultraciteArgs.push("--features", "husky", "lint-staged");
2621
+ if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2350
2622
  const ultraciteArgsString = ultraciteArgs.join(" ");
2351
2623
  const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2352
2624
  const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
@@ -2384,7 +2656,7 @@ function ensureArrayProperty(obj, name) {
2384
2656
  }
2385
2657
 
2386
2658
  //#endregion
2387
- //#region src/helpers/setup/vite-pwa-setup.ts
2659
+ //#region src/helpers/addons/vite-pwa-setup.ts
2388
2660
  async function addPwaToViteConfig(viteConfigPath, projectName) {
2389
2661
  const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2390
2662
  if (!sourceFile) throw new Error("vite config not found");
@@ -2418,7 +2690,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
2418
2690
  }
2419
2691
 
2420
2692
  //#endregion
2421
- //#region src/helpers/setup/addons-setup.ts
2693
+ //#region src/helpers/addons/addons-setup.ts
2422
2694
  async function setupAddons(config, isAddCommand = false) {
2423
2695
  const { addons, frontend, projectDir, packageManager } = config;
2424
2696
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
@@ -2552,7 +2824,7 @@ async function setupOxlint(projectDir, packageManager) {
2552
2824
  }
2553
2825
 
2554
2826
  //#endregion
2555
- //#region src/helpers/project-generation/detect-project-config.ts
2827
+ //#region src/helpers/core/detect-project-config.ts
2556
2828
  async function detectProjectConfig(projectDir) {
2557
2829
  try {
2558
2830
  const btsConfig = await readBtsConfig(projectDir);
@@ -2570,7 +2842,8 @@ async function detectProjectConfig(projectDir) {
2570
2842
  packageManager: btsConfig.packageManager,
2571
2843
  dbSetup: btsConfig.dbSetup,
2572
2844
  api: btsConfig.api,
2573
- webDeploy: btsConfig.webDeploy
2845
+ webDeploy: btsConfig.webDeploy,
2846
+ serverDeploy: btsConfig.serverDeploy
2574
2847
  };
2575
2848
  return null;
2576
2849
  } catch (_error) {
@@ -2586,7 +2859,7 @@ async function isBetterTStackProject(projectDir) {
2586
2859
  }
2587
2860
 
2588
2861
  //#endregion
2589
- //#region src/helpers/project-generation/install-dependencies.ts
2862
+ //#region src/helpers/core/install-dependencies.ts
2590
2863
  async function installDependencies({ projectDir, packageManager }) {
2591
2864
  const s = spinner();
2592
2865
  try {
@@ -2603,7 +2876,7 @@ async function installDependencies({ projectDir, packageManager }) {
2603
2876
  }
2604
2877
 
2605
2878
  //#endregion
2606
- //#region src/helpers/project-generation/add-addons.ts
2879
+ //#region src/helpers/core/add-addons.ts
2607
2880
  async function addAddonsToProject(input) {
2608
2881
  try {
2609
2882
  const projectDir = input.projectDir || process.cwd();
@@ -2628,7 +2901,8 @@ async function addAddonsToProject(input) {
2628
2901
  install: input.install || false,
2629
2902
  dbSetup: detectedConfig.dbSetup || "none",
2630
2903
  api: detectedConfig.api || "none",
2631
- webDeploy: detectedConfig.webDeploy || "none"
2904
+ webDeploy: detectedConfig.webDeploy || "none",
2905
+ serverDeploy: detectedConfig.serverDeploy || "none"
2632
2906
  };
2633
2907
  for (const addon of input.addons) {
2634
2908
  const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
@@ -2651,12 +2925,92 @@ async function addAddonsToProject(input) {
2651
2925
  }
2652
2926
 
2653
2927
  //#endregion
2654
- //#region src/helpers/setup/workers-nuxt-setup.ts
2655
- async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2928
+ //#region src/helpers/deployment/server-deploy-setup.ts
2929
+ async function setupServerDeploy(config) {
2930
+ const { serverDeploy, webDeploy, projectDir } = config;
2931
+ const { packageManager } = config;
2932
+ if (serverDeploy === "none") return;
2933
+ if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
2934
+ const serverDir = path.join(projectDir, "apps/server");
2935
+ if (!await fs.pathExists(serverDir)) return;
2936
+ if (serverDeploy === "wrangler") {
2937
+ await setupWorkersServerDeploy(serverDir, packageManager);
2938
+ await generateCloudflareWorkerTypes({
2939
+ serverDir,
2940
+ packageManager
2941
+ });
2942
+ } else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
2943
+ }
2944
+ async function setupWorkersServerDeploy(serverDir, _packageManager) {
2945
+ const packageJsonPath = path.join(serverDir, "package.json");
2946
+ if (!await fs.pathExists(packageJsonPath)) return;
2947
+ const packageJson = await fs.readJson(packageJsonPath);
2948
+ packageJson.scripts = {
2949
+ ...packageJson.scripts,
2950
+ dev: "wrangler dev --port=3000",
2951
+ start: "wrangler dev",
2952
+ deploy: "wrangler deploy",
2953
+ build: "wrangler deploy --dry-run",
2954
+ "cf-typegen": "wrangler types --env-interface CloudflareBindings"
2955
+ };
2956
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2957
+ await addPackageDependency({
2958
+ devDependencies: [
2959
+ "wrangler",
2960
+ "@types/node",
2961
+ "@cloudflare/workers-types"
2962
+ ],
2963
+ projectDir: serverDir
2964
+ });
2965
+ }
2966
+ async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
2967
+ if (!await fs.pathExists(serverDir)) return;
2968
+ const s = spinner();
2969
+ try {
2970
+ s.start("Generating Cloudflare Workers types...");
2971
+ const runCmd = packageManager === "npm" ? "npm" : packageManager;
2972
+ await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
2973
+ s.stop("Cloudflare Workers types generated successfully!");
2974
+ } catch {
2975
+ s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
2976
+ const managerCmd = `${packageManager} run`;
2977
+ log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
2978
+ }
2979
+ }
2980
+ async function setupAlchemyServerDeploy(serverDir, _packageManager) {
2981
+ if (!await fs.pathExists(serverDir)) return;
2982
+ await addPackageDependency({
2983
+ devDependencies: [
2984
+ "alchemy",
2985
+ "wrangler",
2986
+ "@types/node",
2987
+ "@cloudflare/workers-types",
2988
+ "dotenv"
2989
+ ],
2990
+ projectDir: serverDir
2991
+ });
2992
+ const packageJsonPath = path.join(serverDir, "package.json");
2993
+ if (await fs.pathExists(packageJsonPath)) {
2994
+ const packageJson = await fs.readJson(packageJsonPath);
2995
+ packageJson.scripts = {
2996
+ ...packageJson.scripts,
2997
+ dev: "wrangler dev --port=3000",
2998
+ build: "wrangler deploy --dry-run",
2999
+ deploy: "alchemy deploy",
3000
+ destroy: "alchemy destroy",
3001
+ "alchemy:dev": "alchemy dev"
3002
+ };
3003
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3004
+ }
3005
+ }
3006
+
3007
+ //#endregion
3008
+ //#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
3009
+ async function setupNextAlchemyDeploy(projectDir, _packageManager) {
2656
3010
  const webAppDir = path.join(projectDir, "apps/web");
2657
3011
  if (!await fs.pathExists(webAppDir)) return;
2658
3012
  await addPackageDependency({
2659
- devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3013
+ devDependencies: ["alchemy", "dotenv"],
2660
3014
  projectDir: webAppDir
2661
3015
  });
2662
3016
  const pkgPath = path.join(webAppDir, "package.json");
@@ -2664,63 +3018,25 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2664
3018
  const pkg = await fs.readJson(pkgPath);
2665
3019
  pkg.scripts = {
2666
3020
  ...pkg.scripts,
2667
- deploy: `${packageManager} run build && wrangler deploy`,
2668
- "cf-typegen": "wrangler types"
3021
+ deploy: "alchemy deploy",
3022
+ destroy: "alchemy destroy",
3023
+ "alchemy:dev": "alchemy dev"
2669
3024
  };
2670
3025
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
2671
3026
  }
2672
- const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
2673
- if (!await fs.pathExists(nuxtConfigPath)) return;
2674
- const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
2675
- if (!sourceFile) return;
2676
- const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
2677
- const expression = expr.getExpression();
2678
- return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
2679
- });
2680
- if (!defineCall) return;
2681
- const configObj = defineCall.getArguments()[0];
2682
- if (!configObj) return;
2683
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2684
- const compatProp = configObj.getProperty("compatibilityDate");
2685
- if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
2686
- else configObj.addPropertyAssignment({
2687
- name: "compatibilityDate",
2688
- initializer: `'${today}'`
2689
- });
2690
- const nitroInitializer = `{
2691
- preset: "cloudflare_module",
2692
- cloudflare: {
2693
- deployConfig: true,
2694
- nodeCompat: true
2695
- }
2696
- }`;
2697
- const nitroProp = configObj.getProperty("nitro");
2698
- if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
2699
- else configObj.addPropertyAssignment({
2700
- name: "nitro",
2701
- initializer: nitroInitializer
2702
- });
2703
- const modulesProp = configObj.getProperty("modules");
2704
- if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
2705
- const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
2706
- if (arrayExpr) {
2707
- const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
2708
- if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
2709
- }
2710
- } else configObj.addPropertyAssignment({
2711
- name: "modules",
2712
- initializer: "['nitro-cloudflare-dev']"
2713
- });
2714
- await tsProject.save();
2715
3027
  }
2716
3028
 
2717
3029
  //#endregion
2718
- //#region src/helpers/setup/workers-svelte-setup.ts
2719
- async function setupSvelteWorkersDeploy(projectDir, packageManager) {
3030
+ //#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
3031
+ async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
2720
3032
  const webAppDir = path.join(projectDir, "apps/web");
2721
3033
  if (!await fs.pathExists(webAppDir)) return;
2722
3034
  await addPackageDependency({
2723
- devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
3035
+ devDependencies: [
3036
+ "alchemy",
3037
+ "nitro-cloudflare-dev",
3038
+ "dotenv"
3039
+ ],
2724
3040
  projectDir: webAppDir
2725
3041
  });
2726
3042
  const pkgPath = path.join(webAppDir, "package.json");
@@ -2728,7 +3044,520 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
2728
3044
  const pkg = await fs.readJson(pkgPath);
2729
3045
  pkg.scripts = {
2730
3046
  ...pkg.scripts,
2731
- deploy: `${packageManager} run build && wrangler deploy`,
3047
+ deploy: "alchemy deploy",
3048
+ destroy: "alchemy destroy",
3049
+ "alchemy:dev": "alchemy dev"
3050
+ };
3051
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3052
+ }
3053
+ const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
3054
+ if (!await fs.pathExists(nuxtConfigPath)) return;
3055
+ try {
3056
+ const project = new Project({ manipulationSettings: {
3057
+ indentationText: IndentationText.TwoSpaces,
3058
+ quoteKind: QuoteKind.Double
3059
+ } });
3060
+ project.addSourceFileAtPath(nuxtConfigPath);
3061
+ const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
3062
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3063
+ if (!exportAssignment) return;
3064
+ const defineConfigCall = exportAssignment.getExpression();
3065
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
3066
+ let configObject = defineConfigCall.getArguments()[0];
3067
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3068
+ if (Node.isObjectLiteralExpression(configObject)) {
3069
+ if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
3070
+ name: "nitro",
3071
+ initializer: `{
3072
+ preset: "cloudflare_module",
3073
+ cloudflare: {
3074
+ deployConfig: true,
3075
+ nodeCompat: true
3076
+ }
3077
+ }`
3078
+ });
3079
+ const modulesProperty = configObject.getProperty("modules");
3080
+ if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
3081
+ const initializer = modulesProperty.getInitializer();
3082
+ if (Node.isArrayLiteralExpression(initializer)) {
3083
+ const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
3084
+ if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
3085
+ }
3086
+ } else if (!modulesProperty) configObject.addPropertyAssignment({
3087
+ name: "modules",
3088
+ initializer: "[\"nitro-cloudflare-dev\"]"
3089
+ });
3090
+ }
3091
+ await project.save();
3092
+ } catch (error) {
3093
+ console.warn("Failed to update nuxt.config.ts:", error);
3094
+ }
3095
+ }
3096
+
3097
+ //#endregion
3098
+ //#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
3099
+ async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
3100
+ const webAppDir = path.join(projectDir, "apps/web");
3101
+ if (!await fs.pathExists(webAppDir)) return;
3102
+ await addPackageDependency({
3103
+ devDependencies: [
3104
+ "alchemy",
3105
+ "@cloudflare/vite-plugin",
3106
+ "dotenv"
3107
+ ],
3108
+ projectDir: webAppDir
3109
+ });
3110
+ const pkgPath = path.join(webAppDir, "package.json");
3111
+ if (await fs.pathExists(pkgPath)) {
3112
+ const pkg = await fs.readJson(pkgPath);
3113
+ pkg.scripts = {
3114
+ ...pkg.scripts,
3115
+ deploy: "alchemy deploy",
3116
+ destroy: "alchemy destroy",
3117
+ "alchemy:dev": "alchemy dev"
3118
+ };
3119
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3120
+ }
3121
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3122
+ if (await fs.pathExists(viteConfigPath)) try {
3123
+ const project = new Project({ manipulationSettings: {
3124
+ indentationText: IndentationText.TwoSpaces,
3125
+ quoteKind: QuoteKind.Double
3126
+ } });
3127
+ project.addSourceFileAtPath(viteConfigPath);
3128
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3129
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
3130
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3131
+ moduleSpecifier: "alchemy/cloudflare/react-router",
3132
+ defaultImport: "alchemy"
3133
+ });
3134
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3135
+ if (!exportAssignment) return;
3136
+ const defineConfigCall = exportAssignment.getExpression();
3137
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3138
+ let configObject = defineConfigCall.getArguments()[0];
3139
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3140
+ if (Node.isObjectLiteralExpression(configObject)) {
3141
+ const pluginsProperty = configObject.getProperty("plugins");
3142
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3143
+ const initializer = pluginsProperty.getInitializer();
3144
+ if (Node.isArrayLiteralExpression(initializer)) {
3145
+ const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
3146
+ if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
3147
+ }
3148
+ } else if (!pluginsProperty) configObject.addPropertyAssignment({
3149
+ name: "plugins",
3150
+ initializer: "[alchemy()]"
3151
+ });
3152
+ }
3153
+ await project.save();
3154
+ } catch (error) {
3155
+ console.warn("Failed to update vite.config.ts:", error);
3156
+ }
3157
+ const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
3158
+ if (await fs.pathExists(reactRouterConfigPath)) try {
3159
+ const project = new Project({ manipulationSettings: {
3160
+ indentationText: IndentationText.TwoSpaces,
3161
+ quoteKind: QuoteKind.Double
3162
+ } });
3163
+ project.addSourceFileAtPath(reactRouterConfigPath);
3164
+ const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
3165
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3166
+ if (!exportAssignment) return;
3167
+ const configExpression = exportAssignment.getExpression();
3168
+ let configObject;
3169
+ if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
3170
+ else if (Node.isSatisfiesExpression(configExpression)) {
3171
+ const expression = configExpression.getExpression();
3172
+ if (Node.isObjectLiteralExpression(expression)) configObject = expression;
3173
+ }
3174
+ if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
3175
+ const futureProperty = configObject.getProperty("future");
3176
+ if (!futureProperty) configObject.addPropertyAssignment({
3177
+ name: "future",
3178
+ initializer: `{
3179
+ unstable_viteEnvironmentApi: true,
3180
+ }`
3181
+ });
3182
+ else if (Node.isPropertyAssignment(futureProperty)) {
3183
+ const futureInitializer = futureProperty.getInitializer();
3184
+ if (Node.isObjectLiteralExpression(futureInitializer)) {
3185
+ const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
3186
+ if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
3187
+ name: "unstable_viteEnvironmentApi",
3188
+ initializer: "true"
3189
+ });
3190
+ else if (Node.isPropertyAssignment(viteEnvApiProp)) {
3191
+ const value = viteEnvApiProp.getInitializer()?.getText();
3192
+ if (value === "false") viteEnvApiProp.setInitializer("true");
3193
+ }
3194
+ }
3195
+ }
3196
+ await project.save();
3197
+ } catch (error) {
3198
+ console.warn("Failed to update react-router.config.ts:", error);
3199
+ }
3200
+ }
3201
+
3202
+ //#endregion
3203
+ //#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
3204
+ async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
3205
+ const webAppDir = path.join(projectDir, "apps/web");
3206
+ if (!await fs.pathExists(webAppDir)) return;
3207
+ await addPackageDependency({
3208
+ devDependencies: ["alchemy", "dotenv"],
3209
+ projectDir: webAppDir
3210
+ });
3211
+ const pkgPath = path.join(webAppDir, "package.json");
3212
+ if (await fs.pathExists(pkgPath)) {
3213
+ const pkg = await fs.readJson(pkgPath);
3214
+ pkg.scripts = {
3215
+ ...pkg.scripts,
3216
+ deploy: "alchemy deploy",
3217
+ destroy: "alchemy destroy",
3218
+ "alchemy:dev": "alchemy dev"
3219
+ };
3220
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3221
+ }
3222
+ }
3223
+
3224
+ //#endregion
3225
+ //#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
3226
+ async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
3227
+ const webAppDir = path.join(projectDir, "apps/web");
3228
+ if (!await fs.pathExists(webAppDir)) return;
3229
+ await addPackageDependency({
3230
+ devDependencies: [
3231
+ "alchemy",
3232
+ "@sveltejs/adapter-cloudflare",
3233
+ "dotenv"
3234
+ ],
3235
+ projectDir: webAppDir
3236
+ });
3237
+ const pkgPath = path.join(webAppDir, "package.json");
3238
+ if (await fs.pathExists(pkgPath)) {
3239
+ const pkg = await fs.readJson(pkgPath);
3240
+ pkg.scripts = {
3241
+ ...pkg.scripts,
3242
+ deploy: "alchemy deploy",
3243
+ destroy: "alchemy destroy",
3244
+ "alchemy:dev": "alchemy dev"
3245
+ };
3246
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3247
+ }
3248
+ const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
3249
+ if (!await fs.pathExists(svelteConfigPath)) return;
3250
+ try {
3251
+ const project = new Project({ manipulationSettings: {
3252
+ indentationText: IndentationText.TwoSpaces,
3253
+ quoteKind: QuoteKind.Single
3254
+ } });
3255
+ project.addSourceFileAtPath(svelteConfigPath);
3256
+ const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
3257
+ const importDeclarations = sourceFile.getImportDeclarations();
3258
+ const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
3259
+ if (adapterImport) {
3260
+ adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
3261
+ adapterImport.removeDefaultImport();
3262
+ adapterImport.setDefaultImport("alchemy");
3263
+ } else sourceFile.insertImportDeclaration(0, {
3264
+ moduleSpecifier: "alchemy/cloudflare/sveltekit",
3265
+ defaultImport: "alchemy"
3266
+ });
3267
+ const configVariable = sourceFile.getVariableDeclaration("config");
3268
+ if (configVariable) {
3269
+ const initializer = configVariable.getInitializer();
3270
+ if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
3271
+ }
3272
+ await project.save();
3273
+ } catch (error) {
3274
+ console.warn("Failed to update svelte.config.js:", error);
3275
+ }
3276
+ }
3277
+ function updateAdapterInConfig(configObject) {
3278
+ if (!Node.isObjectLiteralExpression(configObject)) return;
3279
+ const kitProperty = configObject.getProperty("kit");
3280
+ if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
3281
+ const kitInitializer = kitProperty.getInitializer();
3282
+ if (Node.isObjectLiteralExpression(kitInitializer)) {
3283
+ const adapterProperty = kitInitializer.getProperty("adapter");
3284
+ if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
3285
+ const initializer = adapterProperty.getInitializer();
3286
+ if (Node.isCallExpression(initializer)) {
3287
+ const expression = initializer.getExpression();
3288
+ if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
3289
+ }
3290
+ }
3291
+ }
3292
+ }
3293
+ }
3294
+
3295
+ //#endregion
3296
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
3297
+ async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
3298
+ const webAppDir = path.join(projectDir, "apps/web");
3299
+ if (!await fs.pathExists(webAppDir)) return;
3300
+ await addPackageDependency({
3301
+ devDependencies: ["alchemy", "dotenv"],
3302
+ projectDir: webAppDir
3303
+ });
3304
+ const pkgPath = path.join(webAppDir, "package.json");
3305
+ if (await fs.pathExists(pkgPath)) {
3306
+ const pkg = await fs.readJson(pkgPath);
3307
+ pkg.scripts = {
3308
+ ...pkg.scripts,
3309
+ deploy: "alchemy deploy",
3310
+ destroy: "alchemy destroy",
3311
+ "alchemy:dev": "alchemy dev"
3312
+ };
3313
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3314
+ }
3315
+ }
3316
+
3317
+ //#endregion
3318
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
3319
+ async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
3320
+ const webAppDir = path.join(projectDir, "apps/web");
3321
+ if (!await fs.pathExists(webAppDir)) return;
3322
+ await addPackageDependency({
3323
+ devDependencies: [
3324
+ "alchemy",
3325
+ "nitropack",
3326
+ "dotenv"
3327
+ ],
3328
+ projectDir: webAppDir
3329
+ });
3330
+ const pkgPath = path.join(webAppDir, "package.json");
3331
+ if (await fs.pathExists(pkgPath)) {
3332
+ const pkg = await fs.readJson(pkgPath);
3333
+ pkg.scripts = {
3334
+ ...pkg.scripts,
3335
+ deploy: "alchemy deploy",
3336
+ destroy: "alchemy destroy",
3337
+ "alchemy:dev": "alchemy dev"
3338
+ };
3339
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3340
+ }
3341
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3342
+ if (await fs.pathExists(viteConfigPath)) try {
3343
+ const project = new Project({ manipulationSettings: {
3344
+ indentationText: IndentationText.TwoSpaces,
3345
+ quoteKind: QuoteKind.Double
3346
+ } });
3347
+ project.addSourceFileAtPath(viteConfigPath);
3348
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3349
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
3350
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3351
+ moduleSpecifier: "alchemy/cloudflare/tanstack-start",
3352
+ defaultImport: "alchemy"
3353
+ });
3354
+ else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
3355
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3356
+ if (!exportAssignment) return;
3357
+ const defineConfigCall = exportAssignment.getExpression();
3358
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3359
+ let configObject = defineConfigCall.getArguments()[0];
3360
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3361
+ if (Node.isObjectLiteralExpression(configObject)) {
3362
+ if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
3363
+ name: "build",
3364
+ initializer: `{
3365
+ target: "esnext",
3366
+ rollupOptions: {
3367
+ external: ["node:async_hooks", "cloudflare:workers"],
3368
+ },
3369
+ }`
3370
+ });
3371
+ const pluginsProperty = configObject.getProperty("plugins");
3372
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3373
+ const initializer = pluginsProperty.getInitializer();
3374
+ if (Node.isArrayLiteralExpression(initializer)) {
3375
+ const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
3376
+ if (!hasShim) initializer.addElement("alchemy()");
3377
+ const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
3378
+ tanstackElements.forEach((element) => {
3379
+ if (Node.isCallExpression(element)) {
3380
+ const args = element.getArguments();
3381
+ if (args.length === 0) element.addArgument(`{
3382
+ target: "cloudflare-module",
3383
+ customViteReactPlugin: true,
3384
+ }`);
3385
+ else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
3386
+ const configObj = args[0];
3387
+ if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
3388
+ name: "target",
3389
+ initializer: "\"cloudflare-module\""
3390
+ });
3391
+ if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
3392
+ name: "customViteReactPlugin",
3393
+ initializer: "true"
3394
+ });
3395
+ }
3396
+ }
3397
+ });
3398
+ }
3399
+ } else configObject.addPropertyAssignment({
3400
+ name: "plugins",
3401
+ initializer: "[alchemy()]"
3402
+ });
3403
+ }
3404
+ await project.save();
3405
+ } catch (error) {
3406
+ console.warn("Failed to update vite.config.ts:", error);
3407
+ }
3408
+ const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
3409
+ const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
3410
+
3411
+ export default defineNitroConfig({
3412
+ preset: "cloudflare-module",
3413
+ cloudflare: {
3414
+ nodeCompat: true,
3415
+ },
3416
+ });
3417
+ `;
3418
+ await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
3419
+ }
3420
+
3421
+ //#endregion
3422
+ //#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
3423
+ async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
3424
+ await addPackageDependency({
3425
+ devDependencies: ["alchemy", "dotenv"],
3426
+ projectDir
3427
+ });
3428
+ const rootPkgPath = path.join(projectDir, "package.json");
3429
+ if (await fs.pathExists(rootPkgPath)) {
3430
+ const pkg = await fs.readJson(rootPkgPath);
3431
+ pkg.scripts = {
3432
+ ...pkg.scripts,
3433
+ deploy: "alchemy deploy",
3434
+ destroy: "alchemy destroy",
3435
+ "alchemy:dev": "alchemy dev"
3436
+ };
3437
+ await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
3438
+ }
3439
+ const serverDir = path.join(projectDir, "apps/server");
3440
+ if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
3441
+ const frontend = config.frontend;
3442
+ const isNext = frontend.includes("next");
3443
+ const isNuxt = frontend.includes("nuxt");
3444
+ const isSvelte = frontend.includes("svelte");
3445
+ const isTanstackRouter = frontend.includes("tanstack-router");
3446
+ const isTanstackStart = frontend.includes("tanstack-start");
3447
+ const isReactRouter = frontend.includes("react-router");
3448
+ const isSolid = frontend.includes("solid");
3449
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3450
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3451
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3452
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3453
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3454
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3455
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3456
+ }
3457
+
3458
+ //#endregion
3459
+ //#region src/helpers/deployment/workers/workers-next-setup.ts
3460
+ async function setupNextWorkersDeploy(projectDir, _packageManager) {
3461
+ const webAppDir = path.join(projectDir, "apps/web");
3462
+ if (!await fs.pathExists(webAppDir)) return;
3463
+ await addPackageDependency({
3464
+ dependencies: ["@opennextjs/cloudflare"],
3465
+ devDependencies: ["wrangler"],
3466
+ projectDir: webAppDir
3467
+ });
3468
+ const packageJsonPath = path.join(webAppDir, "package.json");
3469
+ if (await fs.pathExists(packageJsonPath)) {
3470
+ const pkg = await fs.readJson(packageJsonPath);
3471
+ pkg.scripts = {
3472
+ ...pkg.scripts,
3473
+ preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
3474
+ deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
3475
+ upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
3476
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
3477
+ };
3478
+ await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
3479
+ }
3480
+ }
3481
+
3482
+ //#endregion
3483
+ //#region src/helpers/deployment/workers/workers-nuxt-setup.ts
3484
+ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
3485
+ const webAppDir = path.join(projectDir, "apps/web");
3486
+ if (!await fs.pathExists(webAppDir)) return;
3487
+ await addPackageDependency({
3488
+ devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3489
+ projectDir: webAppDir
3490
+ });
3491
+ const pkgPath = path.join(webAppDir, "package.json");
3492
+ if (await fs.pathExists(pkgPath)) {
3493
+ const pkg = await fs.readJson(pkgPath);
3494
+ pkg.scripts = {
3495
+ ...pkg.scripts,
3496
+ deploy: `${packageManager} run build && wrangler deploy`,
3497
+ "cf-typegen": "wrangler types"
3498
+ };
3499
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3500
+ }
3501
+ const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
3502
+ if (!await fs.pathExists(nuxtConfigPath)) return;
3503
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
3504
+ if (!sourceFile) return;
3505
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
3506
+ const expression = expr.getExpression();
3507
+ return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
3508
+ });
3509
+ if (!defineCall) return;
3510
+ const configObj = defineCall.getArguments()[0];
3511
+ if (!configObj) return;
3512
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3513
+ const compatProp = configObj.getProperty("compatibilityDate");
3514
+ if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
3515
+ else configObj.addPropertyAssignment({
3516
+ name: "compatibilityDate",
3517
+ initializer: `'${today}'`
3518
+ });
3519
+ const nitroInitializer = `{
3520
+ preset: "cloudflare_module",
3521
+ cloudflare: {
3522
+ deployConfig: true,
3523
+ nodeCompat: true
3524
+ }
3525
+ }`;
3526
+ const nitroProp = configObj.getProperty("nitro");
3527
+ if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
3528
+ else configObj.addPropertyAssignment({
3529
+ name: "nitro",
3530
+ initializer: nitroInitializer
3531
+ });
3532
+ const modulesProp = configObj.getProperty("modules");
3533
+ if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
3534
+ const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
3535
+ if (arrayExpr) {
3536
+ const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
3537
+ if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
3538
+ }
3539
+ } else configObj.addPropertyAssignment({
3540
+ name: "modules",
3541
+ initializer: "['nitro-cloudflare-dev']"
3542
+ });
3543
+ await tsProject.save();
3544
+ }
3545
+
3546
+ //#endregion
3547
+ //#region src/helpers/deployment/workers/workers-svelte-setup.ts
3548
+ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
3549
+ const webAppDir = path.join(projectDir, "apps/web");
3550
+ if (!await fs.pathExists(webAppDir)) return;
3551
+ await addPackageDependency({
3552
+ devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
3553
+ projectDir: webAppDir
3554
+ });
3555
+ const pkgPath = path.join(webAppDir, "package.json");
3556
+ if (await fs.pathExists(pkgPath)) {
3557
+ const pkg = await fs.readJson(pkgPath);
3558
+ pkg.scripts = {
3559
+ ...pkg.scripts,
3560
+ deploy: `${packageManager} run build && wrangler deploy`,
2732
3561
  "cf-typegen": "wrangler types ./src/worker-configuration.d.ts"
2733
3562
  };
2734
3563
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
@@ -2752,7 +3581,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
2752
3581
  }
2753
3582
 
2754
3583
  //#endregion
2755
- //#region src/helpers/setup/workers-tanstack-start-setup.ts
3584
+ //#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
2756
3585
  async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2757
3586
  const webAppDir = path.join(projectDir, "apps/web");
2758
3587
  if (!await fs.pathExists(webAppDir)) return;
@@ -2790,7 +3619,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2790
3619
  }
2791
3620
 
2792
3621
  //#endregion
2793
- //#region src/helpers/setup/workers-vite-setup.ts
3622
+ //#region src/helpers/deployment/workers/workers-vite-setup.ts
2794
3623
  async function setupWorkersVitePlugin(projectDir) {
2795
3624
  const webAppDir = path.join(projectDir, "apps/web");
2796
3625
  const viteConfigPath = path.join(webAppDir, "vite.config.ts");
@@ -2821,12 +3650,16 @@ async function setupWorkersVitePlugin(projectDir) {
2821
3650
  }
2822
3651
 
2823
3652
  //#endregion
2824
- //#region src/helpers/setup/web-deploy-setup.ts
3653
+ //#region src/helpers/deployment/web-deploy-setup.ts
2825
3654
  async function setupWebDeploy(config) {
2826
- const { webDeploy, frontend, projectDir } = config;
3655
+ const { webDeploy, serverDeploy, frontend, projectDir } = config;
2827
3656
  const { packageManager } = config;
2828
3657
  if (webDeploy === "none") return;
2829
- if (webDeploy !== "workers") return;
3658
+ if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
3659
+ if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
3660
+ await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
3661
+ return;
3662
+ }
2830
3663
  const isNext = frontend.includes("next");
2831
3664
  const isNuxt = frontend.includes("nuxt");
2832
3665
  const isSvelte = frontend.includes("svelte");
@@ -2834,11 +3667,21 @@ async function setupWebDeploy(config) {
2834
3667
  const isTanstackStart = frontend.includes("tanstack-start");
2835
3668
  const isReactRouter = frontend.includes("react-router");
2836
3669
  const isSolid = frontend.includes("solid");
2837
- if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
2838
- else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
2839
- else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
2840
- else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
2841
- else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
3670
+ if (webDeploy === "wrangler") {
3671
+ if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
3672
+ else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
3673
+ else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
3674
+ else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
3675
+ else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
3676
+ } else if (webDeploy === "alchemy") {
3677
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3678
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3679
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3680
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3681
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3682
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3683
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3684
+ }
2842
3685
  }
2843
3686
  async function setupWorkersWebDeploy(projectDir, pkgManager) {
2844
3687
  const webAppDir = path.join(projectDir, "apps/web");
@@ -2855,30 +3698,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
2855
3698
  }
2856
3699
  await setupWorkersVitePlugin(projectDir);
2857
3700
  }
2858
- async function setupNextWorkersDeploy(projectDir, _packageManager) {
2859
- const webAppDir = path.join(projectDir, "apps/web");
2860
- if (!await fs.pathExists(webAppDir)) return;
2861
- await addPackageDependency({
2862
- dependencies: ["@opennextjs/cloudflare"],
2863
- devDependencies: ["wrangler"],
2864
- projectDir: webAppDir
2865
- });
2866
- const packageJsonPath = path.join(webAppDir, "package.json");
2867
- if (await fs.pathExists(packageJsonPath)) {
2868
- const pkg = await fs.readJson(packageJsonPath);
2869
- pkg.scripts = {
2870
- ...pkg.scripts,
2871
- preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
2872
- deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
2873
- upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
2874
- "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
2875
- };
2876
- await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
2877
- }
2878
- }
2879
3701
 
2880
3702
  //#endregion
2881
- //#region src/helpers/project-generation/add-deployment.ts
3703
+ //#region src/helpers/core/add-deployment.ts
2882
3704
  async function addDeploymentToProject(input) {
2883
3705
  try {
2884
3706
  const projectDir = input.projectDir || process.cwd();
@@ -2886,7 +3708,8 @@ async function addDeploymentToProject(input) {
2886
3708
  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.");
2887
3709
  const detectedConfig = await detectProjectConfig(projectDir);
2888
3710
  if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
2889
- if (detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} deployment is already configured for this project.`);
3711
+ if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
3712
+ if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
2890
3713
  const config = {
2891
3714
  projectName: detectedConfig.projectName || path.basename(projectDir),
2892
3715
  projectDir,
@@ -2902,225 +3725,80 @@ async function addDeploymentToProject(input) {
2902
3725
  git: false,
2903
3726
  packageManager: input.packageManager || detectedConfig.packageManager || "npm",
2904
3727
  install: input.install || false,
2905
- dbSetup: detectedConfig.dbSetup || "none",
2906
- api: detectedConfig.api || "none",
2907
- webDeploy: input.webDeploy
2908
- };
2909
- log.info(pc.green(`Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`));
2910
- await setupDeploymentTemplates(projectDir, config);
2911
- await setupWebDeploy(config);
2912
- await updateBtsConfig(projectDir, { webDeploy: input.webDeploy });
2913
- if (config.install) await installDependencies({
2914
- projectDir,
2915
- packageManager: config.packageManager
2916
- });
2917
- else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
2918
- } catch (error) {
2919
- const message = error instanceof Error ? error.message : String(error);
2920
- exitWithError(`Error adding deployment: ${message}`);
2921
- }
2922
- }
2923
-
2924
- //#endregion
2925
- //#region src/helpers/setup/api-setup.ts
2926
- async function setupApi(config) {
2927
- const { api, projectName, frontend, backend, packageManager, projectDir } = config;
2928
- const isConvex = backend === "convex";
2929
- const webDir = path.join(projectDir, "apps/web");
2930
- const nativeDir = path.join(projectDir, "apps/native");
2931
- const webDirExists = await fs.pathExists(webDir);
2932
- const nativeDirExists = await fs.pathExists(nativeDir);
2933
- const hasReactWeb = frontend.some((f) => [
2934
- "tanstack-router",
2935
- "react-router",
2936
- "tanstack-start",
2937
- "next"
2938
- ].includes(f));
2939
- const hasNuxtWeb = frontend.includes("nuxt");
2940
- const hasSvelteWeb = frontend.includes("svelte");
2941
- const hasSolidWeb = frontend.includes("solid");
2942
- if (!isConvex && api !== "none") {
2943
- const serverDir = path.join(projectDir, "apps/server");
2944
- const serverDirExists = await fs.pathExists(serverDir);
2945
- if (serverDirExists) {
2946
- if (api === "orpc") await addPackageDependency({
2947
- dependencies: ["@orpc/server", "@orpc/client"],
2948
- projectDir: serverDir
2949
- });
2950
- else if (api === "trpc") {
2951
- await addPackageDependency({
2952
- dependencies: ["@trpc/server", "@trpc/client"],
2953
- projectDir: serverDir
2954
- });
2955
- if (config.backend === "hono") await addPackageDependency({
2956
- dependencies: ["@hono/trpc-server"],
2957
- projectDir: serverDir
2958
- });
2959
- else if (config.backend === "elysia") await addPackageDependency({
2960
- dependencies: ["@elysiajs/trpc"],
2961
- projectDir: serverDir
2962
- });
2963
- }
2964
- }
2965
- if (webDirExists) {
2966
- if (hasReactWeb) {
2967
- if (api === "orpc") await addPackageDependency({
2968
- dependencies: ["@orpc/tanstack-query", "@orpc/client"],
2969
- projectDir: webDir
2970
- });
2971
- else if (api === "trpc") await addPackageDependency({
2972
- dependencies: [
2973
- "@trpc/tanstack-react-query",
2974
- "@trpc/client",
2975
- "@trpc/server"
2976
- ],
2977
- projectDir: webDir
2978
- });
2979
- } else if (hasNuxtWeb) {
2980
- if (api === "orpc") await addPackageDependency({
2981
- dependencies: [
2982
- "@tanstack/vue-query",
2983
- "@tanstack/vue-query-devtools",
2984
- "@orpc/tanstack-query",
2985
- "@orpc/client"
2986
- ],
2987
- projectDir: webDir
2988
- });
2989
- } else if (hasSvelteWeb) {
2990
- if (api === "orpc") await addPackageDependency({
2991
- dependencies: [
2992
- "@orpc/tanstack-query",
2993
- "@orpc/client",
2994
- "@tanstack/svelte-query"
2995
- ],
2996
- projectDir: webDir
2997
- });
2998
- } else if (hasSolidWeb) {
2999
- if (api === "orpc") await addPackageDependency({
3000
- dependencies: [
3001
- "@orpc/tanstack-query",
3002
- "@orpc/client",
3003
- "@tanstack/solid-query"
3004
- ],
3005
- projectDir: webDir
3006
- });
3007
- }
3008
- }
3009
- if (nativeDirExists) {
3010
- if (api === "trpc") await addPackageDependency({
3011
- dependencies: [
3012
- "@trpc/tanstack-react-query",
3013
- "@trpc/client",
3014
- "@trpc/server"
3015
- ],
3016
- projectDir: nativeDir
3017
- });
3018
- else if (api === "orpc") await addPackageDependency({
3019
- dependencies: ["@orpc/tanstack-query", "@orpc/client"],
3020
- projectDir: nativeDir
3021
- });
3022
- }
3023
- }
3024
- const reactBasedFrontends = [
3025
- "react-router",
3026
- "tanstack-router",
3027
- "tanstack-start",
3028
- "next",
3029
- "native-nativewind",
3030
- "native-unistyles"
3031
- ];
3032
- const needsSolidQuery = frontend.includes("solid");
3033
- const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
3034
- if (needsReactQuery && !isConvex) {
3035
- const reactQueryDeps = ["@tanstack/react-query"];
3036
- const reactQueryDevDeps = ["@tanstack/react-query-devtools"];
3037
- const hasReactWeb$1 = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
3038
- const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3039
- if (hasReactWeb$1 && webDirExists) {
3040
- const webPkgJsonPath = path.join(webDir, "package.json");
3041
- if (await fs.pathExists(webPkgJsonPath)) try {
3042
- await addPackageDependency({
3043
- dependencies: reactQueryDeps,
3044
- devDependencies: reactQueryDevDeps,
3045
- projectDir: webDir
3046
- });
3047
- } catch (_error) {}
3048
- }
3049
- if (hasNative && nativeDirExists) {
3050
- const nativePkgJsonPath = path.join(nativeDir, "package.json");
3051
- if (await fs.pathExists(nativePkgJsonPath)) try {
3052
- await addPackageDependency({
3053
- dependencies: reactQueryDeps,
3054
- projectDir: nativeDir
3055
- });
3056
- } catch (_error) {}
3057
- }
3058
- }
3059
- if (needsSolidQuery && !isConvex) {
3060
- const solidQueryDeps = ["@tanstack/solid-query"];
3061
- const solidQueryDevDeps = ["@tanstack/solid-query-devtools"];
3062
- if (webDirExists) {
3063
- const webPkgJsonPath = path.join(webDir, "package.json");
3064
- if (await fs.pathExists(webPkgJsonPath)) try {
3065
- await addPackageDependency({
3066
- dependencies: solidQueryDeps,
3067
- devDependencies: solidQueryDevDeps,
3068
- projectDir: webDir
3069
- });
3070
- } catch (_error) {}
3071
- }
3072
- }
3073
- if (isConvex) {
3074
- if (webDirExists) {
3075
- const webPkgJsonPath = path.join(webDir, "package.json");
3076
- if (await fs.pathExists(webPkgJsonPath)) try {
3077
- const webDepsToAdd = ["convex"];
3078
- if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
3079
- if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
3080
- if (hasNuxtWeb) {
3081
- webDepsToAdd.push("convex-nuxt");
3082
- webDepsToAdd.push("convex-vue");
3083
- }
3084
- await addPackageDependency({
3085
- dependencies: webDepsToAdd,
3086
- projectDir: webDir
3087
- });
3088
- } catch (_error) {}
3089
- }
3090
- if (nativeDirExists) {
3091
- const nativePkgJsonPath = path.join(nativeDir, "package.json");
3092
- if (await fs.pathExists(nativePkgJsonPath)) try {
3093
- await addPackageDependency({
3094
- dependencies: ["convex"],
3095
- projectDir: nativeDir
3096
- });
3097
- } catch (_error) {}
3098
- }
3099
- const backendPackageName = `@${projectName}/backend`;
3100
- const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
3101
- const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => {
3102
- try {
3103
- const pkgJson = await fs.readJson(pkgJsonPath);
3104
- if (!pkgJson.dependencies) pkgJson.dependencies = {};
3105
- if (pkgJson.dependencies[depName] !== depVersion) {
3106
- pkgJson.dependencies[depName] = depVersion;
3107
- await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
3108
- }
3109
- } catch (_error) {}
3728
+ dbSetup: detectedConfig.dbSetup || "none",
3729
+ api: detectedConfig.api || "none",
3730
+ webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
3731
+ serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
3110
3732
  };
3111
- if (webDirExists) {
3112
- const webPkgJsonPath = path.join(webDir, "package.json");
3113
- if (await fs.pathExists(webPkgJsonPath)) await addWorkspaceDepManually(webPkgJsonPath, backendPackageName, backendWorkspaceVersion);
3114
- }
3115
- if (nativeDirExists) {
3116
- const nativePkgJsonPath = path.join(nativeDir, "package.json");
3117
- if (await fs.pathExists(nativePkgJsonPath)) await addWorkspaceDepManually(nativePkgJsonPath, backendPackageName, backendWorkspaceVersion);
3118
- }
3733
+ if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
3734
+ if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
3735
+ await setupDeploymentTemplates(projectDir, config);
3736
+ await setupWebDeploy(config);
3737
+ await setupServerDeploy(config);
3738
+ await updateBtsConfig(projectDir, {
3739
+ webDeploy: input.webDeploy || config.webDeploy,
3740
+ serverDeploy: input.serverDeploy || config.serverDeploy
3741
+ });
3742
+ if (config.install) await installDependencies({
3743
+ projectDir,
3744
+ packageManager: config.packageManager
3745
+ });
3746
+ else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
3747
+ } catch (error) {
3748
+ const message = error instanceof Error ? error.message : String(error);
3749
+ exitWithError(`Error adding deployment: ${message}`);
3119
3750
  }
3120
3751
  }
3121
3752
 
3122
3753
  //#endregion
3123
- //#region src/helpers/setup/auth-setup.ts
3754
+ //#region src/utils/format-with-biome.ts
3755
+ async function formatProjectWithBiome(projectDir) {
3756
+ const biome = new Biome();
3757
+ const { projectKey } = biome.openProject(projectDir);
3758
+ biome.applyConfiguration(projectKey, {
3759
+ formatter: {
3760
+ enabled: true,
3761
+ indentStyle: "tab"
3762
+ },
3763
+ javascript: { formatter: { quoteStyle: "double" } }
3764
+ });
3765
+ const files = await glob("**/*", {
3766
+ cwd: projectDir,
3767
+ dot: true,
3768
+ absolute: true,
3769
+ onlyFiles: true
3770
+ });
3771
+ for (const filePath of files) try {
3772
+ const ext = path.extname(filePath).toLowerCase();
3773
+ const supported = new Set([
3774
+ ".ts",
3775
+ ".tsx",
3776
+ ".js",
3777
+ ".jsx",
3778
+ ".cjs",
3779
+ ".mjs",
3780
+ ".cts",
3781
+ ".mts",
3782
+ ".json",
3783
+ ".jsonc",
3784
+ ".md",
3785
+ ".mdx",
3786
+ ".css",
3787
+ ".scss",
3788
+ ".html"
3789
+ ]);
3790
+ if (!supported.has(ext)) continue;
3791
+ const original = await fs.readFile(filePath, "utf8");
3792
+ const result = biome.formatContent(projectKey, original, { filePath });
3793
+ const content = result?.content;
3794
+ if (typeof content !== "string") continue;
3795
+ if (content.length === 0 && original.length > 0) continue;
3796
+ if (content !== original) await fs.writeFile(filePath, content);
3797
+ } catch {}
3798
+ }
3799
+
3800
+ //#endregion
3801
+ //#region src/helpers/addons/auth-setup.ts
3124
3802
  async function setupAuth(config) {
3125
3803
  const { auth, frontend, backend, projectDir } = config;
3126
3804
  if (backend === "convex" || !auth) return;
@@ -3172,7 +3850,217 @@ function generateAuthSecret(length = 32) {
3172
3850
  }
3173
3851
 
3174
3852
  //#endregion
3175
- //#region src/helpers/setup/backend-setup.ts
3853
+ //#region src/helpers/addons/examples-setup.ts
3854
+ async function setupExamples(config) {
3855
+ const { examples, frontend, backend, projectDir } = config;
3856
+ if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
3857
+ if (examples.includes("ai")) {
3858
+ const webClientDir = path.join(projectDir, "apps/web");
3859
+ const nativeClientDir = path.join(projectDir, "apps/native");
3860
+ const serverDir = path.join(projectDir, "apps/server");
3861
+ const webClientDirExists = await fs.pathExists(webClientDir);
3862
+ const nativeClientDirExists = await fs.pathExists(nativeClientDir);
3863
+ const serverDirExists = await fs.pathExists(serverDir);
3864
+ const hasNuxt = frontend.includes("nuxt");
3865
+ const hasSvelte = frontend.includes("svelte");
3866
+ const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
3867
+ const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3868
+ if (webClientDirExists) {
3869
+ const dependencies = ["ai"];
3870
+ if (hasNuxt) dependencies.push("@ai-sdk/vue");
3871
+ else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
3872
+ else if (hasReactWeb) dependencies.push("@ai-sdk/react");
3873
+ await addPackageDependency({
3874
+ dependencies,
3875
+ projectDir: webClientDir
3876
+ });
3877
+ }
3878
+ if (nativeClientDirExists && hasReactNative) await addPackageDependency({
3879
+ dependencies: ["ai", "@ai-sdk/react"],
3880
+ projectDir: nativeClientDir
3881
+ });
3882
+ if (serverDirExists && backend !== "none") await addPackageDependency({
3883
+ dependencies: ["ai", "@ai-sdk/google"],
3884
+ projectDir: serverDir
3885
+ });
3886
+ }
3887
+ }
3888
+
3889
+ //#endregion
3890
+ //#region src/helpers/core/api-setup.ts
3891
+ async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
3892
+ const pkgJsonPath = path.join(projectDir, "package.json");
3893
+ try {
3894
+ const pkgJson = await fs.readJson(pkgJsonPath);
3895
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
3896
+ pkgJson.dependencies[backendPackageName] = workspaceVersion;
3897
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
3898
+ } catch (_error) {}
3899
+ }
3900
+ function getFrontendType(frontend) {
3901
+ const reactBasedFrontends = [
3902
+ "tanstack-router",
3903
+ "react-router",
3904
+ "tanstack-start",
3905
+ "next"
3906
+ ];
3907
+ const nativeFrontends = ["native-nativewind", "native-unistyles"];
3908
+ return {
3909
+ hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
3910
+ hasNuxtWeb: frontend.includes("nuxt"),
3911
+ hasSvelteWeb: frontend.includes("svelte"),
3912
+ hasSolidWeb: frontend.includes("solid"),
3913
+ hasNative: frontend.some((f) => nativeFrontends.includes(f))
3914
+ };
3915
+ }
3916
+ function getApiDependencies(api, frontendType) {
3917
+ const deps = {};
3918
+ if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
3919
+ else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
3920
+ if (frontendType.hasReactWeb) {
3921
+ if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3922
+ else if (api === "trpc") deps.web = { dependencies: [
3923
+ "@trpc/tanstack-react-query",
3924
+ "@trpc/client",
3925
+ "@trpc/server"
3926
+ ] };
3927
+ } else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
3928
+ dependencies: [
3929
+ "@tanstack/vue-query",
3930
+ "@orpc/tanstack-query",
3931
+ "@orpc/client"
3932
+ ],
3933
+ devDependencies: ["@tanstack/vue-query-devtools"]
3934
+ };
3935
+ else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
3936
+ dependencies: [
3937
+ "@orpc/tanstack-query",
3938
+ "@orpc/client",
3939
+ "@tanstack/svelte-query"
3940
+ ],
3941
+ devDependencies: ["@tanstack/svelte-query-devtools"]
3942
+ };
3943
+ else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
3944
+ dependencies: [
3945
+ "@orpc/tanstack-query",
3946
+ "@orpc/client",
3947
+ "@tanstack/solid-query"
3948
+ ],
3949
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3950
+ };
3951
+ if (api === "trpc") deps.native = { dependencies: [
3952
+ "@trpc/tanstack-react-query",
3953
+ "@trpc/client",
3954
+ "@trpc/server"
3955
+ ] };
3956
+ else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3957
+ return deps;
3958
+ }
3959
+ function getQueryDependencies(frontend) {
3960
+ const reactBasedFrontends = [
3961
+ "react-router",
3962
+ "tanstack-router",
3963
+ "tanstack-start",
3964
+ "next",
3965
+ "native-nativewind",
3966
+ "native-unistyles"
3967
+ ];
3968
+ const deps = {};
3969
+ const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
3970
+ if (needsReactQuery) {
3971
+ const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
3972
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3973
+ if (hasReactWeb) deps.web = {
3974
+ dependencies: ["@tanstack/react-query"],
3975
+ devDependencies: ["@tanstack/react-query-devtools"]
3976
+ };
3977
+ if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
3978
+ }
3979
+ if (frontend.includes("solid")) deps.web = {
3980
+ dependencies: ["@tanstack/solid-query"],
3981
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3982
+ };
3983
+ return deps;
3984
+ }
3985
+ function getConvexDependencies(frontend) {
3986
+ const deps = {
3987
+ web: { dependencies: ["convex"] },
3988
+ native: { dependencies: ["convex"] }
3989
+ };
3990
+ if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
3991
+ if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
3992
+ if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
3993
+ return deps;
3994
+ }
3995
+ async function setupApi(config) {
3996
+ const { api, projectName, frontend, backend, packageManager, projectDir } = config;
3997
+ const isConvex = backend === "convex";
3998
+ const webDir = path.join(projectDir, "apps/web");
3999
+ const nativeDir = path.join(projectDir, "apps/native");
4000
+ const serverDir = path.join(projectDir, "apps/server");
4001
+ const webDirExists = await fs.pathExists(webDir);
4002
+ const nativeDirExists = await fs.pathExists(nativeDir);
4003
+ const serverDirExists = await fs.pathExists(serverDir);
4004
+ const frontendType = getFrontendType(frontend);
4005
+ if (!isConvex && api !== "none") {
4006
+ const apiDeps = getApiDependencies(api, frontendType);
4007
+ if (serverDirExists && apiDeps.server) {
4008
+ await addPackageDependency({
4009
+ dependencies: apiDeps.server.dependencies,
4010
+ projectDir: serverDir
4011
+ });
4012
+ if (api === "trpc") {
4013
+ if (backend === "hono") await addPackageDependency({
4014
+ dependencies: ["@hono/trpc-server"],
4015
+ projectDir: serverDir
4016
+ });
4017
+ else if (backend === "elysia") await addPackageDependency({
4018
+ dependencies: ["@elysiajs/trpc"],
4019
+ projectDir: serverDir
4020
+ });
4021
+ }
4022
+ }
4023
+ if (webDirExists && apiDeps.web) await addPackageDependency({
4024
+ dependencies: apiDeps.web.dependencies,
4025
+ devDependencies: apiDeps.web.devDependencies,
4026
+ projectDir: webDir
4027
+ });
4028
+ if (nativeDirExists && apiDeps.native) await addPackageDependency({
4029
+ dependencies: apiDeps.native.dependencies,
4030
+ projectDir: nativeDir
4031
+ });
4032
+ }
4033
+ if (!isConvex) {
4034
+ const queryDeps = getQueryDependencies(frontend);
4035
+ if (webDirExists && queryDeps.web) await addPackageDependency({
4036
+ dependencies: queryDeps.web.dependencies,
4037
+ devDependencies: queryDeps.web.devDependencies,
4038
+ projectDir: webDir
4039
+ });
4040
+ if (nativeDirExists && queryDeps.native) await addPackageDependency({
4041
+ dependencies: queryDeps.native.dependencies,
4042
+ projectDir: nativeDir
4043
+ });
4044
+ }
4045
+ if (isConvex) {
4046
+ const convexDeps = getConvexDependencies(frontend);
4047
+ if (webDirExists) await addPackageDependency({
4048
+ dependencies: convexDeps.web.dependencies,
4049
+ projectDir: webDir
4050
+ });
4051
+ if (nativeDirExists) await addPackageDependency({
4052
+ dependencies: convexDeps.native.dependencies,
4053
+ projectDir: nativeDir
4054
+ });
4055
+ const backendPackageName = `@${projectName}/backend`;
4056
+ const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
4057
+ if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
4058
+ if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
4059
+ }
4060
+ }
4061
+
4062
+ //#endregion
4063
+ //#region src/helpers/core/backend-setup.ts
3176
4064
  async function setupBackendDependencies(config) {
3177
4065
  const { backend, runtime, api, projectDir } = config;
3178
4066
  if (backend === "convex") return;
@@ -3211,7 +4099,7 @@ async function setupBackendDependencies(config) {
3211
4099
  }
3212
4100
 
3213
4101
  //#endregion
3214
- //#region src/helpers/project-generation/env-setup.ts
4102
+ //#region src/helpers/core/env-setup.ts
3215
4103
  async function addEnvVariablesToFile(filePath, variables) {
3216
4104
  await fs.ensureDir(path.dirname(filePath));
3217
4105
  let envContent = "";
@@ -3259,7 +4147,7 @@ async function addEnvVariablesToFile(filePath, variables) {
3259
4147
  if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
3260
4148
  }
3261
4149
  async function setupEnvironmentVariables(config) {
3262
- const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
4150
+ const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
3263
4151
  const hasReactRouter = frontend.includes("react-router");
3264
4152
  const hasTanStackRouter = frontend.includes("tanstack-router");
3265
4153
  const hasTanStackStart = frontend.includes("tanstack-start");
@@ -3359,39 +4247,69 @@ async function setupEnvironmentVariables(config) {
3359
4247
  }
3360
4248
  ];
3361
4249
  await addEnvVariablesToFile(envPath, serverVars);
3362
- if (config.runtime === "workers") {
3363
- const devVarsPath = path.join(serverDir, ".dev.vars");
3364
- try {
3365
- await fs.copy(envPath, devVarsPath);
3366
- } catch (_err) {}
4250
+ const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
4251
+ const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
4252
+ if (isUnifiedAlchemy) {
4253
+ const rootEnvPath = path.join(projectDir, ".env");
4254
+ const rootAlchemyVars = [{
4255
+ key: "ALCHEMY_PASSWORD",
4256
+ value: "please-change-this",
4257
+ condition: true
4258
+ }];
4259
+ await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
4260
+ } else if (isIndividualAlchemy) {
4261
+ if (webDeploy === "alchemy") {
4262
+ const webDir = path.join(projectDir, "apps/web");
4263
+ if (await fs.pathExists(webDir)) {
4264
+ const webAlchemyVars = [{
4265
+ key: "ALCHEMY_PASSWORD",
4266
+ value: "please-change-this",
4267
+ condition: true
4268
+ }];
4269
+ await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
4270
+ }
4271
+ }
4272
+ if (serverDeploy === "alchemy") {
4273
+ const serverDir$1 = path.join(projectDir, "apps/server");
4274
+ if (await fs.pathExists(serverDir$1)) {
4275
+ const serverAlchemyVars = [{
4276
+ key: "ALCHEMY_PASSWORD",
4277
+ value: "please-change-this",
4278
+ condition: true
4279
+ }];
4280
+ await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
4281
+ }
4282
+ }
3367
4283
  }
3368
4284
  }
3369
4285
 
3370
4286
  //#endregion
3371
4287
  //#region src/helpers/database-providers/d1-setup.ts
3372
4288
  async function setupCloudflareD1(config) {
3373
- const { projectDir } = config;
3374
- const envPath = path.join(projectDir, "apps/server", ".env");
3375
- const variables = [
3376
- {
3377
- key: "CLOUDFLARE_ACCOUNT_ID",
3378
- value: "",
3379
- condition: true
3380
- },
3381
- {
3382
- key: "CLOUDFLARE_DATABASE_ID",
3383
- value: "",
3384
- condition: true
3385
- },
3386
- {
3387
- key: "CLOUDFLARE_D1_TOKEN",
3388
- value: "",
3389
- condition: true
3390
- }
3391
- ];
3392
- try {
3393
- await addEnvVariablesToFile(envPath, variables);
3394
- } catch (_err) {}
4289
+ const { projectDir, serverDeploy } = config;
4290
+ if (serverDeploy === "wrangler") {
4291
+ const envPath = path.join(projectDir, "apps/server", ".env");
4292
+ const variables = [
4293
+ {
4294
+ key: "CLOUDFLARE_ACCOUNT_ID",
4295
+ value: "",
4296
+ condition: true
4297
+ },
4298
+ {
4299
+ key: "CLOUDFLARE_DATABASE_ID",
4300
+ value: "",
4301
+ condition: true
4302
+ },
4303
+ {
4304
+ key: "CLOUDFLARE_D1_TOKEN",
4305
+ value: "",
4306
+ condition: true
4307
+ }
4308
+ ];
4309
+ try {
4310
+ await addEnvVariablesToFile(envPath, variables);
4311
+ } catch (_err) {}
4312
+ }
3395
4313
  }
3396
4314
 
3397
4315
  //#endregion
@@ -4185,7 +5103,7 @@ async function setupTurso(config) {
4185
5103
  }
4186
5104
 
4187
5105
  //#endregion
4188
- //#region src/helpers/setup/db-setup.ts
5106
+ //#region src/helpers/core/db-setup.ts
4189
5107
  async function setupDatabase(config) {
4190
5108
  const { database, orm, dbSetup, backend, projectDir } = config;
4191
5109
  if (backend === "convex" || database === "none") {
@@ -4250,44 +5168,7 @@ async function setupDatabase(config) {
4250
5168
  }
4251
5169
 
4252
5170
  //#endregion
4253
- //#region src/helpers/setup/examples-setup.ts
4254
- async function setupExamples(config) {
4255
- const { examples, frontend, backend, projectDir } = config;
4256
- if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
4257
- if (examples.includes("ai")) {
4258
- const webClientDir = path.join(projectDir, "apps/web");
4259
- const nativeClientDir = path.join(projectDir, "apps/native");
4260
- const serverDir = path.join(projectDir, "apps/server");
4261
- const webClientDirExists = await fs.pathExists(webClientDir);
4262
- const nativeClientDirExists = await fs.pathExists(nativeClientDir);
4263
- const serverDirExists = await fs.pathExists(serverDir);
4264
- const hasNuxt = frontend.includes("nuxt");
4265
- const hasSvelte = frontend.includes("svelte");
4266
- const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
4267
- const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
4268
- if (webClientDirExists) {
4269
- const dependencies = ["ai"];
4270
- if (hasNuxt) dependencies.push("@ai-sdk/vue");
4271
- else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
4272
- else if (hasReactWeb) dependencies.push("@ai-sdk/react");
4273
- await addPackageDependency({
4274
- dependencies,
4275
- projectDir: webClientDir
4276
- });
4277
- }
4278
- if (nativeClientDirExists && hasReactNative) await addPackageDependency({
4279
- dependencies: ["ai", "@ai-sdk/react"],
4280
- projectDir: nativeClientDir
4281
- });
4282
- if (serverDirExists && backend !== "none") await addPackageDependency({
4283
- dependencies: ["ai", "@ai-sdk/google"],
4284
- projectDir: serverDir
4285
- });
4286
- }
4287
- }
4288
-
4289
- //#endregion
4290
- //#region src/helpers/setup/runtime-setup.ts
5171
+ //#region src/helpers/core/runtime-setup.ts
4291
5172
  async function setupRuntime(config) {
4292
5173
  const { runtime, backend, projectDir } = config;
4293
5174
  if (backend === "convex" || backend === "next" || runtime === "none") return;
@@ -4295,23 +5176,6 @@ async function setupRuntime(config) {
4295
5176
  if (!await fs.pathExists(serverDir)) return;
4296
5177
  if (runtime === "bun") await setupBunRuntime(serverDir, backend);
4297
5178
  else if (runtime === "node") await setupNodeRuntime(serverDir, backend);
4298
- else if (runtime === "workers") await setupWorkersRuntime(serverDir);
4299
- }
4300
- async function generateCloudflareWorkerTypes(config) {
4301
- if (config.runtime !== "workers") return;
4302
- const serverDir = path.join(config.projectDir, "apps/server");
4303
- if (!await fs.pathExists(serverDir)) return;
4304
- const s = spinner();
4305
- try {
4306
- s.start("Generating Cloudflare Workers types...");
4307
- const runCmd = config.packageManager === "npm" ? "npm" : config.packageManager;
4308
- await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
4309
- s.stop("Cloudflare Workers types generated successfully!");
4310
- } catch {
4311
- s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
4312
- const managerCmd = config.packageManager === "npm" ? "npm run" : `${config.packageManager} run`;
4313
- console.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
4314
- }
4315
5179
  }
4316
5180
  async function setupBunRuntime(serverDir, _backend) {
4317
5181
  const packageJsonPath = path.join(serverDir, "package.json");
@@ -4351,27 +5215,20 @@ async function setupNodeRuntime(serverDir, backend) {
4351
5215
  projectDir: serverDir
4352
5216
  });
4353
5217
  }
4354
- async function setupWorkersRuntime(serverDir) {
4355
- const packageJsonPath = path.join(serverDir, "package.json");
4356
- if (!await fs.pathExists(packageJsonPath)) return;
4357
- const packageJson = await fs.readJson(packageJsonPath);
4358
- packageJson.scripts = {
4359
- ...packageJson.scripts,
4360
- dev: "wrangler dev --port=3000",
4361
- start: "wrangler dev",
4362
- deploy: "wrangler deploy",
4363
- build: "wrangler deploy --dry-run",
4364
- "cf-typegen": "wrangler types --env-interface CloudflareBindings"
4365
- };
4366
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
4367
- await addPackageDependency({
4368
- devDependencies: ["wrangler", "@types/node"],
4369
- projectDir: serverDir
5218
+
5219
+ //#endregion
5220
+ //#region src/helpers/core/convex-codegen.ts
5221
+ async function runConvexCodegen(projectDir, packageManager) {
5222
+ const backendDir = path.join(projectDir, "packages/backend");
5223
+ const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
5224
+ await execa(cmd, {
5225
+ cwd: backendDir,
5226
+ shell: true
4370
5227
  });
4371
5228
  }
4372
5229
 
4373
5230
  //#endregion
4374
- //#region src/helpers/project-generation/create-readme.ts
5231
+ //#region src/helpers/core/create-readme.ts
4375
5232
  async function createReadme(projectDir, options) {
4376
5233
  const readmePath = path.join(projectDir, "README.md");
4377
5234
  const content = generateReadmeContent(options);
@@ -4648,7 +5505,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
4648
5505
  }
4649
5506
 
4650
5507
  //#endregion
4651
- //#region src/helpers/project-generation/git.ts
5508
+ //#region src/helpers/core/git.ts
4652
5509
  async function initializeGit(projectDir, useGit) {
4653
5510
  if (!useGit) return;
4654
5511
  const gitVersionResult = await $({
@@ -4724,20 +5581,21 @@ async function getDockerStatus(database) {
4724
5581
  }
4725
5582
 
4726
5583
  //#endregion
4727
- //#region src/helpers/project-generation/post-installation.ts
5584
+ //#region src/helpers/core/post-installation.ts
4728
5585
  async function displayPostInstallInstructions(config) {
4729
- const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
5586
+ const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
4730
5587
  const isConvex = backend === "convex";
4731
5588
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
4732
5589
  const cdCmd = `cd ${relativePath}`;
4733
5590
  const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
4734
- const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
5591
+ const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
4735
5592
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
4736
5593
  const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
4737
5594
  const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
4738
5595
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
4739
5596
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
4740
- const workersDeployInstructions = webDeploy === "workers" ? getWorkersDeployInstructions(runCmd) : "";
5597
+ const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
5598
+ const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
4741
5599
  const hasWeb = frontend?.some((f) => [
4742
5600
  "tanstack-router",
4743
5601
  "react-router",
@@ -4766,8 +5624,8 @@ async function displayPostInstallInstructions(config) {
4766
5624
  if (runtime === "workers") {
4767
5625
  if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
4768
5626
  output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
4769
- output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} run cf-typegen\n\n`;
4770
- } else output += "\n";
5627
+ if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
5628
+ }
4771
5629
  }
4772
5630
  output += `${pc.bold("Your project will be available at:")}\n`;
4773
5631
  if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
@@ -4780,7 +5638,8 @@ async function displayPostInstallInstructions(config) {
4780
5638
  if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
4781
5639
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
4782
5640
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
4783
- if (workersDeployInstructions) output += `\n${workersDeployInstructions.trim()}\n`;
5641
+ if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
5642
+ if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
4784
5643
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
4785
5644
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
4786
5645
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -4801,7 +5660,7 @@ function getNativeInstructions(isConvex) {
4801
5660
  function getLintingInstructions(runCmd) {
4802
5661
  return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
4803
5662
  }
4804
- async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) {
5663
+ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
4805
5664
  const instructions = [];
4806
5665
  if (dbSetup === "docker") {
4807
5666
  const dockerStatus = await getDockerStatus(database);
@@ -4810,7 +5669,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4810
5669
  instructions.push("");
4811
5670
  }
4812
5671
  }
4813
- if (runtime === "workers" && dbSetup === "d1") {
5672
+ if (serverDeploy === "wrangler" && dbSetup === "d1") {
4814
5673
  const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
4815
5674
  instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
4816
5675
  instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
@@ -4818,8 +5677,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4818
5677
  instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
4819
5678
  instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
4820
5679
  instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
4821
- instructions.push("");
4822
5680
  }
5681
+ if (dbSetup === "d1" && serverDeploy === "alchemy") instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
4823
5682
  if (orm === "prisma") {
4824
5683
  if (dbSetup === "turso") instructions.push(`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`);
4825
5684
  if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
@@ -4828,7 +5687,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4828
5687
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4829
5688
  } else if (orm === "drizzle") {
4830
5689
  if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
4831
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
5690
+ if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
4832
5691
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4833
5692
  if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
4834
5693
  } else if (orm === "mongoose") {
@@ -4851,12 +5710,22 @@ function getNoOrmWarning() {
4851
5710
  function getBunWebNativeWarning() {
4852
5711
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
4853
5712
  }
4854
- function getWorkersDeployInstructions(runCmd) {
4855
- return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
5713
+ function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
5714
+ const instructions = [];
5715
+ if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
5716
+ if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
5717
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
5718
+ }
5719
+ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
5720
+ const instructions = [];
5721
+ if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
5722
+ else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
5723
+ else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
5724
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
4856
5725
  }
4857
5726
 
4858
5727
  //#endregion
4859
- //#region src/helpers/project-generation/project-config.ts
5728
+ //#region src/helpers/core/project-config.ts
4860
5729
  async function updatePackageConfigurations(projectDir, options) {
4861
5730
  await updateRootPackageJson(projectDir, options);
4862
5731
  if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
@@ -5037,7 +5906,7 @@ async function updateConvexPackageJson(projectDir, options) {
5037
5906
  }
5038
5907
 
5039
5908
  //#endregion
5040
- //#region src/helpers/project-generation/create-project.ts
5909
+ //#region src/helpers/core/create-project.ts
5041
5910
  async function createProject(options) {
5042
5911
  const projectDir = options.projectDir;
5043
5912
  const isConvex = options.backend === "convex";
@@ -5065,18 +5934,18 @@ async function createProject(options) {
5065
5934
  if (!isConvex && options.auth) await setupAuth(options);
5066
5935
  await handleExtras(projectDir, options);
5067
5936
  await setupWebDeploy(options);
5937
+ await setupServerDeploy(options);
5068
5938
  await setupEnvironmentVariables(options);
5069
5939
  await updatePackageConfigurations(projectDir, options);
5070
5940
  await createReadme(projectDir, options);
5071
5941
  await writeBtsConfig(options);
5942
+ await formatProjectWithBiome(projectDir);
5943
+ if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
5072
5944
  log.success("Project template successfully scaffolded!");
5073
- if (options.install) {
5074
- await installDependencies({
5075
- projectDir,
5076
- packageManager: options.packageManager
5077
- });
5078
- await generateCloudflareWorkerTypes(options);
5079
- }
5945
+ if (options.install) await installDependencies({
5946
+ projectDir,
5947
+ packageManager: options.packageManager
5948
+ });
5080
5949
  await initializeGit(projectDir, options.git);
5081
5950
  await displayPostInstallInstructions({
5082
5951
  ...options,
@@ -5095,7 +5964,7 @@ async function createProject(options) {
5095
5964
  }
5096
5965
 
5097
5966
  //#endregion
5098
- //#region src/helpers/project-generation/command-handlers.ts
5967
+ //#region src/helpers/core/command-handlers.ts
5099
5968
  async function createProjectHandler(input) {
5100
5969
  const startTime = Date.now();
5101
5970
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
@@ -5105,10 +5974,11 @@ async function createProjectHandler(input) {
5105
5974
  let currentPathInput;
5106
5975
  if (input.yes && input.projectName) currentPathInput = input.projectName;
5107
5976
  else if (input.yes) {
5108
- let defaultName = DEFAULT_CONFIG.relativePath;
5977
+ const defaultConfig = getDefaultConfig();
5978
+ let defaultName = defaultConfig.relativePath;
5109
5979
  let counter = 1;
5110
- while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) && fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0) {
5111
- defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
5980
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
5981
+ defaultName = `${defaultConfig.projectName}-${counter}`;
5112
5982
  counter++;
5113
5983
  }
5114
5984
  currentPathInput = defaultName;
@@ -5146,7 +6016,8 @@ async function createProjectHandler(input) {
5146
6016
  install: false,
5147
6017
  dbSetup: "none",
5148
6018
  api: "none",
5149
- webDeploy: "none"
6019
+ webDeploy: "none",
6020
+ serverDeploy: "none"
5150
6021
  },
5151
6022
  reproducibleCommand: "",
5152
6023
  timeScaffolded,
@@ -5162,28 +6033,33 @@ async function createProjectHandler(input) {
5162
6033
  projectDirectory: input.projectName
5163
6034
  };
5164
6035
  const providedFlags = getProvidedFlags(cliInput);
5165
- const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
5166
- const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
5167
- if (!input.yes && Object.keys(otherFlags).length > 0) {
5168
- log.info(pc.yellow("Using these pre-selected options:"));
5169
- log.message(displayConfig(otherFlags));
5170
- log.message("");
5171
- }
5172
6036
  let config;
5173
6037
  if (input.yes) {
6038
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
5174
6039
  config = {
5175
- ...DEFAULT_CONFIG,
6040
+ ...getDefaultConfig(),
5176
6041
  ...flagConfig,
5177
6042
  projectName: finalBaseName,
5178
6043
  projectDir: finalResolvedPath,
5179
6044
  relativePath: finalPathInput
5180
6045
  };
6046
+ coerceBackendPresets(config);
6047
+ validateConfigCompatibility(config, providedFlags, cliInput);
5181
6048
  if (config.backend === "convex") log.info("Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo");
5182
6049
  else if (config.backend === "none") log.info("Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none");
5183
6050
  log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
5184
6051
  log.message(displayConfig(config));
5185
6052
  log.message("");
5186
- } else config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6053
+ } else {
6054
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
6055
+ const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
6056
+ if (Object.keys(otherFlags).length > 0) {
6057
+ log.info(pc.yellow("Using these pre-selected options:"));
6058
+ log.message(displayConfig(otherFlags));
6059
+ log.message("");
6060
+ }
6061
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6062
+ }
5187
6063
  await createProject(config);
5188
6064
  const reproducibleCommand = generateReproducibleCommand(config);
5189
6065
  log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -5203,11 +6079,11 @@ async function createProjectHandler(input) {
5203
6079
  }
5204
6080
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
5205
6081
  const currentPath = path.resolve(process.cwd(), currentPathInput);
5206
- if (!fs.pathExistsSync(currentPath)) return {
6082
+ if (!await fs.pathExists(currentPath)) return {
5207
6083
  finalPathInput: currentPathInput,
5208
6084
  shouldClearDirectory: false
5209
6085
  };
5210
- const dirContents = fs.readdirSync(currentPath);
6086
+ const dirContents = await fs.readdir(currentPath);
5211
6087
  const isNotEmpty = dirContents.length > 0;
5212
6088
  if (!isNotEmpty) return {
5213
6089
  finalPathInput: currentPathInput,
@@ -5226,7 +6102,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5226
6102
  let counter = 1;
5227
6103
  const baseName = currentPathInput;
5228
6104
  let finalPathInput = `${baseName}-${counter}`;
5229
- while (fs.pathExistsSync(path.resolve(process.cwd(), finalPathInput)) && fs.readdirSync(path.resolve(process.cwd(), finalPathInput)).length > 0) {
6105
+ while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
5230
6106
  counter++;
5231
6107
  finalPathInput = `${baseName}-${counter}`;
5232
6108
  }
@@ -5252,6 +6128,10 @@ async function addAddonsHandler(input) {
5252
6128
  const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
5253
6129
  if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
5254
6130
  }
6131
+ if (!input.serverDeploy) {
6132
+ const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
6133
+ if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
6134
+ }
5255
6135
  const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
5256
6136
  let somethingAdded = false;
5257
6137
  if (input.addons && input.addons.length > 0) {
@@ -5272,6 +6152,15 @@ async function addAddonsHandler(input) {
5272
6152
  });
5273
6153
  somethingAdded = true;
5274
6154
  }
6155
+ if (input.serverDeploy && input.serverDeploy !== "none") {
6156
+ await addDeploymentToProject({
6157
+ ...input,
6158
+ install: false,
6159
+ suppressInstallMessage: true,
6160
+ serverDeploy: input.serverDeploy
6161
+ });
6162
+ somethingAdded = true;
6163
+ }
5275
6164
  if (!somethingAdded) {
5276
6165
  outro(pc.yellow("No addons or deployment configurations to add."));
5277
6166
  return;
@@ -5375,6 +6264,7 @@ const router = t.router({
5375
6264
  runtime: RuntimeSchema.optional(),
5376
6265
  api: APISchema.optional(),
5377
6266
  webDeploy: WebDeploySchema.optional(),
6267
+ serverDeploy: ServerDeploySchema.optional(),
5378
6268
  directoryConflict: DirectoryConflictSchema.optional(),
5379
6269
  renderTitle: z.boolean().optional(),
5380
6270
  disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
@@ -5390,6 +6280,7 @@ const router = t.router({
5390
6280
  add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
5391
6281
  addons: z.array(AddonsSchema).optional().default([]),
5392
6282
  webDeploy: WebDeploySchema.optional(),
6283
+ serverDeploy: ServerDeploySchema.optional(),
5393
6284
  projectDir: z.string().optional(),
5394
6285
  install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
5395
6286
  packageManager: PackageManagerSchema.optional()