create-better-t-stack 2.33.8 → 2.33.9-canary.2ec142a9

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 (34) 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-DlHyCY_I.js} +1466 -562
  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/db/drizzle/sqlite/drizzle.config.ts.hbs +2 -0
  15. package/templates/deploy/alchemy/alchemy.run.ts.hbs +208 -0
  16. package/templates/deploy/alchemy/env.d.ts.hbs +20 -0
  17. package/templates/deploy/alchemy/wrangler.jsonc.hbs +11 -0
  18. package/templates/deploy/{web → wrangler/web}/nuxt/wrangler.jsonc.hbs +1 -1
  19. package/templates/deploy/{web → wrangler/web}/react/next/wrangler.jsonc.hbs +1 -1
  20. package/templates/deploy/{web → wrangler/web}/react/react-router/wrangler.jsonc.hbs +1 -1
  21. package/templates/deploy/{web → wrangler/web}/react/tanstack-router/wrangler.jsonc.hbs +1 -1
  22. package/templates/deploy/{web → wrangler/web}/react/tanstack-start/wrangler.jsonc.hbs +1 -1
  23. package/templates/deploy/{web → wrangler/web}/solid/wrangler.jsonc.hbs +1 -1
  24. package/templates/deploy/{web → wrangler/web}/svelte/wrangler.jsonc.hbs +1 -1
  25. package/templates/frontend/nuxt/_gitignore +3 -0
  26. package/templates/frontend/nuxt/tsconfig.json.hbs +1 -3
  27. package/templates/frontend/react/web-base/_gitignore +1 -0
  28. package/templates/frontend/react/web-base/src/components/header.tsx.hbs +0 -1
  29. package/templates/frontend/solid/_gitignore +1 -0
  30. package/templates/frontend/solid/package.json.hbs +0 -1
  31. package/templates/frontend/svelte/_gitignore +1 -0
  32. package/templates/frontend/svelte/package.json.hbs +11 -13
  33. /package/templates/{runtime/workers/apps → deploy/wrangler}/server/wrangler.jsonc.hbs +0 -0
  34. /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,189 @@ 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);
1533
1710
  }
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.");
1536
- validateWorkersCompatibility(providedFlags, options, config);
1537
- const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
1711
+ const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
1538
1712
  validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
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.");
1717
+ }
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);
1726
+ validateWorkersCompatibility(providedFlags, options, config);
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
+ if (options.yolo) {
1769
+ const cfg = processFlags(options, projectName);
1770
+ const validatedProjectName$1 = extractAndValidateProjectName(projectName, options.projectDirectory, true);
1771
+ if (validatedProjectName$1) cfg.projectName = validatedProjectName$1;
1772
+ return cfg;
1773
+ }
1774
+ try {
1775
+ validateArrayOptions(options);
1776
+ } catch (error) {
1777
+ exitWithError(error instanceof Error ? error.message : String(error));
1778
+ }
1779
+ const config = processFlags(options, projectName);
1780
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
1781
+ if (validatedProjectName) config.projectName = validatedProjectName;
1782
+ validateFullConfig(config, providedFlags, options);
1539
1783
  return config;
1540
1784
  }
1541
- function getProvidedFlags(options) {
1542
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1785
+ function processProvidedFlagsWithoutValidation(options, projectName) {
1786
+ const config = processFlags(options, projectName);
1787
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
1788
+ if (validatedProjectName) config.projectName = validatedProjectName;
1789
+ return config;
1790
+ }
1791
+ function validateConfigCompatibility(config, providedFlags, options) {
1792
+ if (options?.yolo) return;
1793
+ if (options && providedFlags) validateFullConfig(config, providedFlags, options);
1794
+ else validateConfigForProgrammaticUse(config);
1543
1795
  }
1544
1796
 
1545
1797
  //#endregion
@@ -1560,7 +1812,8 @@ async function writeBtsConfig(projectConfig) {
1560
1812
  packageManager: projectConfig.packageManager,
1561
1813
  dbSetup: projectConfig.dbSetup,
1562
1814
  api: projectConfig.api,
1563
- webDeploy: projectConfig.webDeploy
1815
+ webDeploy: projectConfig.webDeploy,
1816
+ serverDeploy: projectConfig.serverDeploy
1564
1817
  };
1565
1818
  const baseContent = {
1566
1819
  $schema: "https://r2.better-t-stack.dev/schema.json",
@@ -1577,7 +1830,8 @@ async function writeBtsConfig(projectConfig) {
1577
1830
  packageManager: btsConfig.packageManager,
1578
1831
  dbSetup: btsConfig.dbSetup,
1579
1832
  api: btsConfig.api,
1580
- webDeploy: btsConfig.webDeploy
1833
+ webDeploy: btsConfig.webDeploy,
1834
+ serverDeploy: btsConfig.serverDeploy
1581
1835
  };
1582
1836
  let configContent = JSON.stringify(baseContent);
1583
1837
  const formatResult = JSONC.format(configContent, void 0, {
@@ -1670,7 +1924,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
1670
1924
  }
1671
1925
 
1672
1926
  //#endregion
1673
- //#region src/helpers/setup/fumadocs-setup.ts
1927
+ //#region src/helpers/addons/fumadocs-setup.ts
1674
1928
  const TEMPLATES = {
1675
1929
  "next-mdx": {
1676
1930
  label: "Next.js: Fumadocs MDX",
@@ -1757,9 +2011,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
1757
2011
  handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
1758
2012
 
1759
2013
  //#endregion
1760
- //#region src/helpers/project-generation/template-manager.ts
2014
+ //#region src/helpers/core/template-manager.ts
1761
2015
  async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
1762
- const sourceFiles = await globby(sourcePattern, {
2016
+ const sourceFiles = await glob(sourcePattern, {
1763
2017
  cwd: baseSourceDir,
1764
2018
  dot: true,
1765
2019
  onlyFiles: true,
@@ -2073,10 +2327,6 @@ async function handleExtras(projectDir, context) {
2073
2327
  const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
2074
2328
  if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
2075
2329
  }
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
2330
  }
2081
2331
  async function setupDockerComposeTemplates(projectDir, context) {
2082
2332
  if (context.dbSetup !== "docker" || context.database === "none") return;
@@ -2085,29 +2335,62 @@ async function setupDockerComposeTemplates(projectDir, context) {
2085
2335
  if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
2086
2336
  }
2087
2337
  async function setupDeploymentTemplates(projectDir, context) {
2088
- if (context.webDeploy === "none") return;
2089
- if (context.webDeploy === "workers") {
2338
+ if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
2339
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2340
+ if (await fs.pathExists(alchemyTemplateSrc)) {
2341
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
2342
+ const serverAppDir = path.join(projectDir, "apps/server");
2343
+ if (await fs.pathExists(serverAppDir)) {
2344
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2345
+ await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
2346
+ }
2347
+ }
2348
+ } else {
2349
+ if (context.webDeploy === "alchemy") {
2350
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2351
+ const webAppDir = path.join(projectDir, "apps/web");
2352
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2353
+ }
2354
+ if (context.serverDeploy === "alchemy") {
2355
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2356
+ const serverAppDir = path.join(projectDir, "apps/server");
2357
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2358
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2359
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2360
+ await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
2361
+ }
2362
+ }
2363
+ }
2364
+ if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
2090
2365
  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);
2366
+ if (await fs.pathExists(webAppDir)) {
2367
+ const frontends = context.frontend;
2368
+ const templateMap = {
2369
+ "tanstack-router": "react/tanstack-router",
2370
+ "tanstack-start": "react/tanstack-start",
2371
+ "react-router": "react/react-router",
2372
+ solid: "solid",
2373
+ next: "react/next",
2374
+ nuxt: "nuxt",
2375
+ svelte: "svelte"
2376
+ };
2377
+ for (const f of frontends) if (templateMap[f]) {
2378
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
2379
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2380
+ }
2381
+ }
2382
+ }
2383
+ if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
2384
+ const serverAppDir = path.join(projectDir, "apps/server");
2385
+ if (await fs.pathExists(serverAppDir)) {
2386
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
2387
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
2105
2388
  }
2106
2389
  }
2107
2390
  }
2108
2391
 
2109
2392
  //#endregion
2110
- //#region src/helpers/setup/ruler-setup.ts
2393
+ //#region src/helpers/addons/ruler-setup.ts
2111
2394
  async function setupVibeRules(config) {
2112
2395
  const { packageManager, projectDir } = config;
2113
2396
  try {
@@ -2189,7 +2472,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2189
2472
  }
2190
2473
 
2191
2474
  //#endregion
2192
- //#region src/helpers/setup/starlight-setup.ts
2475
+ //#region src/helpers/addons/starlight-setup.ts
2193
2476
  async function setupStarlight(config) {
2194
2477
  const { packageManager, projectDir } = config;
2195
2478
  const s = spinner();
@@ -2221,7 +2504,7 @@ async function setupStarlight(config) {
2221
2504
  }
2222
2505
 
2223
2506
  //#endregion
2224
- //#region src/helpers/setup/tauri-setup.ts
2507
+ //#region src/helpers/addons/tauri-setup.ts
2225
2508
  async function setupTauri(config) {
2226
2509
  const { packageManager, frontend, projectDir } = config;
2227
2510
  const s = spinner();
@@ -2258,8 +2541,8 @@ async function setupTauri(config) {
2258
2541
  `--window-title=${path.basename(projectDir)}`,
2259
2542
  `--frontend-dist=${frontendDist}`,
2260
2543
  `--dev-url=${devUrl}`,
2261
- `--before-dev-command=\"${packageManager} run dev\"`,
2262
- `--before-build-command=\"${packageManager} run build\"`
2544
+ `--before-dev-command="${packageManager} run dev"`,
2545
+ `--before-build-command="${packageManager} run build"`
2263
2546
  ];
2264
2547
  const tauriArgsString = tauriArgs.join(" ");
2265
2548
  const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
@@ -2277,7 +2560,7 @@ async function setupTauri(config) {
2277
2560
  }
2278
2561
 
2279
2562
  //#endregion
2280
- //#region src/helpers/setup/ultracite-setup.ts
2563
+ //#region src/helpers/addons/ultracite-setup.ts
2281
2564
  const EDITORS = {
2282
2565
  vscode: {
2283
2566
  label: "VSCode / Cursor / Windsurf",
@@ -2346,7 +2629,7 @@ async function setupUltracite(config, hasHusky) {
2346
2629
  ];
2347
2630
  if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2348
2631
  if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2349
- if (hasHusky) ultraciteArgs.push("--features", "husky", "lint-staged");
2632
+ if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2350
2633
  const ultraciteArgsString = ultraciteArgs.join(" ");
2351
2634
  const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2352
2635
  const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
@@ -2384,7 +2667,7 @@ function ensureArrayProperty(obj, name) {
2384
2667
  }
2385
2668
 
2386
2669
  //#endregion
2387
- //#region src/helpers/setup/vite-pwa-setup.ts
2670
+ //#region src/helpers/addons/vite-pwa-setup.ts
2388
2671
  async function addPwaToViteConfig(viteConfigPath, projectName) {
2389
2672
  const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2390
2673
  if (!sourceFile) throw new Error("vite config not found");
@@ -2418,7 +2701,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
2418
2701
  }
2419
2702
 
2420
2703
  //#endregion
2421
- //#region src/helpers/setup/addons-setup.ts
2704
+ //#region src/helpers/addons/addons-setup.ts
2422
2705
  async function setupAddons(config, isAddCommand = false) {
2423
2706
  const { addons, frontend, projectDir, packageManager } = config;
2424
2707
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
@@ -2552,7 +2835,7 @@ async function setupOxlint(projectDir, packageManager) {
2552
2835
  }
2553
2836
 
2554
2837
  //#endregion
2555
- //#region src/helpers/project-generation/detect-project-config.ts
2838
+ //#region src/helpers/core/detect-project-config.ts
2556
2839
  async function detectProjectConfig(projectDir) {
2557
2840
  try {
2558
2841
  const btsConfig = await readBtsConfig(projectDir);
@@ -2570,7 +2853,8 @@ async function detectProjectConfig(projectDir) {
2570
2853
  packageManager: btsConfig.packageManager,
2571
2854
  dbSetup: btsConfig.dbSetup,
2572
2855
  api: btsConfig.api,
2573
- webDeploy: btsConfig.webDeploy
2856
+ webDeploy: btsConfig.webDeploy,
2857
+ serverDeploy: btsConfig.serverDeploy
2574
2858
  };
2575
2859
  return null;
2576
2860
  } catch (_error) {
@@ -2586,7 +2870,7 @@ async function isBetterTStackProject(projectDir) {
2586
2870
  }
2587
2871
 
2588
2872
  //#endregion
2589
- //#region src/helpers/project-generation/install-dependencies.ts
2873
+ //#region src/helpers/core/install-dependencies.ts
2590
2874
  async function installDependencies({ projectDir, packageManager }) {
2591
2875
  const s = spinner();
2592
2876
  try {
@@ -2603,7 +2887,7 @@ async function installDependencies({ projectDir, packageManager }) {
2603
2887
  }
2604
2888
 
2605
2889
  //#endregion
2606
- //#region src/helpers/project-generation/add-addons.ts
2890
+ //#region src/helpers/core/add-addons.ts
2607
2891
  async function addAddonsToProject(input) {
2608
2892
  try {
2609
2893
  const projectDir = input.projectDir || process.cwd();
@@ -2628,7 +2912,8 @@ async function addAddonsToProject(input) {
2628
2912
  install: input.install || false,
2629
2913
  dbSetup: detectedConfig.dbSetup || "none",
2630
2914
  api: detectedConfig.api || "none",
2631
- webDeploy: detectedConfig.webDeploy || "none"
2915
+ webDeploy: detectedConfig.webDeploy || "none",
2916
+ serverDeploy: detectedConfig.serverDeploy || "none"
2632
2917
  };
2633
2918
  for (const addon of input.addons) {
2634
2919
  const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
@@ -2651,12 +2936,92 @@ async function addAddonsToProject(input) {
2651
2936
  }
2652
2937
 
2653
2938
  //#endregion
2654
- //#region src/helpers/setup/workers-nuxt-setup.ts
2655
- async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2939
+ //#region src/helpers/deployment/server-deploy-setup.ts
2940
+ async function setupServerDeploy(config) {
2941
+ const { serverDeploy, webDeploy, projectDir } = config;
2942
+ const { packageManager } = config;
2943
+ if (serverDeploy === "none") return;
2944
+ if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
2945
+ const serverDir = path.join(projectDir, "apps/server");
2946
+ if (!await fs.pathExists(serverDir)) return;
2947
+ if (serverDeploy === "wrangler") {
2948
+ await setupWorkersServerDeploy(serverDir, packageManager);
2949
+ await generateCloudflareWorkerTypes({
2950
+ serverDir,
2951
+ packageManager
2952
+ });
2953
+ } else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
2954
+ }
2955
+ async function setupWorkersServerDeploy(serverDir, _packageManager) {
2956
+ const packageJsonPath = path.join(serverDir, "package.json");
2957
+ if (!await fs.pathExists(packageJsonPath)) return;
2958
+ const packageJson = await fs.readJson(packageJsonPath);
2959
+ packageJson.scripts = {
2960
+ ...packageJson.scripts,
2961
+ dev: "wrangler dev --port=3000",
2962
+ start: "wrangler dev",
2963
+ deploy: "wrangler deploy",
2964
+ build: "wrangler deploy --dry-run",
2965
+ "cf-typegen": "wrangler types --env-interface CloudflareBindings"
2966
+ };
2967
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2968
+ await addPackageDependency({
2969
+ devDependencies: [
2970
+ "wrangler",
2971
+ "@types/node",
2972
+ "@cloudflare/workers-types"
2973
+ ],
2974
+ projectDir: serverDir
2975
+ });
2976
+ }
2977
+ async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
2978
+ if (!await fs.pathExists(serverDir)) return;
2979
+ const s = spinner();
2980
+ try {
2981
+ s.start("Generating Cloudflare Workers types...");
2982
+ const runCmd = packageManager === "npm" ? "npm" : packageManager;
2983
+ await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
2984
+ s.stop("Cloudflare Workers types generated successfully!");
2985
+ } catch {
2986
+ s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
2987
+ const managerCmd = `${packageManager} run`;
2988
+ log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
2989
+ }
2990
+ }
2991
+ async function setupAlchemyServerDeploy(serverDir, _packageManager) {
2992
+ if (!await fs.pathExists(serverDir)) return;
2993
+ await addPackageDependency({
2994
+ devDependencies: [
2995
+ "alchemy",
2996
+ "wrangler",
2997
+ "@types/node",
2998
+ "@cloudflare/workers-types",
2999
+ "dotenv"
3000
+ ],
3001
+ projectDir: serverDir
3002
+ });
3003
+ const packageJsonPath = path.join(serverDir, "package.json");
3004
+ if (await fs.pathExists(packageJsonPath)) {
3005
+ const packageJson = await fs.readJson(packageJsonPath);
3006
+ packageJson.scripts = {
3007
+ ...packageJson.scripts,
3008
+ dev: "wrangler dev --port=3000",
3009
+ build: "wrangler deploy --dry-run",
3010
+ deploy: "alchemy deploy",
3011
+ destroy: "alchemy destroy",
3012
+ "alchemy:dev": "alchemy dev"
3013
+ };
3014
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3015
+ }
3016
+ }
3017
+
3018
+ //#endregion
3019
+ //#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
3020
+ async function setupNextAlchemyDeploy(projectDir, _packageManager) {
2656
3021
  const webAppDir = path.join(projectDir, "apps/web");
2657
3022
  if (!await fs.pathExists(webAppDir)) return;
2658
3023
  await addPackageDependency({
2659
- devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3024
+ devDependencies: ["alchemy", "dotenv"],
2660
3025
  projectDir: webAppDir
2661
3026
  });
2662
3027
  const pkgPath = path.join(webAppDir, "package.json");
@@ -2664,43 +3029,518 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2664
3029
  const pkg = await fs.readJson(pkgPath);
2665
3030
  pkg.scripts = {
2666
3031
  ...pkg.scripts,
2667
- deploy: `${packageManager} run build && wrangler deploy`,
2668
- "cf-typegen": "wrangler types"
3032
+ deploy: "alchemy deploy",
3033
+ destroy: "alchemy destroy",
3034
+ "alchemy:dev": "alchemy dev"
3035
+ };
3036
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3037
+ }
3038
+ }
3039
+
3040
+ //#endregion
3041
+ //#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
3042
+ async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
3043
+ const webAppDir = path.join(projectDir, "apps/web");
3044
+ if (!await fs.pathExists(webAppDir)) return;
3045
+ await addPackageDependency({
3046
+ devDependencies: [
3047
+ "alchemy",
3048
+ "nitro-cloudflare-dev",
3049
+ "dotenv"
3050
+ ],
3051
+ projectDir: webAppDir
3052
+ });
3053
+ const pkgPath = path.join(webAppDir, "package.json");
3054
+ if (await fs.pathExists(pkgPath)) {
3055
+ const pkg = await fs.readJson(pkgPath);
3056
+ pkg.scripts = {
3057
+ ...pkg.scripts,
3058
+ deploy: "alchemy deploy",
3059
+ destroy: "alchemy destroy",
3060
+ "alchemy:dev": "alchemy dev"
2669
3061
  };
2670
3062
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
2671
3063
  }
2672
3064
  const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
2673
3065
  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 = `{
3066
+ try {
3067
+ const project = new Project({ manipulationSettings: {
3068
+ indentationText: IndentationText.TwoSpaces,
3069
+ quoteKind: QuoteKind.Double
3070
+ } });
3071
+ project.addSourceFileAtPath(nuxtConfigPath);
3072
+ const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
3073
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3074
+ if (!exportAssignment) return;
3075
+ const defineConfigCall = exportAssignment.getExpression();
3076
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
3077
+ let configObject = defineConfigCall.getArguments()[0];
3078
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3079
+ if (Node.isObjectLiteralExpression(configObject)) {
3080
+ if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
3081
+ name: "nitro",
3082
+ initializer: `{
2691
3083
  preset: "cloudflare_module",
2692
3084
  cloudflare: {
2693
3085
  deployConfig: true,
2694
3086
  nodeCompat: true
2695
3087
  }
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");
3088
+ }`
3089
+ });
3090
+ const modulesProperty = configObject.getProperty("modules");
3091
+ if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
3092
+ const initializer = modulesProperty.getInitializer();
3093
+ if (Node.isArrayLiteralExpression(initializer)) {
3094
+ const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
3095
+ if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
3096
+ }
3097
+ } else if (!modulesProperty) configObject.addPropertyAssignment({
3098
+ name: "modules",
3099
+ initializer: "[\"nitro-cloudflare-dev\"]"
3100
+ });
3101
+ }
3102
+ await project.save();
3103
+ } catch (error) {
3104
+ console.warn("Failed to update nuxt.config.ts:", error);
3105
+ }
3106
+ }
3107
+
3108
+ //#endregion
3109
+ //#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
3110
+ async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
3111
+ const webAppDir = path.join(projectDir, "apps/web");
3112
+ if (!await fs.pathExists(webAppDir)) return;
3113
+ await addPackageDependency({
3114
+ devDependencies: [
3115
+ "alchemy",
3116
+ "@cloudflare/vite-plugin",
3117
+ "dotenv"
3118
+ ],
3119
+ projectDir: webAppDir
3120
+ });
3121
+ const pkgPath = path.join(webAppDir, "package.json");
3122
+ if (await fs.pathExists(pkgPath)) {
3123
+ const pkg = await fs.readJson(pkgPath);
3124
+ pkg.scripts = {
3125
+ ...pkg.scripts,
3126
+ deploy: "alchemy deploy",
3127
+ destroy: "alchemy destroy",
3128
+ "alchemy:dev": "alchemy dev"
3129
+ };
3130
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3131
+ }
3132
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3133
+ if (await fs.pathExists(viteConfigPath)) try {
3134
+ const project = new Project({ manipulationSettings: {
3135
+ indentationText: IndentationText.TwoSpaces,
3136
+ quoteKind: QuoteKind.Double
3137
+ } });
3138
+ project.addSourceFileAtPath(viteConfigPath);
3139
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3140
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
3141
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3142
+ moduleSpecifier: "alchemy/cloudflare/react-router",
3143
+ defaultImport: "alchemy"
3144
+ });
3145
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3146
+ if (!exportAssignment) return;
3147
+ const defineConfigCall = exportAssignment.getExpression();
3148
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3149
+ let configObject = defineConfigCall.getArguments()[0];
3150
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3151
+ if (Node.isObjectLiteralExpression(configObject)) {
3152
+ const pluginsProperty = configObject.getProperty("plugins");
3153
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3154
+ const initializer = pluginsProperty.getInitializer();
3155
+ if (Node.isArrayLiteralExpression(initializer)) {
3156
+ const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
3157
+ if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
3158
+ }
3159
+ } else if (!pluginsProperty) configObject.addPropertyAssignment({
3160
+ name: "plugins",
3161
+ initializer: "[alchemy()]"
3162
+ });
3163
+ }
3164
+ await project.save();
3165
+ } catch (error) {
3166
+ console.warn("Failed to update vite.config.ts:", error);
3167
+ }
3168
+ const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
3169
+ if (await fs.pathExists(reactRouterConfigPath)) try {
3170
+ const project = new Project({ manipulationSettings: {
3171
+ indentationText: IndentationText.TwoSpaces,
3172
+ quoteKind: QuoteKind.Double
3173
+ } });
3174
+ project.addSourceFileAtPath(reactRouterConfigPath);
3175
+ const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
3176
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3177
+ if (!exportAssignment) return;
3178
+ const configExpression = exportAssignment.getExpression();
3179
+ let configObject;
3180
+ if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
3181
+ else if (Node.isSatisfiesExpression(configExpression)) {
3182
+ const expression = configExpression.getExpression();
3183
+ if (Node.isObjectLiteralExpression(expression)) configObject = expression;
3184
+ }
3185
+ if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
3186
+ const futureProperty = configObject.getProperty("future");
3187
+ if (!futureProperty) configObject.addPropertyAssignment({
3188
+ name: "future",
3189
+ initializer: `{
3190
+ unstable_viteEnvironmentApi: true,
3191
+ }`
3192
+ });
3193
+ else if (Node.isPropertyAssignment(futureProperty)) {
3194
+ const futureInitializer = futureProperty.getInitializer();
3195
+ if (Node.isObjectLiteralExpression(futureInitializer)) {
3196
+ const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
3197
+ if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
3198
+ name: "unstable_viteEnvironmentApi",
3199
+ initializer: "true"
3200
+ });
3201
+ else if (Node.isPropertyAssignment(viteEnvApiProp)) {
3202
+ const value = viteEnvApiProp.getInitializer()?.getText();
3203
+ if (value === "false") viteEnvApiProp.setInitializer("true");
3204
+ }
3205
+ }
3206
+ }
3207
+ await project.save();
3208
+ } catch (error) {
3209
+ console.warn("Failed to update react-router.config.ts:", error);
3210
+ }
3211
+ }
3212
+
3213
+ //#endregion
3214
+ //#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
3215
+ async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
3216
+ const webAppDir = path.join(projectDir, "apps/web");
3217
+ if (!await fs.pathExists(webAppDir)) return;
3218
+ await addPackageDependency({
3219
+ devDependencies: ["alchemy", "dotenv"],
3220
+ projectDir: webAppDir
3221
+ });
3222
+ const pkgPath = path.join(webAppDir, "package.json");
3223
+ if (await fs.pathExists(pkgPath)) {
3224
+ const pkg = await fs.readJson(pkgPath);
3225
+ pkg.scripts = {
3226
+ ...pkg.scripts,
3227
+ deploy: "alchemy deploy",
3228
+ destroy: "alchemy destroy",
3229
+ "alchemy:dev": "alchemy dev"
3230
+ };
3231
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3232
+ }
3233
+ }
3234
+
3235
+ //#endregion
3236
+ //#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
3237
+ async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
3238
+ const webAppDir = path.join(projectDir, "apps/web");
3239
+ if (!await fs.pathExists(webAppDir)) return;
3240
+ await addPackageDependency({
3241
+ devDependencies: [
3242
+ "alchemy",
3243
+ "@sveltejs/adapter-cloudflare",
3244
+ "dotenv"
3245
+ ],
3246
+ projectDir: webAppDir
3247
+ });
3248
+ const pkgPath = path.join(webAppDir, "package.json");
3249
+ if (await fs.pathExists(pkgPath)) {
3250
+ const pkg = await fs.readJson(pkgPath);
3251
+ pkg.scripts = {
3252
+ ...pkg.scripts,
3253
+ deploy: "alchemy deploy",
3254
+ destroy: "alchemy destroy",
3255
+ "alchemy:dev": "alchemy dev"
3256
+ };
3257
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3258
+ }
3259
+ const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
3260
+ if (!await fs.pathExists(svelteConfigPath)) return;
3261
+ try {
3262
+ const project = new Project({ manipulationSettings: {
3263
+ indentationText: IndentationText.TwoSpaces,
3264
+ quoteKind: QuoteKind.Single
3265
+ } });
3266
+ project.addSourceFileAtPath(svelteConfigPath);
3267
+ const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
3268
+ const importDeclarations = sourceFile.getImportDeclarations();
3269
+ const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
3270
+ if (adapterImport) {
3271
+ adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
3272
+ adapterImport.removeDefaultImport();
3273
+ adapterImport.setDefaultImport("alchemy");
3274
+ } else sourceFile.insertImportDeclaration(0, {
3275
+ moduleSpecifier: "alchemy/cloudflare/sveltekit",
3276
+ defaultImport: "alchemy"
3277
+ });
3278
+ const configVariable = sourceFile.getVariableDeclaration("config");
3279
+ if (configVariable) {
3280
+ const initializer = configVariable.getInitializer();
3281
+ if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
3282
+ }
3283
+ await project.save();
3284
+ } catch (error) {
3285
+ console.warn("Failed to update svelte.config.js:", error);
3286
+ }
3287
+ }
3288
+ function updateAdapterInConfig(configObject) {
3289
+ if (!Node.isObjectLiteralExpression(configObject)) return;
3290
+ const kitProperty = configObject.getProperty("kit");
3291
+ if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
3292
+ const kitInitializer = kitProperty.getInitializer();
3293
+ if (Node.isObjectLiteralExpression(kitInitializer)) {
3294
+ const adapterProperty = kitInitializer.getProperty("adapter");
3295
+ if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
3296
+ const initializer = adapterProperty.getInitializer();
3297
+ if (Node.isCallExpression(initializer)) {
3298
+ const expression = initializer.getExpression();
3299
+ if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
3300
+ }
3301
+ }
3302
+ }
3303
+ }
3304
+ }
3305
+
3306
+ //#endregion
3307
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
3308
+ async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
3309
+ const webAppDir = path.join(projectDir, "apps/web");
3310
+ if (!await fs.pathExists(webAppDir)) return;
3311
+ await addPackageDependency({
3312
+ devDependencies: ["alchemy", "dotenv"],
3313
+ projectDir: webAppDir
3314
+ });
3315
+ const pkgPath = path.join(webAppDir, "package.json");
3316
+ if (await fs.pathExists(pkgPath)) {
3317
+ const pkg = await fs.readJson(pkgPath);
3318
+ pkg.scripts = {
3319
+ ...pkg.scripts,
3320
+ deploy: "alchemy deploy",
3321
+ destroy: "alchemy destroy",
3322
+ "alchemy:dev": "alchemy dev"
3323
+ };
3324
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3325
+ }
3326
+ }
3327
+
3328
+ //#endregion
3329
+ //#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
3330
+ async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
3331
+ const webAppDir = path.join(projectDir, "apps/web");
3332
+ if (!await fs.pathExists(webAppDir)) return;
3333
+ await addPackageDependency({
3334
+ devDependencies: [
3335
+ "alchemy",
3336
+ "nitropack",
3337
+ "dotenv"
3338
+ ],
3339
+ projectDir: webAppDir
3340
+ });
3341
+ const pkgPath = path.join(webAppDir, "package.json");
3342
+ if (await fs.pathExists(pkgPath)) {
3343
+ const pkg = await fs.readJson(pkgPath);
3344
+ pkg.scripts = {
3345
+ ...pkg.scripts,
3346
+ deploy: "alchemy deploy",
3347
+ destroy: "alchemy destroy",
3348
+ "alchemy:dev": "alchemy dev"
3349
+ };
3350
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3351
+ }
3352
+ const viteConfigPath = path.join(webAppDir, "vite.config.ts");
3353
+ if (await fs.pathExists(viteConfigPath)) try {
3354
+ const project = new Project({ manipulationSettings: {
3355
+ indentationText: IndentationText.TwoSpaces,
3356
+ quoteKind: QuoteKind.Double
3357
+ } });
3358
+ project.addSourceFileAtPath(viteConfigPath);
3359
+ const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
3360
+ const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
3361
+ if (!alchemyImport) sourceFile.addImportDeclaration({
3362
+ moduleSpecifier: "alchemy/cloudflare/tanstack-start",
3363
+ defaultImport: "alchemy"
3364
+ });
3365
+ else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
3366
+ const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
3367
+ if (!exportAssignment) return;
3368
+ const defineConfigCall = exportAssignment.getExpression();
3369
+ if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
3370
+ let configObject = defineConfigCall.getArguments()[0];
3371
+ if (!configObject) configObject = defineConfigCall.addArgument("{}");
3372
+ if (Node.isObjectLiteralExpression(configObject)) {
3373
+ if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
3374
+ name: "build",
3375
+ initializer: `{
3376
+ target: "esnext",
3377
+ rollupOptions: {
3378
+ external: ["node:async_hooks", "cloudflare:workers"],
3379
+ },
3380
+ }`
3381
+ });
3382
+ const pluginsProperty = configObject.getProperty("plugins");
3383
+ if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
3384
+ const initializer = pluginsProperty.getInitializer();
3385
+ if (Node.isArrayLiteralExpression(initializer)) {
3386
+ const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
3387
+ if (!hasShim) initializer.addElement("alchemy()");
3388
+ const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
3389
+ tanstackElements.forEach((element) => {
3390
+ if (Node.isCallExpression(element)) {
3391
+ const args = element.getArguments();
3392
+ if (args.length === 0) element.addArgument(`{
3393
+ target: "cloudflare-module",
3394
+ customViteReactPlugin: true,
3395
+ }`);
3396
+ else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
3397
+ const configObj = args[0];
3398
+ if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
3399
+ name: "target",
3400
+ initializer: "\"cloudflare-module\""
3401
+ });
3402
+ if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
3403
+ name: "customViteReactPlugin",
3404
+ initializer: "true"
3405
+ });
3406
+ }
3407
+ }
3408
+ });
3409
+ }
3410
+ } else configObject.addPropertyAssignment({
3411
+ name: "plugins",
3412
+ initializer: "[alchemy()]"
3413
+ });
3414
+ }
3415
+ await project.save();
3416
+ } catch (error) {
3417
+ console.warn("Failed to update vite.config.ts:", error);
3418
+ }
3419
+ const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
3420
+ const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
3421
+
3422
+ export default defineNitroConfig({
3423
+ preset: "cloudflare-module",
3424
+ cloudflare: {
3425
+ nodeCompat: true,
3426
+ },
3427
+ });
3428
+ `;
3429
+ await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
3430
+ }
3431
+
3432
+ //#endregion
3433
+ //#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
3434
+ async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
3435
+ await addPackageDependency({
3436
+ devDependencies: ["alchemy", "dotenv"],
3437
+ projectDir
3438
+ });
3439
+ const rootPkgPath = path.join(projectDir, "package.json");
3440
+ if (await fs.pathExists(rootPkgPath)) {
3441
+ const pkg = await fs.readJson(rootPkgPath);
3442
+ pkg.scripts = {
3443
+ ...pkg.scripts,
3444
+ deploy: "alchemy deploy",
3445
+ destroy: "alchemy destroy",
3446
+ "alchemy:dev": "alchemy dev"
3447
+ };
3448
+ await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
3449
+ }
3450
+ const serverDir = path.join(projectDir, "apps/server");
3451
+ if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
3452
+ const frontend = config.frontend;
3453
+ const isNext = frontend.includes("next");
3454
+ const isNuxt = frontend.includes("nuxt");
3455
+ const isSvelte = frontend.includes("svelte");
3456
+ const isTanstackRouter = frontend.includes("tanstack-router");
3457
+ const isTanstackStart = frontend.includes("tanstack-start");
3458
+ const isReactRouter = frontend.includes("react-router");
3459
+ const isSolid = frontend.includes("solid");
3460
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3461
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3462
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3463
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3464
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3465
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3466
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3467
+ }
3468
+
3469
+ //#endregion
3470
+ //#region src/helpers/deployment/workers/workers-next-setup.ts
3471
+ async function setupNextWorkersDeploy(projectDir, _packageManager) {
3472
+ const webAppDir = path.join(projectDir, "apps/web");
3473
+ if (!await fs.pathExists(webAppDir)) return;
3474
+ await addPackageDependency({
3475
+ dependencies: ["@opennextjs/cloudflare"],
3476
+ devDependencies: ["wrangler"],
3477
+ projectDir: webAppDir
3478
+ });
3479
+ const packageJsonPath = path.join(webAppDir, "package.json");
3480
+ if (await fs.pathExists(packageJsonPath)) {
3481
+ const pkg = await fs.readJson(packageJsonPath);
3482
+ pkg.scripts = {
3483
+ ...pkg.scripts,
3484
+ preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
3485
+ deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
3486
+ upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
3487
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
3488
+ };
3489
+ await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
3490
+ }
3491
+ }
3492
+
3493
+ //#endregion
3494
+ //#region src/helpers/deployment/workers/workers-nuxt-setup.ts
3495
+ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
3496
+ const webAppDir = path.join(projectDir, "apps/web");
3497
+ if (!await fs.pathExists(webAppDir)) return;
3498
+ await addPackageDependency({
3499
+ devDependencies: ["nitro-cloudflare-dev", "wrangler"],
3500
+ projectDir: webAppDir
3501
+ });
3502
+ const pkgPath = path.join(webAppDir, "package.json");
3503
+ if (await fs.pathExists(pkgPath)) {
3504
+ const pkg = await fs.readJson(pkgPath);
3505
+ pkg.scripts = {
3506
+ ...pkg.scripts,
3507
+ deploy: `${packageManager} run build && wrangler deploy`,
3508
+ "cf-typegen": "wrangler types"
3509
+ };
3510
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
3511
+ }
3512
+ const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
3513
+ if (!await fs.pathExists(nuxtConfigPath)) return;
3514
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
3515
+ if (!sourceFile) return;
3516
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
3517
+ const expression = expr.getExpression();
3518
+ return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
3519
+ });
3520
+ if (!defineCall) return;
3521
+ const configObj = defineCall.getArguments()[0];
3522
+ if (!configObj) return;
3523
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3524
+ const compatProp = configObj.getProperty("compatibilityDate");
3525
+ if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
3526
+ else configObj.addPropertyAssignment({
3527
+ name: "compatibilityDate",
3528
+ initializer: `'${today}'`
3529
+ });
3530
+ const nitroInitializer = `{
3531
+ preset: "cloudflare_module",
3532
+ cloudflare: {
3533
+ deployConfig: true,
3534
+ nodeCompat: true
3535
+ }
3536
+ }`;
3537
+ const nitroProp = configObj.getProperty("nitro");
3538
+ if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
3539
+ else configObj.addPropertyAssignment({
3540
+ name: "nitro",
3541
+ initializer: nitroInitializer
3542
+ });
3543
+ const modulesProp = configObj.getProperty("modules");
2704
3544
  if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
2705
3545
  const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
2706
3546
  if (arrayExpr) {
@@ -2715,7 +3555,7 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
2715
3555
  }
2716
3556
 
2717
3557
  //#endregion
2718
- //#region src/helpers/setup/workers-svelte-setup.ts
3558
+ //#region src/helpers/deployment/workers/workers-svelte-setup.ts
2719
3559
  async function setupSvelteWorkersDeploy(projectDir, packageManager) {
2720
3560
  const webAppDir = path.join(projectDir, "apps/web");
2721
3561
  if (!await fs.pathExists(webAppDir)) return;
@@ -2752,7 +3592,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
2752
3592
  }
2753
3593
 
2754
3594
  //#endregion
2755
- //#region src/helpers/setup/workers-tanstack-start-setup.ts
3595
+ //#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
2756
3596
  async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2757
3597
  const webAppDir = path.join(projectDir, "apps/web");
2758
3598
  if (!await fs.pathExists(webAppDir)) return;
@@ -2790,7 +3630,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
2790
3630
  }
2791
3631
 
2792
3632
  //#endregion
2793
- //#region src/helpers/setup/workers-vite-setup.ts
3633
+ //#region src/helpers/deployment/workers/workers-vite-setup.ts
2794
3634
  async function setupWorkersVitePlugin(projectDir) {
2795
3635
  const webAppDir = path.join(projectDir, "apps/web");
2796
3636
  const viteConfigPath = path.join(webAppDir, "vite.config.ts");
@@ -2821,12 +3661,16 @@ async function setupWorkersVitePlugin(projectDir) {
2821
3661
  }
2822
3662
 
2823
3663
  //#endregion
2824
- //#region src/helpers/setup/web-deploy-setup.ts
3664
+ //#region src/helpers/deployment/web-deploy-setup.ts
2825
3665
  async function setupWebDeploy(config) {
2826
- const { webDeploy, frontend, projectDir } = config;
3666
+ const { webDeploy, serverDeploy, frontend, projectDir } = config;
2827
3667
  const { packageManager } = config;
2828
3668
  if (webDeploy === "none") return;
2829
- if (webDeploy !== "workers") return;
3669
+ if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
3670
+ if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
3671
+ await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
3672
+ return;
3673
+ }
2830
3674
  const isNext = frontend.includes("next");
2831
3675
  const isNuxt = frontend.includes("nuxt");
2832
3676
  const isSvelte = frontend.includes("svelte");
@@ -2834,11 +3678,21 @@ async function setupWebDeploy(config) {
2834
3678
  const isTanstackStart = frontend.includes("tanstack-start");
2835
3679
  const isReactRouter = frontend.includes("react-router");
2836
3680
  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);
3681
+ if (webDeploy === "wrangler") {
3682
+ if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
3683
+ else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
3684
+ else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
3685
+ else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
3686
+ else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
3687
+ } else if (webDeploy === "alchemy") {
3688
+ if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
3689
+ else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
3690
+ else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
3691
+ else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
3692
+ else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
3693
+ else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
3694
+ else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
3695
+ }
2842
3696
  }
2843
3697
  async function setupWorkersWebDeploy(projectDir, pkgManager) {
2844
3698
  const webAppDir = path.join(projectDir, "apps/web");
@@ -2855,30 +3709,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
2855
3709
  }
2856
3710
  await setupWorkersVitePlugin(projectDir);
2857
3711
  }
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
3712
 
2880
3713
  //#endregion
2881
- //#region src/helpers/project-generation/add-deployment.ts
3714
+ //#region src/helpers/core/add-deployment.ts
2882
3715
  async function addDeploymentToProject(input) {
2883
3716
  try {
2884
3717
  const projectDir = input.projectDir || process.cwd();
@@ -2886,7 +3719,8 @@ async function addDeploymentToProject(input) {
2886
3719
  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
3720
  const detectedConfig = await detectProjectConfig(projectDir);
2888
3721
  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.`);
3722
+ if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
3723
+ if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
2890
3724
  const config = {
2891
3725
  projectName: detectedConfig.projectName || path.basename(projectDir),
2892
3726
  projectDir,
@@ -2903,224 +3737,79 @@ async function addDeploymentToProject(input) {
2903
3737
  packageManager: input.packageManager || detectedConfig.packageManager || "npm",
2904
3738
  install: input.install || false,
2905
3739
  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) {}
3740
+ api: detectedConfig.api || "none",
3741
+ webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
3742
+ serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
3110
3743
  };
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
- }
3744
+ if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
3745
+ if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
3746
+ await setupDeploymentTemplates(projectDir, config);
3747
+ await setupWebDeploy(config);
3748
+ await setupServerDeploy(config);
3749
+ await updateBtsConfig(projectDir, {
3750
+ webDeploy: input.webDeploy || config.webDeploy,
3751
+ serverDeploy: input.serverDeploy || config.serverDeploy
3752
+ });
3753
+ if (config.install) await installDependencies({
3754
+ projectDir,
3755
+ packageManager: config.packageManager
3756
+ });
3757
+ else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
3758
+ } catch (error) {
3759
+ const message = error instanceof Error ? error.message : String(error);
3760
+ exitWithError(`Error adding deployment: ${message}`);
3119
3761
  }
3120
3762
  }
3121
3763
 
3122
3764
  //#endregion
3123
- //#region src/helpers/setup/auth-setup.ts
3765
+ //#region src/utils/format-with-biome.ts
3766
+ async function formatProjectWithBiome(projectDir) {
3767
+ const biome = new Biome();
3768
+ const { projectKey } = biome.openProject(projectDir);
3769
+ biome.applyConfiguration(projectKey, {
3770
+ formatter: {
3771
+ enabled: true,
3772
+ indentStyle: "tab"
3773
+ },
3774
+ javascript: { formatter: { quoteStyle: "double" } }
3775
+ });
3776
+ const files = await glob("**/*", {
3777
+ cwd: projectDir,
3778
+ dot: true,
3779
+ absolute: true,
3780
+ onlyFiles: true
3781
+ });
3782
+ for (const filePath of files) try {
3783
+ const ext = path.extname(filePath).toLowerCase();
3784
+ const supported = new Set([
3785
+ ".ts",
3786
+ ".tsx",
3787
+ ".js",
3788
+ ".jsx",
3789
+ ".cjs",
3790
+ ".mjs",
3791
+ ".cts",
3792
+ ".mts",
3793
+ ".json",
3794
+ ".jsonc",
3795
+ ".md",
3796
+ ".mdx",
3797
+ ".css",
3798
+ ".scss",
3799
+ ".html"
3800
+ ]);
3801
+ if (!supported.has(ext)) continue;
3802
+ const original = await fs.readFile(filePath, "utf8");
3803
+ const result = biome.formatContent(projectKey, original, { filePath });
3804
+ const content = result?.content;
3805
+ if (typeof content !== "string") continue;
3806
+ if (content.length === 0 && original.length > 0) continue;
3807
+ if (content !== original) await fs.writeFile(filePath, content);
3808
+ } catch {}
3809
+ }
3810
+
3811
+ //#endregion
3812
+ //#region src/helpers/addons/auth-setup.ts
3124
3813
  async function setupAuth(config) {
3125
3814
  const { auth, frontend, backend, projectDir } = config;
3126
3815
  if (backend === "convex" || !auth) return;
@@ -3172,7 +3861,217 @@ function generateAuthSecret(length = 32) {
3172
3861
  }
3173
3862
 
3174
3863
  //#endregion
3175
- //#region src/helpers/setup/backend-setup.ts
3864
+ //#region src/helpers/addons/examples-setup.ts
3865
+ async function setupExamples(config) {
3866
+ const { examples, frontend, backend, projectDir } = config;
3867
+ if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
3868
+ if (examples.includes("ai")) {
3869
+ const webClientDir = path.join(projectDir, "apps/web");
3870
+ const nativeClientDir = path.join(projectDir, "apps/native");
3871
+ const serverDir = path.join(projectDir, "apps/server");
3872
+ const webClientDirExists = await fs.pathExists(webClientDir);
3873
+ const nativeClientDirExists = await fs.pathExists(nativeClientDir);
3874
+ const serverDirExists = await fs.pathExists(serverDir);
3875
+ const hasNuxt = frontend.includes("nuxt");
3876
+ const hasSvelte = frontend.includes("svelte");
3877
+ const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
3878
+ const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3879
+ if (webClientDirExists) {
3880
+ const dependencies = ["ai"];
3881
+ if (hasNuxt) dependencies.push("@ai-sdk/vue");
3882
+ else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
3883
+ else if (hasReactWeb) dependencies.push("@ai-sdk/react");
3884
+ await addPackageDependency({
3885
+ dependencies,
3886
+ projectDir: webClientDir
3887
+ });
3888
+ }
3889
+ if (nativeClientDirExists && hasReactNative) await addPackageDependency({
3890
+ dependencies: ["ai", "@ai-sdk/react"],
3891
+ projectDir: nativeClientDir
3892
+ });
3893
+ if (serverDirExists && backend !== "none") await addPackageDependency({
3894
+ dependencies: ["ai", "@ai-sdk/google"],
3895
+ projectDir: serverDir
3896
+ });
3897
+ }
3898
+ }
3899
+
3900
+ //#endregion
3901
+ //#region src/helpers/core/api-setup.ts
3902
+ async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
3903
+ const pkgJsonPath = path.join(projectDir, "package.json");
3904
+ try {
3905
+ const pkgJson = await fs.readJson(pkgJsonPath);
3906
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
3907
+ pkgJson.dependencies[backendPackageName] = workspaceVersion;
3908
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
3909
+ } catch (_error) {}
3910
+ }
3911
+ function getFrontendType(frontend) {
3912
+ const reactBasedFrontends = [
3913
+ "tanstack-router",
3914
+ "react-router",
3915
+ "tanstack-start",
3916
+ "next"
3917
+ ];
3918
+ const nativeFrontends = ["native-nativewind", "native-unistyles"];
3919
+ return {
3920
+ hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
3921
+ hasNuxtWeb: frontend.includes("nuxt"),
3922
+ hasSvelteWeb: frontend.includes("svelte"),
3923
+ hasSolidWeb: frontend.includes("solid"),
3924
+ hasNative: frontend.some((f) => nativeFrontends.includes(f))
3925
+ };
3926
+ }
3927
+ function getApiDependencies(api, frontendType) {
3928
+ const deps = {};
3929
+ if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
3930
+ else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
3931
+ if (frontendType.hasReactWeb) {
3932
+ if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3933
+ else if (api === "trpc") deps.web = { dependencies: [
3934
+ "@trpc/tanstack-react-query",
3935
+ "@trpc/client",
3936
+ "@trpc/server"
3937
+ ] };
3938
+ } else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
3939
+ dependencies: [
3940
+ "@tanstack/vue-query",
3941
+ "@orpc/tanstack-query",
3942
+ "@orpc/client"
3943
+ ],
3944
+ devDependencies: ["@tanstack/vue-query-devtools"]
3945
+ };
3946
+ else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
3947
+ dependencies: [
3948
+ "@orpc/tanstack-query",
3949
+ "@orpc/client",
3950
+ "@tanstack/svelte-query"
3951
+ ],
3952
+ devDependencies: ["@tanstack/svelte-query-devtools"]
3953
+ };
3954
+ else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
3955
+ dependencies: [
3956
+ "@orpc/tanstack-query",
3957
+ "@orpc/client",
3958
+ "@tanstack/solid-query"
3959
+ ],
3960
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3961
+ };
3962
+ if (api === "trpc") deps.native = { dependencies: [
3963
+ "@trpc/tanstack-react-query",
3964
+ "@trpc/client",
3965
+ "@trpc/server"
3966
+ ] };
3967
+ else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
3968
+ return deps;
3969
+ }
3970
+ function getQueryDependencies(frontend) {
3971
+ const reactBasedFrontends = [
3972
+ "react-router",
3973
+ "tanstack-router",
3974
+ "tanstack-start",
3975
+ "next",
3976
+ "native-nativewind",
3977
+ "native-unistyles"
3978
+ ];
3979
+ const deps = {};
3980
+ const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
3981
+ if (needsReactQuery) {
3982
+ const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
3983
+ const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
3984
+ if (hasReactWeb) deps.web = {
3985
+ dependencies: ["@tanstack/react-query"],
3986
+ devDependencies: ["@tanstack/react-query-devtools"]
3987
+ };
3988
+ if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
3989
+ }
3990
+ if (frontend.includes("solid")) deps.web = {
3991
+ dependencies: ["@tanstack/solid-query"],
3992
+ devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
3993
+ };
3994
+ return deps;
3995
+ }
3996
+ function getConvexDependencies(frontend) {
3997
+ const deps = {
3998
+ web: { dependencies: ["convex"] },
3999
+ native: { dependencies: ["convex"] }
4000
+ };
4001
+ if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
4002
+ if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
4003
+ if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
4004
+ return deps;
4005
+ }
4006
+ async function setupApi(config) {
4007
+ const { api, projectName, frontend, backend, packageManager, projectDir } = config;
4008
+ const isConvex = backend === "convex";
4009
+ const webDir = path.join(projectDir, "apps/web");
4010
+ const nativeDir = path.join(projectDir, "apps/native");
4011
+ const serverDir = path.join(projectDir, "apps/server");
4012
+ const webDirExists = await fs.pathExists(webDir);
4013
+ const nativeDirExists = await fs.pathExists(nativeDir);
4014
+ const serverDirExists = await fs.pathExists(serverDir);
4015
+ const frontendType = getFrontendType(frontend);
4016
+ if (!isConvex && api !== "none") {
4017
+ const apiDeps = getApiDependencies(api, frontendType);
4018
+ if (serverDirExists && apiDeps.server) {
4019
+ await addPackageDependency({
4020
+ dependencies: apiDeps.server.dependencies,
4021
+ projectDir: serverDir
4022
+ });
4023
+ if (api === "trpc") {
4024
+ if (backend === "hono") await addPackageDependency({
4025
+ dependencies: ["@hono/trpc-server"],
4026
+ projectDir: serverDir
4027
+ });
4028
+ else if (backend === "elysia") await addPackageDependency({
4029
+ dependencies: ["@elysiajs/trpc"],
4030
+ projectDir: serverDir
4031
+ });
4032
+ }
4033
+ }
4034
+ if (webDirExists && apiDeps.web) await addPackageDependency({
4035
+ dependencies: apiDeps.web.dependencies,
4036
+ devDependencies: apiDeps.web.devDependencies,
4037
+ projectDir: webDir
4038
+ });
4039
+ if (nativeDirExists && apiDeps.native) await addPackageDependency({
4040
+ dependencies: apiDeps.native.dependencies,
4041
+ projectDir: nativeDir
4042
+ });
4043
+ }
4044
+ if (!isConvex) {
4045
+ const queryDeps = getQueryDependencies(frontend);
4046
+ if (webDirExists && queryDeps.web) await addPackageDependency({
4047
+ dependencies: queryDeps.web.dependencies,
4048
+ devDependencies: queryDeps.web.devDependencies,
4049
+ projectDir: webDir
4050
+ });
4051
+ if (nativeDirExists && queryDeps.native) await addPackageDependency({
4052
+ dependencies: queryDeps.native.dependencies,
4053
+ projectDir: nativeDir
4054
+ });
4055
+ }
4056
+ if (isConvex) {
4057
+ const convexDeps = getConvexDependencies(frontend);
4058
+ if (webDirExists) await addPackageDependency({
4059
+ dependencies: convexDeps.web.dependencies,
4060
+ projectDir: webDir
4061
+ });
4062
+ if (nativeDirExists) await addPackageDependency({
4063
+ dependencies: convexDeps.native.dependencies,
4064
+ projectDir: nativeDir
4065
+ });
4066
+ const backendPackageName = `@${projectName}/backend`;
4067
+ const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
4068
+ if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
4069
+ if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
4070
+ }
4071
+ }
4072
+
4073
+ //#endregion
4074
+ //#region src/helpers/core/backend-setup.ts
3176
4075
  async function setupBackendDependencies(config) {
3177
4076
  const { backend, runtime, api, projectDir } = config;
3178
4077
  if (backend === "convex") return;
@@ -3211,7 +4110,7 @@ async function setupBackendDependencies(config) {
3211
4110
  }
3212
4111
 
3213
4112
  //#endregion
3214
- //#region src/helpers/project-generation/env-setup.ts
4113
+ //#region src/helpers/core/env-setup.ts
3215
4114
  async function addEnvVariablesToFile(filePath, variables) {
3216
4115
  await fs.ensureDir(path.dirname(filePath));
3217
4116
  let envContent = "";
@@ -3259,7 +4158,7 @@ async function addEnvVariablesToFile(filePath, variables) {
3259
4158
  if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
3260
4159
  }
3261
4160
  async function setupEnvironmentVariables(config) {
3262
- const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
4161
+ const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
3263
4162
  const hasReactRouter = frontend.includes("react-router");
3264
4163
  const hasTanStackRouter = frontend.includes("tanstack-router");
3265
4164
  const hasTanStackStart = frontend.includes("tanstack-start");
@@ -3359,39 +4258,69 @@ async function setupEnvironmentVariables(config) {
3359
4258
  }
3360
4259
  ];
3361
4260
  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) {}
4261
+ const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
4262
+ const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
4263
+ if (isUnifiedAlchemy) {
4264
+ const rootEnvPath = path.join(projectDir, ".env");
4265
+ const rootAlchemyVars = [{
4266
+ key: "ALCHEMY_PASSWORD",
4267
+ value: "please-change-this",
4268
+ condition: true
4269
+ }];
4270
+ await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
4271
+ } else if (isIndividualAlchemy) {
4272
+ if (webDeploy === "alchemy") {
4273
+ const webDir = path.join(projectDir, "apps/web");
4274
+ if (await fs.pathExists(webDir)) {
4275
+ const webAlchemyVars = [{
4276
+ key: "ALCHEMY_PASSWORD",
4277
+ value: "please-change-this",
4278
+ condition: true
4279
+ }];
4280
+ await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
4281
+ }
4282
+ }
4283
+ if (serverDeploy === "alchemy") {
4284
+ const serverDir$1 = path.join(projectDir, "apps/server");
4285
+ if (await fs.pathExists(serverDir$1)) {
4286
+ const serverAlchemyVars = [{
4287
+ key: "ALCHEMY_PASSWORD",
4288
+ value: "please-change-this",
4289
+ condition: true
4290
+ }];
4291
+ await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
4292
+ }
4293
+ }
3367
4294
  }
3368
4295
  }
3369
4296
 
3370
4297
  //#endregion
3371
4298
  //#region src/helpers/database-providers/d1-setup.ts
3372
4299
  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) {}
4300
+ const { projectDir, serverDeploy } = config;
4301
+ if (serverDeploy === "wrangler") {
4302
+ const envPath = path.join(projectDir, "apps/server", ".env");
4303
+ const variables = [
4304
+ {
4305
+ key: "CLOUDFLARE_ACCOUNT_ID",
4306
+ value: "",
4307
+ condition: true
4308
+ },
4309
+ {
4310
+ key: "CLOUDFLARE_DATABASE_ID",
4311
+ value: "",
4312
+ condition: true
4313
+ },
4314
+ {
4315
+ key: "CLOUDFLARE_D1_TOKEN",
4316
+ value: "",
4317
+ condition: true
4318
+ }
4319
+ ];
4320
+ try {
4321
+ await addEnvVariablesToFile(envPath, variables);
4322
+ } catch (_err) {}
4323
+ }
3395
4324
  }
3396
4325
 
3397
4326
  //#endregion
@@ -3839,8 +4768,10 @@ async function setupPrismaPostgres(config) {
3839
4768
  else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
3840
4769
  if (prismaConfig) {
3841
4770
  await writeEnvFile$1(projectDir, prismaConfig);
3842
- await addDotenvImportToPrismaConfig(projectDir);
3843
- if (orm === "prisma") await addPrismaAccelerateExtension(serverDir);
4771
+ if (orm === "prisma") {
4772
+ await addDotenvImportToPrismaConfig(projectDir);
4773
+ await addPrismaAccelerateExtension(serverDir);
4774
+ }
3844
4775
  log.success(pc.green("Prisma Postgres database configured successfully!"));
3845
4776
  } else {
3846
4777
  await writeEnvFile$1(projectDir);
@@ -4185,7 +5116,7 @@ async function setupTurso(config) {
4185
5116
  }
4186
5117
 
4187
5118
  //#endregion
4188
- //#region src/helpers/setup/db-setup.ts
5119
+ //#region src/helpers/core/db-setup.ts
4189
5120
  async function setupDatabase(config) {
4190
5121
  const { database, orm, dbSetup, backend, projectDir } = config;
4191
5122
  if (backend === "convex" || database === "none") {
@@ -4250,44 +5181,7 @@ async function setupDatabase(config) {
4250
5181
  }
4251
5182
 
4252
5183
  //#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
5184
+ //#region src/helpers/core/runtime-setup.ts
4291
5185
  async function setupRuntime(config) {
4292
5186
  const { runtime, backend, projectDir } = config;
4293
5187
  if (backend === "convex" || backend === "next" || runtime === "none") return;
@@ -4295,23 +5189,6 @@ async function setupRuntime(config) {
4295
5189
  if (!await fs.pathExists(serverDir)) return;
4296
5190
  if (runtime === "bun") await setupBunRuntime(serverDir, backend);
4297
5191
  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
5192
  }
4316
5193
  async function setupBunRuntime(serverDir, _backend) {
4317
5194
  const packageJsonPath = path.join(serverDir, "package.json");
@@ -4351,27 +5228,20 @@ async function setupNodeRuntime(serverDir, backend) {
4351
5228
  projectDir: serverDir
4352
5229
  });
4353
5230
  }
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
5231
+
5232
+ //#endregion
5233
+ //#region src/helpers/core/convex-codegen.ts
5234
+ async function runConvexCodegen(projectDir, packageManager) {
5235
+ const backendDir = path.join(projectDir, "packages/backend");
5236
+ const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
5237
+ await execa(cmd, {
5238
+ cwd: backendDir,
5239
+ shell: true
4370
5240
  });
4371
5241
  }
4372
5242
 
4373
5243
  //#endregion
4374
- //#region src/helpers/project-generation/create-readme.ts
5244
+ //#region src/helpers/core/create-readme.ts
4375
5245
  async function createReadme(projectDir, options) {
4376
5246
  const readmePath = path.join(projectDir, "README.md");
4377
5247
  const content = generateReadmeContent(options);
@@ -4648,7 +5518,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
4648
5518
  }
4649
5519
 
4650
5520
  //#endregion
4651
- //#region src/helpers/project-generation/git.ts
5521
+ //#region src/helpers/core/git.ts
4652
5522
  async function initializeGit(projectDir, useGit) {
4653
5523
  if (!useGit) return;
4654
5524
  const gitVersionResult = await $({
@@ -4724,20 +5594,21 @@ async function getDockerStatus(database) {
4724
5594
  }
4725
5595
 
4726
5596
  //#endregion
4727
- //#region src/helpers/project-generation/post-installation.ts
5597
+ //#region src/helpers/core/post-installation.ts
4728
5598
  async function displayPostInstallInstructions(config) {
4729
- const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
5599
+ const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
4730
5600
  const isConvex = backend === "convex";
4731
5601
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
4732
5602
  const cdCmd = `cd ${relativePath}`;
4733
5603
  const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
4734
- const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
5604
+ const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
4735
5605
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
4736
5606
  const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
4737
5607
  const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
4738
5608
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
4739
5609
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
4740
- const workersDeployInstructions = webDeploy === "workers" ? getWorkersDeployInstructions(runCmd) : "";
5610
+ const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
5611
+ const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
4741
5612
  const hasWeb = frontend?.some((f) => [
4742
5613
  "tanstack-router",
4743
5614
  "react-router",
@@ -4766,8 +5637,8 @@ async function displayPostInstallInstructions(config) {
4766
5637
  if (runtime === "workers") {
4767
5638
  if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
4768
5639
  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";
5640
+ if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
5641
+ }
4771
5642
  }
4772
5643
  output += `${pc.bold("Your project will be available at:")}\n`;
4773
5644
  if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
@@ -4780,7 +5651,8 @@ async function displayPostInstallInstructions(config) {
4780
5651
  if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
4781
5652
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
4782
5653
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
4783
- if (workersDeployInstructions) output += `\n${workersDeployInstructions.trim()}\n`;
5654
+ if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
5655
+ if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
4784
5656
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
4785
5657
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
4786
5658
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -4801,7 +5673,7 @@ function getNativeInstructions(isConvex) {
4801
5673
  function getLintingInstructions(runCmd) {
4802
5674
  return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
4803
5675
  }
4804
- async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) {
5676
+ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
4805
5677
  const instructions = [];
4806
5678
  if (dbSetup === "docker") {
4807
5679
  const dockerStatus = await getDockerStatus(database);
@@ -4810,7 +5682,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4810
5682
  instructions.push("");
4811
5683
  }
4812
5684
  }
4813
- if (runtime === "workers" && dbSetup === "d1") {
5685
+ if (serverDeploy === "wrangler" && dbSetup === "d1") {
4814
5686
  const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
4815
5687
  instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
4816
5688
  instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
@@ -4818,8 +5690,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4818
5690
  instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
4819
5691
  instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
4820
5692
  instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
4821
- instructions.push("");
4822
5693
  }
5694
+ if (dbSetup === "d1" && serverDeploy === "alchemy") {}
4823
5695
  if (orm === "prisma") {
4824
5696
  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
5697
  if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
@@ -4828,7 +5700,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
4828
5700
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4829
5701
  } else if (orm === "drizzle") {
4830
5702
  if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
4831
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
5703
+ if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
4832
5704
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
4833
5705
  if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
4834
5706
  } else if (orm === "mongoose") {
@@ -4851,12 +5723,22 @@ function getNoOrmWarning() {
4851
5723
  function getBunWebNativeWarning() {
4852
5724
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
4853
5725
  }
4854
- function getWorkersDeployInstructions(runCmd) {
4855
- return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
5726
+ function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
5727
+ const instructions = [];
5728
+ if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
5729
+ if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
5730
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
5731
+ }
5732
+ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
5733
+ const instructions = [];
5734
+ if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
5735
+ else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
5736
+ else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
5737
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
4856
5738
  }
4857
5739
 
4858
5740
  //#endregion
4859
- //#region src/helpers/project-generation/project-config.ts
5741
+ //#region src/helpers/core/project-config.ts
4860
5742
  async function updatePackageConfigurations(projectDir, options) {
4861
5743
  await updateRootPackageJson(projectDir, options);
4862
5744
  if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
@@ -5037,7 +5919,7 @@ async function updateConvexPackageJson(projectDir, options) {
5037
5919
  }
5038
5920
 
5039
5921
  //#endregion
5040
- //#region src/helpers/project-generation/create-project.ts
5922
+ //#region src/helpers/core/create-project.ts
5041
5923
  async function createProject(options) {
5042
5924
  const projectDir = options.projectDir;
5043
5925
  const isConvex = options.backend === "convex";
@@ -5065,18 +5947,18 @@ async function createProject(options) {
5065
5947
  if (!isConvex && options.auth) await setupAuth(options);
5066
5948
  await handleExtras(projectDir, options);
5067
5949
  await setupWebDeploy(options);
5950
+ await setupServerDeploy(options);
5068
5951
  await setupEnvironmentVariables(options);
5069
5952
  await updatePackageConfigurations(projectDir, options);
5070
5953
  await createReadme(projectDir, options);
5071
5954
  await writeBtsConfig(options);
5955
+ await formatProjectWithBiome(projectDir);
5956
+ if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
5072
5957
  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
- }
5958
+ if (options.install) await installDependencies({
5959
+ projectDir,
5960
+ packageManager: options.packageManager
5961
+ });
5080
5962
  await initializeGit(projectDir, options.git);
5081
5963
  await displayPostInstallInstructions({
5082
5964
  ...options,
@@ -5095,7 +5977,7 @@ async function createProject(options) {
5095
5977
  }
5096
5978
 
5097
5979
  //#endregion
5098
- //#region src/helpers/project-generation/command-handlers.ts
5980
+ //#region src/helpers/core/command-handlers.ts
5099
5981
  async function createProjectHandler(input) {
5100
5982
  const startTime = Date.now();
5101
5983
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
@@ -5105,10 +5987,11 @@ async function createProjectHandler(input) {
5105
5987
  let currentPathInput;
5106
5988
  if (input.yes && input.projectName) currentPathInput = input.projectName;
5107
5989
  else if (input.yes) {
5108
- let defaultName = DEFAULT_CONFIG.relativePath;
5990
+ const defaultConfig = getDefaultConfig();
5991
+ let defaultName = defaultConfig.relativePath;
5109
5992
  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}`;
5993
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
5994
+ defaultName = `${defaultConfig.projectName}-${counter}`;
5112
5995
  counter++;
5113
5996
  }
5114
5997
  currentPathInput = defaultName;
@@ -5146,7 +6029,8 @@ async function createProjectHandler(input) {
5146
6029
  install: false,
5147
6030
  dbSetup: "none",
5148
6031
  api: "none",
5149
- webDeploy: "none"
6032
+ webDeploy: "none",
6033
+ serverDeploy: "none"
5150
6034
  },
5151
6035
  reproducibleCommand: "",
5152
6036
  timeScaffolded,
@@ -5162,28 +6046,33 @@ async function createProjectHandler(input) {
5162
6046
  projectDirectory: input.projectName
5163
6047
  };
5164
6048
  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
6049
  let config;
5173
6050
  if (input.yes) {
6051
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
5174
6052
  config = {
5175
- ...DEFAULT_CONFIG,
6053
+ ...getDefaultConfig(),
5176
6054
  ...flagConfig,
5177
6055
  projectName: finalBaseName,
5178
6056
  projectDir: finalResolvedPath,
5179
6057
  relativePath: finalPathInput
5180
6058
  };
6059
+ coerceBackendPresets(config);
6060
+ validateConfigCompatibility(config, providedFlags, cliInput);
5181
6061
  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
6062
  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
6063
  log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
5184
6064
  log.message(displayConfig(config));
5185
6065
  log.message("");
5186
- } else config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6066
+ } else {
6067
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
6068
+ const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
6069
+ if (Object.keys(otherFlags).length > 0) {
6070
+ log.info(pc.yellow("Using these pre-selected options:"));
6071
+ log.message(displayConfig(otherFlags));
6072
+ log.message("");
6073
+ }
6074
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6075
+ }
5187
6076
  await createProject(config);
5188
6077
  const reproducibleCommand = generateReproducibleCommand(config);
5189
6078
  log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -5203,11 +6092,11 @@ async function createProjectHandler(input) {
5203
6092
  }
5204
6093
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
5205
6094
  const currentPath = path.resolve(process.cwd(), currentPathInput);
5206
- if (!fs.pathExistsSync(currentPath)) return {
6095
+ if (!await fs.pathExists(currentPath)) return {
5207
6096
  finalPathInput: currentPathInput,
5208
6097
  shouldClearDirectory: false
5209
6098
  };
5210
- const dirContents = fs.readdirSync(currentPath);
6099
+ const dirContents = await fs.readdir(currentPath);
5211
6100
  const isNotEmpty = dirContents.length > 0;
5212
6101
  if (!isNotEmpty) return {
5213
6102
  finalPathInput: currentPathInput,
@@ -5226,7 +6115,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5226
6115
  let counter = 1;
5227
6116
  const baseName = currentPathInput;
5228
6117
  let finalPathInput = `${baseName}-${counter}`;
5229
- while (fs.pathExistsSync(path.resolve(process.cwd(), finalPathInput)) && fs.readdirSync(path.resolve(process.cwd(), finalPathInput)).length > 0) {
6118
+ while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
5230
6119
  counter++;
5231
6120
  finalPathInput = `${baseName}-${counter}`;
5232
6121
  }
@@ -5252,6 +6141,10 @@ async function addAddonsHandler(input) {
5252
6141
  const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
5253
6142
  if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
5254
6143
  }
6144
+ if (!input.serverDeploy) {
6145
+ const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
6146
+ if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
6147
+ }
5255
6148
  const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
5256
6149
  let somethingAdded = false;
5257
6150
  if (input.addons && input.addons.length > 0) {
@@ -5272,6 +6165,15 @@ async function addAddonsHandler(input) {
5272
6165
  });
5273
6166
  somethingAdded = true;
5274
6167
  }
6168
+ if (input.serverDeploy && input.serverDeploy !== "none") {
6169
+ await addDeploymentToProject({
6170
+ ...input,
6171
+ install: false,
6172
+ suppressInstallMessage: true,
6173
+ serverDeploy: input.serverDeploy
6174
+ });
6175
+ somethingAdded = true;
6176
+ }
5275
6177
  if (!somethingAdded) {
5276
6178
  outro(pc.yellow("No addons or deployment configurations to add."));
5277
6179
  return;
@@ -5375,6 +6277,7 @@ const router = t.router({
5375
6277
  runtime: RuntimeSchema.optional(),
5376
6278
  api: APISchema.optional(),
5377
6279
  webDeploy: WebDeploySchema.optional(),
6280
+ serverDeploy: ServerDeploySchema.optional(),
5378
6281
  directoryConflict: DirectoryConflictSchema.optional(),
5379
6282
  renderTitle: z.boolean().optional(),
5380
6283
  disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
@@ -5390,6 +6293,7 @@ const router = t.router({
5390
6293
  add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
5391
6294
  addons: z.array(AddonsSchema).optional().default([]),
5392
6295
  webDeploy: WebDeploySchema.optional(),
6296
+ serverDeploy: ServerDeploySchema.optional(),
5393
6297
  projectDir: z.string().optional(),
5394
6298
  install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
5395
6299
  packageManager: PackageManagerSchema.optional()