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

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,7 @@ 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);
4429
+ if (config.packageManager === "bun" && hasNative) processSingleTemplate(vfs, templates, "extras/bunfig.toml", "bunfig.toml", config);
4237
4430
  if (config.packageManager === "pnpm" && (hasNative || hasNuxt)) processSingleTemplate(vfs, templates, "extras/_npmrc", ".npmrc", config);
4238
4431
  if (config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare") processSingleTemplate(vfs, templates, "extras/env.d.ts", "packages/env/env.d.ts", config);
4239
4432
  }
@@ -4243,7 +4436,8 @@ async function processDeployTemplates(vfs, templates, config) {
4243
4436
  const isBackendSelf = config.backend === "self";
4244
4437
  if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") processTemplatesFromPrefix(vfs, templates, "packages/infra", "packages/infra", config);
4245
4438
  if (config.webDeploy === "docker" || config.serverDeploy === "docker") processTemplatesFromPrefix(vfs, templates, "deploy/docker/compose", "", config);
4246
- if (config.webDeploy !== "none" && config.webDeploy !== "cloudflare") {
4439
+ if (config.webDeploy === "vercel" || config.serverDeploy === "vercel") processTemplatesFromPrefix(vfs, templates, "deploy/vercel", "", config);
4440
+ if (config.webDeploy !== "none" && config.webDeploy !== "cloudflare" && config.webDeploy !== "vercel") {
4247
4441
  const templateMap = {
4248
4442
  "tanstack-router": "react/tanstack-router",
4249
4443
  "tanstack-start": "react/tanstack-start",
@@ -4256,7 +4450,7 @@ async function processDeployTemplates(vfs, templates, config) {
4256
4450
  };
4257
4451
  for (const f of config.frontend) if (templateMap[f]) processTemplatesFromPrefix(vfs, templates, `deploy/${config.webDeploy}/web/${templateMap[f]}`, "apps/web", config);
4258
4452
  }
4259
- if (config.serverDeploy !== "none" && config.serverDeploy !== "cloudflare" && !isBackendSelf) processTemplatesFromPrefix(vfs, templates, `deploy/${config.serverDeploy}/server`, "apps/server", config);
4453
+ if (config.serverDeploy !== "none" && config.serverDeploy !== "cloudflare" && config.serverDeploy !== "vercel" && !isBackendSelf) processTemplatesFromPrefix(vfs, templates, `deploy/${config.serverDeploy}/server`, "apps/server", config);
4260
4454
  }
4261
4455
  //#endregion
4262
4456
  //#region src/utils/reproducible-command.ts
@@ -4344,6 +4538,7 @@ async function generate(options) {
4344
4538
  processAlchemyPlugins(vfs, config);
4345
4539
  processPwaPlugins(vfs, config);
4346
4540
  processCatalogs(vfs, config);
4541
+ processVercelConfig(vfs, config);
4347
4542
  processReadme(vfs, config);
4348
4543
  if (options.version) {
4349
4544
  const reproducibleCommand = generateReproducibleCommand(config);
@@ -5625,13 +5820,16 @@ import { RPCLink } from "@orpc/client/fetch";
5625
5820
 
5626
5821
  {{#if (eq backend "self")}}
5627
5822
  export const link = new RPCLink({
5628
- url: \`\${window.location.origin}/rpc\`,
5823
+ // Lazy: this module is imported during SSR where window does not exist
5824
+ url: () => \`\${window.location.origin}/rpc\`,
5629
5825
  });
5630
5826
  {{else}}
5631
5827
  import { PUBLIC_SERVER_URL } from "astro:env/client";
5632
5828
 
5829
+ {{> getServerUrlSpaces}}
5830
+
5633
5831
  export const link = new RPCLink({
5634
- url: \`\${PUBLIC_SERVER_URL}/rpc\`,
5832
+ url: \`\${getServerUrl(PUBLIC_SERVER_URL)}/rpc\`,
5635
5833
  {{#if (eq auth "better-auth")}}
5636
5834
  fetch(url, options) {
5637
5835
  return fetch(url, {
@@ -5651,11 +5849,25 @@ import { createORPCClient } from '@orpc/client'
5651
5849
  import { RPCLink } from '@orpc/client/fetch'
5652
5850
  import { createTanstackQueryUtils } from "@orpc/tanstack-query";
5653
5851
 
5852
+ function getServerUrl(url: string) {
5853
+ const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
5854
+
5855
+ if (!normalized.startsWith("/")) {
5856
+ return normalized;
5857
+ }
5858
+
5859
+ if (import.meta.server) {
5860
+ return \`\${useRequestURL().origin}\${normalized}\`;
5861
+ }
5862
+
5863
+ return \`\${window.location.origin}\${normalized}\`;
5864
+ }
5865
+
5654
5866
  export default defineNuxtPlugin(() => {
5655
5867
  const config = useRuntimeConfig();
5656
5868
  const serverUrl =
5657
5869
  (import.meta.server && config.serverUrl) || config.public.serverUrl;
5658
- const rpcUrl = \`\${serverUrl}/rpc\`;
5870
+ const rpcUrl = \`\${getServerUrl(serverUrl)}/rpc\`;
5659
5871
 
5660
5872
  const rpcLink = new RPCLink({
5661
5873
  url: rpcUrl,
@@ -5783,6 +5995,10 @@ export function createQueryClient() {
5783
5995
  export const queryClient = createQueryClient();
5784
5996
  {{/unless}}
5785
5997
 
5998
+ {{#unless (eq backend "self")}}
5999
+ {{> getServerUrl}}
6000
+
6001
+ {{/unless}}
5786
6002
  {{#if (and (includes frontend "tanstack-start") (eq backend "self"))}}
5787
6003
  const getORPCClient = createIsomorphicFn()
5788
6004
  .server(() =>
@@ -5811,7 +6027,7 @@ const getORPCClient = createIsomorphicFn()
5811
6027
  export const client: RouterClient<typeof appRouter> = getORPCClient();
5812
6028
  {{else if (includes frontend "tanstack-start")}}
5813
6029
  const link = new RPCLink({
5814
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6030
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5815
6031
  {{#if (eq auth "clerk")}}
5816
6032
  headers: async () => {
5817
6033
  const token = await getClerkAuthToken();
@@ -5838,9 +6054,9 @@ export const link = new RPCLink({
5838
6054
  {{#if (and (eq backend "self") (includes frontend "next"))}}
5839
6055
  url: \`\${typeof window !== "undefined" ? window.location.origin : "http://localhost:3001"}/api/rpc\`,
5840
6056
  {{else if (includes frontend "next")}}
5841
- url: \`\${env.NEXT_PUBLIC_SERVER_URL}/rpc\`,
6057
+ url: \`\${getServerUrl(env.NEXT_PUBLIC_SERVER_URL)}/rpc\`,
5842
6058
  {{else}}
5843
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6059
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5844
6060
  {{/if}}
5845
6061
  {{#if (eq auth "clerk")}}
5846
6062
  headers: async () => {
@@ -5901,8 +6117,10 @@ export const queryClient = new QueryClient({
5901
6117
  }),
5902
6118
  });
5903
6119
 
6120
+ {{> getServerUrl}}
6121
+
5904
6122
  export const link = new RPCLink({
5905
- url: \`\${env.VITE_SERVER_URL}/rpc\`,
6123
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/rpc\`,
5906
6124
  {{#if (eq auth "better-auth")}}
5907
6125
  fetch(url, options) {
5908
6126
  return fetch(url, {
@@ -5934,6 +6152,10 @@ export const queryClient = new QueryClient({
5934
6152
  }),
5935
6153
  });
5936
6154
 
6155
+ {{#unless (eq backend "self")}}
6156
+ {{> getServerUrl}}
6157
+
6158
+ {{/unless}}
5937
6159
  export const link = new RPCLink({
5938
6160
  {{#if (eq backend "self")}}
5939
6161
  url: () => {
@@ -5944,7 +6166,7 @@ export const link = new RPCLink({
5944
6166
  return \`\${window.location.origin}/rpc\`;
5945
6167
  },
5946
6168
  {{else}}
5947
- url: \`\${PUBLIC_SERVER_URL}/rpc\`,
6169
+ url: \`\${getServerUrl(PUBLIC_SERVER_URL)}/rpc\`,
5948
6170
  {{/if}}
5949
6171
  {{#if (eq auth "better-auth")}}
5950
6172
  fetch(url, options) {
@@ -6470,6 +6692,8 @@ import type { AppRouter } from "@{{projectName}}/api/routers/index";
6470
6692
  import { toast } from 'sonner';
6471
6693
  {{#unless (eq backend "self")}}
6472
6694
  import { env } from "@{{projectName}}/env/web";
6695
+
6696
+ {{> getServerUrl}}
6473
6697
  {{/unless}}
6474
6698
  {{#if (eq auth "clerk")}}
6475
6699
  import { getClerkAuthToken } from "@/utils/clerk-auth";
@@ -6496,7 +6720,7 @@ const trpcClient = createTRPCClient<AppRouter>({
6496
6720
  {{#if (eq backend "self")}}
6497
6721
  url: "/api/trpc",
6498
6722
  {{else}}
6499
- url: \`\${env.NEXT_PUBLIC_SERVER_URL}/trpc\`,
6723
+ url: \`\${getServerUrl(env.NEXT_PUBLIC_SERVER_URL)}/trpc\`,
6500
6724
  {{/if}}
6501
6725
  {{#if (eq auth "clerk")}}
6502
6726
  headers: async () => {
@@ -6547,6 +6771,8 @@ import { env } from "@{{projectName}}/env/web";
6547
6771
  import { getClerkAuthToken } from "@/utils/clerk-auth";
6548
6772
  {{/if}}
6549
6773
 
6774
+ {{> getServerUrl}}
6775
+
6550
6776
  export const queryClient = new QueryClient({
6551
6777
  queryCache: new QueryCache({
6552
6778
  onError: (error, query) => {
@@ -6565,7 +6791,7 @@ export const queryClient = new QueryClient({
6565
6791
  export const trpcClient = createTRPCClient<AppRouter>({
6566
6792
  links: [
6567
6793
  httpBatchLink({
6568
- url: \`\${env.VITE_SERVER_URL}/trpc\`,
6794
+ url: \`\${getServerUrl(env.VITE_SERVER_URL)}/trpc\`,
6569
6795
  {{#if (eq auth "clerk")}}
6570
6796
  headers: async () => {
6571
6797
  const token = await getClerkAuthToken();
@@ -13361,9 +13587,15 @@ import { polarClient } from "@polar-sh/better-auth/client";
13361
13587
  import { PUBLIC_SERVER_URL } from "astro:env/client";
13362
13588
  {{/if}}
13363
13589
 
13590
+ {{#if (ne backend "self")}}
13591
+ {{> getServerUrlSpaces}}
13592
+
13593
+ {{/if}}
13364
13594
  export const authClient = createAuthClient({
13365
13595
  {{#if (ne backend "self")}}
13366
- baseURL: PUBLIC_SERVER_URL,
13596
+ // better-auth derives its route-matching base from this URL's path, so the
13597
+ // public auth path must equal the server-side mount (/api/auth everywhere)
13598
+ baseURL: new URL("/api/auth", getServerUrl(PUBLIC_SERVER_URL)).toString(),
13367
13599
  {{/if}}
13368
13600
  {{#if (eq payments "polar")}}
13369
13601
  plugins: [polarClient()],
@@ -13897,11 +14129,17 @@ import { polarClient } from "@polar-sh/better-auth/client";
13897
14129
  export default defineNuxtPlugin(() => {
13898
14130
  {{#if (ne backend "self")}}
13899
14131
  const config = useRuntimeConfig();
14132
+ const rawServerUrl = (import.meta.server && config.serverUrl) || config.public.serverUrl;
14133
+ // Same-origin paths like /api need an absolute base, and better-auth derives
14134
+ // its route matching from this URL's path, so it must be exactly /api/auth
14135
+ const serverOrigin = rawServerUrl.startsWith("/")
14136
+ ? (import.meta.server ? useRequestURL() : window.location).origin + rawServerUrl
14137
+ : rawServerUrl;
13900
14138
  {{/if}}
13901
14139
 
13902
14140
  const authClient = createAuthClient({
13903
14141
  {{#if (ne backend "self")}}
13904
- baseURL: (import.meta.server && config.serverUrl) || config.public.serverUrl,
14142
+ baseURL: new URL("/api/auth", serverOrigin).toString(),
13905
14143
  {{/if}}
13906
14144
  {{#if (eq payments "polar")}}
13907
14145
  plugins: [polarClient()],
@@ -13921,11 +14159,15 @@ import { polarClient } from "@polar-sh/better-auth/client";
13921
14159
  {{/if}}
13922
14160
  {{#unless (eq backend "self")}}
13923
14161
  import { env } from "@{{projectName}}/env/web";
14162
+
14163
+ {{> getServerUrl}}
13924
14164
  {{/unless}}
13925
14165
 
13926
14166
  export const authClient = createAuthClient({
13927
14167
  {{#unless (eq backend "self")}}
13928
- baseURL: env.{{#if (includes frontend "next")}}NEXT_PUBLIC_SERVER_URL{{else}}VITE_SERVER_URL{{/if}},
14168
+ // better-auth derives its route-matching base from this URL's path, so the
14169
+ // public auth path must equal the server-side mount (/api/auth everywhere)
14170
+ baseURL: new URL("/api/auth", getServerUrl(env.{{#if (includes frontend "next")}}NEXT_PUBLIC_SERVER_URL{{else}}VITE_SERVER_URL{{/if}})).toString(),
13929
14171
  {{/unless}}
13930
14172
  {{#if (eq payments "polar")}}
13931
14173
  plugins: [polarClient()]
@@ -16219,8 +16461,12 @@ import { polarClient } from "@polar-sh/better-auth/client";
16219
16461
  {{/if}}
16220
16462
  import { env } from "@{{projectName}}/env/web";
16221
16463
 
16464
+ {{> getServerUrl}}
16465
+
16222
16466
  export const authClient = createAuthClient({
16223
- baseURL: env.VITE_SERVER_URL,
16467
+ // better-auth derives its route-matching base from this URL's path, so the
16468
+ // public auth path must equal the server-side mount (/api/auth everywhere)
16469
+ baseURL: new URL("/api/auth", getServerUrl(env.VITE_SERVER_URL)).toString(),
16224
16470
  {{#if (eq payments "polar")}}
16225
16471
  plugins: [polarClient()]
16226
16472
  {{/if}}
@@ -16636,9 +16882,15 @@ import { createAuthClient } from "better-auth/svelte";
16636
16882
  import { polarClient } from "@polar-sh/better-auth/client";
16637
16883
  {{/if}}
16638
16884
 
16885
+ {{#unless (eq backend "self")}}
16886
+ {{> getServerUrl}}
16887
+
16888
+ {{/unless}}
16639
16889
  export const authClient = createAuthClient({
16640
16890
  {{#unless (eq backend "self")}}
16641
- baseURL: PUBLIC_SERVER_URL,
16891
+ // better-auth derives its route-matching base from this URL's path, so the
16892
+ // public auth path must equal the server-side mount (/api/auth everywhere)
16893
+ baseURL: new URL("/api/auth", getServerUrl(PUBLIC_SERVER_URL)).toString(),
16642
16894
  {{/unless}}
16643
16895
  {{#if (eq payments "polar")}}
16644
16896
  plugins: [polarClient()]
@@ -18664,11 +18916,7 @@ const apiHandler = new OpenAPIHandler(appRouter, {
18664
18916
  });
18665
18917
  {{/if}}
18666
18918
 
18667
- {{#if (eq runtime "node")}}
18668
- new Elysia({ adapter: node() })
18669
- {{else}}
18670
- new Elysia()
18671
- {{/if}}
18919
+ const app = {{#if (eq runtime "node")}}new Elysia({ adapter: node() }){{else}}new Elysia(){{/if}}
18672
18920
  .use(
18673
18921
  cors({
18674
18922
  origin: env.CORS_ORIGIN,
@@ -18774,9 +19022,22 @@ new Elysia()
18774
19022
  })
18775
19023
  {{/if}}
18776
19024
  .get("/", () => "OK")
19025
+ {{#if (eq serverDeploy "vercel")}};
19026
+
19027
+ export default app;
19028
+
19029
+ // Elysia's default export is not auto-served by Bun or Node, so start a local
19030
+ // server outside Vercel while still exporting the app for Vercel functions.
19031
+ if (!process.env.VERCEL) {
19032
+ app.listen(3000, () => {
19033
+ console.log("Server is running on http://localhost:3000");
19034
+ });
19035
+ }
19036
+ {{else}}
18777
19037
  .listen(3000, () => {
18778
19038
  console.log("Server is running on http://localhost:3000");
18779
19039
  });
19040
+ {{/if}}
18780
19041
  `],
18781
19042
  ["backend/server/express/src/index.ts.hbs", `import { env } from "@{{projectName}}/env/server";
18782
19043
  {{#if (eq api "trpc")}}
@@ -19347,15 +19608,23 @@ app.get("/", (c) => {
19347
19608
  {{#if (eq runtime "node")}}
19348
19609
  import { serve } from "@hono/node-server";
19349
19610
 
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
- );
19611
+ {{#if (eq serverDeploy "vercel")}}
19612
+ export default app;
19613
+
19614
+ if (!process.env.VERCEL) {
19615
+ {{/if}}
19616
+ serve(
19617
+ {
19618
+ fetch: app.fetch,
19619
+ port: 3000,
19620
+ },
19621
+ (info) => {
19622
+ console.log(\`Server is running on http://localhost:\${info.port}\`);
19623
+ }
19624
+ );
19625
+ {{#if (eq serverDeploy "vercel")}}
19626
+ }
19627
+ {{/if}}
19359
19628
  {{else}}
19360
19629
  {{#if (eq runtime "bun")}}
19361
19630
  export default app;
@@ -19409,6 +19678,7 @@ lerna-debug.log*
19409
19678
 
19410
19679
  # Better-T-Stack
19411
19680
  .alchemy
19681
+ .vercel
19412
19682
 
19413
19683
  # Testing
19414
19684
  coverage
@@ -20278,7 +20548,7 @@ services:
20278
20548
  {{/if}}
20279
20549
  init: true
20280
20550
  ports:
20281
- {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "solid"))}}
20551
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "solid"))}}
20282
20552
  - "3001:80"
20283
20553
  {{else}}
20284
20554
  - "3001:3001"
@@ -20325,7 +20595,7 @@ services:
20325
20595
  {{/if}}
20326
20596
  {{/if}}
20327
20597
  healthcheck:
20328
- {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "solid"))}}
20598
+ {{#if (or (includes frontend "tanstack-router") (includes frontend "solid"))}}
20329
20599
  test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"]
20330
20600
  {{else}}
20331
20601
  test:
@@ -20686,30 +20956,14 @@ ENV VITE_CLERK_PUBLISHABLE_KEY=\${VITE_CLERK_PUBLISHABLE_KEY}
20686
20956
  ENV NODE_ENV=production
20687
20957
  RUN cd apps/web && {{packageManager}} run build
20688
20958
 
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
- }
20959
+ FROM node:24-slim AS runner
20960
+ WORKDIR /app
20961
+ COPY --from=builder /app .
20962
+ ENV NODE_ENV=production
20963
+ ENV PORT=3001
20964
+ ENV HOST=0.0.0.0
20965
+ EXPOSE 3001
20966
+ CMD ["sh", "-c", "cd apps/web && npm run start"]
20713
20967
  `],
20714
20968
  ["deploy/docker/web/react/tanstack-router/Dockerfile.hbs", `FROM node:24{{#unless (includes addons "vite-plus")}}-slim{{/unless}} AS builder
20715
20969
  {{#if (eq packageManager "bun")}}
@@ -20949,6 +21203,143 @@ EXPOSE 3001
20949
21203
 
20950
21204
  WORKDIR /app/apps/web
20951
21205
  CMD ["node", "build/index.js"]
21206
+ `],
21207
+ ["deploy/vercel/_vercelignore", `# Local env files must never ship in deployments: Vercel project env vars are
21208
+ # the source of truth (bun env:vercel:*), and frameworks like Next.js would
21209
+ # otherwise load these localhost values at runtime.
21210
+ .env
21211
+ .env.*
21212
+ **/.env
21213
+ **/.env.*
21214
+ !**/.env.example
21215
+ local.db
21216
+ local.db-*
21217
+ .alchemy/
21218
+ `],
21219
+ ["deploy/vercel/scripts/sync-vercel-env.ts.hbs", `import { spawnSync } from "node:child_process";
21220
+ import { existsSync, readFileSync } from "node:fs";
21221
+ import dotenv from "dotenv";
21222
+
21223
+ const DEFAULT_ENVIRONMENT = "preview";
21224
+ const VALID_ENVIRONMENTS = new Set(["development", "preview", "production"]);
21225
+ const VERCEL_COMMAND = [{{#if (eq packageManager "npm")}}"npx", "vercel"{{else if (eq packageManager "pnpm")}}"pnpm", "exec", "vercel"{{else}}"bunx", "vercel"{{/if}}] as const;
21226
+ const DEFAULT_FILES = [
21227
+ {{#if (eq webDeploy "vercel")}}
21228
+ "apps/web/.env",
21229
+ {{/if}}
21230
+ {{#if (and (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
21231
+ "apps/server/.env",
21232
+ {{/if}}
21233
+ ];
21234
+ const SKIP_KEYS = new Set([
21235
+ {{#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")))}}
21236
+ "BETTER_AUTH_URL",
21237
+ "CORS_ORIGIN",
21238
+ "NODE_ENV",
21239
+ {{/if}}
21240
+ ]);
21241
+ const OVERRIDE_KEYS = new Map([
21242
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
21243
+ ["NEXT_PUBLIC_SERVER_URL", "/api"],
21244
+ ["NUXT_PUBLIC_SERVER_URL", "/api"],
21245
+ ["PUBLIC_SERVER_URL", "/api"],
21246
+ ["VITE_SERVER_URL", "/api"],
21247
+ {{/if}}
21248
+ ]);
21249
+
21250
+ const args = process.argv.slice(2);
21251
+ const separatorIndex = args.indexOf("--");
21252
+ const scriptArgs = separatorIndex === -1 ? args : args.slice(0, separatorIndex);
21253
+ const forwardedArgs = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1);
21254
+
21255
+ const environment =
21256
+ scriptArgs[0] && VALID_ENVIRONMENTS.has(scriptArgs[0]) ? scriptArgs[0] : DEFAULT_ENVIRONMENT;
21257
+ const remainingArgs = scriptArgs.slice(VALID_ENVIRONMENTS.has(scriptArgs[0] ?? "") ? 1 : 0);
21258
+ // Split remaining args into env-file paths and passthrough Vercel CLI flags.
21259
+ // A bare token counts as a file only when it exists on disk, so flags and their
21260
+ // values (e.g. \`--scope my-team\`) forward correctly regardless of argument order.
21261
+ const files: string[] = [];
21262
+ const passthroughArgs: string[] = [];
21263
+ for (const arg of remainingArgs) {
21264
+ if (!arg.startsWith("-") && existsSync(arg)) {
21265
+ files.push(arg);
21266
+ } else {
21267
+ passthroughArgs.push(arg);
21268
+ }
21269
+ }
21270
+ const vercelArgs = [...passthroughArgs, ...forwardedArgs];
21271
+ const envFiles = files.length > 0 ? files : DEFAULT_FILES;
21272
+
21273
+ if (envFiles.length === 0) {
21274
+ console.log("No env files configured for this Vercel stack.");
21275
+ process.exit(0);
21276
+ }
21277
+
21278
+ const env = new Map<string, string>();
21279
+
21280
+ for (const file of envFiles) {
21281
+ if (!existsSync(file)) {
21282
+ console.warn(\`Skipping missing env file: \${file}\`);
21283
+ continue;
21284
+ }
21285
+
21286
+ for (const [key, value] of Object.entries(dotenv.parse(readFileSync(file, "utf8")))) {
21287
+ if (SKIP_KEYS.has(key)) continue;
21288
+ env.set(key, OVERRIDE_KEYS.get(key) ?? value);
21289
+ }
21290
+ }
21291
+
21292
+ if (env.size === 0) {
21293
+ console.log("No Vercel env vars found to sync.");
21294
+ process.exit(0);
21295
+ }
21296
+
21297
+ const LOCAL_VALUE_PATTERN = /localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|^file:/i;
21298
+ const localKeys = [...env.entries()]
21299
+ .filter(([, value]) => LOCAL_VALUE_PATTERN.test(value))
21300
+ .map(([key]) => key);
21301
+ if (localKeys.length > 0) {
21302
+ console.warn(
21303
+ \`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.\`,
21304
+ );
21305
+ }
21306
+
21307
+ console.log(\`Syncing \${env.size} env var(s) to Vercel \${environment}.\`);
21308
+ for (const [key, value] of env.entries()) {
21309
+ const result = spawnSync(
21310
+ VERCEL_COMMAND[0],
21311
+ [
21312
+ ...VERCEL_COMMAND.slice(1),
21313
+ "env",
21314
+ "add",
21315
+ key,
21316
+ environment,
21317
+ "--force",
21318
+ "--yes",
21319
+ "--non-interactive",
21320
+ ...vercelArgs,
21321
+ ],
21322
+ {
21323
+ input: \`\${value}\\n\`,
21324
+ stdio: ["pipe", "inherit", "inherit"],
21325
+ encoding: "utf8",
21326
+ // Windows resolves bunx/npx/pnpm via .cmd shims, which need a shell
21327
+ shell: process.platform === "win32",
21328
+ },
21329
+ );
21330
+
21331
+ if (result.error) {
21332
+ console.error(\`Failed to sync \${key}: \${result.error.message}\`);
21333
+ process.exit(1);
21334
+ }
21335
+
21336
+ if (result.status !== 0) {
21337
+ console.error(\`Failed to sync \${key}\`);
21338
+ process.exit(result.status ?? 1);
21339
+ }
21340
+ }
21341
+
21342
+ console.log("Vercel env sync complete. Redeploy for changes to take effect.");
20952
21343
  `],
20953
21344
  ["examples/ai/convex/packages/backend/convex/agent.ts.hbs", `import { Agent } from "@convex-dev/agent";
20954
21345
  import { google } from "@ai-sdk/google";
@@ -29645,14 +30036,7 @@ shamefully-hoist=true
29645
30036
  strict-peer-dependencies=false
29646
30037
  {{/if}}`],
29647
30038
  ["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
30039
  peer = false # Expo native projects declare SDK peers explicitly; this keeps Bun isolated installs deduped for native modules
29654
- {{/if}}
29655
- {{/if}}
29656
30040
  `],
29657
30041
  ["extras/env.d.ts.hbs", `{{#if (eq serverDeploy "cloudflare")}}
29658
30042
  import { type server } from "@{{projectName}}/infra/alchemy.run";
@@ -29678,19 +30062,19 @@ declare module "cloudflare:workers" {
29678
30062
  ["extras/pnpm-workspace.yaml.hbs", `packages:
29679
30063
  - "apps/*"
29680
30064
  - "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"))}}
30065
+ {{#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
30066
 
29683
30067
  # pnpm 11 blocks dependency lifecycle scripts unless they are approved here.
29684
30068
  # Entries are scoped to packages this generated stack can pull in.
29685
30069
  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"))}}
30070
+ {{#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
30071
  esbuild: true
29688
30072
  {{/if}}
29689
30073
  {{#if (includes frontend "nuxt")}}
29690
30074
  "@parcel/watcher": true
29691
30075
  vue-demi: true
29692
30076
  {{/if}}
29693
- {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (includes addons "pwa") (includes frontend "next"))}}
30077
+ {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare") (eq webDeploy "docker") (eq webDeploy "vercel") (includes addons "pwa") (includes frontend "next"))}}
29694
30078
  sharp: true
29695
30079
  {{/if}}
29696
30080
  {{#if (or (eq webDeploy "cloudflare") (eq serverDeploy "cloudflare"))}}
@@ -29737,6 +30121,8 @@ pnpm-debug.log*
29737
30121
  import tailwindcss from "@tailwindcss/vite";
29738
30122
  import { defineConfig, envField } from "astro/config";
29739
30123
  {{#if (or (includes addons "electrobun") (includes addons "tauri"))}}
30124
+ {{else if (eq webDeploy "vercel")}}
30125
+ import vercel from "@astrojs/vercel";
29740
30126
  {{else}}
29741
30127
  import node from "@astrojs/node";
29742
30128
  {{/if}}
@@ -29745,6 +30131,9 @@ import node from "@astrojs/node";
29745
30131
  export default defineConfig({
29746
30132
  {{#if (or (includes addons "electrobun") (includes addons "tauri"))}}
29747
30133
  output: "static",
30134
+ {{else if (eq webDeploy "vercel")}}
30135
+ output: "server",
30136
+ adapter: vercel(),
29748
30137
  {{else}}
29749
30138
  output: "server",
29750
30139
  adapter: node({ mode: "standalone" }),
@@ -29905,9 +30294,6 @@ const { title = "{{projectName}}" } = Astro.props;
29905
30294
  `],
29906
30295
  ["frontend/astro/src/pages/index.astro.hbs", `---
29907
30296
  import Layout from "../layouts/Layout.astro";
29908
- {{#if (eq api "orpc")}}
29909
- import { orpc } from "../lib/orpc";
29910
- {{/if}}
29911
30297
 
29912
30298
  const TITLE_TEXT = \`
29913
30299
  ██████╗ ███████╗████████╗████████╗███████╗██████╗
@@ -34429,10 +34815,16 @@ export function ThemeProvider({
34429
34815
  }
34430
34816
  `],
34431
34817
  ["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";
34818
+ ["frontend/react/react-router/react-router.config.ts.hbs", `import type { Config } from "@react-router/dev/config";
34433
34819
 
34434
34820
  export default {
34821
+ {{#if (and (eq webDeploy "vercel") (not (or (includes addons "tauri") (includes addons "electrobun"))))}}
34822
+ // Adopt vercelPreset() once @vercel/react-router supports RR8 (vercel/vercel#16730)
34823
+ {{/if}}
34824
+ {{#if (or (includes addons "tauri") (includes addons "electrobun"))}}
34825
+ // Desktop addons package static web assets; SSR output cannot be bundled
34435
34826
  ssr: false,
34827
+ {{/if}}
34436
34828
  appDirectory: "src",
34437
34829
  } satisfies Config;
34438
34830
  `],
@@ -34910,6 +35302,12 @@ export default defineConfig({
34910
35302
  reactRouter(),
34911
35303
  tsconfigPaths(),
34912
35304
  ],
35305
+ {{#if (and (eq webDeploy "vercel") (not (or (includes addons "tauri") (includes addons "electrobun"))))}}
35306
+ ssr: {
35307
+ // Vercel functions have no node_modules; bundle all deps into the server build
35308
+ noExternal: true,
35309
+ },
35310
+ {{/if}}
34913
35311
  });
34914
35312
  `],
34915
35313
  ["frontend/react/tanstack-router/index.html.hbs", `<!DOCTYPE html>
@@ -35480,6 +35878,10 @@ export function getRouter() {
35480
35878
  }
35481
35879
  {{else}}
35482
35880
  {{#if (eq api "trpc")}}
35881
+ {{#unless (eq backend "self")}}
35882
+ {{> getServerUrl}}
35883
+
35884
+ {{/unless}}
35483
35885
  function createQueryClient() {
35484
35886
  return new QueryClient({
35485
35887
  queryCache: new QueryCache({
@@ -35501,7 +35903,7 @@ function createQueryClient() {
35501
35903
  const trpcClient = createTRPCClient<AppRouter>({
35502
35904
  links: [
35503
35905
  httpBatchLink({
35504
- url: {{#if (eq backend "self")}}"/api/trpc"{{else}}\`\${env.VITE_SERVER_URL}/trpc\`{{/if}},
35906
+ url: {{#if (eq backend "self")}}"/api/trpc"{{else}}\`\${getServerUrl(env.VITE_SERVER_URL)}/trpc\`{{/if}},
35505
35907
  {{#if (eq auth "clerk")}}
35506
35908
  headers: async () => {
35507
35909
  const token = await getClerkAuthToken();
@@ -35944,7 +36346,7 @@ function HomeComponent() {
35944
36346
  `],
35945
36347
  ["frontend/react/tanstack-start/vite.config.ts.hbs", `import { defineConfig } from "{{#if (includes addons "vite-plus")}}vite-plus{{else}}vite{{/if}}";
35946
36348
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
35947
- {{#if (eq webDeploy "docker")}}
36349
+ {{#if (or (eq webDeploy "docker") (eq webDeploy "vercel"))}}
35948
36350
  import { nitro } from "nitro/vite";
35949
36351
  {{/if}}
35950
36352
  import tailwindcss from "@tailwindcss/vite";
@@ -35966,12 +36368,17 @@ export default defineConfig({
35966
36368
  },
35967
36369
  },
35968
36370
  {{/if}}),
35969
- {{#if (eq webDeploy "docker")}}
36371
+ {{#if (or (eq webDeploy "docker") (eq webDeploy "vercel"))}}
35970
36372
  nitro(),
35971
36373
  {{/if}}
35972
36374
  viteReact(),
35973
36375
  ],
35974
- {{#if (and (eq backend "convex") (eq auth "better-auth"))}}
36376
+ {{#if (eq webDeploy "vercel")}}
36377
+ // Bundle all SSR deps: Vercel functions have no node_modules at runtime
36378
+ ssr: {
36379
+ noExternal: true,
36380
+ },
36381
+ {{else if (and (eq backend "convex") (eq auth "better-auth"))}}
35975
36382
  ssr: {
35976
36383
  noExternal: ["@convex-dev/better-auth"],
35977
36384
  },
@@ -36767,6 +37174,8 @@ import adapter from '@sveltejs/adapter-static';
36767
37174
  import alchemy from 'alchemy/cloudflare/sveltekit';
36768
37175
  {{else if (eq webDeploy "docker")}}
36769
37176
  import adapter from '@sveltejs/adapter-node';
37177
+ {{else if (eq webDeploy "vercel")}}
37178
+ import adapter from '@sveltejs/adapter-vercel';
36770
37179
  {{else}}
36771
37180
  import adapter from '@sveltejs/adapter-auto';
36772
37181
  {{/if}}
@@ -36792,6 +37201,8 @@ const config = {
36792
37201
  {{else if (eq webDeploy "docker")}}
36793
37202
  // adapter-node builds a standalone Node server (run with \`node build/index.js\`).
36794
37203
  adapter: adapter()
37204
+ {{else if (eq webDeploy "vercel")}}
37205
+ adapter: adapter()
36795
37206
  {{else}}
36796
37207
  // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
36797
37208
  // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
@@ -37022,6 +37433,33 @@ import "dotenv/config";
37022
37433
  import { createEnv } from "@t3-oss/env-core";
37023
37434
  import { z } from "zod";
37024
37435
 
37436
+ {{#if (or (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}
37437
+ function getVercelOrigin() {
37438
+ const vercelUrl =
37439
+ process.env.VERCEL_ENV === "production"
37440
+ ? (process.env.VERCEL_PROJECT_PRODUCTION_URL ?? process.env.VERCEL_URL)
37441
+ : (process.env.VERCEL_URL ?? process.env.VERCEL_PROJECT_PRODUCTION_URL);
37442
+ if (!vercelUrl) return undefined;
37443
+ return vercelUrl.startsWith("http") ? vercelUrl : \`https://\${vercelUrl}\`;
37444
+ }
37445
+
37446
+ const vercelOrigin = getVercelOrigin();
37447
+
37448
+ const runtimeEnv = {
37449
+ ...process.env,
37450
+ {{#if (eq auth "better-auth")}}
37451
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self"))}}
37452
+ // Public auth base: /api/auth bypasses the rewrite's path strip, so the
37453
+ // same URL works for incoming matching and generated callbacks
37454
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? (vercelOrigin ? \`\${vercelOrigin}/api/auth\` : undefined),
37455
+ {{else}}
37456
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? vercelOrigin,
37457
+ {{/if}}
37458
+ {{/if}}
37459
+ CORS_ORIGIN: process.env.CORS_ORIGIN ?? vercelOrigin,
37460
+ };
37461
+
37462
+ {{/if}}
37025
37463
  export const env = createEnv({
37026
37464
  server: {
37027
37465
  {{#if (ne database "none")}}
@@ -37053,7 +37491,7 @@ export const env = createEnv({
37053
37491
  CORS_ORIGIN: z.url(),
37054
37492
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
37055
37493
  },
37056
- runtimeEnv: process.env,
37494
+ runtimeEnv: {{#if (or (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}runtimeEnv{{else}}process.env{{/if}},
37057
37495
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
37058
37496
  emptyStringAsUndefined: true,
37059
37497
  });
@@ -37070,6 +37508,13 @@ import { createEnv } from "@t3-oss/env-core";
37070
37508
  {{/if}}
37071
37509
  import { z } from "zod";
37072
37510
 
37511
+ {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel") (ne backend "self") (ne backend "none") (ne backend "convex"))}}
37512
+ const serverUrlSchema = z.union([
37513
+ z.url(),
37514
+ z.string().regex(/^\\/(?!\\/)/, "Use an absolute URL or a same-origin path like /api"),
37515
+ ]);
37516
+
37517
+ {{/if}}
37073
37518
  {{#if (eq backend "convex")}}
37074
37519
  const convexUrlSchema = (exampleHost: string) =>
37075
37520
  z.url().refine((url) => new URL(url).hostname !== exampleHost, {
@@ -37155,7 +37600,7 @@ export const env = createEnv({
37155
37600
  {{else if (ne backend "none")}}
37156
37601
  {{#if (includes frontend "next")}}
37157
37602
  client: {
37158
- NEXT_PUBLIC_SERVER_URL: z.url(),
37603
+ NEXT_PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37159
37604
  {{#if (eq auth "clerk")}}
37160
37605
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
37161
37606
  {{/if}}
@@ -37168,18 +37613,18 @@ export const env = createEnv({
37168
37613
  },
37169
37614
  {{else if (includes frontend "nuxt")}}
37170
37615
  client: {
37171
- NUXT_PUBLIC_SERVER_URL: z.url(),
37616
+ NUXT_PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37172
37617
  },
37173
37618
  {{else if (or (includes frontend "svelte") (includes frontend "astro"))}}
37174
37619
  clientPrefix: "PUBLIC_",
37175
37620
  client: {
37176
- PUBLIC_SERVER_URL: z.url(),
37621
+ PUBLIC_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37177
37622
  },
37178
37623
  runtimeEnv: (import.meta as any).env,
37179
37624
  {{else}}
37180
37625
  clientPrefix: "VITE_",
37181
37626
  client: {
37182
- VITE_SERVER_URL: z.url(),
37627
+ VITE_SERVER_URL: {{#if (and (eq webDeploy "vercel") (eq serverDeploy "vercel"))}}serverUrlSchema{{else}}z.url(){{/if}},
37183
37628
  {{#if (eq auth "clerk")}}
37184
37629
  VITE_CLERK_PUBLISHABLE_KEY: z.string().min(1),
37185
37630
  {{/if}}
@@ -39642,8 +40087,8 @@ function SuccessPage() {
39642
40087
  </div>
39643
40088
  `]
39644
40089
  ]);
39645
- const TEMPLATE_COUNT = 506;
40090
+ const TEMPLATE_COUNT = 507;
39646
40091
  //#endregion
39647
- export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig, transformFilename, writeBtsConfigToVfs };
40092
+ export { EMBEDDED_TEMPLATES, GeneratorError, Handlebars, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generateReproducibleCommand, isBinaryFile, processAddonTemplates, processAddonsDeps, processFileContent, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVercelConfig, processVitePlusConfig, transformFilename, writeBtsConfigToVfs };
39648
40093
 
39649
40094
  //# sourceMappingURL=index.mjs.map