create-better-t-stack 2.33.8 → 2.33.9-canary.38a6c011

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 (33) 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-pVOzV3_5.js} +1479 -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/alchemy/wrangler.jsonc.hbs +11 -0
  17. package/templates/deploy/{web → wrangler/web}/nuxt/wrangler.jsonc.hbs +1 -1
  18. package/templates/deploy/{web → wrangler/web}/react/next/wrangler.jsonc.hbs +1 -1
  19. package/templates/deploy/{web → wrangler/web}/react/react-router/wrangler.jsonc.hbs +1 -1
  20. package/templates/deploy/{web → wrangler/web}/react/tanstack-router/wrangler.jsonc.hbs +1 -1
  21. package/templates/deploy/{web → wrangler/web}/react/tanstack-start/wrangler.jsonc.hbs +1 -1
  22. package/templates/deploy/{web → wrangler/web}/solid/wrangler.jsonc.hbs +1 -1
  23. package/templates/deploy/{web → wrangler/web}/svelte/wrangler.jsonc.hbs +1 -1
  24. package/templates/frontend/nuxt/_gitignore +3 -0
  25. package/templates/frontend/nuxt/tsconfig.json.hbs +4 -4
  26. package/templates/frontend/react/web-base/_gitignore +1 -0
  27. package/templates/frontend/react/web-base/src/components/header.tsx.hbs +0 -1
  28. package/templates/frontend/solid/_gitignore +1 -0
  29. package/templates/frontend/solid/package.json.hbs +0 -1
  30. package/templates/frontend/svelte/_gitignore +1 -0
  31. package/templates/frontend/svelte/package.json.hbs +11 -13
  32. /package/templates/{runtime/workers/apps → deploy/wrangler}/server/wrangler.jsonc.hbs +0 -0
  33. /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,62 @@ 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)) {
2337
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2338
+ await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
2339
+ }
2340
+ }
2341
+ } else {
2342
+ if (context.webDeploy === "alchemy") {
2343
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2344
+ const webAppDir = path.join(projectDir, "apps/web");
2345
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2346
+ }
2347
+ if (context.serverDeploy === "alchemy") {
2348
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2349
+ const serverAppDir = path.join(projectDir, "apps/server");
2350
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2351
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2352
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2353
+ await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
2354
+ }
2355
+ }
2356
+ }
2357
+ if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
2090
2358
  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);
2359
+ if (await fs.pathExists(webAppDir)) {
2360
+ const frontends = context.frontend;
2361
+ const templateMap = {
2362
+ "tanstack-router": "react/tanstack-router",
2363
+ "tanstack-start": "react/tanstack-start",
2364
+ "react-router": "react/react-router",
2365
+ solid: "solid",
2366
+ next: "react/next",
2367
+ nuxt: "nuxt",
2368
+ svelte: "svelte"
2369
+ };
2370
+ for (const f of frontends) if (templateMap[f]) {
2371
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
2372
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2373
+ }
2374
+ }
2375
+ }
2376
+ if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
2377
+ const serverAppDir = path.join(projectDir, "apps/server");
2378
+ if (await fs.pathExists(serverAppDir)) {
2379
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
2380
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
2105
2381
  }
2106
2382
  }
2107
2383
  }
2108
2384
 
2109
2385
  //#endregion
2110
- //#region src/helpers/setup/ruler-setup.ts
2386
+ //#region src/helpers/addons/ruler-setup.ts
2111
2387
  async function setupVibeRules(config) {
2112
2388
  const { packageManager, projectDir } = config;
2113
2389
  try {
@@ -2189,7 +2465,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2189
2465
  }
2190
2466
 
2191
2467
  //#endregion
2192
- //#region src/helpers/setup/starlight-setup.ts
2468
+ //#region src/helpers/addons/starlight-setup.ts
2193
2469
  async function setupStarlight(config) {
2194
2470
  const { packageManager, projectDir } = config;
2195
2471
  const s = spinner();
@@ -2221,7 +2497,7 @@ async function setupStarlight(config) {
2221
2497
  }
2222
2498
 
2223
2499
  //#endregion
2224
- //#region src/helpers/setup/tauri-setup.ts
2500
+ //#region src/helpers/addons/tauri-setup.ts
2225
2501
  async function setupTauri(config) {
2226
2502
  const { packageManager, frontend, projectDir } = config;
2227
2503
  const s = spinner();
@@ -2258,8 +2534,8 @@ async function setupTauri(config) {
2258
2534
  `--window-title=${path.basename(projectDir)}`,
2259
2535
  `--frontend-dist=${frontendDist}`,
2260
2536
  `--dev-url=${devUrl}`,
2261
- `--before-dev-command=\"${packageManager} run dev\"`,
2262
- `--before-build-command=\"${packageManager} run build\"`
2537
+ `--before-dev-command="${packageManager} run dev"`,
2538
+ `--before-build-command="${packageManager} run build"`
2263
2539
  ];
2264
2540
  const tauriArgsString = tauriArgs.join(" ");
2265
2541
  const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
@@ -2277,7 +2553,7 @@ async function setupTauri(config) {
2277
2553
  }
2278
2554
 
2279
2555
  //#endregion
2280
- //#region src/helpers/setup/ultracite-setup.ts
2556
+ //#region src/helpers/addons/ultracite-setup.ts
2281
2557
  const EDITORS = {
2282
2558
  vscode: {
2283
2559
  label: "VSCode / Cursor / Windsurf",
@@ -2346,7 +2622,7 @@ async function setupUltracite(config, hasHusky) {
2346
2622
  ];
2347
2623
  if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2348
2624
  if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2349
- if (hasHusky) ultraciteArgs.push("--features", "husky", "lint-staged");
2625
+ if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2350
2626
  const ultraciteArgsString = ultraciteArgs.join(" ");
2351
2627
  const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2352
2628
  const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
@@ -2384,7 +2660,7 @@ function ensureArrayProperty(obj, name) {
2384
2660
  }
2385
2661
 
2386
2662
  //#endregion
2387
- //#region src/helpers/setup/vite-pwa-setup.ts
2663
+ //#region src/helpers/addons/vite-pwa-setup.ts
2388
2664
  async function addPwaToViteConfig(viteConfigPath, projectName) {
2389
2665
  const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2390
2666
  if (!sourceFile) throw new Error("vite config not found");
@@ -2418,7 +2694,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
2418
2694
  }
2419
2695
 
2420
2696
  //#endregion
2421
- //#region src/helpers/setup/addons-setup.ts
2697
+ //#region src/helpers/addons/addons-setup.ts
2422
2698
  async function setupAddons(config, isAddCommand = false) {
2423
2699
  const { addons, frontend, projectDir, packageManager } = config;
2424
2700
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
@@ -2552,7 +2828,7 @@ async function setupOxlint(projectDir, packageManager) {
2552
2828
  }
2553
2829
 
2554
2830
  //#endregion
2555
- //#region src/helpers/project-generation/detect-project-config.ts
2831
+ //#region src/helpers/core/detect-project-config.ts
2556
2832
  async function detectProjectConfig(projectDir) {
2557
2833
  try {
2558
2834
  const btsConfig = await readBtsConfig(projectDir);
@@ -2570,7 +2846,8 @@ async function detectProjectConfig(projectDir) {
2570
2846
  packageManager: btsConfig.packageManager,
2571
2847
  dbSetup: btsConfig.dbSetup,
2572
2848
  api: btsConfig.api,
2573
- webDeploy: btsConfig.webDeploy
2849
+ webDeploy: btsConfig.webDeploy,
2850
+ serverDeploy: btsConfig.serverDeploy
2574
2851
  };
2575
2852
  return null;
2576
2853
  } catch (_error) {
@@ -2586,7 +2863,7 @@ async function isBetterTStackProject(projectDir) {
2586
2863
  }
2587
2864
 
2588
2865
  //#endregion
2589
- //#region src/helpers/project-generation/install-dependencies.ts
2866
+ //#region src/helpers/core/install-dependencies.ts
2590
2867
  async function installDependencies({ projectDir, packageManager }) {
2591
2868
  const s = spinner();
2592
2869
  try {
@@ -2603,7 +2880,7 @@ async function installDependencies({ projectDir, packageManager }) {
2603
2880
  }
2604
2881
 
2605
2882
  //#endregion
2606
- //#region src/helpers/project-generation/add-addons.ts
2883
+ //#region src/helpers/core/add-addons.ts
2607
2884
  async function addAddonsToProject(input) {
2608
2885
  try {
2609
2886
  const projectDir = input.projectDir || process.cwd();
@@ -2628,7 +2905,8 @@ async function addAddonsToProject(input) {
2628
2905
  install: input.install || false,
2629
2906
  dbSetup: detectedConfig.dbSetup || "none",
2630
2907
  api: detectedConfig.api || "none",
2631
- webDeploy: detectedConfig.webDeploy || "none"
2908
+ webDeploy: detectedConfig.webDeploy || "none",
2909
+ serverDeploy: detectedConfig.serverDeploy || "none"
2632
2910
  };
2633
2911
  for (const addon of input.addons) {
2634
2912
  const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
@@ -2651,12 +2929,92 @@ async function addAddonsToProject(input) {
2651
2929
  }
2652
2930
 
2653
2931
  //#endregion
2654
- //#region src/helpers/setup/workers-nuxt-setup.ts
2655
- async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2932
+ //#region src/helpers/deployment/server-deploy-setup.ts
2933
+ async function setupServerDeploy(config) {
2934
+ const { serverDeploy, webDeploy, projectDir } = config;
2935
+ const { packageManager } = config;
2936
+ if (serverDeploy === "none") return;
2937
+ if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
2938
+ const serverDir = path.join(projectDir, "apps/server");
2939
+ if (!await fs.pathExists(serverDir)) return;
2940
+ if (serverDeploy === "wrangler") {
2941
+ await setupWorkersServerDeploy(serverDir, packageManager);
2942
+ await generateCloudflareWorkerTypes({
2943
+ serverDir,
2944
+ packageManager
2945
+ });
2946
+ } else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
2947
+ }
2948
+ async function setupWorkersServerDeploy(serverDir, _packageManager) {
2949
+ const packageJsonPath = path.join(serverDir, "package.json");
2950
+ if (!await fs.pathExists(packageJsonPath)) return;
2951
+ const packageJson = await fs.readJson(packageJsonPath);
2952
+ packageJson.scripts = {
2953
+ ...packageJson.scripts,
2954
+ dev: "wrangler dev --port=3000",
2955
+ start: "wrangler dev",
2956
+ deploy: "wrangler deploy",
2957
+ build: "wrangler deploy --dry-run",
2958
+ "cf-typegen": "wrangler types --env-interface CloudflareBindings"
2959
+ };
2960
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2961
+ await addPackageDependency({
2962
+ devDependencies: [
2963
+ "wrangler",
2964
+ "@types/node",
2965
+ "@cloudflare/workers-types"
2966
+ ],
2967
+ projectDir: serverDir
2968
+ });
2969
+ }
2970
+ async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
2971
+ if (!await fs.pathExists(serverDir)) return;
2972
+ const s = spinner();
2973
+ try {
2974
+ s.start("Generating Cloudflare Workers types...");
2975
+ const runCmd = packageManager === "npm" ? "npm" : packageManager;
2976
+ await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
2977
+ s.stop("Cloudflare Workers types generated successfully!");
2978
+ } catch {
2979
+ s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
2980
+ const managerCmd = `${packageManager} run`;
2981
+ log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
2982
+ }
2983
+ }
2984
+ async function setupAlchemyServerDeploy(serverDir, _packageManager) {
2985
+ if (!await fs.pathExists(serverDir)) return;
2986
+ await addPackageDependency({
2987
+ devDependencies: [
2988
+ "alchemy",
2989
+ "wrangler",
2990
+ "@types/node",
2991
+ "@cloudflare/workers-types",
2992
+ "dotenv"
2993
+ ],
2994
+ projectDir: serverDir
2995
+ });
2996
+ const packageJsonPath = path.join(serverDir, "package.json");
2997
+ if (await fs.pathExists(packageJsonPath)) {
2998
+ const packageJson = await fs.readJson(packageJsonPath);
2999
+ packageJson.scripts = {
3000
+ ...packageJson.scripts,
3001
+ dev: "wrangler dev --port=3000",
3002
+ build: "wrangler deploy --dry-run",
3003
+ deploy: "alchemy deploy",
3004
+ destroy: "alchemy destroy",
3005
+ "alchemy:dev": "alchemy dev"
3006
+ };
3007
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3008
+ }
3009
+ }
3010
+
3011
+ //#endregion
3012
+ //#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
3013
+ async function setupNextAlchemyDeploy(projectDir, _packageManager) {
2656
3014
  const webAppDir = path.join(projectDir, "apps/web");
2657
3015
  if (!await fs.pathExists(webAppDir)) return;
2658
3016
  await addPackageDependency({
2659
- devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3017
+ devDependencies: ["alchemy", "dotenv"],
2660
3018
  projectDir: webAppDir
2661
3019
  });
2662
3020
  const pkgPath = path.join(webAppDir, "package.json");
@@ -2664,63 +3022,538 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2664
3022
  const pkg = await fs.readJson(pkgPath);
2665
3023
  pkg.scripts = {
2666
3024
  ...pkg.scripts,
2667
- deploy: `${packageManager} run build && wrangler deploy`,
2668
- "cf-typegen": "wrangler types"
3025
+ deploy: "alchemy deploy",
3026
+ destroy: "alchemy destroy",
3027
+ "alchemy:dev": "alchemy dev"
2669
3028
  };
2670
3029
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
2671
3030
  }
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
3031
  }
2716
3032
 
2717
3033
  //#endregion
2718
- //#region src/helpers/setup/workers-svelte-setup.ts
2719
- async function setupSvelteWorkersDeploy(projectDir, packageManager) {
3034
+ //#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
3035
+ async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
2720
3036
  const webAppDir = path.join(projectDir, "apps/web");
2721
3037
  if (!await fs.pathExists(webAppDir)) return;
2722
3038
  await addPackageDependency({
2723
- devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
3039
+ devDependencies: [
3040
+ "alchemy",
3041
+ "nitro-cloudflare-dev",
3042
+ "dotenv"
3043
+ ],
3044
+ projectDir: webAppDir
3045
+ });
3046
+ const pkgPath = path.join(webAppDir, "package.json");
3047
+ if (await fs.pathExists(pkgPath)) {
3048
+ const pkg = await fs.readJson(pkgPath);
3049
+ pkg.scripts = {
3050
+ ...pkg.scripts,
3051
+ deploy: "alchemy deploy",
3052
+ destroy: "alchemy destroy",
3053
+ "alchemy:dev": "alchemy dev"
3054
+ };
3055
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3056
+ }
3057
+ const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
3058
+ if (!await fs.pathExists(nuxtConfigPath)) return;
3059
+ try {
3060
+ const project = new Project({ manipulationSettings: {
3061
+ indentationText: IndentationText.TwoSpaces,
3062
+ quoteKind: QuoteKind.Double
3063
+ } });
3064
+ project.addSourceFileAtPath(nuxtConfigPath);
3065
+ const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
3066
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3067
+ if (!exportAssignment) return;
3068
+ const defineConfigCall = exportAssignment.getExpression();
3069
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
3070
+ let configObject = defineConfigCall.getArguments()[0];
3071
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3072
+ if (Node.isObjectLiteralExpression(configObject)) {
3073
+ if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
3074
+ name: "nitro",
3075
+ initializer: `{
3076
+ preset: "cloudflare_module",
3077
+ cloudflare: {
3078
+ deployConfig: true,
3079
+ nodeCompat: true
3080
+ }
3081
+ }`
3082
+ });
3083
+ const modulesProperty = configObject.getProperty("modules");
3084
+ if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
3085
+ const initializer = modulesProperty.getInitializer();
3086
+ if (Node.isArrayLiteralExpression(initializer)) {
3087
+ const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
3088
+ if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
3089
+ }
3090
+ } else if (!modulesProperty) configObject.addPropertyAssignment({
3091
+ name: "modules",
3092
+ initializer: "[\"nitro-cloudflare-dev\"]"
3093
+ });
3094
+ }
3095
+ await project.save();
3096
+ } catch (error) {
3097
+ console.warn("Failed to update nuxt.config.ts:", error);
3098
+ }
3099
+ }
3100
+
3101
+ //#endregion
3102
+ //#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
3103
+ async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
3104
+ const webAppDir = path.join(projectDir, "apps/web");
3105
+ if (!await fs.pathExists(webAppDir)) return;
3106
+ await addPackageDependency({
3107
+ devDependencies: [
3108
+ "alchemy",
3109
+ "@cloudflare/vite-plugin",
3110
+ "dotenv"
3111
+ ],
3112
+ projectDir: webAppDir
3113
+ });
3114
+ const pkgPath = path.join(webAppDir, "package.json");
3115
+ if (await fs.pathExists(pkgPath)) {
3116
+ const pkg = await fs.readJson(pkgPath);
3117
+ pkg.scripts = {
3118
+ ...pkg.scripts,
3119
+ deploy: "alchemy deploy",
3120
+ destroy: "alchemy destroy",
3121
+ "alchemy:dev": "alchemy dev"
3122
+ };
3123
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3124
+ }
3125
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3126
+ if (await fs.pathExists(viteConfigPath)) try {
3127
+ const project = new Project({ manipulationSettings: {
3128
+ indentationText: IndentationText.TwoSpaces,
3129
+ quoteKind: QuoteKind.Double
3130
+ } });
3131
+ project.addSourceFileAtPath(viteConfigPath);
3132
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3133
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
3134
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3135
+ moduleSpecifier: "alchemy/cloudflare/react-router",
3136
+ defaultImport: "alchemy"
3137
+ });
3138
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3139
+ if (!exportAssignment) return;
3140
+ const defineConfigCall = exportAssignment.getExpression();
3141
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3142
+ let configObject = defineConfigCall.getArguments()[0];
3143
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3144
+ if (Node.isObjectLiteralExpression(configObject)) {
3145
+ const pluginsProperty = configObject.getProperty("plugins");
3146
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3147
+ const initializer = pluginsProperty.getInitializer();
3148
+ if (Node.isArrayLiteralExpression(initializer)) {
3149
+ const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
3150
+ if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
3151
+ }
3152
+ } else if (!pluginsProperty) configObject.addPropertyAssignment({
3153
+ name: "plugins",
3154
+ initializer: "[alchemy()]"
3155
+ });
3156
+ }
3157
+ await project.save();
3158
+ } catch (error) {
3159
+ console.warn("Failed to update vite.config.ts:", error);
3160
+ }
3161
+ const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
3162
+ if (await fs.pathExists(reactRouterConfigPath)) try {
3163
+ const project = new Project({ manipulationSettings: {
3164
+ indentationText: IndentationText.TwoSpaces,
3165
+ quoteKind: QuoteKind.Double
3166
+ } });
3167
+ project.addSourceFileAtPath(reactRouterConfigPath);
3168
+ const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
3169
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3170
+ if (!exportAssignment) return;
3171
+ const configExpression = exportAssignment.getExpression();
3172
+ let configObject;
3173
+ if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
3174
+ else if (Node.isSatisfiesExpression(configExpression)) {
3175
+ const expression = configExpression.getExpression();
3176
+ if (Node.isObjectLiteralExpression(expression)) configObject = expression;
3177
+ }
3178
+ if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
3179
+ const futureProperty = configObject.getProperty("future");
3180
+ if (!futureProperty) configObject.addPropertyAssignment({
3181
+ name: "future",
3182
+ initializer: `{
3183
+ unstable_viteEnvironmentApi: true,
3184
+ }`
3185
+ });
3186
+ else if (Node.isPropertyAssignment(futureProperty)) {
3187
+ const futureInitializer = futureProperty.getInitializer();
3188
+ if (Node.isObjectLiteralExpression(futureInitializer)) {
3189
+ const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
3190
+ if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
3191
+ name: "unstable_viteEnvironmentApi",
3192
+ initializer: "true"
3193
+ });
3194
+ else if (Node.isPropertyAssignment(viteEnvApiProp)) {
3195
+ const value = viteEnvApiProp.getInitializer()?.getText();
3196
+ if (value === "false") viteEnvApiProp.setInitializer("true");
3197
+ }
3198
+ }
3199
+ }
3200
+ await project.save();
3201
+ } catch (error) {
3202
+ console.warn("Failed to update react-router.config.ts:", error);
3203
+ }
3204
+ }
3205
+
3206
+ //#endregion
3207
+ //#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
3208
+ async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
3209
+ const webAppDir = path.join(projectDir, "apps/web");
3210
+ if (!await fs.pathExists(webAppDir)) return;
3211
+ await addPackageDependency({
3212
+ devDependencies: ["alchemy", "dotenv"],
3213
+ projectDir: webAppDir
3214
+ });
3215
+ const pkgPath = path.join(webAppDir, "package.json");
3216
+ if (await fs.pathExists(pkgPath)) {
3217
+ const pkg = await fs.readJson(pkgPath);
3218
+ pkg.scripts = {
3219
+ ...pkg.scripts,
3220
+ deploy: "alchemy deploy",
3221
+ destroy: "alchemy destroy",
3222
+ "alchemy:dev": "alchemy dev"
3223
+ };
3224
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3225
+ }
3226
+ }
3227
+
3228
+ //#endregion
3229
+ //#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
3230
+ async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
3231
+ const webAppDir = path.join(projectDir, "apps/web");
3232
+ if (!await fs.pathExists(webAppDir)) return;
3233
+ await addPackageDependency({
3234
+ devDependencies: [
3235
+ "alchemy",
3236
+ "@sveltejs/adapter-cloudflare",
3237
+ "dotenv"
3238
+ ],
3239
+ projectDir: webAppDir
3240
+ });
3241
+ const pkgPath = path.join(webAppDir, "package.json");
3242
+ if (await fs.pathExists(pkgPath)) {
3243
+ const pkg = await fs.readJson(pkgPath);
3244
+ pkg.scripts = {
3245
+ ...pkg.scripts,
3246
+ deploy: "alchemy deploy",
3247
+ destroy: "alchemy destroy",
3248
+ "alchemy:dev": "alchemy dev"
3249
+ };
3250
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3251
+ }
3252
+ const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
3253
+ if (!await fs.pathExists(svelteConfigPath)) return;
3254
+ try {
3255
+ const project = new Project({ manipulationSettings: {
3256
+ indentationText: IndentationText.TwoSpaces,
3257
+ quoteKind: QuoteKind.Single
3258
+ } });
3259
+ project.addSourceFileAtPath(svelteConfigPath);
3260
+ const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
3261
+ const importDeclarations = sourceFile.getImportDeclarations();
3262
+ const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
3263
+ if (adapterImport) {
3264
+ adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
3265
+ adapterImport.removeDefaultImport();
3266
+ adapterImport.setDefaultImport("alchemy");
3267
+ } else sourceFile.insertImportDeclaration(0, {
3268
+ moduleSpecifier: "alchemy/cloudflare/sveltekit",
3269
+ defaultImport: "alchemy"
3270
+ });
3271
+ const configVariable = sourceFile.getVariableDeclaration("config");
3272
+ if (configVariable) {
3273
+ const initializer = configVariable.getInitializer();
3274
+ if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
3275
+ }
3276
+ await project.save();
3277
+ } catch (error) {
3278
+ console.warn("Failed to update svelte.config.js:", error);
3279
+ }
3280
+ }
3281
+ function updateAdapterInConfig(configObject) {
3282
+ if (!Node.isObjectLiteralExpression(configObject)) return;
3283
+ const kitProperty = configObject.getProperty("kit");
3284
+ if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
3285
+ const kitInitializer = kitProperty.getInitializer();
3286
+ if (Node.isObjectLiteralExpression(kitInitializer)) {
3287
+ const adapterProperty = kitInitializer.getProperty("adapter");
3288
+ if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
3289
+ const initializer = adapterProperty.getInitializer();
3290
+ if (Node.isCallExpression(initializer)) {
3291
+ const expression = initializer.getExpression();
3292
+ if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
3293
+ }
3294
+ }
3295
+ }
3296
+ }
3297
+ }
3298
+
3299
+ //#endregion
3300
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
3301
+ async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
3302
+ const webAppDir = path.join(projectDir, "apps/web");
3303
+ if (!await fs.pathExists(webAppDir)) return;
3304
+ await addPackageDependency({
3305
+ devDependencies: ["alchemy", "dotenv"],
3306
+ projectDir: webAppDir
3307
+ });
3308
+ const pkgPath = path.join(webAppDir, "package.json");
3309
+ if (await fs.pathExists(pkgPath)) {
3310
+ const pkg = await fs.readJson(pkgPath);
3311
+ pkg.scripts = {
3312
+ ...pkg.scripts,
3313
+ deploy: "alchemy deploy",
3314
+ destroy: "alchemy destroy",
3315
+ "alchemy:dev": "alchemy dev"
3316
+ };
3317
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3318
+ }
3319
+ }
3320
+
3321
+ //#endregion
3322
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
3323
+ async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
3324
+ const webAppDir = path.join(projectDir, "apps/web");
3325
+ if (!await fs.pathExists(webAppDir)) return;
3326
+ await addPackageDependency({
3327
+ devDependencies: [
3328
+ "alchemy",
3329
+ "nitropack",
3330
+ "dotenv"
3331
+ ],
3332
+ projectDir: webAppDir
3333
+ });
3334
+ const pkgPath = path.join(webAppDir, "package.json");
3335
+ if (await fs.pathExists(pkgPath)) {
3336
+ const pkg = await fs.readJson(pkgPath);
3337
+ pkg.scripts = {
3338
+ ...pkg.scripts,
3339
+ deploy: "alchemy deploy",
3340
+ destroy: "alchemy destroy",
3341
+ "alchemy:dev": "alchemy dev"
3342
+ };
3343
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3344
+ }
3345
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3346
+ if (await fs.pathExists(viteConfigPath)) try {
3347
+ const project = new Project({ manipulationSettings: {
3348
+ indentationText: IndentationText.TwoSpaces,
3349
+ quoteKind: QuoteKind.Double
3350
+ } });
3351
+ project.addSourceFileAtPath(viteConfigPath);
3352
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3353
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
3354
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3355
+ moduleSpecifier: "alchemy/cloudflare/tanstack-start",
3356
+ defaultImport: "alchemy"
3357
+ });
3358
+ else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
3359
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3360
+ if (!exportAssignment) return;
3361
+ const defineConfigCall = exportAssignment.getExpression();
3362
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3363
+ let configObject = defineConfigCall.getArguments()[0];
3364
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3365
+ if (Node.isObjectLiteralExpression(configObject)) {
3366
+ if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
3367
+ name: "build",
3368
+ initializer: `{
3369
+ target: "esnext",
3370
+ rollupOptions: {
3371
+ external: ["node:async_hooks", "cloudflare:workers"],
3372
+ },
3373
+ }`
3374
+ });
3375
+ const pluginsProperty = configObject.getProperty("plugins");
3376
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3377
+ const initializer = pluginsProperty.getInitializer();
3378
+ if (Node.isArrayLiteralExpression(initializer)) {
3379
+ const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
3380
+ if (!hasShim) initializer.addElement("alchemy()");
3381
+ const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
3382
+ tanstackElements.forEach((element) => {
3383
+ if (Node.isCallExpression(element)) {
3384
+ const args = element.getArguments();
3385
+ if (args.length === 0) element.addArgument(`{
3386
+ target: "cloudflare-module",
3387
+ customViteReactPlugin: true,
3388
+ }`);
3389
+ else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
3390
+ const configObj = args[0];
3391
+ if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
3392
+ name: "target",
3393
+ initializer: "\"cloudflare-module\""
3394
+ });
3395
+ if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
3396
+ name: "customViteReactPlugin",
3397
+ initializer: "true"
3398
+ });
3399
+ }
3400
+ }
3401
+ });
3402
+ }
3403
+ } else configObject.addPropertyAssignment({
3404
+ name: "plugins",
3405
+ initializer: "[alchemy()]"
3406
+ });
3407
+ }
3408
+ await project.save();
3409
+ } catch (error) {
3410
+ console.warn("Failed to update vite.config.ts:", error);
3411
+ }
3412
+ const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
3413
+ const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
3414
+
3415
+ export default defineNitroConfig({
3416
+ preset: "cloudflare-module",
3417
+ cloudflare: {
3418
+ nodeCompat: true,
3419
+ },
3420
+ });
3421
+ `;
3422
+ await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
3423
+ }
3424
+
3425
+ //#endregion
3426
+ //#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
3427
+ async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
3428
+ await addPackageDependency({
3429
+ devDependencies: ["alchemy", "dotenv"],
3430
+ projectDir
3431
+ });
3432
+ const rootPkgPath = path.join(projectDir, "package.json");
3433
+ if (await fs.pathExists(rootPkgPath)) {
3434
+ const pkg = await fs.readJson(rootPkgPath);
3435
+ pkg.scripts = {
3436
+ ...pkg.scripts,
3437
+ deploy: "alchemy deploy",
3438
+ destroy: "alchemy destroy",
3439
+ "alchemy:dev": "alchemy dev"
3440
+ };
3441
+ await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
3442
+ }
3443
+ const serverDir = path.join(projectDir, "apps/server");
3444
+ if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
3445
+ const frontend = config.frontend;
3446
+ const isNext = frontend.includes("next");
3447
+ const isNuxt = frontend.includes("nuxt");
3448
+ const isSvelte = frontend.includes("svelte");
3449
+ const isTanstackRouter = frontend.includes("tanstack-router");
3450
+ const isTanstackStart = frontend.includes("tanstack-start");
3451
+ const isReactRouter = frontend.includes("react-router");
3452
+ const isSolid = frontend.includes("solid");
3453
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3454
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3455
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3456
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3457
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3458
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3459
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3460
+ }
3461
+
3462
+ //#endregion
3463
+ //#region src/helpers/deployment/workers/workers-next-setup.ts
3464
+ async function setupNextWorkersDeploy(projectDir, _packageManager) {
3465
+ const webAppDir = path.join(projectDir, "apps/web");
3466
+ if (!await fs.pathExists(webAppDir)) return;
3467
+ await addPackageDependency({
3468
+ dependencies: ["@opennextjs/cloudflare"],
3469
+ devDependencies: ["wrangler"],
3470
+ projectDir: webAppDir
3471
+ });
3472
+ const packageJsonPath = path.join(webAppDir, "package.json");
3473
+ if (await fs.pathExists(packageJsonPath)) {
3474
+ const pkg = await fs.readJson(packageJsonPath);
3475
+ pkg.scripts = {
3476
+ ...pkg.scripts,
3477
+ preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
3478
+ deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
3479
+ upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
3480
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
3481
+ };
3482
+ await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
3483
+ }
3484
+ }
3485
+
3486
+ //#endregion
3487
+ //#region src/helpers/deployment/workers/workers-nuxt-setup.ts
3488
+ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
3489
+ const webAppDir = path.join(projectDir, "apps/web");
3490
+ if (!await fs.pathExists(webAppDir)) return;
3491
+ await addPackageDependency({
3492
+ devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3493
+ projectDir: webAppDir
3494
+ });
3495
+ const pkgPath = path.join(webAppDir, "package.json");
3496
+ if (await fs.pathExists(pkgPath)) {
3497
+ const pkg = await fs.readJson(pkgPath);
3498
+ pkg.scripts = {
3499
+ ...pkg.scripts,
3500
+ deploy: `${packageManager} run build && wrangler deploy`,
3501
+ "cf-typegen": "wrangler types"
3502
+ };
3503
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3504
+ }
3505
+ const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
3506
+ if (!await fs.pathExists(nuxtConfigPath)) return;
3507
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
3508
+ if (!sourceFile) return;
3509
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
3510
+ const expression = expr.getExpression();
3511
+ return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
3512
+ });
3513
+ if (!defineCall) return;
3514
+ const configObj = defineCall.getArguments()[0];
3515
+ if (!configObj) return;
3516
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3517
+ const compatProp = configObj.getProperty("compatibilityDate");
3518
+ if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
3519
+ else configObj.addPropertyAssignment({
3520
+ name: "compatibilityDate",
3521
+ initializer: `'${today}'`
3522
+ });
3523
+ const nitroInitializer = `{
3524
+ preset: "cloudflare_module",
3525
+ cloudflare: {
3526
+ deployConfig: true,
3527
+ nodeCompat: true
3528
+ }
3529
+ }`;
3530
+ const nitroProp = configObj.getProperty("nitro");
3531
+ if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
3532
+ else configObj.addPropertyAssignment({
3533
+ name: "nitro",
3534
+ initializer: nitroInitializer
3535
+ });
3536
+ const modulesProp = configObj.getProperty("modules");
3537
+ if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
3538
+ const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
3539
+ if (arrayExpr) {
3540
+ const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
3541
+ if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
3542
+ }
3543
+ } else configObj.addPropertyAssignment({
3544
+ name: "modules",
3545
+ initializer: "['nitro-cloudflare-dev']"
3546
+ });
3547
+ await tsProject.save();
3548
+ }
3549
+
3550
+ //#endregion
3551
+ //#region src/helpers/deployment/workers/workers-svelte-setup.ts
3552
+ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
3553
+ const webAppDir = path.join(projectDir, "apps/web");
3554
+ if (!await fs.pathExists(webAppDir)) return;
3555
+ await addPackageDependency({
3556
+ devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
2724
3557
  projectDir: webAppDir
2725
3558
  });
2726
3559
  const pkgPath = path.join(webAppDir, "package.json");
@@ -2752,7 +3585,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
2752
3585
  }
2753
3586
 
2754
3587
  //#endregion
2755
- //#region src/helpers/setup/workers-tanstack-start-setup.ts
3588
+ //#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
2756
3589
  async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2757
3590
  const webAppDir = path.join(projectDir, "apps/web");
2758
3591
  if (!await fs.pathExists(webAppDir)) return;
@@ -2790,7 +3623,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2790
3623
  }
2791
3624
 
2792
3625
  //#endregion
2793
- //#region src/helpers/setup/workers-vite-setup.ts
3626
+ //#region src/helpers/deployment/workers/workers-vite-setup.ts
2794
3627
  async function setupWorkersVitePlugin(projectDir) {
2795
3628
  const webAppDir = path.join(projectDir, "apps/web");
2796
3629
  const viteConfigPath = path.join(webAppDir, "vite.config.ts");
@@ -2821,12 +3654,16 @@ async function setupWorkersVitePlugin(projectDir) {
2821
3654
  }
2822
3655
 
2823
3656
  //#endregion
2824
- //#region src/helpers/setup/web-deploy-setup.ts
3657
+ //#region src/helpers/deployment/web-deploy-setup.ts
2825
3658
  async function setupWebDeploy(config) {
2826
- const { webDeploy, frontend, projectDir } = config;
3659
+ const { webDeploy, serverDeploy, frontend, projectDir } = config;
2827
3660
  const { packageManager } = config;
2828
3661
  if (webDeploy === "none") return;
2829
- if (webDeploy !== "workers") return;
3662
+ if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
3663
+ if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
3664
+ await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
3665
+ return;
3666
+ }
2830
3667
  const isNext = frontend.includes("next");
2831
3668
  const isNuxt = frontend.includes("nuxt");
2832
3669
  const isSvelte = frontend.includes("svelte");
@@ -2834,11 +3671,21 @@ async function setupWebDeploy(config) {
2834
3671
  const isTanstackStart = frontend.includes("tanstack-start");
2835
3672
  const isReactRouter = frontend.includes("react-router");
2836
3673
  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);
3674
+ if (webDeploy === "wrangler") {
3675
+ if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
3676
+ else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
3677
+ else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
3678
+ else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
3679
+ else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
3680
+ } else if (webDeploy === "alchemy") {
3681
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3682
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3683
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3684
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3685
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3686
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3687
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3688
+ }
2842
3689
  }
2843
3690
  async function setupWorkersWebDeploy(projectDir, pkgManager) {
2844
3691
  const webAppDir = path.join(projectDir, "apps/web");
@@ -2855,30 +3702,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
2855
3702
  }
2856
3703
  await setupWorkersVitePlugin(projectDir);
2857
3704
  }
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
3705
 
2880
3706
  //#endregion
2881
- //#region src/helpers/project-generation/add-deployment.ts
3707
+ //#region src/helpers/core/add-deployment.ts
2882
3708
  async function addDeploymentToProject(input) {
2883
3709
  try {
2884
3710
  const projectDir = input.projectDir || process.cwd();
@@ -2886,7 +3712,8 @@ async function addDeploymentToProject(input) {
2886
3712
  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
3713
  const detectedConfig = await detectProjectConfig(projectDir);
2888
3714
  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.`);
3715
+ if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
3716
+ if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
2890
3717
  const config = {
2891
3718
  projectName: detectedConfig.projectName || path.basename(projectDir),
2892
3719
  projectDir,
@@ -2903,224 +3730,79 @@ async function addDeploymentToProject(input) {
2903
3730
  packageManager: input.packageManager || detectedConfig.packageManager || "npm",
2904
3731
  install: input.install || false,
2905
3732
  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) {}
3733
+ api: detectedConfig.api || "none",
3734
+ webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
3735
+ serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
3110
3736
  };
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
- }
3737
+ if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
3738
+ if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
3739
+ await setupDeploymentTemplates(projectDir, config);
3740
+ await setupWebDeploy(config);
3741
+ await setupServerDeploy(config);
3742
+ await updateBtsConfig(projectDir, {
3743
+ webDeploy: input.webDeploy || config.webDeploy,
3744
+ serverDeploy: input.serverDeploy || config.serverDeploy
3745
+ });
3746
+ if (config.install) await installDependencies({
3747
+ projectDir,
3748
+ packageManager: config.packageManager
3749
+ });
3750
+ else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
3751
+ } catch (error) {
3752
+ const message = error instanceof Error ? error.message : String(error);
3753
+ exitWithError(`Error adding deployment: ${message}`);
3119
3754
  }
3120
3755
  }
3121
3756
 
3122
3757
  //#endregion
3123
- //#region src/helpers/setup/auth-setup.ts
3758
+ //#region src/utils/format-with-biome.ts
3759
+ async function formatProjectWithBiome(projectDir) {
3760
+ const biome = new Biome();
3761
+ const { projectKey } = biome.openProject(projectDir);
3762
+ biome.applyConfiguration(projectKey, {
3763
+ formatter: {
3764
+ enabled: true,
3765
+ indentStyle: "tab"
3766
+ },
3767
+ javascript: { formatter: { quoteStyle: "double" } }
3768
+ });
3769
+ const files = await glob("**/*", {
3770
+ cwd: projectDir,
3771
+ dot: true,
3772
+ absolute: true,
3773
+ onlyFiles: true
3774
+ });
3775
+ for (const filePath of files) try {
3776
+ const ext = path.extname(filePath).toLowerCase();
3777
+ const supported = new Set([
3778
+ ".ts",
3779
+ ".tsx",
3780
+ ".js",
3781
+ ".jsx",
3782
+ ".cjs",
3783
+ ".mjs",
3784
+ ".cts",
3785
+ ".mts",
3786
+ ".json",
3787
+ ".jsonc",
3788
+ ".md",
3789
+ ".mdx",
3790
+ ".css",
3791
+ ".scss",
3792
+ ".html"
3793
+ ]);
3794
+ if (!supported.has(ext)) continue;
3795
+ const original = await fs.readFile(filePath, "utf8");
3796
+ const result = biome.formatContent(projectKey, original, { filePath });
3797
+ const content = result?.content;
3798
+ if (typeof content !== "string") continue;
3799
+ if (content.length === 0 && original.length > 0) continue;
3800
+ if (content !== original) await fs.writeFile(filePath, content);
3801
+ } catch {}
3802
+ }
3803
+
3804
+ //#endregion
3805
+ //#region src/helpers/addons/auth-setup.ts
3124
3806
  async function setupAuth(config) {
3125
3807
  const { auth, frontend, backend, projectDir } = config;
3126
3808
  if (backend === "convex" || !auth) return;
@@ -3172,7 +3854,217 @@ function generateAuthSecret(length = 32) {
3172
3854
  }
3173
3855
 
3174
3856
  //#endregion
3175
- //#region src/helpers/setup/backend-setup.ts
3857
+ //#region src/helpers/addons/examples-setup.ts
3858
+ async function setupExamples(config) {
3859
+ const { examples, frontend, backend, projectDir } = config;
3860
+ if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
3861
+ if (examples.includes("ai")) {
3862
+ const webClientDir = path.join(projectDir, "apps/web");
3863
+ const nativeClientDir = path.join(projectDir, "apps/native");
3864
+ const serverDir = path.join(projectDir, "apps/server");
3865
+ const webClientDirExists = await fs.pathExists(webClientDir);
3866
+ const nativeClientDirExists = await fs.pathExists(nativeClientDir);
3867
+ const serverDirExists = await fs.pathExists(serverDir);
3868
+ const hasNuxt = frontend.includes("nuxt");
3869
+ const hasSvelte = frontend.includes("svelte");
3870
+ const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
3871
+ const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3872
+ if (webClientDirExists) {
3873
+ const dependencies = ["ai"];
3874
+ if (hasNuxt) dependencies.push("@ai-sdk/vue");
3875
+ else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
3876
+ else if (hasReactWeb) dependencies.push("@ai-sdk/react");
3877
+ await addPackageDependency({
3878
+ dependencies,
3879
+ projectDir: webClientDir
3880
+ });
3881
+ }
3882
+ if (nativeClientDirExists && hasReactNative) await addPackageDependency({
3883
+ dependencies: ["ai", "@ai-sdk/react"],
3884
+ projectDir: nativeClientDir
3885
+ });
3886
+ if (serverDirExists && backend !== "none") await addPackageDependency({
3887
+ dependencies: ["ai", "@ai-sdk/google"],
3888
+ projectDir: serverDir
3889
+ });
3890
+ }
3891
+ }
3892
+
3893
+ //#endregion
3894
+ //#region src/helpers/core/api-setup.ts
3895
+ async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
3896
+ const pkgJsonPath = path.join(projectDir, "package.json");
3897
+ try {
3898
+ const pkgJson = await fs.readJson(pkgJsonPath);
3899
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
3900
+ pkgJson.dependencies[backendPackageName] = workspaceVersion;
3901
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
3902
+ } catch (_error) {}
3903
+ }
3904
+ function getFrontendType(frontend) {
3905
+ const reactBasedFrontends = [
3906
+ "tanstack-router",
3907
+ "react-router",
3908
+ "tanstack-start",
3909
+ "next"
3910
+ ];
3911
+ const nativeFrontends = ["native-nativewind", "native-unistyles"];
3912
+ return {
3913
+ hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
3914
+ hasNuxtWeb: frontend.includes("nuxt"),
3915
+ hasSvelteWeb: frontend.includes("svelte"),
3916
+ hasSolidWeb: frontend.includes("solid"),
3917
+ hasNative: frontend.some((f) => nativeFrontends.includes(f))
3918
+ };
3919
+ }
3920
+ function getApiDependencies(api, frontendType) {
3921
+ const deps = {};
3922
+ if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
3923
+ else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
3924
+ if (frontendType.hasReactWeb) {
3925
+ if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3926
+ else if (api === "trpc") deps.web = { dependencies: [
3927
+ "@trpc/tanstack-react-query",
3928
+ "@trpc/client",
3929
+ "@trpc/server"
3930
+ ] };
3931
+ } else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
3932
+ dependencies: [
3933
+ "@tanstack/vue-query",
3934
+ "@orpc/tanstack-query",
3935
+ "@orpc/client"
3936
+ ],
3937
+ devDependencies: ["@tanstack/vue-query-devtools"]
3938
+ };
3939
+ else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
3940
+ dependencies: [
3941
+ "@orpc/tanstack-query",
3942
+ "@orpc/client",
3943
+ "@tanstack/svelte-query"
3944
+ ],
3945
+ devDependencies: ["@tanstack/svelte-query-devtools"]
3946
+ };
3947
+ else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
3948
+ dependencies: [
3949
+ "@orpc/tanstack-query",
3950
+ "@orpc/client",
3951
+ "@tanstack/solid-query"
3952
+ ],
3953
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3954
+ };
3955
+ if (api === "trpc") deps.native = { dependencies: [
3956
+ "@trpc/tanstack-react-query",
3957
+ "@trpc/client",
3958
+ "@trpc/server"
3959
+ ] };
3960
+ else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3961
+ return deps;
3962
+ }
3963
+ function getQueryDependencies(frontend) {
3964
+ const reactBasedFrontends = [
3965
+ "react-router",
3966
+ "tanstack-router",
3967
+ "tanstack-start",
3968
+ "next",
3969
+ "native-nativewind",
3970
+ "native-unistyles"
3971
+ ];
3972
+ const deps = {};
3973
+ const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
3974
+ if (needsReactQuery) {
3975
+ const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
3976
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3977
+ if (hasReactWeb) deps.web = {
3978
+ dependencies: ["@tanstack/react-query"],
3979
+ devDependencies: ["@tanstack/react-query-devtools"]
3980
+ };
3981
+ if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
3982
+ }
3983
+ if (frontend.includes("solid")) deps.web = {
3984
+ dependencies: ["@tanstack/solid-query"],
3985
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3986
+ };
3987
+ return deps;
3988
+ }
3989
+ function getConvexDependencies(frontend) {
3990
+ const deps = {
3991
+ web: { dependencies: ["convex"] },
3992
+ native: { dependencies: ["convex"] }
3993
+ };
3994
+ if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
3995
+ if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
3996
+ if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
3997
+ return deps;
3998
+ }
3999
+ async function setupApi(config) {
4000
+ const { api, projectName, frontend, backend, packageManager, projectDir } = config;
4001
+ const isConvex = backend === "convex";
4002
+ const webDir = path.join(projectDir, "apps/web");
4003
+ const nativeDir = path.join(projectDir, "apps/native");
4004
+ const serverDir = path.join(projectDir, "apps/server");
4005
+ const webDirExists = await fs.pathExists(webDir);
4006
+ const nativeDirExists = await fs.pathExists(nativeDir);
4007
+ const serverDirExists = await fs.pathExists(serverDir);
4008
+ const frontendType = getFrontendType(frontend);
4009
+ if (!isConvex && api !== "none") {
4010
+ const apiDeps = getApiDependencies(api, frontendType);
4011
+ if (serverDirExists && apiDeps.server) {
4012
+ await addPackageDependency({
4013
+ dependencies: apiDeps.server.dependencies,
4014
+ projectDir: serverDir
4015
+ });
4016
+ if (api === "trpc") {
4017
+ if (backend === "hono") await addPackageDependency({
4018
+ dependencies: ["@hono/trpc-server"],
4019
+ projectDir: serverDir
4020
+ });
4021
+ else if (backend === "elysia") await addPackageDependency({
4022
+ dependencies: ["@elysiajs/trpc"],
4023
+ projectDir: serverDir
4024
+ });
4025
+ }
4026
+ }
4027
+ if (webDirExists && apiDeps.web) await addPackageDependency({
4028
+ dependencies: apiDeps.web.dependencies,
4029
+ devDependencies: apiDeps.web.devDependencies,
4030
+ projectDir: webDir
4031
+ });
4032
+ if (nativeDirExists && apiDeps.native) await addPackageDependency({
4033
+ dependencies: apiDeps.native.dependencies,
4034
+ projectDir: nativeDir
4035
+ });
4036
+ }
4037
+ if (!isConvex) {
4038
+ const queryDeps = getQueryDependencies(frontend);
4039
+ if (webDirExists && queryDeps.web) await addPackageDependency({
4040
+ dependencies: queryDeps.web.dependencies,
4041
+ devDependencies: queryDeps.web.devDependencies,
4042
+ projectDir: webDir
4043
+ });
4044
+ if (nativeDirExists && queryDeps.native) await addPackageDependency({
4045
+ dependencies: queryDeps.native.dependencies,
4046
+ projectDir: nativeDir
4047
+ });
4048
+ }
4049
+ if (isConvex) {
4050
+ const convexDeps = getConvexDependencies(frontend);
4051
+ if (webDirExists) await addPackageDependency({
4052
+ dependencies: convexDeps.web.dependencies,
4053
+ projectDir: webDir
4054
+ });
4055
+ if (nativeDirExists) await addPackageDependency({
4056
+ dependencies: convexDeps.native.dependencies,
4057
+ projectDir: nativeDir
4058
+ });
4059
+ const backendPackageName = `@${projectName}/backend`;
4060
+ const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
4061
+ if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
4062
+ if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
4063
+ }
4064
+ }
4065
+
4066
+ //#endregion
4067
+ //#region src/helpers/core/backend-setup.ts
3176
4068
  async function setupBackendDependencies(config) {
3177
4069
  const { backend, runtime, api, projectDir } = config;
3178
4070
  if (backend === "convex") return;
@@ -3211,7 +4103,7 @@ async function setupBackendDependencies(config) {
3211
4103
  }
3212
4104
 
3213
4105
  //#endregion
3214
- //#region src/helpers/project-generation/env-setup.ts
4106
+ //#region src/helpers/core/env-setup.ts
3215
4107
  async function addEnvVariablesToFile(filePath, variables) {
3216
4108
  await fs.ensureDir(path.dirname(filePath));
3217
4109
  let envContent = "";
@@ -3259,7 +4151,7 @@ async function addEnvVariablesToFile(filePath, variables) {
3259
4151
  if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
3260
4152
  }
3261
4153
  async function setupEnvironmentVariables(config) {
3262
- const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
4154
+ const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
3263
4155
  const hasReactRouter = frontend.includes("react-router");
3264
4156
  const hasTanStackRouter = frontend.includes("tanstack-router");
3265
4157
  const hasTanStackStart = frontend.includes("tanstack-start");
@@ -3359,39 +4251,69 @@ async function setupEnvironmentVariables(config) {
3359
4251
  }
3360
4252
  ];
3361
4253
  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) {}
4254
+ const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
4255
+ const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
4256
+ if (isUnifiedAlchemy) {
4257
+ const rootEnvPath = path.join(projectDir, ".env");
4258
+ const rootAlchemyVars = [{
4259
+ key: "ALCHEMY_PASSWORD",
4260
+ value: "please-change-this",
4261
+ condition: true
4262
+ }];
4263
+ await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
4264
+ } else if (isIndividualAlchemy) {
4265
+ if (webDeploy === "alchemy") {
4266
+ const webDir = path.join(projectDir, "apps/web");
4267
+ if (await fs.pathExists(webDir)) {
4268
+ const webAlchemyVars = [{
4269
+ key: "ALCHEMY_PASSWORD",
4270
+ value: "please-change-this",
4271
+ condition: true
4272
+ }];
4273
+ await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
4274
+ }
4275
+ }
4276
+ if (serverDeploy === "alchemy") {
4277
+ const serverDir$1 = path.join(projectDir, "apps/server");
4278
+ if (await fs.pathExists(serverDir$1)) {
4279
+ const serverAlchemyVars = [{
4280
+ key: "ALCHEMY_PASSWORD",
4281
+ value: "please-change-this",
4282
+ condition: true
4283
+ }];
4284
+ await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
4285
+ }
4286
+ }
3367
4287
  }
3368
4288
  }
3369
4289
 
3370
4290
  //#endregion
3371
4291
  //#region src/helpers/database-providers/d1-setup.ts
3372
4292
  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) {}
4293
+ const { projectDir, serverDeploy } = config;
4294
+ if (serverDeploy === "wrangler") {
4295
+ const envPath = path.join(projectDir, "apps/server", ".env");
4296
+ const variables = [
4297
+ {
4298
+ key: "CLOUDFLARE_ACCOUNT_ID",
4299
+ value: "",
4300
+ condition: true
4301
+ },
4302
+ {
4303
+ key: "CLOUDFLARE_DATABASE_ID",
4304
+ value: "",
4305
+ condition: true
4306
+ },
4307
+ {
4308
+ key: "CLOUDFLARE_D1_TOKEN",
4309
+ value: "",
4310
+ condition: true
4311
+ }
4312
+ ];
4313
+ try {
4314
+ await addEnvVariablesToFile(envPath, variables);
4315
+ } catch (_err) {}
4316
+ }
3395
4317
  }
3396
4318
 
3397
4319
  //#endregion
@@ -3839,8 +4761,10 @@ async function setupPrismaPostgres(config) {
3839
4761
  else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
3840
4762
  if (prismaConfig) {
3841
4763
  await writeEnvFile$1(projectDir, prismaConfig);
3842
- await addDotenvImportToPrismaConfig(projectDir);
3843
- if (orm === "prisma") await addPrismaAccelerateExtension(serverDir);
4764
+ if (orm === "prisma") {
4765
+ await addDotenvImportToPrismaConfig(projectDir);
4766
+ await addPrismaAccelerateExtension(serverDir);
4767
+ }
3844
4768
  log.success(pc.green("Prisma Postgres database configured successfully!"));
3845
4769
  } else {
3846
4770
  await writeEnvFile$1(projectDir);
@@ -4185,7 +5109,7 @@ async function setupTurso(config) {
4185
5109
  }
4186
5110
 
4187
5111
  //#endregion
4188
- //#region src/helpers/setup/db-setup.ts
5112
+ //#region src/helpers/core/db-setup.ts
4189
5113
  async function setupDatabase(config) {
4190
5114
  const { database, orm, dbSetup, backend, projectDir } = config;
4191
5115
  if (backend === "convex" || database === "none") {
@@ -4250,44 +5174,7 @@ async function setupDatabase(config) {
4250
5174
  }
4251
5175
 
4252
5176
  //#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
5177
+ //#region src/helpers/core/runtime-setup.ts
4291
5178
  async function setupRuntime(config) {
4292
5179
  const { runtime, backend, projectDir } = config;
4293
5180
  if (backend === "convex" || backend === "next" || runtime === "none") return;
@@ -4295,23 +5182,6 @@ async function setupRuntime(config) {
4295
5182
  if (!await fs.pathExists(serverDir)) return;
4296
5183
  if (runtime === "bun") await setupBunRuntime(serverDir, backend);
4297
5184
  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
5185
  }
4316
5186
  async function setupBunRuntime(serverDir, _backend) {
4317
5187
  const packageJsonPath = path.join(serverDir, "package.json");
@@ -4351,27 +5221,20 @@ async function setupNodeRuntime(serverDir, backend) {
4351
5221
  projectDir: serverDir
4352
5222
  });
4353
5223
  }
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
5224
+
5225
+ //#endregion
5226
+ //#region src/helpers/core/convex-codegen.ts
5227
+ async function runConvexCodegen(projectDir, packageManager) {
5228
+ const backendDir = path.join(projectDir, "packages/backend");
5229
+ const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
5230
+ await execa(cmd, {
5231
+ cwd: backendDir,
5232
+ shell: true
4370
5233
  });
4371
5234
  }
4372
5235
 
4373
5236
  //#endregion
4374
- //#region src/helpers/project-generation/create-readme.ts
5237
+ //#region src/helpers/core/create-readme.ts
4375
5238
  async function createReadme(projectDir, options) {
4376
5239
  const readmePath = path.join(projectDir, "README.md");
4377
5240
  const content = generateReadmeContent(options);
@@ -4648,7 +5511,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
4648
5511
  }
4649
5512
 
4650
5513
  //#endregion
4651
- //#region src/helpers/project-generation/git.ts
5514
+ //#region src/helpers/core/git.ts
4652
5515
  async function initializeGit(projectDir, useGit) {
4653
5516
  if (!useGit) return;
4654
5517
  const gitVersionResult = await $({
@@ -4724,20 +5587,21 @@ async function getDockerStatus(database) {
4724
5587
  }
4725
5588
 
4726
5589
  //#endregion
4727
- //#region src/helpers/project-generation/post-installation.ts
5590
+ //#region src/helpers/core/post-installation.ts
4728
5591
  async function displayPostInstallInstructions(config) {
4729
- const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
5592
+ const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
4730
5593
  const isConvex = backend === "convex";
4731
5594
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
4732
5595
  const cdCmd = `cd ${relativePath}`;
4733
5596
  const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
4734
- const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
5597
+ const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
4735
5598
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
4736
5599
  const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
4737
5600
  const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
4738
5601
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
4739
5602
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
4740
- const workersDeployInstructions = webDeploy === "workers" ? getWorkersDeployInstructions(runCmd) : "";
5603
+ const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
5604
+ const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
4741
5605
  const hasWeb = frontend?.some((f) => [
4742
5606
  "tanstack-router",
4743
5607
  "react-router",
@@ -4766,8 +5630,8 @@ async function displayPostInstallInstructions(config) {
4766
5630
  if (runtime === "workers") {
4767
5631
  if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
4768
5632
  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";
5633
+ if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
5634
+ }
4771
5635
  }
4772
5636
  output += `${pc.bold("Your project will be available at:")}\n`;
4773
5637
  if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
@@ -4780,7 +5644,8 @@ async function displayPostInstallInstructions(config) {
4780
5644
  if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
4781
5645
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
4782
5646
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
4783
- if (workersDeployInstructions) output += `\n${workersDeployInstructions.trim()}\n`;
5647
+ if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
5648
+ if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
4784
5649
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
4785
5650
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
4786
5651
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -4801,7 +5666,7 @@ function getNativeInstructions(isConvex) {
4801
5666
  function getLintingInstructions(runCmd) {
4802
5667
  return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
4803
5668
  }
4804
- async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) {
5669
+ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
4805
5670
  const instructions = [];
4806
5671
  if (dbSetup === "docker") {
4807
5672
  const dockerStatus = await getDockerStatus(database);
@@ -4810,7 +5675,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4810
5675
  instructions.push("");
4811
5676
  }
4812
5677
  }
4813
- if (runtime === "workers" && dbSetup === "d1") {
5678
+ if (serverDeploy === "wrangler" && dbSetup === "d1") {
4814
5679
  const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
4815
5680
  instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
4816
5681
  instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
@@ -4818,8 +5683,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4818
5683
  instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
4819
5684
  instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
4820
5685
  instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
4821
- instructions.push("");
4822
5686
  }
5687
+ if (dbSetup === "d1" && serverDeploy === "alchemy") instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
4823
5688
  if (orm === "prisma") {
4824
5689
  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
5690
  if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
@@ -4828,7 +5693,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4828
5693
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4829
5694
  } else if (orm === "drizzle") {
4830
5695
  if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
4831
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
5696
+ if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
4832
5697
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4833
5698
  if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
4834
5699
  } else if (orm === "mongoose") {
@@ -4851,12 +5716,22 @@ function getNoOrmWarning() {
4851
5716
  function getBunWebNativeWarning() {
4852
5717
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
4853
5718
  }
4854
- function getWorkersDeployInstructions(runCmd) {
4855
- return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
5719
+ function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
5720
+ const instructions = [];
5721
+ if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
5722
+ if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
5723
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
5724
+ }
5725
+ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
5726
+ const instructions = [];
5727
+ if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
5728
+ else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
5729
+ else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
5730
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
4856
5731
  }
4857
5732
 
4858
5733
  //#endregion
4859
- //#region src/helpers/project-generation/project-config.ts
5734
+ //#region src/helpers/core/project-config.ts
4860
5735
  async function updatePackageConfigurations(projectDir, options) {
4861
5736
  await updateRootPackageJson(projectDir, options);
4862
5737
  if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
@@ -5037,7 +5912,7 @@ async function updateConvexPackageJson(projectDir, options) {
5037
5912
  }
5038
5913
 
5039
5914
  //#endregion
5040
- //#region src/helpers/project-generation/create-project.ts
5915
+ //#region src/helpers/core/create-project.ts
5041
5916
  async function createProject(options) {
5042
5917
  const projectDir = options.projectDir;
5043
5918
  const isConvex = options.backend === "convex";
@@ -5065,18 +5940,18 @@ async function createProject(options) {
5065
5940
  if (!isConvex && options.auth) await setupAuth(options);
5066
5941
  await handleExtras(projectDir, options);
5067
5942
  await setupWebDeploy(options);
5943
+ await setupServerDeploy(options);
5068
5944
  await setupEnvironmentVariables(options);
5069
5945
  await updatePackageConfigurations(projectDir, options);
5070
5946
  await createReadme(projectDir, options);
5071
5947
  await writeBtsConfig(options);
5948
+ await formatProjectWithBiome(projectDir);
5949
+ if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
5072
5950
  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
- }
5951
+ if (options.install) await installDependencies({
5952
+ projectDir,
5953
+ packageManager: options.packageManager
5954
+ });
5080
5955
  await initializeGit(projectDir, options.git);
5081
5956
  await displayPostInstallInstructions({
5082
5957
  ...options,
@@ -5095,7 +5970,7 @@ async function createProject(options) {
5095
5970
  }
5096
5971
 
5097
5972
  //#endregion
5098
- //#region src/helpers/project-generation/command-handlers.ts
5973
+ //#region src/helpers/core/command-handlers.ts
5099
5974
  async function createProjectHandler(input) {
5100
5975
  const startTime = Date.now();
5101
5976
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
@@ -5105,10 +5980,11 @@ async function createProjectHandler(input) {
5105
5980
  let currentPathInput;
5106
5981
  if (input.yes && input.projectName) currentPathInput = input.projectName;
5107
5982
  else if (input.yes) {
5108
- let defaultName = DEFAULT_CONFIG.relativePath;
5983
+ const defaultConfig = getDefaultConfig();
5984
+ let defaultName = defaultConfig.relativePath;
5109
5985
  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}`;
5986
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
5987
+ defaultName = `${defaultConfig.projectName}-${counter}`;
5112
5988
  counter++;
5113
5989
  }
5114
5990
  currentPathInput = defaultName;
@@ -5146,7 +6022,8 @@ async function createProjectHandler(input) {
5146
6022
  install: false,
5147
6023
  dbSetup: "none",
5148
6024
  api: "none",
5149
- webDeploy: "none"
6025
+ webDeploy: "none",
6026
+ serverDeploy: "none"
5150
6027
  },
5151
6028
  reproducibleCommand: "",
5152
6029
  timeScaffolded,
@@ -5162,28 +6039,33 @@ async function createProjectHandler(input) {
5162
6039
  projectDirectory: input.projectName
5163
6040
  };
5164
6041
  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
6042
  let config;
5173
6043
  if (input.yes) {
6044
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
5174
6045
  config = {
5175
- ...DEFAULT_CONFIG,
6046
+ ...getDefaultConfig(),
5176
6047
  ...flagConfig,
5177
6048
  projectName: finalBaseName,
5178
6049
  projectDir: finalResolvedPath,
5179
6050
  relativePath: finalPathInput
5180
6051
  };
6052
+ coerceBackendPresets(config);
6053
+ validateConfigCompatibility(config, providedFlags, cliInput);
5181
6054
  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
6055
  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
6056
  log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
5184
6057
  log.message(displayConfig(config));
5185
6058
  log.message("");
5186
- } else config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6059
+ } else {
6060
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
6061
+ const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
6062
+ if (Object.keys(otherFlags).length > 0) {
6063
+ log.info(pc.yellow("Using these pre-selected options:"));
6064
+ log.message(displayConfig(otherFlags));
6065
+ log.message("");
6066
+ }
6067
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6068
+ }
5187
6069
  await createProject(config);
5188
6070
  const reproducibleCommand = generateReproducibleCommand(config);
5189
6071
  log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -5203,11 +6085,11 @@ async function createProjectHandler(input) {
5203
6085
  }
5204
6086
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
5205
6087
  const currentPath = path.resolve(process.cwd(), currentPathInput);
5206
- if (!fs.pathExistsSync(currentPath)) return {
6088
+ if (!await fs.pathExists(currentPath)) return {
5207
6089
  finalPathInput: currentPathInput,
5208
6090
  shouldClearDirectory: false
5209
6091
  };
5210
- const dirContents = fs.readdirSync(currentPath);
6092
+ const dirContents = await fs.readdir(currentPath);
5211
6093
  const isNotEmpty = dirContents.length > 0;
5212
6094
  if (!isNotEmpty) return {
5213
6095
  finalPathInput: currentPathInput,
@@ -5226,7 +6108,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5226
6108
  let counter = 1;
5227
6109
  const baseName = currentPathInput;
5228
6110
  let finalPathInput = `${baseName}-${counter}`;
5229
- while (fs.pathExistsSync(path.resolve(process.cwd(), finalPathInput)) && fs.readdirSync(path.resolve(process.cwd(), finalPathInput)).length > 0) {
6111
+ while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
5230
6112
  counter++;
5231
6113
  finalPathInput = `${baseName}-${counter}`;
5232
6114
  }
@@ -5252,6 +6134,10 @@ async function addAddonsHandler(input) {
5252
6134
  const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
5253
6135
  if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
5254
6136
  }
6137
+ if (!input.serverDeploy) {
6138
+ const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
6139
+ if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
6140
+ }
5255
6141
  const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
5256
6142
  let somethingAdded = false;
5257
6143
  if (input.addons && input.addons.length > 0) {
@@ -5272,6 +6158,15 @@ async function addAddonsHandler(input) {
5272
6158
  });
5273
6159
  somethingAdded = true;
5274
6160
  }
6161
+ if (input.serverDeploy && input.serverDeploy !== "none") {
6162
+ await addDeploymentToProject({
6163
+ ...input,
6164
+ install: false,
6165
+ suppressInstallMessage: true,
6166
+ serverDeploy: input.serverDeploy
6167
+ });
6168
+ somethingAdded = true;
6169
+ }
5275
6170
  if (!somethingAdded) {
5276
6171
  outro(pc.yellow("No addons or deployment configurations to add."));
5277
6172
  return;
@@ -5375,6 +6270,7 @@ const router = t.router({
5375
6270
  runtime: RuntimeSchema.optional(),
5376
6271
  api: APISchema.optional(),
5377
6272
  webDeploy: WebDeploySchema.optional(),
6273
+ serverDeploy: ServerDeploySchema.optional(),
5378
6274
  directoryConflict: DirectoryConflictSchema.optional(),
5379
6275
  renderTitle: z.boolean().optional(),
5380
6276
  disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
@@ -5390,6 +6286,7 @@ const router = t.router({
5390
6286
  add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
5391
6287
  addons: z.array(AddonsSchema).optional().default([]),
5392
6288
  webDeploy: WebDeploySchema.optional(),
6289
+ serverDeploy: ServerDeploySchema.optional(),
5393
6290
  projectDir: z.string().optional(),
5394
6291
  install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
5395
6292
  packageManager: PackageManagerSchema.optional()