@better-t-stack/template-generator 3.35.3 → 3.36.1

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.
package/dist/index.mjs CHANGED
@@ -189,6 +189,33 @@ Handlebars.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
189
189
  Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
190
190
  Handlebars.registerHelper("not", (a) => !a);
191
191
  Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
192
+ const getServerUrlSource = `function getServerUrl(url: string) {
193
+ const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
194
+
195
+ if (!normalized.startsWith("/")) {
196
+ return normalized;
197
+ }
198
+
199
+ if (typeof window !== "undefined") {
200
+ return \`\${window.location.origin}\${normalized}\`;
201
+ }
202
+
203
+ const processEnv = (globalThis as {
204
+ process?: { env?: Record<string, string | undefined> };
205
+ }).process?.env;
206
+ const vercelUrl =
207
+ processEnv?.VERCEL_ENV === "production"
208
+ ? (processEnv?.VERCEL_PROJECT_PRODUCTION_URL ?? processEnv?.VERCEL_URL)
209
+ : (processEnv?.VERCEL_URL ?? processEnv?.VERCEL_PROJECT_PRODUCTION_URL);
210
+ if (vercelUrl) {
211
+ const origin = vercelUrl.startsWith("http") ? vercelUrl : \`https://\${vercelUrl}\`;
212
+ return \`\${origin}\${normalized}\`;
213
+ }
214
+
215
+ return \`http://localhost:3000\${normalized}\`;
216
+ }`;
217
+ Handlebars.registerPartial("getServerUrl", getServerUrlSource);
218
+ Handlebars.registerPartial("getServerUrlSpaces", getServerUrlSource.replaceAll(" ", " "));
192
219
  function processTemplateString(content, context) {
193
220
  return Handlebars.compile(content)(context);
194
221
  }
@@ -201,6 +228,7 @@ function transformFilename(filename) {
201
228
  if (basename === "_gitignore") result = result.replace(/_gitignore$/, ".gitignore");
202
229
  else if (basename === "_npmrc") result = result.replace(/_npmrc$/, ".npmrc");
203
230
  else if (basename === "_dockerignore") result = result.replace(/_dockerignore$/, ".dockerignore");
231
+ else if (basename === "_vercelignore") result = result.replace(/_vercelignore$/, ".vercelignore");
204
232
  return result;
205
233
  }
206
234
  function processFileContent(filePath, content, context) {
@@ -492,10 +520,13 @@ const dependencyVersionMap = {
492
520
  "nitro-cloudflare-dev": "^0.2.2",
493
521
  "@sveltejs/adapter-cloudflare": "^7.2.9",
494
522
  "@sveltejs/adapter-node": "^5.5.7",
523
+ "@sveltejs/adapter-vercel": "^6.3.4",
495
524
  "@cloudflare/workers-types": "^4.20260702.1",
496
525
  "@astrojs/cloudflare": "^14.1.0",
497
526
  "@astrojs/node": "^11.0.1",
527
+ "@astrojs/vercel": "^11.0.1",
498
528
  alchemy: "^0.93.12",
529
+ vercel: "^54.18.6",
499
530
  dotenv: "^17.4.2",
500
531
  tsdown: "^0.22.3",
501
532
  zod: "^4.4.3",
@@ -522,8 +553,9 @@ function addPackageDependency(options) {
522
553
  const version = dependencyVersionMap[dep];
523
554
  if (!version) throw new Error(`Missing version for dependency: ${dep}. Add it to dependencyVersionMap in add-deps.ts`);
524
555
  pkgJson.dependencies[dep] = version;
556
+ delete pkgJson.devDependencies[dep];
525
557
  }
526
- for (const dep of devDependencies) if (!pkgJson.devDependencies[dep]) {
558
+ for (const dep of devDependencies) if (!pkgJson.devDependencies[dep] && !pkgJson.dependencies[dep]) {
527
559
  const version = dependencyVersionMap[dep];
528
560
  if (!version) throw new Error(`Missing version for devDependency: ${dep}. Add it to dependencyVersionMap in add-deps.ts`);
529
561
  pkgJson.devDependencies[dep] = version;
@@ -659,10 +691,25 @@ function updateRootPackageJson(vfs, config) {
659
691
  scripts["db:down"] = pmConfig.filter(dbPackageName, "db:down");
660
692
  }
661
693
  const infraPackageName = `@${projectName}/infra`;
662
- if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") {
663
- scripts.deploy = pmConfig.filter(infraPackageName, "deploy");
694
+ const hasCloudflareDeploy = config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare";
695
+ const hasVercelDeploy = config.webDeploy === "vercel" || config.serverDeploy === "vercel";
696
+ const isMixedCloud = hasCloudflareDeploy && hasVercelDeploy;
697
+ if (hasCloudflareDeploy) {
698
+ const cfDeployScript = isMixedCloud ? config.webDeploy === "cloudflare" ? "deploy:web" : "deploy:server" : "deploy";
699
+ scripts[cfDeployScript] = pmConfig.filter(infraPackageName, "deploy");
664
700
  scripts.destroy = pmConfig.filter(infraPackageName, "destroy");
665
701
  }
702
+ if (hasVercelDeploy) {
703
+ const vercelTarget = config.webDeploy === "vercel" ? "web" : "server";
704
+ const vercelDeploy = isMixedCloud ? `deploy:${vercelTarget}` : "deploy";
705
+ scripts["deploy:setup"] = "vercel link";
706
+ scripts["dev:vercel"] = "vercel dev -L";
707
+ scripts["env:preview"] = "tsx scripts/sync-vercel-env.ts preview";
708
+ scripts["env:production"] = "tsx scripts/sync-vercel-env.ts production";
709
+ scripts[vercelDeploy] = "vercel deploy";
710
+ scripts[`${vercelDeploy}:prod`] = "vercel deploy --prod";
711
+ scripts["deploy:check"] = "vercel deploy --dry";
712
+ }
666
713
  if (config.webDeploy === "docker" || config.serverDeploy === "docker") {
667
714
  scripts["docker:build"] = "docker compose build";
668
715
  scripts["docker:up"] = "docker compose up -d --build";
@@ -941,6 +988,86 @@ function updateVitePlusPackageScripts(vfs, config) {
941
988
  vfs.writeJson(webPkgPath, webPkg);
942
989
  }
943
990
  //#endregion
991
+ //#region src/post-process/vercel-config.ts
992
+ function getWebFramework(frontend, isDesktop) {
993
+ if (frontend.includes("next")) return "nextjs";
994
+ if (frontend.includes("nuxt")) return "nuxtjs";
995
+ if (frontend.includes("svelte")) return "sveltekit";
996
+ if (frontend.includes("astro")) return "astro";
997
+ if (frontend.includes("tanstack-start")) return "tanstack-start";
998
+ if (frontend.includes("react-router") && !isDesktop) return "react-router";
999
+ return "vite";
1000
+ }
1001
+ function getPublicServerUrlVar(frontend) {
1002
+ if (frontend.includes("next")) return "NEXT_PUBLIC_SERVER_URL";
1003
+ if (frontend.includes("nuxt")) return "NUXT_PUBLIC_SERVER_URL";
1004
+ if (frontend.includes("svelte") || frontend.includes("astro")) return "PUBLIC_SERVER_URL";
1005
+ return "VITE_SERVER_URL";
1006
+ }
1007
+ function processVercelConfig(vfs, config) {
1008
+ const { webDeploy, serverDeploy, backend, runtime, frontend, addons, packageManager } = config;
1009
+ if (webDeploy !== "vercel" && serverDeploy !== "vercel") return;
1010
+ const hasWeb = webDeploy === "vercel";
1011
+ const hasServer = serverDeploy === "vercel" && backend !== "self";
1012
+ const isDesktop = addons.includes("tauri") || addons.includes("electrobun");
1013
+ const isStaticSpa = frontend.includes("tanstack-router") || frontend.includes("solid") || frontend.includes("react-router") && isDesktop;
1014
+ const installCommand = `cd ../.. && ${packageManager} install`;
1015
+ const services = {};
1016
+ if (hasWeb) {
1017
+ const web = {
1018
+ root: "apps/web",
1019
+ framework: getWebFramework(frontend, isDesktop),
1020
+ installCommand
1021
+ };
1022
+ if (hasServer) web.buildCommand = `${getPublicServerUrlVar(frontend)}=/api ${packageManager} run build`;
1023
+ if (frontend.includes("react-router") && isDesktop) web.outputDirectory = "build/client";
1024
+ if (isStaticSpa) web.rewrites = [{
1025
+ source: "/(.*)",
1026
+ destination: "/index.html"
1027
+ }];
1028
+ services.web = web;
1029
+ }
1030
+ if (hasServer) {
1031
+ const server = {
1032
+ root: "apps/server",
1033
+ framework: backend,
1034
+ entrypoint: "src/index.ts",
1035
+ installCommand
1036
+ };
1037
+ if (hasWeb) server.routes = [{
1038
+ src: "/api/((?!auth(?:/|$)).*)",
1039
+ transforms: [{
1040
+ type: "request.path",
1041
+ op: "set",
1042
+ args: "/$1"
1043
+ }]
1044
+ }];
1045
+ services.server = server;
1046
+ }
1047
+ const rewrites = [];
1048
+ if (hasWeb && hasServer) rewrites.push({
1049
+ source: "/api/(.*)",
1050
+ destination: { service: "server" }
1051
+ }, {
1052
+ source: "/(.*)",
1053
+ destination: { service: "web" }
1054
+ });
1055
+ else if (hasWeb) rewrites.push({
1056
+ source: "/(.*)",
1057
+ destination: { service: "web" }
1058
+ });
1059
+ else if (hasServer) rewrites.push({
1060
+ source: "/(.*)",
1061
+ destination: { service: "server" }
1062
+ });
1063
+ vfs.writeJson("vercel.json", {
1064
+ $schema: "https://openapi.vercel.sh/vercel.json",
1065
+ ...hasServer && runtime === "bun" ? { bunVersion: "1.x" } : {},
1066
+ services,
1067
+ rewrites
1068
+ });
1069
+ }
1070
+ //#endregion
944
1071
  //#region src/processors/addons-deps.ts
945
1072
  function processAddonsDeps(vfs, config) {
946
1073
  if (!config.addons || config.addons.length === 0) return;
@@ -2062,12 +2189,47 @@ function processDrizzleDeps(vfs, config, dbPkgPath, webPkgPath, webExists) {
2062
2189
  //#endregion
2063
2190
  //#region src/processors/deploy-deps.ts
2064
2191
  function processDeployDeps(vfs, config) {
2065
- const { webDeploy, serverDeploy, frontend, backend } = config;
2192
+ const { webDeploy, serverDeploy, frontend, backend, addons } = config;
2066
2193
  const isCloudflareWeb = webDeploy === "cloudflare";
2067
2194
  const isCloudflareServer = serverDeploy === "cloudflare";
2068
2195
  const isDockerWeb = webDeploy === "docker";
2196
+ const isVercelWeb = webDeploy === "vercel";
2197
+ const isVercelServer = serverDeploy === "vercel";
2069
2198
  const isBackendSelf = backend === "self";
2070
- if (!isCloudflareWeb && !isCloudflareServer && !isDockerWeb) return;
2199
+ if (!isCloudflareWeb && !isCloudflareServer && !isDockerWeb && !isVercelWeb && !isVercelServer) return;
2200
+ if (isVercelWeb || isVercelServer) addPackageDependency({
2201
+ vfs,
2202
+ packagePath: "package.json",
2203
+ devDependencies: [
2204
+ "@types/node",
2205
+ "tsx",
2206
+ "vercel"
2207
+ ]
2208
+ });
2209
+ if (isVercelWeb && frontend.includes("tanstack-start")) {
2210
+ const webPkgPath = "apps/web/package.json";
2211
+ if (vfs.exists(webPkgPath)) addPackageDependency({
2212
+ vfs,
2213
+ packagePath: webPkgPath,
2214
+ dependencies: ["nitro"]
2215
+ });
2216
+ }
2217
+ if (isVercelWeb && frontend.includes("astro") && !addons.includes("electrobun") && !addons.includes("tauri")) {
2218
+ const webPkgPath = "apps/web/package.json";
2219
+ if (vfs.exists(webPkgPath)) addPackageDependency({
2220
+ vfs,
2221
+ packagePath: webPkgPath,
2222
+ dependencies: ["@astrojs/vercel"]
2223
+ });
2224
+ }
2225
+ if (isVercelWeb && frontend.includes("svelte") && !addons.includes("electrobun") && !addons.includes("tauri")) {
2226
+ const webPkgPath = "apps/web/package.json";
2227
+ if (vfs.exists(webPkgPath)) addPackageDependency({
2228
+ vfs,
2229
+ packagePath: webPkgPath,
2230
+ devDependencies: ["@sveltejs/adapter-vercel"]
2231
+ });
2232
+ }
2071
2233
  if (isDockerWeb) {
2072
2234
  const webPkgPath = "apps/web/package.json";
2073
2235
  if (vfs.exists(webPkgPath)) {
@@ -2686,7 +2848,7 @@ function setupAIDependencies(vfs, config) {
2686
2848
  //#region src/processors/frontend-deps.ts
2687
2849
  function processFrontendDeps(vfs, config) {
2688
2850
  const { addons, frontend, webDeploy } = config;
2689
- if (!frontend.includes("astro") || addons.includes("electrobun") || addons.includes("tauri") || webDeploy === "cloudflare") return;
2851
+ if (!frontend.includes("astro") || addons.includes("electrobun") || addons.includes("tauri") || webDeploy === "cloudflare" || webDeploy === "vercel") return;
2690
2852
  const webPackagePath = "apps/web/package.json";
2691
2853
  if (!vfs.exists(webPackagePath)) return;
2692
2854
  addPackageDependency({
@@ -2786,6 +2948,7 @@ function getStackGeneratedIgnorePatterns(config) {
2786
2948
  patterns.add("**/.wrangler/**");
2787
2949
  if (config.frontend.includes("next")) patterns.add("apps/web/.open-next/**");
2788
2950
  }
2951
+ if (config.webDeploy === "vercel" || config.serverDeploy === "vercel") patterns.add(".vercel/**");
2789
2952
  return [...patterns];
2790
2953
  }
2791
2954
  //#endregion
@@ -3456,20 +3619,39 @@ function generateScriptsList(packageManagerRunCmd, config, hasNative) {
3456
3619
  - \`${packageManagerRunCmd} docker:up\`: Build and start the Docker Compose stack
3457
3620
  - \`${packageManagerRunCmd} docker:logs\`: Tail logs from the Docker Compose stack
3458
3621
  - \`${packageManagerRunCmd} docker:down\`: Stop the Docker Compose stack`;
3622
+ if (webDeploy === "vercel" || serverDeploy === "vercel") {
3623
+ const v = getVercelScriptNames(webDeploy, serverDeploy);
3624
+ scripts += `\n- \`${packageManagerRunCmd} ${v.setup}\`: Link this repo to a Vercel project (first-time setup)
3625
+ - \`${packageManagerRunCmd} dev:vercel\`: Run the Vercel Services dev environment locally
3626
+ - \`${packageManagerRunCmd} ${v.envPreview}\`: Sync local env files to the Vercel preview environment
3627
+ - \`${packageManagerRunCmd} ${v.envProduction}\`: Sync local env files to the Vercel production environment
3628
+ - \`${packageManagerRunCmd} ${v.deploy}\`: Create a Vercel preview deployment
3629
+ - \`${packageManagerRunCmd} ${v.deployProd}\`: Deploy to Vercel production
3630
+ - \`${packageManagerRunCmd} ${v.deployCheck}\`: Dry-run a deploy to preview framework detection and included files without uploading`;
3631
+ }
3459
3632
  return scripts;
3460
3633
  }
3461
3634
  function generateDeploymentCommands(packageManagerRunCmd, webDeploy, serverDeploy, backend) {
3462
3635
  const hasCloudflare = webDeploy === "cloudflare" || serverDeploy === "cloudflare";
3463
3636
  const hasDocker = webDeploy === "docker" || serverDeploy === "docker";
3464
- if (!hasCloudflare && !hasDocker) return "";
3637
+ const hasVercel = webDeploy === "vercel" || serverDeploy === "vercel";
3638
+ if (!hasCloudflare && !hasDocker && !hasVercel) return "";
3465
3639
  const lines = ["## Deployment"];
3466
3640
  if (hasCloudflare) {
3467
3641
  const targetLabel = webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || backend === "self") ? "web + server" : webDeploy === "cloudflare" ? "web" : "server";
3468
- lines.push("", "### Cloudflare via Alchemy", "", `- Target: ${targetLabel}`, `- Dev: ${packageManagerRunCmd} dev`, `- Deploy: ${packageManagerRunCmd} deploy`, `- Destroy: ${packageManagerRunCmd} destroy`, "", "For more details, see the guide on [Deploying to Cloudflare with Alchemy](https://www.better-t-stack.dev/docs/guides/cloudflare-alchemy).");
3642
+ const cfDeployScript = hasVercel ? webDeploy === "cloudflare" ? "deploy:web" : "deploy:server" : "deploy";
3643
+ lines.push("", "### Cloudflare via Alchemy", "", `- Target: ${targetLabel}`, `- Dev: ${packageManagerRunCmd} dev`, `- Deploy: ${packageManagerRunCmd} ${cfDeployScript}`, `- Destroy: ${packageManagerRunCmd} destroy`, "", "For more details, see the guide on [Deploying to Cloudflare with Alchemy](https://www.better-t-stack.dev/docs/guides/cloudflare-alchemy).");
3469
3644
  }
3470
3645
  if (hasDocker) {
3471
3646
  const targetLabel = webDeploy === "docker" && (serverDeploy === "docker" || backend === "self") ? "web + server" : webDeploy === "docker" ? "web" : "server";
3472
- lines.push("", "### Docker Compose", "", `- Target: ${targetLabel}`, "- Config: `docker-compose.yml` (app Dockerfiles live in `apps/*/Dockerfile`)", `- Build images: ${packageManagerRunCmd} docker:build`, `- Start: ${packageManagerRunCmd} docker:up`, `- Logs: ${packageManagerRunCmd} docker:logs`, `- Stop: ${packageManagerRunCmd} docker:down`, "", "Environment variables are read from each app's `.env` file (baked into web builds for public variables) and overridden in `docker-compose.yml` for container networking.");
3647
+ lines.push("", "### Docker Compose", "", `- Target: ${targetLabel}`, "- Config: `docker-compose.yml` (app Dockerfiles live in `apps/*/Dockerfile`)", `- Build images: ${packageManagerRunCmd} docker:build`, `- Start: ${packageManagerRunCmd} docker:up`, `- Logs: ${packageManagerRunCmd} docker:logs`, `- Stop: ${packageManagerRunCmd} docker:down`, "", "Environment variables are read from each app's `.env` file (baked into web builds for public variables) and overridden in `docker-compose.yml` for container networking.", "", "For more details, see the guide on [Deploying with Docker Compose](https://www.better-t-stack.dev/docs/guides/docker).");
3648
+ }
3649
+ if (hasVercel) {
3650
+ const vercelNames = getVercelScriptNames(webDeploy, serverDeploy);
3651
+ const targetLabel = webDeploy === "vercel" && (serverDeploy === "vercel" || backend === "self") ? "web + server" : webDeploy === "vercel" ? "web" : "server";
3652
+ lines.push("", "### Vercel Services", "", `- Target: ${targetLabel}`, "- Config: `vercel.json`", `- Link the project first: ${packageManagerRunCmd} ${vercelNames.setup}`, `- Local Vercel dev: ${packageManagerRunCmd} dev:vercel`, `- Sync preview env: ${packageManagerRunCmd} ${vercelNames.envPreview}`, `- Sync production env: ${packageManagerRunCmd} ${vercelNames.envProduction}`, `- Dry-run check (no upload): ${packageManagerRunCmd} ${vercelNames.deployCheck}`, `- Preview deploy: ${packageManagerRunCmd} ${vercelNames.deploy}`, `- Production deploy: ${packageManagerRunCmd} ${vercelNames.deployProd}`);
3653
+ if (webDeploy === "vercel" && serverDeploy === "vercel" && backend !== "self") lines.push("- Web requests under `/api/*` route to the server service and are rewritten before reaching the backend.");
3654
+ lines.push("Vercel Services share project environment variables, but deploys do not upload local `.env` files automatically. Link the project with `vercel link`, then run the env sync command before your first deploy (otherwise the deployment starts with no env vars), or pass one-off envs with `vercel deploy -e KEY=value`.", `Pass Vercel CLI flags to the env sync command directly, for example: \`${packageManagerRunCmd} ${vercelNames.envProduction} --scope your-team\`.`, "", "For more details, see the guide on [Deploying to Vercel](https://www.better-t-stack.dev/docs/guides/vercel).");
3473
3655
  }
3474
3656
  return `${lines.join("\n")}\n`;
3475
3657
  }
@@ -3486,6 +3668,17 @@ function generateGitHooksSection(packageManagerRunCmd, addons) {
3486
3668
  if (hasLinting) lines.push(`- Run checks: \`${packageManagerRunCmd} check\``);
3487
3669
  return `${lines.join("\n")}\n\n`;
3488
3670
  }
3671
+ function getVercelScriptNames(webDeploy, serverDeploy) {
3672
+ const deploy = webDeploy === "cloudflare" || serverDeploy === "cloudflare" ? `deploy:${webDeploy === "vercel" ? "web" : "server"}` : "deploy";
3673
+ return {
3674
+ setup: "deploy:setup",
3675
+ envPreview: "env:preview",
3676
+ envProduction: "env:production",
3677
+ deploy,
3678
+ deployProd: `${deploy}:prod`,
3679
+ deployCheck: "deploy:check"
3680
+ };
3681
+ }
3489
3682
  //#endregion
3490
3683
  //#region src/processors/runtime-deps.ts
3491
3684
  function processRuntimeDeps(vfs, config) {
@@ -4233,7 +4426,6 @@ async function processExtrasTemplates(vfs, templates, config) {
4233
4426
  ].includes(f));
4234
4427
  const hasNuxt = config.frontend.includes("nuxt");
4235
4428
  if (config.packageManager === "pnpm") processSingleTemplate(vfs, templates, "extras/pnpm-workspace.yaml", "pnpm-workspace.yaml", config);
4236
- if (config.packageManager === "bun") processSingleTemplate(vfs, templates, "extras/bunfig.toml", "bunfig.toml", config);
4237
4429
  if (config.packageManager === "pnpm" && (hasNative || hasNuxt)) processSingleTemplate(vfs, templates, "extras/_npmrc", ".npmrc", config);
4238
4430
  if (config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare") processSingleTemplate(vfs, templates, "extras/env.d.ts", "packages/env/env.d.ts", config);
4239
4431
  }
@@ -4243,7 +4435,8 @@ async function processDeployTemplates(vfs, templates, config) {
4243
4435
  const isBackendSelf = config.backend === "self";
4244
4436
  if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") processTemplatesFromPrefix(vfs, templates, "packages/infra", "packages/infra", config);
4245
4437
  if (config.webDeploy === "docker" || config.serverDeploy === "docker") processTemplatesFromPrefix(vfs, templates, "deploy/docker/compose", "", config);
4246
- if (config.webDeploy !== "none" && config.webDeploy !== "cloudflare") {
4438
+ if (config.webDeploy === "vercel" || config.serverDeploy === "vercel") processTemplatesFromPrefix(vfs, templates, "deploy/vercel", "", config);
4439
+ if (config.webDeploy !== "none" && config.webDeploy !== "cloudflare" && config.webDeploy !== "vercel") {
4247
4440
  const templateMap = {
4248
4441
  "tanstack-router": "react/tanstack-router",
4249
4442
  "tanstack-start": "react/tanstack-start",
@@ -4256,7 +4449,7 @@ async function processDeployTemplates(vfs, templates, config) {
4256
4449
  };
4257
4450
  for (const f of config.frontend) if (templateMap[f]) processTemplatesFromPrefix(vfs, templates, `deploy/${config.webDeploy}/web/${templateMap[f]}`, "apps/web", config);
4258
4451
  }
4259
- if (config.serverDeploy !== "none" && config.serverDeploy !== "cloudflare" && !isBackendSelf) processTemplatesFromPrefix(vfs, templates, `deploy/${config.serverDeploy}/server`, "apps/server", config);
4452
+ if (config.serverDeploy !== "none" && config.serverDeploy !== "cloudflare" && config.serverDeploy !== "vercel" && !isBackendSelf) processTemplatesFromPrefix(vfs, templates, `deploy/${config.serverDeploy}/server`, "apps/server", config);
4260
4453
  }
4261
4454
  //#endregion
4262
4455
  //#region src/utils/reproducible-command.ts
@@ -4344,6 +4537,7 @@ async function generate(options) {
4344
4537
  processAlchemyPlugins(vfs, config);
4345
4538
  processPwaPlugins(vfs, config);
4346
4539
  processCatalogs(vfs, config);
4540
+ processVercelConfig(vfs, config);
4347
4541
  processReadme(vfs, config);
4348
4542
  if (options.version) {
4349
4543
  const reproducibleCommand = generateReproducibleCommand(config);
@@ -5625,13 +5819,16 @@ import { RPCLink } from "@orpc/client/fetch";
5625
5819
 
5626
5820
  {{#if (eq backend "self")}}
5627
5821
  export const link = new RPCLink({
5628
- url: \`\${window.location.origin}/rpc\`,
5822
+ // Lazy: this module is imported during SSR where window does not exist
5823
+ url: () => \`\${window.location.origin}/rpc\`,
5629
5824
  });
5630
5825
  {{else}}
5631
5826
  import { PUBLIC_SERVER_URL } from "astro:env/client";
5632
5827
 
5828
+ {{> getServerUrlSpaces}}
5829
+
5633
5830
  export const link = new RPCLink({
5634
- url: \`\${PUBLIC_SERVER_URL}/rpc\`,
5831
+ url: \`\${getServerUrl(PUBLIC_SERVER_URL)}/rpc\`,
5635
5832
  {{#if (eq auth "better-auth")}}
5636
5833
  fetch(url, options) {
5637
5834
  return fetch(url, {
@@ -5651,11 +5848,25 @@ import { createORPCClient } from '@orpc/client'
5651
5848
  import { RPCLink } from '@orpc/client/fetch'
5652
5849
  import { createTanstackQueryUtils } from "@orpc/tanstack-query";
5653
5850
 
5851
+ function getServerUrl(url: string) {
5852
+ const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
5853
+
5854
+ if (!normalized.startsWith("/")) {
5855
+ return normalized;
5856
+ }
5857
+
5858
+ if (import.meta.server) {
5859
+ return \`\${useRequestURL().origin}\${normalized}\`;
5860
+ }
5861
+
5862
+ return \`\${window.location.origin}\${normalized}\`;
5863
+ }
5864
+
5654
5865
  export default defineNuxtPlugin(() => {
5655
5866
  const config = useRuntimeConfig();
5656
5867
  const serverUrl =
5657
5868
  (import.meta.server && config.serverUrl) || config.public.serverUrl;
5658
- const rpcUrl = \`\${serverUrl}/rpc\`;
5869
+ const rpcUrl = \`\${getServerUrl(serverUrl)}/rpc\`;
5659
5870
 
5660
5871
  const rpcLink = new RPCLink({
5661
5872
  url: rpcUrl,
@@ -5783,6 +5994,10 @@ export function createQueryClient() {
5783
5994
  export const queryClient = createQueryClient();
5784
5995
  {{/unless}}
5785
5996
 
5997
+ {{#unless (eq backend "self")}}
5998
+ {{> getServerUrl}}
5999
+
6000
+ {{/unless}}
5786
6001
  {{#if (and (includes frontend "tanstack-start") (eq backend "self"))}}
5787
6002
  const getORPCClient = createIsomorphicFn()
5788
6003
  .server(() =>
@@ -5811,7 +6026,7 @@ const getORPCClient = createIsomorphicFn()
5811
6026
  export const client: RouterClient<typeof appRouter> = getORPCClient();
5812
6027
  {{else if (includes frontend "tanstack-start")}}
5813
6028
  const link = new RPCLink({
5814
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6029
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5815
6030
  {{#if (eq auth "clerk")}}
5816
6031
  headers: async () => {
5817
6032
  const token = await getClerkAuthToken();
@@ -5838,9 +6053,9 @@ export const link = new RPCLink({
5838
6053
  {{#if (and (eq backend "self") (includes frontend "next"))}}
5839
6054
  url: \`\${typeof window !== "undefined" ? window.location.origin : "http://localhost:3001"}/api/rpc\`,
5840
6055
  {{else if (includes frontend "next")}}
5841
- url: \`\${env.NEXT_PUBLIC_SERVER_URL}/rpc\`,
6056
+ url: \`\${getServerUrl(env.NEXT_PUBLIC_SERVER_URL)}/rpc\`,
5842
6057
  {{else}}
5843
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6058
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5844
6059
  {{/if}}
5845
6060
  {{#if (eq auth "clerk")}}
5846
6061
  headers: async () => {
@@ -5901,8 +6116,10 @@ export const queryClient = new QueryClient({
5901
6116
  }),
5902
6117
  });
5903
6118
 
6119
+ {{> getServerUrl}}
6120
+
5904
6121
  export const link = new RPCLink({
5905
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6122
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5906
6123
  {{#if (eq auth "better-auth")}}
5907
6124
  fetch(url, options) {
5908
6125
  return fetch(url, {
@@ -5934,6 +6151,10 @@ export const queryClient = new QueryClient({
5934
6151
  }),
5935
6152
  });
5936
6153
 
6154
+ {{#unless (eq backend "self")}}
6155
+ {{> getServerUrl}}
6156
+
6157
+ {{/unless}}
5937
6158
  export const link = new RPCLink({
5938
6159
  {{#if (eq backend "self")}}
5939
6160
  url: () => {
@@ -5944,7 +6165,7 @@ export const link = new RPCLink({
5944
6165
  return \`\${window.location.origin}/rpc\`;
5945
6166
  },
5946
6167
  {{else}}
5947
- url: \`\${PUBLIC_SERVER_URL}/rpc\`,
6168
+ url: \`\${getServerUrl(PUBLIC_SERVER_URL)}/rpc\`,
5948
6169
  {{/if}}
5949
6170
  {{#if (eq auth "better-auth")}}
5950
6171
  fetch(url, options) {
@@ -6470,6 +6691,8 @@ import type { AppRouter } from "@{{projectName}}/api/routers/index";
6470
6691
  import { toast } from 'sonner';
6471
6692
  {{#unless (eq backend "self")}}
6472
6693
  import { env } from "@{{projectName}}/env/web";
6694
+
6695
+ {{> getServerUrl}}
6473
6696
  {{/unless}}
6474
6697
  {{#if (eq auth "clerk")}}
6475
6698
  import { getClerkAuthToken } from "@/utils/clerk-auth";
@@ -6496,7 +6719,7 @@ const trpcClient = createTRPCClient<AppRouter>({
6496
6719
  {{#if (eq backend "self")}}
6497
6720
  url: "/api/trpc",
6498
6721
  {{else}}
6499
- url: \`\${env.NEXT_PUBLIC_SERVER_URL}/trpc\`,
6722
+ url: \`\${getServerUrl(env.NEXT_PUBLIC_SERVER_URL)}/trpc\`,
6500
6723
  {{/if}}
6501
6724
  {{#if (eq auth "clerk")}}
6502
6725
  headers: async () => {
@@ -6547,6 +6770,8 @@ import { env } from "@{{projectName}}/env/web";
6547
6770
  import { getClerkAuthToken } from "@/utils/clerk-auth";
6548
6771
  {{/if}}
6549
6772
 
6773
+ {{> getServerUrl}}
6774
+
6550
6775
  export const queryClient = new QueryClient({
6551
6776
  queryCache: new QueryCache({
6552
6777
  onError: (error, query) => {
@@ -6565,7 +6790,7 @@ export const queryClient = new QueryClient({
6565
6790
  export const trpcClient = createTRPCClient<AppRouter>({
6566
6791
  links: [
6567
6792
  httpBatchLink({
6568
- url: \`\${env.VITE_SERVER_URL}/trpc\`,
6793
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/trpc\`,
6569
6794
  {{#if (eq auth "clerk")}}
6570
6795
  headers: async () => {
6571
6796
  const token = await getClerkAuthToken();
@@ -13361,9 +13586,15 @@ import { polarClient } from "@polar-sh/better-auth/client";
13361
13586
  import { PUBLIC_SERVER_URL } from "astro:env/client";
13362
13587
  {{/if}}
13363
13588
 
13589
+ {{#if (ne backend "self")}}
13590
+ {{> getServerUrlSpaces}}
13591
+
13592
+ {{/if}}
13364
13593
  export const authClient = createAuthClient({
13365
13594
  {{#if (ne backend "self")}}
13366
- baseURL: PUBLIC_SERVER_URL,
13595
+ // better-auth derives its route-matching base from this URL's path, so the
13596
+ // public auth path must equal the server-side mount (/api/auth everywhere)
13597
+ baseURL: new URL("/api/auth", getServerUrl(PUBLIC_SERVER_URL)).toString(),
13367
13598
  {{/if}}
13368
13599
  {{#if (eq payments "polar")}}
13369
13600
  plugins: [polarClient()],
@@ -13897,11 +14128,17 @@ import { polarClient } from "@polar-sh/better-auth/client";
13897
14128
  export default defineNuxtPlugin(() => {
13898
14129
  {{#if (ne backend "self")}}
13899
14130
  const config = useRuntimeConfig();
14131
+ const rawServerUrl = (import.meta.server && config.serverUrl) || config.public.serverUrl;
14132
+ // Same-origin paths like /api need an absolute base, and better-auth derives
14133
+ // its route matching from this URL's path, so it must be exactly /api/auth
14134
+ const serverOrigin = rawServerUrl.startsWith("/")
14135
+ ? (import.meta.server ? useRequestURL() : window.location).origin + rawServerUrl
14136
+ : rawServerUrl;
13900
14137
  {{/if}}
13901
14138
 
13902
14139
  const authClient = createAuthClient({
13903
14140
  {{#if (ne backend "self")}}
13904
- baseURL: (import.meta.server && config.serverUrl) || config.public.serverUrl,
14141
+ baseURL: new URL("/api/auth", serverOrigin).toString(),
13905
14142
  {{/if}}
13906
14143
  {{#if (eq payments "polar")}}
13907
14144
  plugins: [polarClient()],
@@ -13921,11 +14158,15 @@ import { polarClient } from "@polar-sh/better-auth/client";
13921
14158
  {{/if}}
13922
14159
  {{#unless (eq backend "self")}}
13923
14160
  import { env } from "@{{projectName}}/env/web";
14161
+
14162
+ {{> getServerUrl}}
13924
14163
  {{/unless}}
13925
14164
 
13926
14165
  export const authClient = createAuthClient({
13927
14166
  {{#unless (eq backend "self")}}
13928
- baseURL: env.{{#if (includes frontend "next")}}NEXT_PUBLIC_SERVER_URL{{else}}VITE_SERVER_URL{{/if}},
14167
+ // better-auth derives its route-matching base from this URL's path, so the
14168
+ // public auth path must equal the server-side mount (/api/auth everywhere)
14169
+ baseURL: new URL("/api/auth", getServerUrl(env.{{#if (includes frontend "next")}}NEXT_PUBLIC_SERVER_URL{{else}}VITE_SERVER_URL{{/if}})).toString(),
13929
14170
  {{/unless}}
13930
14171
  {{#if (eq payments "polar")}}
13931
14172
  plugins: [polarClient()]
@@ -16219,8 +16460,12 @@ import { polarClient } from "@polar-sh/better-auth/client";
16219
16460
  {{/if}}
16220
16461
  import { env } from "@{{projectName}}/env/web";
16221
16462
 
16463
+ {{> getServerUrl}}
16464
+
16222
16465
  export const authClient = createAuthClient({
16223
- baseURL: env.VITE_SERVER_URL,
16466
+ // better-auth derives its route-matching base from this URL's path, so the
16467
+ // public auth path must equal the server-side mount (/api/auth everywhere)
16468
+ baseURL: new URL("/api/auth", getServerUrl(env.VITE_SERVER_URL)).toString(),
16224
16469
  {{#if (eq payments "polar")}}
16225
16470
  plugins: [polarClient()]
16226
16471
  {{/if}}
@@ -16636,9 +16881,15 @@ import { createAuthClient } from "better-auth/svelte";
16636
16881
  import { polarClient } from "@polar-sh/better-auth/client";
16637
16882
  {{/if}}
16638
16883
 
16884
+ {{#unless (eq backend "self")}}
16885
+ {{> getServerUrl}}
16886
+
16887
+ {{/unless}}
16639
16888
  export const authClient = createAuthClient({
16640
16889
  {{#unless (eq backend "self")}}
16641
- baseURL: PUBLIC_SERVER_URL,
16890
+ // better-auth derives its route-matching base from this URL's path, so the
16891
+ // public auth path must equal the server-side mount (/api/auth everywhere)
16892
+ baseURL: new URL("/api/auth", getServerUrl(PUBLIC_SERVER_URL)).toString(),
16642
16893
  {{/unless}}
16643
16894
  {{#if (eq payments "polar")}}
16644
16895
  plugins: [polarClient()]
@@ -18664,11 +18915,7 @@ const apiHandler = new OpenAPIHandler(appRouter, {
18664
18915
  });
18665
18916
  {{/if}}
18666
18917
 
18667
- {{#if (eq runtime "node")}}
18668
- new Elysia({ adapter: node() })
18669
- {{else}}
18670
- new Elysia()
18671
- {{/if}}
18918
+ const app = {{#if (eq runtime "node")}}new Elysia({ adapter: node() }){{else}}new Elysia(){{/if}}
18672
18919
  .use(
18673
18920
  cors({
18674
18921
  origin: env.CORS_ORIGIN,
@@ -18774,9 +19021,22 @@ new Elysia()
18774
19021
  })
18775
19022
  {{/if}}
18776
19023
  .get("/", () => "OK")
19024
+ {{#if (eq serverDeploy "vercel")}};
19025
+
19026
+ export default app;
19027
+
19028
+ // Elysia's default export is not auto-served by Bun or Node, so start a local
19029
+ // server outside Vercel while still exporting the app for Vercel functions.
19030
+ if (!process.env.VERCEL) {
19031
+ app.listen(3000, () => {
19032
+ console.log("Server is running on http://localhost:3000");
19033
+ });
19034
+ }
19035
+ {{else}}
18777
19036
  .listen(3000, () => {
18778
19037
  console.log("Server is running on http://localhost:3000");
18779
19038
  });
19039
+ {{/if}}
18780
19040
  `],
18781
19041
  ["backend/server/express/src/index.ts.hbs", `import { env } from "@{{projectName}}/env/server";
18782
19042
  {{#if (eq api "trpc")}}
@@ -19347,15 +19607,23 @@ app.get("/", (c) => {
19347
19607
  {{#if (eq runtime "node")}}
19348
19608
  import { serve } from "@hono/node-server";
19349
19609
 
19350
- serve(
19351
- {
19352
- fetch: app.fetch,
19353
- port: 3000,
19354
- },
19355
- (info) => {
19356
- console.log(\`Server is running on http://localhost:\${info.port}\`);
19357
- }
19358
- );
19610
+ {{#if (eq serverDeploy "vercel")}}
19611
+ export default app;
19612
+
19613
+ if (!process.env.VERCEL) {
19614
+ {{/if}}
19615
+ serve(
19616
+ {
19617
+ fetch: app.fetch,
19618
+ port: 3000,
19619
+ },
19620
+ (info) => {
19621
+ console.log(\`Server is running on http://localhost:\${info.port}\`);
19622
+ }
19623
+ );
19624
+ {{#if (eq serverDeploy "vercel")}}
19625
+ }
19626
+ {{/if}}
19359
19627
  {{else}}
19360
19628
  {{#if (eq runtime "bun")}}
19361
19629
  export default app;
@@ -19409,6 +19677,7 @@ lerna-debug.log*
19409
19677
 
19410
19678
  # Better-T-Stack
19411
19679
  .alchemy
19680
+ .vercel
19412
19681
 
19413
19682
  # Testing
19414
19683
  coverage
@@ -20278,7 +20547,7 @@ services:
20278
20547
  {{/if}}
20279
20548
  init: true
20280
20549
  ports:
20281
- {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "solid"))}}
20550
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "solid"))}}
20282
20551
  - "3001:80"
20283
20552
  {{else}}
20284
20553
  - "3001:3001"
@@ -20325,7 +20594,7 @@ services:
20325
20594
  {{/if}}
20326
20595
  {{/if}}
20327
20596
  healthcheck:
20328
- {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "solid"))}}
20597
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "solid"))}}
20329
20598
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"]
20330
20599
  {{else}}
20331
20600
  test:
@@ -20686,30 +20955,14 @@ ENV VITE_CLERK_PUBLISHABLE_KEY=\${VITE_CLERK_PUBLISHABLE_KEY}
20686
20955
  ENV NODE_ENV=production
20687
20956
  RUN cd apps/web && {{packageManager}} run build
20688
20957
 
20689
- FROM nginx:alpine AS runner
20690
- COPY --from=builder /app/apps/web/build/client /usr/share/nginx/html
20691
- COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
20692
- EXPOSE 80
20693
- CMD ["nginx", "-g", "daemon off;"]
20694
- `],
20695
- ["deploy/docker/web/react/react-router/nginx.conf", `server {
20696
- listen 80;
20697
- server_name _;
20698
- root /usr/share/nginx/html;
20699
- index index.html;
20700
-
20701
- gzip on;
20702
- gzip_types text/plain text/css application/json application/javascript image/svg+xml;
20703
-
20704
- location /assets/ {
20705
- add_header Cache-Control "public, max-age=31536000, immutable";
20706
- }
20707
-
20708
- # SPA fallback
20709
- location / {
20710
- try_files $uri $uri/ /index.html;
20711
- }
20712
- }
20958
+ FROM node:24-slim AS runner
20959
+ WORKDIR /app
20960
+ COPY --from=builder /app .
20961
+ ENV NODE_ENV=production
20962
+ ENV PORT=3001
20963
+ ENV HOST=0.0.0.0
20964
+ EXPOSE 3001
20965
+ CMD ["sh", "-c", "cd apps/web && npm run start"]
20713
20966
  `],
20714
20967
  ["deploy/docker/web/react/tanstack-router/Dockerfile.hbs", `FROM node:24{{#unless (includes addons "vite-plus")}}-slim{{/unless}} AS builder
20715
20968
  {{#if (eq packageManager "bun")}}
@@ -20949,6 +21202,148 @@ EXPOSE 3001
20949
21202
 
20950
21203
  WORKDIR /app/apps/web
20951
21204
  CMD ["node", "build/index.js"]
21205
+ `],
21206
+ ["deploy/vercel/_vercelignore", `# Local env files must never ship in deployments: Vercel project env vars are
21207
+ # the source of truth (bun env:vercel:*), and frameworks like Next.js would
21208
+ # otherwise load these localhost values at runtime.
21209
+ .env
21210
+ .env.*
21211
+ **/.env
21212
+ **/.env.*
21213
+ !**/.env.example
21214
+ local.db
21215
+ local.db-*
21216
+ .alchemy/
21217
+ `],
21218
+ ["deploy/vercel/scripts/sync-vercel-env.ts.hbs", `import { spawnSync } from "node:child_process";
21219
+ import { existsSync, readFileSync } from "node:fs";
21220
+ import dotenv from "dotenv";
21221
+
21222
+ const DEFAULT_ENVIRONMENT = "preview";
21223
+ const VALID_ENVIRONMENTS = new Set(["development", "preview", "production"]);
21224
+ const VERCEL_COMMAND = [{{#if (eq packageManager "npm")}}"npx", "vercel"{{else if (eq packageManager "pnpm")}}"pnpm", "exec", "vercel"{{else}}"bunx", "vercel"{{/if}}] as const;
21225
+ const DEFAULT_FILES = [
21226
+ {{#if (eq webDeploy "vercel")}}
21227
+ "apps/web/.env",
21228
+ {{/if}}
21229
+ {{#if (and (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
21230
+ "apps/server/.env",
21231
+ {{/if}}
21232
+ ];
21233
+ const SKIP_KEYS = new Set([
21234
+ {{#if (or (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex")) (and (eq webDeploy "vercel") (eq backend "self")))}}
21235
+ "BETTER_AUTH_URL",
21236
+ "CORS_ORIGIN",
21237
+ "NODE_ENV",
21238
+ {{else if (and (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
21239
+ // BETTER_AUTH_URL derives from the deployment's own origin at runtime;
21240
+ // CORS_ORIGIN stays synced because the web app (if any) lives on another host
21241
+ "BETTER_AUTH_URL",
21242
+ "NODE_ENV",
21243
+ {{/if}}
21244
+ ]);
21245
+ const OVERRIDE_KEYS = new Map([
21246
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
21247
+ ["NEXT_PUBLIC_SERVER_URL", "/api"],
21248
+ ["NUXT_PUBLIC_SERVER_URL", "/api"],
21249
+ ["PUBLIC_SERVER_URL", "/api"],
21250
+ ["VITE_SERVER_URL", "/api"],
21251
+ {{/if}}
21252
+ ]);
21253
+
21254
+ const args = process.argv.slice(2);
21255
+ const separatorIndex = args.indexOf("--");
21256
+ const scriptArgs = separatorIndex === -1 ? args : args.slice(0, separatorIndex);
21257
+ const forwardedArgs = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1);
21258
+
21259
+ const environment =
21260
+ scriptArgs[0] && VALID_ENVIRONMENTS.has(scriptArgs[0]) ? scriptArgs[0] : DEFAULT_ENVIRONMENT;
21261
+ const remainingArgs = scriptArgs.slice(VALID_ENVIRONMENTS.has(scriptArgs[0] ?? "") ? 1 : 0);
21262
+ // Split remaining args into env-file paths and passthrough Vercel CLI flags.
21263
+ // A bare token counts as a file only when it exists on disk, so flags and their
21264
+ // values (e.g. \`--scope my-team\`) forward correctly regardless of argument order.
21265
+ const files: string[] = [];
21266
+ const passthroughArgs: string[] = [];
21267
+ for (const arg of remainingArgs) {
21268
+ if (!arg.startsWith("-") && existsSync(arg)) {
21269
+ files.push(arg);
21270
+ } else {
21271
+ passthroughArgs.push(arg);
21272
+ }
21273
+ }
21274
+ const vercelArgs = [...passthroughArgs, ...forwardedArgs];
21275
+ const envFiles = files.length > 0 ? files : DEFAULT_FILES;
21276
+
21277
+ if (envFiles.length === 0) {
21278
+ console.log("No env files configured for this Vercel stack.");
21279
+ process.exit(0);
21280
+ }
21281
+
21282
+ const env = new Map<string, string>();
21283
+
21284
+ for (const file of envFiles) {
21285
+ if (!existsSync(file)) {
21286
+ console.warn(\`Skipping missing env file: \${file}\`);
21287
+ continue;
21288
+ }
21289
+
21290
+ for (const [key, value] of Object.entries(dotenv.parse(readFileSync(file, "utf8")))) {
21291
+ if (SKIP_KEYS.has(key)) continue;
21292
+ env.set(key, OVERRIDE_KEYS.get(key) ?? value);
21293
+ }
21294
+ }
21295
+
21296
+ if (env.size === 0) {
21297
+ console.log("No Vercel env vars found to sync.");
21298
+ process.exit(0);
21299
+ }
21300
+
21301
+ const LOCAL_VALUE_PATTERN = /localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|^file:/i;
21302
+ const localKeys = [...env.entries()]
21303
+ .filter(([, value]) => LOCAL_VALUE_PATTERN.test(value))
21304
+ .map(([key]) => key);
21305
+ if (localKeys.length > 0) {
21306
+ console.warn(
21307
+ \`Warning: \${localKeys.join(", ")} look\${localKeys.length === 1 ? "s" : ""} like local-only value(s). Update them in your .env file(s) and re-run this sync if your deployed app should not point at local endpoints.\`,
21308
+ );
21309
+ }
21310
+
21311
+ console.log(\`Syncing \${env.size} env var(s) to Vercel \${environment}.\`);
21312
+ for (const [key, value] of env.entries()) {
21313
+ const result = spawnSync(
21314
+ VERCEL_COMMAND[0],
21315
+ [
21316
+ ...VERCEL_COMMAND.slice(1),
21317
+ "env",
21318
+ "add",
21319
+ key,
21320
+ environment,
21321
+ "--force",
21322
+ "--yes",
21323
+ "--non-interactive",
21324
+ ...vercelArgs,
21325
+ ],
21326
+ {
21327
+ input: \`\${value}\\n\`,
21328
+ stdio: ["pipe", "inherit", "inherit"],
21329
+ encoding: "utf8",
21330
+ // Windows resolves bunx/npx/pnpm via .cmd shims, which need a shell
21331
+ shell: process.platform === "win32",
21332
+ },
21333
+ );
21334
+
21335
+ if (result.error) {
21336
+ console.error(\`Failed to sync \${key}: \${result.error.message}\`);
21337
+ process.exit(1);
21338
+ }
21339
+
21340
+ if (result.status !== 0) {
21341
+ console.error(\`Failed to sync \${key}\`);
21342
+ process.exit(result.status ?? 1);
21343
+ }
21344
+ }
21345
+
21346
+ console.log("Vercel env sync complete. Redeploy for changes to take effect.");
20952
21347
  `],
20953
21348
  ["examples/ai/convex/packages/backend/convex/agent.ts.hbs", `import { Agent } from "@convex-dev/agent";
20954
21349
  import { google } from "@ai-sdk/google";
@@ -29644,16 +30039,6 @@ function TodosRoute() {
29644
30039
  shamefully-hoist=true
29645
30040
  strict-peer-dependencies=false
29646
30041
  {{/if}}`],
29647
- ["extras/bunfig.toml.hbs", `[install]
29648
- {{#if (includes frontend "nuxt")}}
29649
- linker = "hoisted" # Nuxt needs hoisting for its dependency resolver
29650
- {{else}}
29651
- linker = "isolated"
29652
- {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}}
29653
- peer = false # Expo native projects declare SDK peers explicitly; this keeps Bun isolated installs deduped for native modules
29654
- {{/if}}
29655
- {{/if}}
29656
- `],
29657
30042
  ["extras/env.d.ts.hbs", `{{#if (eq serverDeploy "cloudflare")}}
29658
30043
  import { type server } from "@{{projectName}}/infra/alchemy.run";
29659
30044
  {{else}}
@@ -29678,19 +30063,19 @@ declare module "cloudflare:workers" {
29678
30063
  ["extras/pnpm-workspace.yaml.hbs", `packages:
29679
30064
  - "apps/*"
29680
30065
  - "packages/*"
29681
- {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq serverDeploy "docker") (eq orm "prisma") (includes addons "lefthook") (includes addons "nx") (includes addons "pwa") (includes addons "turborepo") (includes addons "vite-plus") (includes frontend "react-router") (includes frontend "next") (includes frontend "nuxt"))}}
30066
+ {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq serverDeploy "docker") (eq webDeploy "vercel") (eq serverDeploy "vercel") (eq orm "prisma") (includes addons "lefthook") (includes addons "nx") (includes addons "pwa") (includes addons "turborepo") (includes addons "vite-plus") (includes frontend "react-router") (includes frontend "next") (includes frontend "nuxt"))}}
29682
30067
 
29683
30068
  # pnpm 11 blocks dependency lifecycle scripts unless they are approved here.
29684
30069
  # Entries are scoped to packages this generated stack can pull in.
29685
30070
  allowBuilds:
29686
- {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq serverDeploy "docker") (includes addons "turborepo") (includes addons "vite-plus") (includes frontend "react-router") (includes frontend "nuxt"))}}
30071
+ {{#if (or (eq runtime "node") (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq serverDeploy "docker") (eq webDeploy "vercel") (eq serverDeploy "vercel") (includes addons "turborepo") (includes addons "vite-plus") (includes frontend "react-router") (includes frontend "nuxt"))}}
29687
30072
  esbuild: true
29688
30073
  {{/if}}
29689
30074
  {{#if (includes frontend "nuxt")}}
29690
30075
  "@parcel/watcher": true
29691
30076
  vue-demi: true
29692
30077
  {{/if}}
29693
- {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (includes addons "pwa") (includes frontend "next"))}}
30078
+ {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq webDeploy "vercel") (includes addons "pwa") (includes frontend "next"))}}
29694
30079
  sharp: true
29695
30080
  {{/if}}
29696
30081
  {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare"))}}
@@ -29737,6 +30122,8 @@ pnpm-debug.log*
29737
30122
  import tailwindcss from "@tailwindcss/vite";
29738
30123
  import { defineConfig, envField } from "astro/config";
29739
30124
  {{#if (or (includes addons "electrobun") (includes addons "tauri"))}}
30125
+ {{else if (eq webDeploy "vercel")}}
30126
+ import vercel from "@astrojs/vercel";
29740
30127
  {{else}}
29741
30128
  import node from "@astrojs/node";
29742
30129
  {{/if}}
@@ -29745,6 +30132,9 @@ import node from "@astrojs/node";
29745
30132
  export default defineConfig({
29746
30133
  {{#if (or (includes addons "electrobun") (includes addons "tauri"))}}
29747
30134
  output: "static",
30135
+ {{else if (eq webDeploy "vercel")}}
30136
+ output: "server",
30137
+ adapter: vercel(),
29748
30138
  {{else}}
29749
30139
  output: "server",
29750
30140
  adapter: node({ mode: "standalone" }),
@@ -29905,9 +30295,6 @@ const { title = "{{projectName}}" } = Astro.props;
29905
30295
  `],
29906
30296
  ["frontend/astro/src/pages/index.astro.hbs", `---
29907
30297
  import Layout from "../layouts/Layout.astro";
29908
- {{#if (eq api "orpc")}}
29909
- import { orpc } from "../lib/orpc";
29910
- {{/if}}
29911
30298
 
29912
30299
  const TITLE_TEXT = \`
29913
30300
  ██████╗ ███████╗████████╗████████╗███████╗██████╗
@@ -34429,10 +34816,16 @@ export function ThemeProvider({
34429
34816
  }
34430
34817
  `],
34431
34818
  ["frontend/react/react-router/public/favicon.ico", `[Binary file]`],
34432
- ["frontend/react/react-router/react-router.config.ts", `import type { Config } from "@react-router/dev/config";
34819
+ ["frontend/react/react-router/react-router.config.ts.hbs", `import type { Config } from "@react-router/dev/config";
34433
34820
 
34434
34821
  export default {
34822
+ {{#if (and (eq webDeploy "vercel") (not (or (includes addons "tauri") (includes addons "electrobun"))))}}
34823
+ // Adopt vercelPreset() once @vercel/react-router supports RR8 (vercel/vercel#16730)
34824
+ {{/if}}
34825
+ {{#if (or (includes addons "tauri") (includes addons "electrobun"))}}
34826
+ // Desktop addons package static web assets; SSR output cannot be bundled
34435
34827
  ssr: false,
34828
+ {{/if}}
34436
34829
  appDirectory: "src",
34437
34830
  } satisfies Config;
34438
34831
  `],
@@ -34910,6 +35303,12 @@ export default defineConfig({
34910
35303
  reactRouter(),
34911
35304
  tsconfigPaths(),
34912
35305
  ],
35306
+ {{#if (and (eq webDeploy "vercel") (not (or (includes addons "tauri") (includes addons "electrobun"))))}}
35307
+ ssr: {
35308
+ // Vercel functions have no node_modules; bundle all deps into the server build
35309
+ noExternal: true,
35310
+ },
35311
+ {{/if}}
34913
35312
  });
34914
35313
  `],
34915
35314
  ["frontend/react/tanstack-router/index.html.hbs", `<!DOCTYPE html>
@@ -35480,6 +35879,10 @@ export function getRouter() {
35480
35879
  }
35481
35880
  {{else}}
35482
35881
  {{#if (eq api "trpc")}}
35882
+ {{#unless (eq backend "self")}}
35883
+ {{> getServerUrl}}
35884
+
35885
+ {{/unless}}
35483
35886
  function createQueryClient() {
35484
35887
  return new QueryClient({
35485
35888
  queryCache: new QueryCache({
@@ -35501,7 +35904,7 @@ function createQueryClient() {
35501
35904
  const trpcClient = createTRPCClient<AppRouter>({
35502
35905
  links: [
35503
35906
  httpBatchLink({
35504
- url: {{#if (eq backend "self")}}"/api/trpc"{{else}}\`\${env.VITE_SERVER_URL}/trpc\`{{/if}},
35907
+ url: {{#if (eq backend "self")}}"/api/trpc"{{else}}\`\${getServerUrl(env.VITE_SERVER_URL)}/trpc\`{{/if}},
35505
35908
  {{#if (eq auth "clerk")}}
35506
35909
  headers: async () => {
35507
35910
  const token = await getClerkAuthToken();
@@ -35944,7 +36347,7 @@ function HomeComponent() {
35944
36347
  `],
35945
36348
  ["frontend/react/tanstack-start/vite.config.ts.hbs", `import { defineConfig } from "{{#if (includes addons "vite-plus")}}vite-plus{{else}}vite{{/if}}";
35946
36349
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
35947
- {{#if (eq webDeploy "docker")}}
36350
+ {{#if (or (eq webDeploy "docker") (eq webDeploy "vercel"))}}
35948
36351
  import { nitro } from "nitro/vite";
35949
36352
  {{/if}}
35950
36353
  import tailwindcss from "@tailwindcss/vite";
@@ -35966,12 +36369,17 @@ export default defineConfig({
35966
36369
  },
35967
36370
  },
35968
36371
  {{/if}}),
35969
- {{#if (eq webDeploy "docker")}}
36372
+ {{#if (or (eq webDeploy "docker") (eq webDeploy "vercel"))}}
35970
36373
  nitro(),
35971
36374
  {{/if}}
35972
36375
  viteReact(),
35973
36376
  ],
35974
- {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
36377
+ {{#if (eq webDeploy "vercel")}}
36378
+ // Bundle all SSR deps: Vercel functions have no node_modules at runtime
36379
+ ssr: {
36380
+ noExternal: true,
36381
+ },
36382
+ {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
35975
36383
  ssr: {
35976
36384
  noExternal: ["@convex-dev/better-auth"],
35977
36385
  },
@@ -36767,6 +37175,8 @@ import adapter from '@sveltejs/adapter-static';
36767
37175
  import alchemy from 'alchemy/cloudflare/sveltekit';
36768
37176
  {{else if (eq webDeploy "docker")}}
36769
37177
  import adapter from '@sveltejs/adapter-node';
37178
+ {{else if (eq webDeploy "vercel")}}
37179
+ import adapter from '@sveltejs/adapter-vercel';
36770
37180
  {{else}}
36771
37181
  import adapter from '@sveltejs/adapter-auto';
36772
37182
  {{/if}}
@@ -36792,6 +37202,8 @@ const config = {
36792
37202
  {{else if (eq webDeploy "docker")}}
36793
37203
  // adapter-node builds a standalone Node server (run with \`node build/index.js\`).
36794
37204
  adapter: adapter()
37205
+ {{else if (eq webDeploy "vercel")}}
37206
+ adapter: adapter()
36795
37207
  {{else}}
36796
37208
  // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
36797
37209
  // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
@@ -37022,6 +37434,33 @@ import "dotenv/config";
37022
37434
  import { createEnv } from "@t3-oss/env-core";
37023
37435
  import { z } from "zod";
37024
37436
 
37437
+ {{#if (or (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}
37438
+ function getVercelOrigin() {
37439
+ const vercelUrl =
37440
+ process.env.VERCEL_ENV === "production"
37441
+ ? (process.env.VERCEL_PROJECT_PRODUCTION_URL ?? process.env.VERCEL_URL)
37442
+ : (process.env.VERCEL_URL ?? process.env.VERCEL_PROJECT_PRODUCTION_URL);
37443
+ if (!vercelUrl) return undefined;
37444
+ return vercelUrl.startsWith("http") ? vercelUrl : \`https://\${vercelUrl}\`;
37445
+ }
37446
+
37447
+ const vercelOrigin = getVercelOrigin();
37448
+
37449
+ const runtimeEnv = {
37450
+ ...process.env,
37451
+ {{#if (eq auth "better-auth")}}
37452
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self"))}}
37453
+ // Public auth base: /api/auth bypasses the rewrite's path strip, so the
37454
+ // same URL works for incoming matching and generated callbacks
37455
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? (vercelOrigin ? \`\${vercelOrigin}/api/auth\` : undefined),
37456
+ {{else}}
37457
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? vercelOrigin,
37458
+ {{/if}}
37459
+ {{/if}}
37460
+ CORS_ORIGIN: process.env.CORS_ORIGIN ?? vercelOrigin,
37461
+ };
37462
+
37463
+ {{/if}}
37025
37464
  export const env = createEnv({
37026
37465
  server: {
37027
37466
  {{#if (ne database "none")}}
@@ -37053,7 +37492,7 @@ export const env = createEnv({
37053
37492
  CORS_ORIGIN: z.url(),
37054
37493
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
37055
37494
  },
37056
- runtimeEnv: process.env,
37495
+ runtimeEnv: {{#if (or (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}runtimeEnv{{else}}process.env{{/if}},
37057
37496
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
37058
37497
  emptyStringAsUndefined: true,
37059
37498
  });
@@ -37070,6 +37509,13 @@ import { createEnv } from "@t3-oss/env-core";
37070
37509
  {{/if}}
37071
37510
  import { z } from "zod";
37072
37511
 
37512
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
37513
+ const serverUrlSchema = z.union([
37514
+ z.url(),
37515
+ z.string().regex(/^\\/(?!\\/)/, "Use an absolute URL or a same-origin path like /api"),
37516
+ ]);
37517
+
37518
+ {{/if}}
37073
37519
  {{#if (eq backend "convex")}}
37074
37520
  const convexUrlSchema = (exampleHost: string) =>
37075
37521
  z.url().refine((url) => new URL(url).hostname !== exampleHost, {
@@ -37155,7 +37601,7 @@ export const env = createEnv({
37155
37601
  {{else if (ne backend "none")}}
37156
37602
  {{#if (includes frontend "next")}}
37157
37603
  client: {
37158
- NEXT_PUBLIC_SERVER_URL: z.url(),
37604
+ NEXT_PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37159
37605
  {{#if (eq auth "clerk")}}
37160
37606
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
37161
37607
  {{/if}}
@@ -37168,18 +37614,18 @@ export const env = createEnv({
37168
37614
  },
37169
37615
  {{else if (includes frontend "nuxt")}}
37170
37616
  client: {
37171
- NUXT_PUBLIC_SERVER_URL: z.url(),
37617
+ NUXT_PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37172
37618
  },
37173
37619
  {{else if (or (includes frontend "svelte") (includes frontend "astro"))}}
37174
37620
  clientPrefix: "PUBLIC_",
37175
37621
  client: {
37176
- PUBLIC_SERVER_URL: z.url(),
37622
+ PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37177
37623
  },
37178
37624
  runtimeEnv: (import.meta as any).env,
37179
37625
  {{else}}
37180
37626
  clientPrefix: "VITE_",
37181
37627
  client: {
37182
- VITE_SERVER_URL: z.url(),
37628
+ VITE_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37183
37629
  {{#if (eq auth "clerk")}}
37184
37630
  VITE_CLERK_PUBLISHABLE_KEY: z.string().min(1),
37185
37631
  {{/if}}
@@ -39644,6 +40090,6 @@ function SuccessPage() {
39644
40090
  ]);
39645
40091
  const TEMPLATE_COUNT = 506;
39646
40092
  //#endregion
39647
- export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig, transformFilename, writeBtsConfigToVfs };
40093
+ export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVercelConfig, processVitePlusConfig, transformFilename, writeBtsConfigToVfs };
39648
40094
 
39649
40095
  //# sourceMappingURL=index.mjs.map