void 0.10.10 → 0.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/AGENT_PROMPT.md +4 -0
  2. package/README.md +1 -1
  3. package/dist/{agents-Bmr5tFFb.mjs → agents-CtgBYqld.mjs} +1 -1
  4. package/dist/{auth-cmd-Cw1edJdZ.mjs → auth-cmd-BqsdZJp5.mjs} +3 -3
  5. package/dist/{better-auth-shared-BvnM9px6.d.mts → better-auth-shared-DealXecJ.d.mts} +1 -1
  6. package/dist/{build-cmd-C6kNDJBv.mjs → build-cmd-Bujrv5q-.mjs} +3 -3
  7. package/dist/{cache-DIUSnIjJ.mjs → cache-C11V8Fxq.mjs} +3 -3
  8. package/dist/{cancel-deploy-BosnzEHC.mjs → cancel-deploy-fwFYF04b.mjs} +2 -2
  9. package/dist/cli/cli.mjs +124 -41
  10. package/dist/cli/env-schema-probe.d.mts +96 -0
  11. package/dist/cli/env-schema-probe.mjs +272 -0
  12. package/dist/{client-C9FG6Vzc.mjs → client-Gb71-XkG.mjs} +12 -2
  13. package/dist/{config-BcAeIPe3.mjs → config-BkTvs43g.mjs} +15 -2
  14. package/dist/{config-BdUctCZD.mjs → config-CutEMNGJ.mjs} +3 -3
  15. package/dist/{create-project-CjYX23M_.mjs → create-project-DsYvl3TB.mjs} +3 -3
  16. package/dist/{db-BvKV34IK.mjs → db-DOiJMRt2.mjs} +27 -27
  17. package/dist/{delete-CThKLXfi.mjs → delete-mh6p-zkQ.mjs} +3 -3
  18. package/dist/deploy-u7Rv9q_q.mjs +7126 -0
  19. package/dist/{discover-BuVVSAum.mjs → discover-CJHyvYfR.mjs} +2 -2
  20. package/dist/dist-DaKKDf8D.mjs +41 -0
  21. package/dist/{domain-DMrBabBQ.mjs → domain-DiaNQbrl.mjs} +2 -2
  22. package/dist/entry-D7yy4xVH.mjs +100 -0
  23. package/dist/{env-BfG7O71F.mjs → env-CZy5MorI.mjs} +5 -5
  24. package/dist/env-mask-Dd47NbR6.mjs +90 -0
  25. package/dist/env-public-BfiLcMBk.d.mts +140 -0
  26. package/dist/{env-types-D51bnR-c.mjs → env-types-QBj-ndax.mjs} +2 -2
  27. package/dist/env-validation-Dea3v3ej.mjs +1069 -0
  28. package/dist/{gen-Bee261SV.mjs → gen-o_w-8yI8.mjs} +8 -8
  29. package/dist/{git-metadata-CBKaL0v5.mjs → git-metadata-D-gurjzG.mjs} +6 -5
  30. package/dist/{github-cmd-DfeFEasr.mjs → github-cmd-DKcGUNsj.mjs} +4 -4
  31. package/dist/{handler-imD0UVDT.d.mts → handler-Cjh8uM3Y.d.mts} +1 -1
  32. package/dist/{headers-CTAjX-UO.mjs → headers-nsHIFixA.mjs} +2 -2
  33. package/dist/index.d.mts +1 -1
  34. package/dist/index.mjs +98 -52
  35. package/dist/{init-xfwW7cLp.mjs → init-3rgHKBVi.mjs} +13 -14
  36. package/dist/{link-Co136MEs.mjs → link-CdGHSIy-.mjs} +4 -4
  37. package/dist/{list-BKDjA3M5.mjs → list-CPwFDZ_c.mjs} +3 -3
  38. package/dist/{login-CrOTnR_6.mjs → login-BT3H8PN3.mjs} +2 -2
  39. package/dist/{logs-DlAg25ww.mjs → logs-Bt313ax7.mjs} +3 -2
  40. package/dist/{mcp-BNjMGMB0.mjs → mcp-DoM3_nhd.mjs} +7 -2
  41. package/dist/{node-CnE-SZCk.mjs → node-Da0UcsGA.mjs} +5 -5
  42. package/dist/{package-json-B0NuUWGd.mjs → package-json-Cx1osYo6.mjs} +1 -1
  43. package/dist/pages/client.d.mts +1 -1
  44. package/dist/pages/index.d.mts +1 -23
  45. package/dist/pages/index.mjs +5 -5
  46. package/dist/pages/islands-plugin.mjs +2 -2
  47. package/dist/pages/protocol.d.mts +2 -2
  48. package/dist/{plugin-inference-CJxi_fWI.mjs → plugin-inference-DMeavIJ6.mjs} +4 -98
  49. package/dist/{prepare-BRyC4TCC.mjs → prepare-BoKHgMNx.mjs} +10 -10
  50. package/dist/preset-BjyR3lzz.mjs +592 -0
  51. package/dist/{project-cmd-B2bcKefD.mjs → project-cmd-D_w-4w5B.mjs} +13 -9
  52. package/dist/{project-paths-tpdR1mJR.mjs → project-paths-BQd7OmIo.mjs} +1 -1
  53. package/dist/project-paths-GpziKeQQ.d.mts +25 -0
  54. package/dist/{project-tsconfig-D9uSVVpA.mjs → project-tsconfig-B-QtXjLQ.mjs} +2 -2
  55. package/dist/{protocol-6hTJ04T1.d.mts → protocol-Bnb0LFp3.d.mts} +1 -1
  56. package/dist/provision-rShh6MKY.mjs +2557 -0
  57. package/dist/requests-B8sZxaFM.mjs +50 -0
  58. package/dist/{resolve-project-D2HI3TrG.mjs → resolve-project-BBMtLLV9.mjs} +1 -1
  59. package/dist/{rollback-9dfmB-YZ.mjs → rollback-CkvTFXx5.mjs} +2 -2
  60. package/dist/{route-types-CfKfhbIg.mjs → route-types-COI2DsZv.mjs} +2 -2
  61. package/dist/{runner-h272wcPj.mjs → runner-kapo9aPs.mjs} +3 -3
  62. package/dist/{runner-pg-waxJOnBb.mjs → runner-pg-CHM76xuC.mjs} +1 -1
  63. package/dist/runtime/better-auth-pg.d.mts +1 -1
  64. package/dist/runtime/better-auth.d.mts +1 -1
  65. package/dist/runtime/env-public.d.mts +1 -139
  66. package/dist/runtime/env-public.mjs +2 -90
  67. package/dist/runtime/handler.d.mts +1 -1
  68. package/dist/runtime/live.d.mts +1 -1
  69. package/dist/runtime/validator.d.mts +1 -1
  70. package/dist/runtime/ws-server.d.mts +1 -1
  71. package/dist/runtime/ws.d.mts +2 -2
  72. package/dist/{scan-Dp_Gyzs3.mjs → scan-DEwlM_Xy.mjs} +2 -2
  73. package/dist/{scan-i7Yz54fv.mjs → scan-DYXkrasO.mjs} +4 -4
  74. package/dist/{secret-DeMnMcV0.mjs → secret-Dt32J6RI.mjs} +3 -3
  75. package/dist/{skills-DsdNDtX3.mjs → skills-CLjN0uUO.mjs} +2 -2
  76. package/dist/{subcommand-prompt-DtES-oP6.mjs → subcommand-prompt-BzV8iQZo.mjs} +1 -1
  77. package/dist/sveltekit.mjs +1 -1
  78. package/dist/{validate-DT7nFMlf.mjs → validate-Cw_RLeTj.mjs} +1 -1
  79. package/dist/{yarn-pnp-CW8LB6g_.mjs → yarn-pnp-DJn3SAHF.mjs} +1 -1
  80. package/getting-started-prompt.txt +3 -1
  81. package/package.json +5 -3
  82. package/schema.json +5 -0
  83. package/skills/void/SKILL.md +34 -33
  84. package/skills/void/docs/guide/app-types.md +43 -0
  85. package/skills/void/docs/guide/auth.md +8 -0
  86. package/skills/void/docs/guide/deployment.md +1 -1
  87. package/skills/void/docs/guide/edge/static-assets.md +23 -0
  88. package/skills/void/docs/guide/env-vars.md +14 -6
  89. package/skills/void/docs/guide/queues.md +4 -0
  90. package/skills/void/docs/integrations/cloudflare.md +64 -5
  91. package/skills/void/docs/integrations/frameworks/overview.md +1 -0
  92. package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +4 -0
  93. package/skills/void/docs/node_modules/void/README.md +1 -1
  94. package/skills/void/docs/node_modules/void/node_modules/@types/proper-lockfile/README.md +51 -0
  95. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathslash/README.md +64 -0
  96. package/skills/void/docs/node_modules/void/node_modules/pathslash/README.md +64 -0
  97. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  98. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/README.md +183 -0
  99. package/skills/void/docs/node_modules/void/skills/void/SKILL.md +34 -33
  100. package/skills/void/docs/node_modules/void/test/e2e/README.md +85 -0
  101. package/skills/void/docs/reference/cli.md +74 -14
  102. package/skills/void/docs/reference/config.md +27 -1
  103. package/dist/deploy-B02JyKP9.mjs +0 -3748
  104. package/dist/dotenv-D_UbC_vc.mjs +0 -173
  105. package/dist/env-validation-CeC2FL66.mjs +0 -163
  106. package/dist/pathe.M-eThtNZ-CQzLbt4c.mjs +0 -150
  107. package/dist/preset-CVvwCeIy.mjs +0 -208
  108. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathe/README.md +0 -73
  109. package/skills/void/docs/node_modules/void/node_modules/pathe/README.md +0 -73
@@ -1,3748 +0,0 @@
1
- import { n as __exportAll } from "./rolldown-runtime-DJK8HYOj.mjs";
2
- import { a as join, c as resolve, i as isAbsolute, n as dirname, r as extname, s as relative } from "./pathe.M-eThtNZ-CQzLbt4c.mjs";
3
- import { D as outro, E as note, S as confirm, T as log, a as cliTitle, j as isCancel, o as createSpinner, u as import_picocolors, w as intro } from "./agents-Bmr5tFFb.mjs";
4
- import { i as resolveProjectPaths, n as readProjectPaths } from "./project-paths-tpdR1mJR.mjs";
5
- import { a as scanQueuesSync, i as scanWebSocketRoutesSync, o as scanJobsSync, r as scanRoutes, s as findVoidAuthConfig } from "./scan-i7Yz54fv.mjs";
6
- import { t as cfAccessHeaders } from "./cf-access-Bqw81xAf.mjs";
7
- import { C as ensureCloudflaredToken, S as writeProjectConfig, a as isExpiredTokenError, b as readProjectConfig, f as getToken, i as isCliOutdatedError, m as isStagingMode, n as PlatformClient, o as parsePlatformErrorBody, p as getTokenSource, t as PlatformApiError, u as getApiUrl } from "./client-C9FG6Vzc.mjs";
8
- import { c as getDatabaseDialect, f as readConfig, l as isNodeTarget, p as resolveBindingNames } from "./config-BcAeIPe3.mjs";
9
- import { i as inferProjectBindings, l as validateSsrEntry, n as FRAMEWORK_SCAN_DIRS, r as detectFramework } from "./plugin-inference-CJxi_fWI.mjs";
10
- import { i as voidWarn, r as voidLog } from "./log-ChfPKsVd.mjs";
11
- import { n as mergeAndExpand, r as parseDotenvFile } from "./dotenv-D_UbC_vc.mjs";
12
- import { t as discoverSchema } from "./discover-BuVVSAum.mjs";
13
- import { t as findEnvFile } from "./env-types-D51bnR-c.mjs";
14
- import { i as validateProdEnv, n as formatEnvReport, r as getDeployEnvDefaults, t as fetchRemoteSecretNames } from "./env-validation-CeC2FL66.mjs";
15
- import { n as writeDrizzleConfig } from "./config-BdUctCZD.mjs";
16
- import { a as buildMigrationPlanBundle, c as collectMigrations, r as validateMigrations, t as assertJournalCoherence } from "./validate-DT7nFMlf.mjs";
17
- import { t as scanPages } from "./scan-Dp_Gyzs3.mjs";
18
- import { n as STAGING_PROXY_URL, t as DEFAULT_PROXY_URL } from "./proxy-D-3_D-Gl.mjs";
19
- import { i as promptProjectSetupAction, n as promptAndCreateProject, r as promptProjectSelection } from "./create-project-CjYX23M_.mjs";
20
- import { r as resolveProjectBySlug, t as getRequestedProjectSlug } from "./resolve-project-D2HI3TrG.mjs";
21
- import { t as collectGitDeployMetadata } from "./git-metadata-CBKaL0v5.mjs";
22
- import { n as lintDuplicateSources, r as mergeRoutingRules, t as lintDestinationSplats } from "./headers-CTAjX-UO.mjs";
23
- import { t as promptForLoginToken } from "./login-CrOTnR_6.mjs";
24
- import { i as resolveProjectCommand, n as detectPreset, r as formatProjectCommand, t as FRAMEWORK_PRESETS } from "./preset-CVvwCeIy.mjs";
25
- import { createRequire } from "node:module";
26
- import { copyFileSync, cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
27
- import { fileURLToPath, pathToFileURL } from "node:url";
28
- import { build, loadEnv } from "vite";
29
- import { createHash } from "node:crypto";
30
- import { homedir } from "node:os";
31
- import { execFile, execSync, spawnSync } from "node:child_process";
32
- import { parse } from "jsonc-parser";
33
- import { hash } from "blake3-jit";
34
- import ignore from "ignore";
35
- //#region src/asset-routing.ts
36
- function isApiRoutePattern(pattern) {
37
- return pattern === "/api" || pattern.startsWith("/api/");
38
- }
39
- function hasDocumentRoutes(routes) {
40
- return routes.some((route) => !isApiRoutePattern(route.pattern));
41
- }
42
- function hasApiRoutes(routes) {
43
- return routes.some((route) => isApiRoutePattern(route.pattern));
44
- }
45
- /**
46
- * Returns true when browser document navigations must reach the worker before
47
- * a static asset fallback can serve HTML.
48
- */
49
- function workerOwnsHtmlRouting(inputs) {
50
- return inputs.isSsr || inputs.hasPages || inputs.hasDocumentRoutes || inputs.hasMiddleware || inputs.hasWebSockets === true || inputs.liveEnabled === true;
51
- }
52
- function resolveAssetConfig(inputs) {
53
- return resolveAssetRoutingPolicy(inputs).assetConfig;
54
- }
55
- const VOID_INTERNAL_WORKER_FIRST = ["/__void", "/__void/*"];
56
- function resolveAssetRoutingPolicy(inputs) {
57
- const workerFirst = workerOwnsHtmlRouting(inputs);
58
- const apiWorkerFirst = !workerFirst && (inputs.hasApiRoutes === true || inputs.authEnabled === true);
59
- return {
60
- assetConfig: workerFirst ? {
61
- not_found_handling: "none",
62
- run_worker_first: ["/**"]
63
- } : apiWorkerFirst ? {
64
- not_found_handling: "single-page-application",
65
- run_worker_first: [
66
- "/api",
67
- "/api/*",
68
- ...VOID_INTERNAL_WORKER_FIRST
69
- ]
70
- } : {
71
- not_found_handling: "single-page-application",
72
- run_worker_first: [...VOID_INTERNAL_WORKER_FIRST]
73
- },
74
- apiWorkerFirst,
75
- workerFirst,
76
- workerSpaFallback: workerFirst && !inputs.isSsr && !inputs.hasPages
77
- };
78
- }
79
- const SANDBOX_MIGRATION_TAG = "void-sandbox-v1";
80
- const require = createRequire(import.meta.url);
81
- function isSandboxEnabled(config) {
82
- return config.sandbox !== void 0 && config.sandbox !== false;
83
- }
84
- function getDefaultSandboxImage() {
85
- return join(getSandboxPackageRoot(), "Dockerfile");
86
- }
87
- function getDefaultSandboxPlatformImage() {
88
- return `docker.io/cloudflare/sandbox:${JSON.parse(readFileSync(join(getSandboxPackageRoot(), "package.json"), "utf-8")).version ?? "latest"}`;
89
- }
90
- function isLocalSandboxImage(image) {
91
- const lowerImage = image.toLowerCase();
92
- return isAbsolute(image) || image.startsWith("./") || image.startsWith("../") || lowerImage === "dockerfile" || lowerImage.includes("dockerfile") || lowerImage.endsWith("/dockerfile") || lowerImage.endsWith("\\dockerfile");
93
- }
94
- function requiresSandboxPlatformImage(config) {
95
- if (typeof config.sandbox !== "object" || config.sandbox === null) return false;
96
- return typeof config.sandbox.image === "string" && isLocalSandboxImage(config.sandbox.image) && config.sandbox.platformImage === void 0;
97
- }
98
- function resolveSandboxConfig(config, root = process.cwd()) {
99
- const raw = typeof config.sandbox === "object" ? config.sandbox : {};
100
- const image = raw.image ?? getDefaultSandboxImage();
101
- const imageBuildContext = raw.imageBuildContext;
102
- const platformImage = raw.platformImage ?? (raw.image && !isLocalSandboxImage(raw.image) ? raw.image : getDefaultSandboxPlatformImage());
103
- return {
104
- binding: raw.binding ?? "SANDBOX",
105
- className: raw.className ?? "Sandbox",
106
- containerName: raw.containerName ?? "void-sandbox",
107
- image: resolveSandboxImage(image, root),
108
- platformImage,
109
- ...imageBuildContext && { imageBuildContext: isAbsolute(imageBuildContext) ? imageBuildContext : join(root, imageBuildContext) },
110
- ...raw.instanceType && { instanceType: raw.instanceType },
111
- ...raw.maxInstances != null && { maxInstances: raw.maxInstances }
112
- };
113
- }
114
- function getSandboxPackageRoot() {
115
- return join(dirname(require.resolve("@cloudflare/sandbox")), "..");
116
- }
117
- function resolveSandboxImage(image, root) {
118
- if (!isLocalSandboxImage(image) || isAbsolute(image)) return image;
119
- return join(root, image);
120
- }
121
- //#endregion
122
- //#region src/cli/wrangler.ts
123
- const COMPAT_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
124
- const NODEJS_ALS_FLAG = "nodejs_als";
125
- const NODEJS_COMPAT_FLAG = "nodejs_compat";
126
- const VOID_CONFIG_FILE = "void.json";
127
- const DEFAULT_LIVE_BINDING = "VOID_LIVE";
128
- const DEFAULT_LIVE_CLASS = "VoidLiveStreamDurableObject";
129
- const LIVE_MIGRATION_TAG = "live-v1";
130
- /** Latest compatibility date the Void test suite is pinned against. */
131
- const LATEST_KNOWN_COMPAT_DATE = "2026-02-24";
132
- function normalizeWranglerCompatibilityFlags(flags, options) {
133
- const existingFlags = flags ?? [];
134
- if (options?.needsNodejsCompat && !existingFlags.includes(NODEJS_COMPAT_FLAG)) return [...existingFlags.filter((flag) => flag !== NODEJS_ALS_FLAG), NODEJS_COMPAT_FLAG];
135
- if (existingFlags.length === 0 || !existingFlags.includes(NODEJS_ALS_FLAG) && !existingFlags.includes(NODEJS_COMPAT_FLAG)) return [...existingFlags, NODEJS_ALS_FLAG];
136
- return flags;
137
- }
138
- function hasDurableObjectSqliteMigration(migrations, className) {
139
- return migrations.some((migration) => migration.new_sqlite_classes?.includes(className));
140
- }
141
- function nextDurableObjectMigrationTag(migrations, baseTag) {
142
- const existingTags = new Set(migrations.map((migration) => migration.tag).filter(Boolean));
143
- if (!existingTags.has(baseTag)) return baseTag;
144
- let index = 2;
145
- while (existingTags.has(`${baseTag}-${index}`)) index++;
146
- return `${baseTag}-${index}`;
147
- }
148
- function resolveLiveBindingConfig(root, options) {
149
- const durableObjectBindings = [...readWranglerDurableObjectBindings(root), ...options?.existingBindings ?? []];
150
- const generated = findGeneratedLiveBinding(durableObjectBindings);
151
- if (generated) return {
152
- bindingName: generated.name,
153
- className: generated.class_name
154
- };
155
- const usedBindings = /* @__PURE__ */ new Set([...durableObjectBindings.map((binding) => binding.name), ...(options?.reservedBindings ?? []).filter((name) => typeof name === "string")]);
156
- const usedClasses = /* @__PURE__ */ new Set([...durableObjectBindings.map((binding) => binding.class_name), ...(options?.reservedClasses ?? []).filter((name) => typeof name === "string")]);
157
- return {
158
- bindingName: resolveGeneratedName(DEFAULT_LIVE_BINDING, usedBindings, "binding"),
159
- className: resolveGeneratedName(DEFAULT_LIVE_CLASS, usedClasses, "class")
160
- };
161
- }
162
- function readExistingLiveBindingConfig(root) {
163
- const generated = findGeneratedLiveBinding(readWranglerDurableObjectBindings(root));
164
- return generated ? {
165
- bindingName: generated.name,
166
- className: generated.class_name
167
- } : void 0;
168
- }
169
- function readWranglerDurableObjectBindings(root) {
170
- for (const name of ["wrangler.jsonc", "wrangler.json"]) try {
171
- const bindings = parseJsonc(readFileSync(join(root, name), "utf-8")).durable_objects?.bindings;
172
- if (!Array.isArray(bindings)) return [];
173
- return bindings.filter((binding) => typeof binding.name === "string" && typeof binding.class_name === "string");
174
- } catch {}
175
- return [];
176
- }
177
- function isGeneratedLiveName(name, base) {
178
- return name === base || name.match(generatedLiveNamePattern(base)) !== null;
179
- }
180
- function generatedLiveNamePattern(base) {
181
- return new RegExp(`^${escapeRegExp(base)}_[0-9a-f]{8}$`);
182
- }
183
- function escapeRegExp(value) {
184
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
185
- }
186
- function findGeneratedLiveBinding(bindings) {
187
- return bindings.find((binding) => isGeneratedLiveName(binding.name, "VOID_LIVE") && isGeneratedLiveName(binding.class_name, "VoidLiveStreamDurableObject"));
188
- }
189
- function resolveGeneratedName(base, used, kind) {
190
- if (!used.has(base)) return base;
191
- for (let attempt = 0;; attempt++) {
192
- const candidate = `${base}_${createHash("sha256").update(`void/live:${kind}:${base}:${attempt}`).digest("hex").slice(0, 8)}`;
193
- if (!used.has(candidate)) return candidate;
194
- }
195
- }
196
- /**
197
- * Read compatibility settings from `wrangler.jsonc`/`wrangler.json`,
198
- * `void.json`, and build output, with priority:
199
- * void.json > wrangler.jsonc > build output > latest known-good date.
200
- *
201
- * This function is read-only: when no explicit `compatibility_date` exists, it
202
- * returns the latest known-good date without writing it anywhere. Use
203
- * `ensureWranglerCompat()` when the caller is in a real dev/build/deploy
204
- * lifecycle and wants to persist the default in `void.json`.
205
- *
206
- * @param buildOutputDir - Optional path to the build output directory (e.g.
207
- * `dist/ssr`) which may contain a plugin-generated `wrangler.json` with the
208
- * correct compat settings for the built worker.
209
- */
210
- function readWranglerCompat(root, config, buildOutputDir) {
211
- return resolveWranglerCompat(readWranglerCompatFields(root, config, buildOutputDir));
212
- }
213
- /**
214
- * Read compatibility settings and ensure the default date is pinned in
215
- * `void.json` when no explicit date exists in void.json, wrangler config, or
216
- * build output.
217
- */
218
- function ensureWranglerCompat(root, config, buildOutputDir) {
219
- const fields = readWranglerCompatFields(root, config, buildOutputDir);
220
- const defaultDate = fields.voidDate ?? fields.wranglerDate ?? fields.buildDate ? void 0 : ensureVoidCompatibilityDate(root, fields.voidConfig);
221
- return resolveWranglerCompat(fields, defaultDate ? { defaultDate } : void 0);
222
- }
223
- function readWranglerCompatFields(root, config, buildOutputDir) {
224
- const voidConfig = config ?? readVoidConfigIfExists(root);
225
- let raw;
226
- for (const name of ["wrangler.jsonc", "wrangler.json"]) try {
227
- raw = readFileSync(join(root, name), "utf-8");
228
- break;
229
- } catch {}
230
- let wranglerDate;
231
- let wranglerFlags;
232
- if (raw) {
233
- const parsed = parseJsonc(raw);
234
- if (typeof parsed.compatibility_date === "string") {
235
- validateCompatDate(parsed.compatibility_date, "wrangler config");
236
- wranglerDate = parsed.compatibility_date;
237
- }
238
- if (Array.isArray(parsed.compatibility_flags) && parsed.compatibility_flags.every((f) => typeof f === "string")) wranglerFlags = parsed.compatibility_flags;
239
- }
240
- let buildDate;
241
- let buildFlags;
242
- if (buildOutputDir) try {
243
- const buildRaw = readFileSync(join(buildOutputDir, "wrangler.json"), "utf-8");
244
- const buildParsed = JSON.parse(buildRaw);
245
- if (typeof buildParsed.compatibility_date === "string") {
246
- validateCompatDate(buildParsed.compatibility_date, "build output wrangler.json");
247
- buildDate = buildParsed.compatibility_date;
248
- }
249
- if (Array.isArray(buildParsed.compatibility_flags) && buildParsed.compatibility_flags.every((f) => typeof f === "string")) buildFlags = buildParsed.compatibility_flags;
250
- } catch {}
251
- const voidDate = voidConfig?.worker?.compatibility_date;
252
- const voidFlags = voidConfig?.worker?.compatibility_flags;
253
- if (voidDate !== void 0) validateCompatDate(voidDate, "void.json worker config");
254
- return {
255
- voidConfig,
256
- voidDate,
257
- voidFlags,
258
- wranglerDate,
259
- wranglerFlags,
260
- buildDate,
261
- buildFlags
262
- };
263
- }
264
- function resolveWranglerCompat(fields, options) {
265
- const result = { compatibilityDate: fields.voidDate ?? fields.wranglerDate ?? fields.buildDate ?? options?.defaultDate ?? "2026-02-24" };
266
- const flags = fields.voidFlags ?? fields.wranglerFlags ?? fields.buildFlags;
267
- if (flags && flags.length > 0) result.compatibilityFlags = flags;
268
- return result;
269
- }
270
- function validateCompatDate(value, source) {
271
- if (!COMPAT_DATE_RE.test(value)) throw new Error(`config: "compatibility_date" in ${source} must use YYYY-MM-DD format, got '${value}'.`);
272
- }
273
- function readVoidConfigIfExists(root) {
274
- const filePath = join(root, VOID_CONFIG_FILE);
275
- if (!existsSync(filePath)) return;
276
- try {
277
- const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
278
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`config: ${VOID_CONFIG_FILE} must be a JSON object.`);
279
- return parsed;
280
- } catch (error) {
281
- if (error instanceof SyntaxError) throw new Error(`config: Invalid JSON in ${VOID_CONFIG_FILE}.`);
282
- throw error;
283
- }
284
- }
285
- function ensureVoidCompatibilityDate(root, config) {
286
- const filePath = join(root, VOID_CONFIG_FILE);
287
- let nextConfig = {};
288
- if (existsSync(filePath)) try {
289
- const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
290
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`config: ${VOID_CONFIG_FILE} must be a JSON object.`);
291
- nextConfig = parsed;
292
- } catch (error) {
293
- if (error instanceof SyntaxError) throw new Error(`config: Invalid JSON in ${VOID_CONFIG_FILE}.`);
294
- throw error;
295
- }
296
- else if (config) nextConfig = { ...config };
297
- nextConfig.worker = {
298
- ...nextConfig.worker ?? {},
299
- compatibility_date: LATEST_KNOWN_COMPAT_DATE
300
- };
301
- writeFileSync(filePath, `${JSON.stringify(nextConfig, null, 2)}\n`);
302
- return LATEST_KNOWN_COMPAT_DATE;
303
- }
304
- /**
305
- * Sync inferred/configured bindings into the project's wrangler.jsonc for local dev.
306
- * Only adds bindings that are missing — existing bindings are preserved.
307
- * Returns true if the file was modified.
308
- */
309
- function syncWranglerBindings(root, bindings, options) {
310
- let configPath;
311
- let parsed;
312
- let created = false;
313
- for (const name of ["wrangler.jsonc", "wrangler.json"]) {
314
- const p = join(root, name);
315
- if (existsSync(p)) {
316
- configPath = p;
317
- try {
318
- parsed = parseJsonc(readFileSync(p, "utf-8"));
319
- } catch {}
320
- break;
321
- }
322
- }
323
- if (!configPath || !parsed) {
324
- configPath = join(root, "wrangler.jsonc");
325
- parsed = {};
326
- created = true;
327
- }
328
- let modified = false;
329
- const d1Name = options?.bindingNames?.d1 ?? "DB";
330
- if (bindings.needsD1) {
331
- const existing = parsed.d1_databases ?? [];
332
- if (!existing.some((b) => b.binding === d1Name)) {
333
- parsed.d1_databases = [...existing, {
334
- binding: d1Name,
335
- database_name: "default",
336
- database_id: "local"
337
- }];
338
- modified = true;
339
- }
340
- }
341
- const kvName = options?.bindingNames?.kv ?? "KV";
342
- if (bindings.needsKV) {
343
- const existing = parsed.kv_namespaces ?? [];
344
- if (!existing.some((b) => b.binding === kvName)) {
345
- parsed.kv_namespaces = [...existing, {
346
- binding: kvName,
347
- id: "local"
348
- }];
349
- modified = true;
350
- }
351
- }
352
- const r2Name = options?.bindingNames?.r2 ?? "STORAGE";
353
- if (bindings.needsR2) {
354
- const existing = parsed.r2_buckets ?? [];
355
- if (!existing.some((b) => b.binding === r2Name)) {
356
- parsed.r2_buckets = [...existing, {
357
- binding: r2Name,
358
- bucket_name: "default"
359
- }];
360
- modified = true;
361
- }
362
- }
363
- if (options?.websockets && options.websockets.length > 0) {
364
- const existing = parsed.durable_objects?.bindings ?? [];
365
- const additions = options.websockets.filter((route) => !existing.some((binding) => binding.name === route.bindingName)).map((route) => ({
366
- name: route.bindingName,
367
- class_name: route.className
368
- }));
369
- if (additions.length > 0) {
370
- parsed.durable_objects = { bindings: [...existing, ...additions] };
371
- modified = true;
372
- }
373
- if (!Array.isArray(parsed.migrations)) {
374
- parsed.migrations = [{
375
- tag: "void-ws-v1",
376
- new_classes: options.websockets.map((route) => route.className)
377
- }];
378
- modified = true;
379
- }
380
- }
381
- if (bindings.needsSandbox) {
382
- const sandbox = options?.sandbox ?? resolveSandboxConfig({ sandbox: true }, root);
383
- const bindingName = options?.bindingNames?.sandbox ?? sandbox?.binding ?? "SANDBOX";
384
- const className = sandbox?.className ?? "Sandbox";
385
- const existingDurableObjectBindings = parsed.durable_objects?.bindings ?? [];
386
- if (!existingDurableObjectBindings.some((binding) => binding.name === bindingName)) {
387
- parsed.durable_objects = { bindings: [...existingDurableObjectBindings, {
388
- name: bindingName,
389
- class_name: className
390
- }] };
391
- modified = true;
392
- }
393
- const existingContainers = parsed.containers ?? [];
394
- if (!existingContainers.some((container) => container.class_name === className)) {
395
- parsed.containers = [...existingContainers, {
396
- name: sandbox?.containerName ?? "void-sandbox",
397
- class_name: className,
398
- image: sandbox?.image,
399
- ...sandbox?.imageBuildContext && { image_build_context: sandbox.imageBuildContext },
400
- ...sandbox?.instanceType && { instance_type: sandbox.instanceType },
401
- ...sandbox?.maxInstances != null && { max_instances: sandbox.maxInstances }
402
- }];
403
- modified = true;
404
- }
405
- const migrations = Array.isArray(parsed.migrations) ? parsed.migrations : [];
406
- if (!migrations.some((migration) => migration.tag === "void-sandbox-v1" || migration.new_sqlite_classes?.includes(className))) {
407
- parsed.migrations = [...migrations, {
408
- tag: SANDBOX_MIGRATION_TAG,
409
- new_sqlite_classes: [className]
410
- }];
411
- modified = true;
412
- }
413
- }
414
- if (bindings.needsLive || options?.live) {
415
- const existingDurableObjectBindings = parsed.durable_objects?.bindings ?? [];
416
- const liveConfig = typeof options?.live === "object" ? options.live : resolveLiveBindingConfig(root, {
417
- existingBindings: existingDurableObjectBindings,
418
- reservedBindings: options?.websockets?.map((route) => route.bindingName),
419
- reservedClasses: [...options?.websockets?.map((route) => route.className) ?? [], options?.sandbox?.className]
420
- });
421
- if (!existingDurableObjectBindings.some((binding) => binding.name === liveConfig.bindingName)) {
422
- parsed.durable_objects = { bindings: [...existingDurableObjectBindings, {
423
- name: liveConfig.bindingName,
424
- class_name: liveConfig.className
425
- }] };
426
- modified = true;
427
- }
428
- const migrations = Array.isArray(parsed.migrations) ? parsed.migrations : [];
429
- if (!hasDurableObjectSqliteMigration(migrations, liveConfig.className)) {
430
- parsed.migrations = [...migrations, {
431
- tag: nextDurableObjectMigrationTag(migrations, LIVE_MIGRATION_TAG),
432
- new_sqlite_classes: [liveConfig.className]
433
- }];
434
- modified = true;
435
- }
436
- }
437
- if (created || modified) writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
438
- return created || modified;
439
- }
440
- /**
441
- * Ensure a wrangler config has an explicit compatibility date.
442
- * Used when `worker.compatibility_date` is configured in void.json but the
443
- * active framework reads Cloudflare settings from wrangler.jsonc/json.
444
- */
445
- function ensureWranglerCompatibilityDate(root, compatibilityDate) {
446
- validateCompatDate(compatibilityDate, "void.json worker config");
447
- let configPath;
448
- let parsed;
449
- for (const name of ["wrangler.jsonc", "wrangler.json"]) {
450
- const p = join(root, name);
451
- if (existsSync(p)) {
452
- configPath = p;
453
- try {
454
- parsed = parseJsonc(readFileSync(p, "utf-8"));
455
- } catch {}
456
- break;
457
- }
458
- }
459
- if (!configPath || !parsed) {
460
- configPath = join(root, "wrangler.jsonc");
461
- parsed = {};
462
- }
463
- if (parsed.compatibility_date === compatibilityDate) return false;
464
- parsed.compatibility_date = compatibilityDate;
465
- writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
466
- return true;
467
- }
468
- /**
469
- * Ensure the `nodejs_als` compatibility flag exists in wrangler config.
470
- * Returns true if the config file was modified.
471
- */
472
- function ensureWranglerNodejsAlsFlag(root) {
473
- let configPath;
474
- let parsed;
475
- for (const name of ["wrangler.jsonc", "wrangler.json"]) {
476
- const p = join(root, name);
477
- if (existsSync(p)) {
478
- configPath = p;
479
- try {
480
- parsed = parseJsonc(readFileSync(p, "utf-8"));
481
- } catch {}
482
- break;
483
- }
484
- }
485
- if (!configPath || !parsed) {
486
- configPath = join(root, "wrangler.jsonc");
487
- parsed = {};
488
- }
489
- const compatibilityFlags = Array.isArray(parsed.compatibility_flags) ? parsed.compatibility_flags : [];
490
- if (compatibilityFlags.includes(NODEJS_ALS_FLAG)) return false;
491
- parsed.compatibility_flags = [...compatibilityFlags, NODEJS_ALS_FLAG];
492
- writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
493
- return true;
494
- }
495
- function parseJsonc(text) {
496
- const errors = [];
497
- const result = parse(text, errors, { allowTrailingComma: true });
498
- if (errors.length > 0) throw new SyntaxError("Failed to parse JSONC");
499
- return result;
500
- }
501
- //#endregion
502
- //#region src/shared/env.ts
503
- /**
504
- * Strip shell environment pollution from a Vite `loadEnv(mode, root, '')`
505
- * result. Vite returns `.env`-file entries AND every `process.env` entry
506
- * when called with an empty prefix; we want only the keys that came from
507
- * `.env` files (either new keys or keys that override a shell value).
508
- *
509
- * Used anywhere we need to populate worker vars from `.env` without
510
- * shipping the developer's shell environment (PATH, HOME, etc.) into
511
- * `wrangler.json` or the prerender Miniflare instance.
512
- */
513
- function filterLoadedEnv(loaded) {
514
- const shellKeys = new Set(Object.keys(process.env));
515
- const env = {};
516
- for (const [key, value] of Object.entries(loaded)) if (!shellKeys.has(key) || process.env[key] !== value) env[key] = value;
517
- return env;
518
- }
519
- /**
520
- * Resolve whether the plugin should run in "remote" mode (proxy real
521
- * D1/KV/R2 via the platform) based on the project config plus an optional
522
- * `VOID_REMOTE` env override.
523
- *
524
- * Tri-state override semantics:
525
- * - `VOID_REMOTE=1` → force remote mode on
526
- * - `VOID_REMOTE=0` → force remote mode off (e.g. local/CI builds of a
527
- * project whose `void.json` has `remote: true`)
528
- * - unset / any other value → fall back to `config.remote`
529
- *
530
- * Both the dev-server plugin (index.ts) and the prerender build plugin
531
- * (plugin-prerender.ts) must derive `remoteMode` through this helper so
532
- * dev and build stay in lockstep.
533
- */
534
- function resolveRemoteMode(configRemote) {
535
- const override = process.env.VOID_REMOTE;
536
- if (override === "1") return true;
537
- if (override === "0") return false;
538
- return configRemote === true;
539
- }
540
- const INTERNAL_ENV_KEY_SET = /* @__PURE__ */ new Set([
541
- "__VOID_TOKEN",
542
- "__VOID_PROJECT_ID",
543
- "__VOID_REMOTE",
544
- "__VOID_STAGING",
545
- "__VOID_PROXY_URL",
546
- "__VOID_PROXY_TOKEN"
547
- ]);
548
- /**
549
- * Remove any internal Void env keys from a user-supplied env record. Returns
550
- * the sanitized record plus the list of keys that were stripped so callers
551
- * (which live in non-leaf modules) can emit a warning via `voidWarn`.
552
- *
553
- * This module is a leaf — it must not import from `../log` or any non-leaf
554
- * module, otherwise the prerender subprocess's worker bundle acquires
555
- * unwanted transitive dependencies. That's why warnings are the caller's
556
- * responsibility.
557
- */
558
- function stripInternalEnvKeys(input) {
559
- if (!input) return {
560
- sanitized: {},
561
- stripped: []
562
- };
563
- const sanitized = {};
564
- const stripped = [];
565
- for (const [key, value] of Object.entries(input)) {
566
- if (INTERNAL_ENV_KEY_SET.has(key)) {
567
- stripped.push(key);
568
- continue;
569
- }
570
- sanitized[key] = value;
571
- }
572
- return {
573
- sanitized,
574
- stripped
575
- };
576
- }
577
- //#endregion
578
- //#region src/isr-config.ts
579
- /**
580
- * Whether a resolved `revalidate` value represents an active ISR configuration.
581
- *
582
- * Mirrors the presence check used when writing the deploy manifest
583
- * (`packages/void/src/cli/package.ts`): `null`/`undefined`/`0`/`{}` are treated
584
- * as "no ISR", a positive number or a non-empty path map is treated as "ISR on".
585
- */
586
- function hasRevalidate(r) {
587
- if (r == null) return false;
588
- if (typeof r === "number") return r > 0;
589
- if (typeof r === "object") return Object.keys(r).length > 0;
590
- return false;
591
- }
592
- /**
593
- * Merge the app's `void.json` `routing.revalidate` config with per-page
594
- * `export const revalidate` rules discovered by the page scan, producing the
595
- * final revalidate map that gets baked into the deploy manifest (managed
596
- * `void deploy`) or the self-hosted worker build.
597
- *
598
- * This is the single source of truth for the merge so the managed and
599
- * self-host build paths cannot diverge. Behaviour is preserved verbatim from
600
- * the original inline logic in `void deploy`:
601
- *
602
- * - Config value seeds the result (`number | Record<string, number> | undefined`).
603
- * - Per-page rules (page pattern → revalidate seconds) are collected from the
604
- * already-computed page scan (`null` when the app has no pages dir).
605
- * - When there are per-page rules:
606
- * - config is a plain number → path map with per-page entries first and the
607
- * config number applied to `"*"` as a fallback;
608
- * - config is an object map → per-page entries first (higher precedence in
609
- * first-match ordering), then the config entries;
610
- * - config is absent → the per-page rules alone.
611
- * - Empty results (no config, no per-page rules) return `undefined`.
612
- */
613
- function collectRevalidate(config, pageScan) {
614
- const vcRevalidate = config.routing?.revalidate;
615
- let revalidate;
616
- if (vcRevalidate != null) revalidate = vcRevalidate;
617
- if (pageScan) {
618
- const perPageRules = {};
619
- for (const page of pageScan.pages) if (page.revalidate != null) {
620
- const key = page.pattern.replace(/:[A-Za-z0-9_]+\{[^}]*\}/g, "*");
621
- perPageRules[key] = page.revalidate;
622
- }
623
- if (Object.keys(perPageRules).length > 0) if (typeof revalidate === "number") revalidate = Object.fromEntries([...Object.entries(perPageRules), ["*", revalidate]]);
624
- else if (typeof revalidate === "object") revalidate = Object.fromEntries([...Object.entries(perPageRules), ...Object.entries(revalidate)]);
625
- else revalidate = perPageRules;
626
- }
627
- return revalidate;
628
- }
629
- //#endregion
630
- //#region src/prerender.ts
631
- const PRERENDER_PATHS_ENDPOINT = "/__void/prerender-paths";
632
- const STATIC_PAGE_DATA_DIR = "_void/pages";
633
- function outputFileForPath(path) {
634
- return path === "/" ? "index.html" : `${path.slice(1)}.html`;
635
- }
636
- function pageDataFileForPath(path) {
637
- return path === "/" ? `${STATIC_PAGE_DATA_DIR}/index.json` : `${STATIC_PAGE_DATA_DIR}/${path.slice(1)}.json`;
638
- }
639
- async function readPrerenderPathsResponse(response) {
640
- if (!response.ok) throw new Error(`prerender: Failed to collect prerender paths. HTTP ${response.status}.`);
641
- const payload = await response.json();
642
- if (!Array.isArray(payload.paths) || !payload.paths.every((path) => typeof path === "string")) throw new Error("prerender: Failed to collect prerender paths. Response payload is invalid.");
643
- return payload.paths;
644
- }
645
- async function renderPrerenderPaths(paths, clientDir, fetchPath, fetchPageData) {
646
- const result = {
647
- rendered: [],
648
- failed: []
649
- };
650
- for (const path of paths) try {
651
- const response = await fetchPath(path);
652
- const html = await response.text();
653
- if (!response.ok) {
654
- result.failed.push({
655
- path,
656
- error: `HTTP ${response.status}`
657
- });
658
- voidWarn(`prerender ${path} failed: HTTP ${response.status}`);
659
- continue;
660
- }
661
- const outputFile = outputFileForPath(path);
662
- const outputPath = join(clientDir, outputFile);
663
- mkdirSync(dirname(outputPath), { recursive: true });
664
- writeFileSync(outputPath, html);
665
- if (fetchPageData) {
666
- const pageDataResponse = await fetchPageData(path);
667
- if (pageDataResponse.headers.get("X-VoidPages")) {
668
- const pageData = await pageDataResponse.json();
669
- const pageDataPath = join(clientDir, pageDataFileForPath(path));
670
- mkdirSync(dirname(pageDataPath), { recursive: true });
671
- writeFileSync(pageDataPath, JSON.stringify(pageData));
672
- }
673
- }
674
- result.rendered.push(path);
675
- voidLog(`prerender \x1b[36m${outputFile}\x1b[39m`);
676
- } catch (err) {
677
- const error = err instanceof Error ? err.message : String(err);
678
- result.failed.push({
679
- path,
680
- error
681
- });
682
- voidWarn(`prerender ${path} failed: ${error}`);
683
- }
684
- return result;
685
- }
686
- function normalizeWorkerModulePath(path) {
687
- return path.replace(/\\/g, "/").replace(/^\.\//, "");
688
- }
689
- function resolveWorkerModuleType(relativePath) {
690
- const normalized = normalizeWorkerModulePath(relativePath);
691
- if (normalized.includes("/.vite/") || normalized.startsWith(".vite/")) return null;
692
- if (normalized === "wrangler.json" || normalized === "wrangler.jsonc") return null;
693
- if (normalized.endsWith(".mjs") || normalized.endsWith(".js")) return "ESModule";
694
- if (normalized.endsWith(".cjs")) return "CommonJS";
695
- if (normalized.endsWith(".wasm")) return "CompiledWasm";
696
- if (normalized.endsWith(".txt")) return "Text";
697
- if (normalized.endsWith(".bin")) return "Data";
698
- return null;
699
- }
700
- function collectFiles$1(dir) {
701
- const files = [];
702
- const entries = readdirSync(dir, { withFileTypes: true });
703
- for (const entry of entries) {
704
- const fullPath = join(dir, entry.name);
705
- if (entry.isDirectory()) files.push(...collectFiles$1(fullPath));
706
- else files.push(fullPath);
707
- }
708
- return files;
709
- }
710
- function collectPrerenderWorkerModules(workerDir, workerMain) {
711
- const normalizedEntry = normalizeWorkerModulePath(workerMain);
712
- const modules = collectFiles$1(workerDir).flatMap((filePath) => {
713
- const relativePath = normalizeWorkerModulePath(relative(workerDir, filePath));
714
- const type = resolveWorkerModuleType(relativePath);
715
- if (!type) return [];
716
- const file = readFileSync(filePath);
717
- return [{
718
- path: filePath,
719
- relativePath,
720
- type,
721
- contents: type === "CompiledWasm" || type === "Data" ? new Uint8Array(file) : file.toString("utf-8")
722
- }];
723
- });
724
- modules.sort((a, b) => {
725
- if (a.relativePath === normalizedEntry) return -1;
726
- if (b.relativePath === normalizedEntry) return 1;
727
- return a.relativePath.localeCompare(b.relativePath);
728
- });
729
- if (!modules.some((module) => module.relativePath === normalizedEntry)) throw new Error(`prerender: Worker entry '${normalizedEntry}' not found in '${workerDir}'.`);
730
- return modules.map(({ relativePath: _relativePath, ...module }) => module);
731
- }
732
- function buildPrerenderBindings(input) {
733
- const { sanitized: wranglerVars, stripped: wranglerStripped } = stripInternalEnvKeys(input.wranglerVars);
734
- const { sanitized: env, stripped: envStripped } = stripInternalEnvKeys(input.env);
735
- const allStripped = [.../* @__PURE__ */ new Set([...wranglerStripped, ...envStripped])];
736
- if (allStripped.length > 0) voidWarn(`Ignoring internal env keys in prerender inputs: ${allStripped.join(", ")}. These are reserved for Void runtime use.`);
737
- const voidBindings = {};
738
- if (input.voidToken && input.voidProjectId) {
739
- voidBindings.__VOID_TOKEN = input.voidToken;
740
- voidBindings.__VOID_PROJECT_ID = input.voidProjectId;
741
- if (input.voidStaging) voidBindings.__VOID_STAGING = "1";
742
- voidBindings.__VOID_PROXY_URL = input.voidStaging ? STAGING_PROXY_URL : DEFAULT_PROXY_URL;
743
- if (input.remoteMode) voidBindings.__VOID_REMOTE = "1";
744
- }
745
- return {
746
- ...wranglerVars,
747
- ...env,
748
- ...voidBindings
749
- };
750
- }
751
- async function createCloudflarePrerenderRunner(options) {
752
- const { root, workerDir, persistDir, clientDir, remoteMode } = options;
753
- const wranglerPath = join(workerDir, "wrangler.json");
754
- if (!existsSync(wranglerPath)) throw new Error(`prerender: wrangler.json not found in '${workerDir}'.`);
755
- const wranglerConfig = JSON.parse(readFileSync(wranglerPath, "utf-8"));
756
- const wranglerCompat = ensureWranglerCompat(root, readConfig(root), workerDir);
757
- const workerMain = wranglerConfig.main ?? "index.js";
758
- const env = filterLoadedEnv(loadEnv("production", root, ""));
759
- const voidToken = getToken(root);
760
- let voidProjectId = options.projectId;
761
- if (!voidProjectId) voidProjectId = process.env.VOID_DEPLOY_PROJECT_ID || void 0;
762
- if (!voidProjectId) {
763
- const projectJsonPath = join(root, ".void", "project.json");
764
- try {
765
- voidProjectId = JSON.parse(readFileSync(projectJsonPath, "utf-8")).projectId;
766
- } catch {}
767
- }
768
- const voidStaging = isStagingMode(root);
769
- const bindings = buildPrerenderBindings({
770
- wranglerVars: wranglerConfig.vars,
771
- env,
772
- voidToken,
773
- voidProjectId,
774
- voidStaging,
775
- remoteMode
776
- });
777
- const { Miniflare } = await import(pathToFileURL(createRequire(import.meta.resolve("wrangler/package.json")).resolve("miniflare")).href);
778
- const mfOptions = {
779
- modules: collectPrerenderWorkerModules(workerDir, workerMain),
780
- modulesRoot: workerDir,
781
- compatibilityDate: wranglerCompat.compatibilityDate,
782
- compatibilityFlags: wranglerCompat.compatibilityFlags ?? [],
783
- bindings
784
- };
785
- if (clientDir) mfOptions.serviceBindings = { [wranglerConfig.assets?.binding ?? "ASSETS"](request) {
786
- const url = new URL(request.url);
787
- const filePath = join(clientDir, url.pathname);
788
- try {
789
- const content = readFileSync(filePath);
790
- return new Response(content);
791
- } catch {
792
- return new Response("Not Found", { status: 404 });
793
- }
794
- } };
795
- const d1 = wranglerConfig.d1_databases;
796
- if (Array.isArray(d1) && d1.length > 0) {
797
- const databases = {};
798
- for (const db of d1) databases[db.binding] = db.database_id;
799
- mfOptions.d1Databases = databases;
800
- mfOptions.d1Persist = persistDir;
801
- }
802
- const kv = wranglerConfig.kv_namespaces;
803
- if (Array.isArray(kv) && kv.length > 0) {
804
- const namespaces = {};
805
- for (const ns of kv) namespaces[ns.binding] = ns.id;
806
- mfOptions.kvNamespaces = namespaces;
807
- mfOptions.kvPersist = persistDir;
808
- }
809
- const r2 = wranglerConfig.r2_buckets;
810
- if (Array.isArray(r2) && r2.length > 0) {
811
- const buckets = {};
812
- for (const bucket of r2) buckets[bucket.binding] = bucket.bucket_name;
813
- mfOptions.r2Buckets = buckets;
814
- mfOptions.r2Persist = persistDir;
815
- }
816
- const mf = new Miniflare(mfOptions);
817
- const proxyToken = mfOptions.bindings?.__VOID_PROXY_TOKEN;
818
- return {
819
- async collectPaths() {
820
- const headers = {};
821
- if (proxyToken) headers["x-void-internal"] = proxyToken;
822
- return readPrerenderPathsResponse(await mf.dispatchFetch(`http://localhost${PRERENDER_PATHS_ENDPOINT}`, {
823
- method: "POST",
824
- headers
825
- }));
826
- },
827
- async fetchPath(path) {
828
- return mf.dispatchFetch(`http://localhost${path}`);
829
- },
830
- async fetchPageData(path) {
831
- return mf.dispatchFetch(`http://localhost${path}`, { headers: {
832
- Accept: "application/json",
833
- Purpose: "prefetch",
834
- "X-VoidPages": "true"
835
- } });
836
- },
837
- async close() {
838
- await mf.dispose();
839
- }
840
- };
841
- }
842
- async function collectPrerenderPaths(options) {
843
- const runner = await createCloudflarePrerenderRunner(options);
844
- try {
845
- return await runner.collectPaths();
846
- } finally {
847
- await runner.close();
848
- }
849
- }
850
- /**
851
- * Prerender pages by spinning up Miniflare with the built worker.
852
- * Collects paths through the built worker's internal endpoint and then
853
- * fetches each page, writing HTML responses to `clientDir`.
854
- */
855
- async function prerenderPages(options) {
856
- const runner = await createCloudflarePrerenderRunner(options);
857
- try {
858
- const paths = await runner.collectPaths();
859
- if (paths.length === 0) return {
860
- paths,
861
- rendered: [],
862
- failed: []
863
- };
864
- return {
865
- paths,
866
- ...await renderPrerenderPaths(paths, options.clientDir, (path) => runner.fetchPath(path), (path) => runner.fetchPageData(path))
867
- };
868
- } finally {
869
- await runner.close();
870
- }
871
- }
872
- async function runNodePrerenderRunner(root, config) {
873
- const { sanitized: env, stripped } = stripInternalEnvKeys(filterLoadedEnv(loadEnv("production", root, "")));
874
- if (stripped.length > 0) voidWarn(`Ignoring internal env keys from .env files in Node prerender runner: ${stripped.join(", ")}. These are reserved for Void runtime use.`);
875
- const runnerPath = join(root, ".void", "prerender-runner.mjs");
876
- mkdirSync(dirname(runnerPath), { recursive: true });
877
- writeFileSync(runnerPath, `
878
- import { pathToFileURL } from "node:url";
879
- import { join, dirname } from "pathe";
880
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
881
-
882
- const config = JSON.parse(process.argv[2]);
883
- const { mode, ssrDir, clientDir } = config;
884
-
885
- if (clientDir) {
886
- try {
887
- const manifestPath = join(clientDir, ".vite", "manifest.json");
888
- globalThis.__VITE_MANIFEST__ = JSON.parse(readFileSync(manifestPath, "utf-8"));
889
- } catch {
890
- // Ignore missing manifests so prerender can continue without client assets.
891
- }
892
- }
893
-
894
- const appUrl = pathToFileURL(join(ssrDir, "app.js")).href;
895
- const app = await import(appUrl);
896
- const fetch = app.default?.fetch;
897
- if (!fetch) {
898
- console.error("[void] No default export with fetch() found in app.js");
899
- process.exit(1);
900
- }
901
-
902
- async function collectPaths() {
903
- const headers = {};
904
- if (process.env.__VOID_PROXY_TOKEN) headers["x-void-internal"] = process.env.__VOID_PROXY_TOKEN;
905
- const response = await fetch(
906
- new Request("http://localhost${PRERENDER_PATHS_ENDPOINT}", { method: "POST", headers }),
907
- );
908
- if (!response.ok) {
909
- throw new Error("prerender: Failed to collect prerender paths. HTTP " + response.status + ".");
910
- }
911
- const payload = await response.json();
912
- if (!payload || !Array.isArray(payload.paths) || !payload.paths.every((path) => typeof path === "string")) {
913
- throw new Error("prerender: Failed to collect prerender paths. Response payload is invalid.");
914
- }
915
- return payload.paths;
916
- }
917
-
918
- async function renderPaths(paths) {
919
- if (!clientDir) return { rendered: [], failed: [] };
920
- const result = { rendered: [], failed: [] };
921
-
922
- for (const path of paths) {
923
- try {
924
- const response = await fetch(new Request("http://localhost" + path));
925
- const html = await response.text();
926
- if (!response.ok) {
927
- result.failed.push({ path, error: "HTTP " + response.status });
928
- continue;
929
- }
930
- const outputFile = path === "/" ? "index.html" : path.slice(1) + ".html";
931
- const outputPath = join(clientDir, outputFile);
932
- mkdirSync(dirname(outputPath), { recursive: true });
933
- writeFileSync(outputPath, html);
934
- const pageDataResponse = await fetch(new Request("http://localhost" + path, {
935
- headers: { "Accept": "application/json", "Purpose": "prefetch", "X-VoidPages": "true" },
936
- }));
937
- if (pageDataResponse.headers.get("X-VoidPages")) {
938
- const pageData = await pageDataResponse.json();
939
- const pageDataFile = path === "/" ? "_void/pages/index.json" : "_void/pages/" + path.slice(1) + ".json";
940
- const pageDataPath = join(clientDir, pageDataFile);
941
- mkdirSync(dirname(pageDataPath), { recursive: true });
942
- writeFileSync(pageDataPath, JSON.stringify(pageData));
943
- }
944
- result.rendered.push(path);
945
- } catch (err) {
946
- result.failed.push({ path, error: err instanceof Error ? err.message : String(err) });
947
- }
948
- }
949
-
950
- return result;
951
- }
952
-
953
- if (mode === "collect") {
954
- process.stdout.write(JSON.stringify({ paths: await collectPaths() }));
955
- } else {
956
- const paths = await collectPaths();
957
- const result = await renderPaths(paths);
958
- process.stdout.write(JSON.stringify({ paths, ...result }));
959
- }
960
- `);
961
- try {
962
- return await new Promise((resolve, reject) => {
963
- execFile(process.execPath, [runnerPath, JSON.stringify(config)], {
964
- env: {
965
- ...process.env,
966
- ...env
967
- },
968
- cwd: root,
969
- maxBuffer: 10 * 1024 * 1024
970
- }, (error, stdout, stderr) => {
971
- if (stderr) process.stderr.write(stderr);
972
- if (error) {
973
- reject(/* @__PURE__ */ new Error(`Prerender child process failed: ${error.message}`));
974
- return;
975
- }
976
- try {
977
- resolve(JSON.parse(stdout));
978
- } catch {
979
- reject(/* @__PURE__ */ new Error(`Failed to parse prerender output: ${stdout}`));
980
- }
981
- });
982
- });
983
- } finally {
984
- try {
985
- unlinkSync(runnerPath);
986
- } catch {}
987
- }
988
- }
989
- async function collectPrerenderPathsNode(options) {
990
- return (await runNodePrerenderRunner(options.root, {
991
- mode: "collect",
992
- ssrDir: options.ssrDir,
993
- clientDir: options.clientDir
994
- })).paths;
995
- }
996
- /**
997
- * Prerender pages for Node.js targets by spawning a child process.
998
- *
999
- * Uses a child process to avoid mutating `process.env` in the parent Vite
1000
- * build process. The child imports the built app, collects prerender paths
1001
- * from the internal endpoint, renders each page, and returns the result.
1002
- */
1003
- async function prerenderPagesNode(options) {
1004
- const result = await runNodePrerenderRunner(options.root, {
1005
- mode: "collect-and-render",
1006
- ssrDir: options.ssrDir,
1007
- clientDir: options.clientDir
1008
- });
1009
- for (const path of result.rendered) voidLog(`prerender \x1b[36m${outputFileForPath(path)}\x1b[39m`);
1010
- for (const { path, error } of result.failed) voidWarn(`prerender ${path} failed: ${error}`);
1011
- return result;
1012
- }
1013
- //#endregion
1014
- //#region src/cli/deploy-errors.ts
1015
- const BODY_SNIPPET_MAX = 200;
1016
- function trimSnippet(body) {
1017
- const collapsed = body.replace(/\s+/g, " ").trim();
1018
- if (collapsed.length <= BODY_SNIPPET_MAX) return collapsed;
1019
- return `${collapsed.slice(0, BODY_SNIPPET_MAX)}...`;
1020
- }
1021
- function appendDeploymentLine(message, deploymentId) {
1022
- return deploymentId ? `${message}\nDeployment: ${deploymentId}` : message;
1023
- }
1024
- function appendLogLine(message, logPath) {
1025
- return logPath ? `${message}\nDetailed log: ${logPath}` : message;
1026
- }
1027
- function appendTrailingLines(message, deploymentId, logPath) {
1028
- return appendLogLine(appendDeploymentLine(message, deploymentId), logPath);
1029
- }
1030
- /**
1031
- * Format a server-emitted `event: 'error'` payload with an optional deployment id.
1032
- */
1033
- function formatDeployFailureMessage(message, deploymentId, logPath) {
1034
- return appendTrailingLines(message, deploymentId, logPath);
1035
- }
1036
- /**
1037
- * Format the "stream ended cleanly but no terminal event arrived" case.
1038
- *
1039
- * This is the only call site that should emit the bare "Connection lost during
1040
- * deploy" message — the server closed the NDJSON stream without emitting
1041
- * `done` or `error`, so there is no underlying error to carry.
1042
- */
1043
- function formatConnectionLostMessage(deploymentId, logPath) {
1044
- return appendTrailingLines("deploy: Connection lost during deploy.", deploymentId, logPath);
1045
- }
1046
- /**
1047
- * Convert an exception thrown during the deploy NDJSON stream into a
1048
- * user-facing `Error` with a readable message.
1049
- *
1050
- * Three cases:
1051
- * 1. `PlatformApiError` with 409 + a JSON body carrying `{error, deployment}`
1052
- * — surface the server-supplied error and prefer the body's deployment id.
1053
- * 2. Any other `PlatformApiError` — surface as `Deploy failed (HTTP <status>): <detail>`
1054
- * where detail is the parsed `error`/`message` field or a trimmed body snippet.
1055
- * 3. Anything else — preserve the original message under a `Connection lost
1056
- * during deploy: <original>` prefix so the caller can still tell network,
1057
- * parse, and transport errors apart.
1058
- */
1059
- function formatStreamDeployError(err, deploymentId, logPath) {
1060
- if (err instanceof PlatformApiError) {
1061
- const parsed = parsePlatformErrorBody(err.body);
1062
- const parsedError = parsed?.error ?? parsed?.message ?? null;
1063
- const effectiveDeploymentId = parsed?.deployment ?? null ?? deploymentId;
1064
- if (isCliOutdatedError(err) && parsed?.message) return new Error(appendTrailingLines(parsed.message, effectiveDeploymentId, logPath));
1065
- if (err.status === 409 && parsedError) return new Error(appendTrailingLines(`deploy: Deploy failed: ${parsedError}`, effectiveDeploymentId, logPath));
1066
- const detail = parsedError ?? trimSnippet(err.body);
1067
- const base = detail ? `deploy: Deploy failed (HTTP ${err.status}): ${detail}` : `deploy: Deploy failed (HTTP ${err.status}).`;
1068
- return new Error(appendTrailingLines(base, effectiveDeploymentId, logPath));
1069
- }
1070
- const original = err instanceof Error ? err.message : String(err);
1071
- return new Error(appendTrailingLines(`deploy: Connection lost during deploy: '${original}'.`, deploymentId, logPath));
1072
- }
1073
- /**
1074
- * Maximum retry attempts for a single R2 PUT before giving up. Matched to
1075
- * the design doc 0068 spec: 5xx triggers retry, 4xx surfaces immediately.
1076
- */
1077
- const MAX_R2_PUT_RETRIES = 3;
1078
- /**
1079
- * Retry backoff delays in milliseconds. After attempt `i` fails (5xx) we
1080
- * sleep `RETRY_BACKOFF_MS[i]` before retrying. Indexed from 0; once the
1081
- * array is exhausted we surface the error.
1082
- */
1083
- const RETRY_BACKOFF_MS = [
1084
- 250,
1085
- 500,
1086
- 1e3
1087
- ];
1088
- var DirectR2PutError = class extends Error {
1089
- status;
1090
- hash;
1091
- path;
1092
- /**
1093
- * `body` is the response body text (may be empty / non-text). Useful in
1094
- * the surface error so users can correlate against R2-side errors like
1095
- * `BadDigest` (Content-MD5 mismatch) or signature errors.
1096
- */
1097
- body;
1098
- constructor(status, hash, path, body) {
1099
- super(`R2 PUT failed for ${path} (${hash}): ${status}${body ? ` ${body}` : ""}`);
1100
- this.name = "DirectR2PutError";
1101
- this.status = status;
1102
- this.hash = hash;
1103
- this.path = path;
1104
- this.body = body;
1105
- }
1106
- };
1107
- /**
1108
- * Issue direct-to-R2 PUTs for every item in `items`, bounded to
1109
- * `R2_PUT_CONCURRENCY`. Surfaces the first failure (4xx immediately, 5xx
1110
- * after retry exhaustion) as a `DirectR2PutError` carrying the asset path
1111
- * and hash so the user can correlate against the JSONL log.
1112
- *
1113
- * Logs `r2_put_start` / `r2_put_end` per asset, plus `r2_put_retry` on
1114
- * each retry attempt. Per-asset durations help identify slow uploads.
1115
- *
1116
- * Memory: each successful PUT clears `item.body` and invokes `onAfterPut`
1117
- * so the caller can drop its own ref (e.g. delete from the in-memory
1118
- * `assetFiles` cache). For huge deploys this keeps peak memory bounded by
1119
- * the upload concurrency rather than the total bundle size.
1120
- */
1121
- async function uploadAssetsToR2(items, cliLog, fetchImpl = fetch, onAfterPut) {
1122
- if (items.length === 0) return;
1123
- let firstError = null;
1124
- let nextIndex = 0;
1125
- const workerCount = Math.min(10, items.length);
1126
- const workers = [];
1127
- for (let w = 0; w < workerCount; w++) workers.push((async () => {
1128
- while (firstError === null) {
1129
- const i = nextIndex++;
1130
- if (i >= items.length) return;
1131
- try {
1132
- await uploadOne(items[i], cliLog, fetchImpl);
1133
- } catch (err) {
1134
- if (firstError === null) firstError = err;
1135
- return;
1136
- }
1137
- items[i].body = void 0;
1138
- onAfterPut?.(items[i]);
1139
- }
1140
- })());
1141
- await Promise.all(workers);
1142
- if (firstError !== null) throw firstError;
1143
- }
1144
- async function uploadOne(item, cliLog, fetchImpl) {
1145
- if (!item.body) throw new Error(`internal: uploadOne invoked for ${item.path} with no body`);
1146
- const body = item.body;
1147
- const startedAt = Date.now();
1148
- cliLog?.info("r2_put_start", {
1149
- hash: item.hash,
1150
- path: item.path,
1151
- sizeBytes: body.length
1152
- });
1153
- let attempt = 0;
1154
- while (true) {
1155
- let res;
1156
- try {
1157
- res = await fetchImpl(item.url, {
1158
- method: "PUT",
1159
- body: new Uint8Array(body),
1160
- headers: item.headers
1161
- });
1162
- } catch (err) {
1163
- if (attempt < MAX_R2_PUT_RETRIES) {
1164
- cliLog?.warn("r2_put_retry", {
1165
- hash: item.hash,
1166
- path: item.path,
1167
- attempt: attempt + 1,
1168
- status: 0,
1169
- error: err instanceof Error ? err.message : String(err)
1170
- });
1171
- await sleep(RETRY_BACKOFF_MS[attempt]);
1172
- attempt++;
1173
- continue;
1174
- }
1175
- cliLog?.error("r2_put_end", err, {
1176
- hash: item.hash,
1177
- path: item.path,
1178
- durationMs: Date.now() - startedAt,
1179
- status: 0
1180
- });
1181
- throw new DirectR2PutError(0, item.hash, item.path, err instanceof Error ? err.message : String(err));
1182
- }
1183
- if (res.ok) {
1184
- cliLog?.info("r2_put_end", {
1185
- hash: item.hash,
1186
- path: item.path,
1187
- durationMs: Date.now() - startedAt,
1188
- status: res.status
1189
- });
1190
- return;
1191
- }
1192
- const errBody = await safeReadText(res);
1193
- if (res.status >= 500 && attempt < MAX_R2_PUT_RETRIES) {
1194
- cliLog?.warn("r2_put_retry", {
1195
- hash: item.hash,
1196
- path: item.path,
1197
- attempt: attempt + 1,
1198
- status: res.status,
1199
- body: truncate(errBody)
1200
- });
1201
- await sleep(RETRY_BACKOFF_MS[attempt]);
1202
- attempt++;
1203
- continue;
1204
- }
1205
- cliLog?.error("r2_put_end", void 0, {
1206
- hash: item.hash,
1207
- path: item.path,
1208
- durationMs: Date.now() - startedAt,
1209
- status: res.status,
1210
- body: truncate(errBody)
1211
- });
1212
- throw new DirectR2PutError(res.status, item.hash, item.path, truncate(errBody));
1213
- }
1214
- }
1215
- async function safeReadText(res) {
1216
- try {
1217
- return await res.text();
1218
- } catch {
1219
- return "";
1220
- }
1221
- }
1222
- function truncate(s) {
1223
- return s.length > 500 ? `${s.slice(0, 500)}...` : s;
1224
- }
1225
- function sleep(ms) {
1226
- return new Promise((resolve) => setTimeout(resolve, ms));
1227
- }
1228
- //#endregion
1229
- //#region src/cli/github-oidc.ts
1230
- /**
1231
- * GitHub Actions OIDC → Void deploy-token exchange.
1232
- *
1233
- * When `void deploy` runs inside GitHub Actions with no explicit `VOID_TOKEN`,
1234
- * the CLI mints a short-lived GitHub OIDC token (audience `void`) and exchanges
1235
- * it at `POST $VOID_API_URL/auth/github-oidc` for a short-lived, project-scoped
1236
- * Void deploy token. Doing the exchange in-process lets the scaffolded workflow
1237
- * collapse to a single `void deploy` step — no curl/jq token-exchange step.
1238
- *
1239
- * Server contract (platform `POST /auth/github-oidc` handler):
1240
- * request { oidcToken: string, project: string } (project = slug)
1241
- * response { token: string, projectId, buildId }
1242
- */
1243
- const OIDC_AUDIENCE = "void";
1244
- /**
1245
- * True when the process is running inside GitHub Actions with the OIDC token
1246
- * request endpoint available — i.e. the workflow granted
1247
- * `permissions: id-token: write`, so GitHub injected the request URL + token.
1248
- */
1249
- function isGithubActionsOidcAvailable(env = process.env) {
1250
- return env.GITHUB_ACTIONS === "true" && typeof env.ACTIONS_ID_TOKEN_REQUEST_URL === "string" && env.ACTIONS_ID_TOKEN_REQUEST_URL.length > 0 && typeof env.ACTIONS_ID_TOKEN_REQUEST_TOKEN === "string" && env.ACTIONS_ID_TOKEN_REQUEST_TOKEN.length > 0;
1251
- }
1252
- /**
1253
- * Mint a GitHub Actions OIDC token (audience `void`) and exchange it for a
1254
- * short-lived, project-scoped Void deploy token.
1255
- *
1256
- * Two hops:
1257
- * 1. GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=void (Bearer request token)
1258
- * → `.value` is the OIDC JWT.
1259
- * 2. POST $apiUrl/auth/github-oidc { oidcToken, project }
1260
- * → `.token` is the Void deploy token.
1261
- *
1262
- * Throws a clear, actionable error (including the HTTP status and server error
1263
- * body when available) on any non-OK response or missing field.
1264
- */
1265
- async function exchangeGithubOidcForDeployToken(apiUrl, project, env = process.env) {
1266
- const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
1267
- const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
1268
- if (!requestUrl || !requestToken) throw new Error("github-oidc: GitHub Actions OIDC is unavailable (missing ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN). Ensure the workflow grants `permissions: id-token: write`.");
1269
- return exchangeOidcTokenAtApi(apiUrl, project, await requestGithubOidcToken(requestUrl, requestToken));
1270
- }
1271
- /**
1272
- * GET the GitHub Actions OIDC token for audience `void`. GitHub's request URL
1273
- * already carries a query string, so `audience` is appended alongside it.
1274
- */
1275
- async function requestGithubOidcToken(requestUrl, requestToken) {
1276
- const url = new URL(requestUrl);
1277
- url.searchParams.set("audience", OIDC_AUDIENCE);
1278
- let res;
1279
- try {
1280
- res = await fetch(url, { headers: { Authorization: `Bearer ${requestToken}` } });
1281
- } catch (err) {
1282
- throw new Error(`github-oidc: Failed to reach the GitHub OIDC token endpoint: ${errText(err)}`);
1283
- }
1284
- if (!res.ok) {
1285
- const body = await safeText(res);
1286
- throw new Error(`github-oidc: Failed to mint a GitHub OIDC token (${res.status})${body ? `: ${body}` : ""}.`);
1287
- }
1288
- let body;
1289
- try {
1290
- body = await res.json();
1291
- } catch {
1292
- throw new Error("github-oidc: GitHub OIDC token endpoint returned an invalid response.");
1293
- }
1294
- const value = body?.value;
1295
- if (typeof value !== "string" || value.length === 0) throw new Error("github-oidc: GitHub OIDC token endpoint did not return a token value.");
1296
- return value;
1297
- }
1298
- /** POST the OIDC JWT to the Void API and return the minted deploy token. */
1299
- async function exchangeOidcTokenAtApi(apiUrl, project, oidcToken) {
1300
- const url = `${apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl}/auth/github-oidc`;
1301
- ensureCloudflaredToken(apiUrl);
1302
- let res;
1303
- try {
1304
- res = await fetch(url, {
1305
- method: "POST",
1306
- headers: {
1307
- "Content-Type": "application/json",
1308
- ...cfAccessHeaders(process.env)
1309
- },
1310
- body: JSON.stringify({
1311
- oidcToken,
1312
- project
1313
- })
1314
- });
1315
- } catch (err) {
1316
- throw new Error(`github-oidc: Failed to reach ${url}: ${errText(err)}`);
1317
- }
1318
- if (!res.ok) {
1319
- const body = await safeText(res);
1320
- throw new Error(`github-oidc: OIDC token exchange failed (${res.status})${body ? `: ${body}` : ""}. Confirm the repository is connected to project "${project}" with the github_actions executor.`);
1321
- }
1322
- let body;
1323
- try {
1324
- body = await res.json();
1325
- } catch {
1326
- throw new Error("github-oidc: OIDC token exchange returned an invalid response.");
1327
- }
1328
- const token = body?.token;
1329
- if (typeof token !== "string" || token.length === 0) throw new Error("github-oidc: OIDC token exchange did not return a deploy token.");
1330
- return token;
1331
- }
1332
- /**
1333
- * Emit a GitHub Actions `::add-mask::` workflow command so `value` is redacted
1334
- * from workflow logs. No-op when not running inside GitHub Actions. GitHub reads
1335
- * workflow commands from stdout.
1336
- */
1337
- function maskGithubActionsSecret(value, env = process.env) {
1338
- if (env.GITHUB_ACTIONS !== "true" || !value) return;
1339
- process.stdout.write(`::add-mask::${value}\n`);
1340
- }
1341
- function errText(err) {
1342
- return err instanceof Error ? err.message : String(err);
1343
- }
1344
- async function safeText(res) {
1345
- try {
1346
- return (await res.text()).trim();
1347
- } catch {
1348
- return "";
1349
- }
1350
- }
1351
- //#endregion
1352
- //#region src/cli/log-file.ts
1353
- /**
1354
- * Walk `err.cause` up to 6 levels and return a plain JSON-friendly chain.
1355
- * Skips stack traces deliberately — keeps output line-oriented and readable.
1356
- */
1357
- function describeErrorChain(err, depth = 0) {
1358
- if (depth > 5 || err == null) return String(err);
1359
- if (err instanceof Error) {
1360
- const node = {
1361
- name: err.name,
1362
- message: err.message
1363
- };
1364
- const code = err.code;
1365
- if (code !== void 0) node.code = code;
1366
- const errno = err.errno;
1367
- if (errno !== void 0) node.errno = errno;
1368
- const cause = err.cause;
1369
- if (cause != null) {
1370
- const sub = describeErrorChain(cause, depth + 1);
1371
- if (typeof sub === "object") node.cause = sub;
1372
- else node.cause = { message: sub };
1373
- }
1374
- return node;
1375
- }
1376
- return String(err);
1377
- }
1378
- /**
1379
- * Flatten an error and its `cause` chain to a single inline string suitable
1380
- * for stderr skimming. Mirrors `describeErrorChain`'s walk (depth cap 6,
1381
- * same fields, no stack traces) but joins levels with ` -> ` instead of
1382
- * nesting JSON.
1383
- */
1384
- function formatErrorChainInline(err, depth = 0) {
1385
- if (depth > 5 || err == null) return String(err);
1386
- if (err instanceof Error) {
1387
- let head = err.name ? `${err.name}: ${err.message}` : err.message;
1388
- const code = err.code;
1389
- const errno = err.errno;
1390
- const tags = [];
1391
- if (code !== void 0) tags.push(`code=${code}`);
1392
- if (errno !== void 0) tags.push(`errno=${errno}`);
1393
- if (tags.length > 0) head = `${head} (${tags.join(" ")})`;
1394
- const cause = err.cause;
1395
- if (cause != null) return `${head} -> ${formatErrorChainInline(cause, depth + 1)}`;
1396
- return head;
1397
- }
1398
- return String(err);
1399
- }
1400
- function formatStderrLine(level, msg, err, fields) {
1401
- const parts = [level, msg];
1402
- if (fields) for (const [k, v] of Object.entries(fields)) {
1403
- if (v === void 0) continue;
1404
- let s;
1405
- if (v == null || typeof v === "number" || typeof v === "boolean") s = String(v);
1406
- else if (typeof v === "string") s = v.length > 200 ? `${v.slice(0, 200)}...` : v;
1407
- else try {
1408
- s = JSON.stringify(v);
1409
- } catch {
1410
- s = String(v);
1411
- }
1412
- parts.push(`${k}=${s}`);
1413
- }
1414
- if (err !== void 0) parts.push(`-> err=${formatErrorChainInline(err)}`);
1415
- return parts.join(" ");
1416
- }
1417
- function openDeployLog(opts) {
1418
- const mirror = opts?.mirrorToStderr === true;
1419
- const startedAt = Date.now();
1420
- let path = null;
1421
- let stream = null;
1422
- let broken = false;
1423
- let closed = false;
1424
- try {
1425
- const dir = join(homedir(), ".void", "logs");
1426
- mkdirSync(dir, { recursive: true });
1427
- const filePath = join(dir, `deploy-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`);
1428
- stream = createWriteStream(filePath, {
1429
- flags: "a",
1430
- autoClose: true
1431
- });
1432
- stream.on("error", () => {
1433
- broken = true;
1434
- });
1435
- path = filePath;
1436
- } catch {
1437
- broken = true;
1438
- path = null;
1439
- stream = null;
1440
- }
1441
- function write(level, msg, err, fields) {
1442
- if (mirror) try {
1443
- process.stderr.write(`${formatStderrLine(level, msg, err, fields)}\n`);
1444
- } catch {}
1445
- if (broken || !stream || closed) return;
1446
- const now = Date.now();
1447
- const record = {
1448
- ts: now,
1449
- elapsedMs: now - startedAt,
1450
- lvl: level,
1451
- msg
1452
- };
1453
- if (fields) {
1454
- for (const [k, v] of Object.entries(fields)) if (v !== void 0 && k !== "ts" && k !== "elapsedMs" && k !== "lvl" && k !== "msg" && k !== "err") record[k] = v;
1455
- }
1456
- if (err !== void 0) record.err = describeErrorChain(err);
1457
- let line;
1458
- try {
1459
- line = `${JSON.stringify(record)}\n`;
1460
- } catch {
1461
- line = `${JSON.stringify({
1462
- ts: now,
1463
- elapsedMs: now - startedAt,
1464
- lvl: level,
1465
- msg,
1466
- err: "unserializable fields"
1467
- })}\n`;
1468
- }
1469
- try {
1470
- stream.write(line);
1471
- } catch {
1472
- broken = true;
1473
- }
1474
- }
1475
- return {
1476
- get path() {
1477
- return path;
1478
- },
1479
- info(msg, fields) {
1480
- write("info", msg, void 0, fields);
1481
- },
1482
- warn(msg, fields) {
1483
- write("warn", msg, void 0, fields);
1484
- },
1485
- error(msg, err, fields) {
1486
- write("error", msg, err, fields);
1487
- },
1488
- debug(msg, fields) {
1489
- write("debug", msg, void 0, fields);
1490
- },
1491
- close() {
1492
- if (closed) return Promise.resolve();
1493
- closed = true;
1494
- if (!stream) return Promise.resolve();
1495
- return new Promise((resolveClose) => {
1496
- try {
1497
- stream.end(() => resolveClose());
1498
- } catch {
1499
- resolveClose();
1500
- }
1501
- });
1502
- }
1503
- };
1504
- }
1505
- //#endregion
1506
- //#region src/shared/utils.ts
1507
- const isWindows = typeof process !== "undefined" && process.platform === "win32";
1508
- function slash(p) {
1509
- return p.replace(/\\/g, "/");
1510
- }
1511
- function toSlash(p) {
1512
- return isWindows ? slash(p) : p;
1513
- }
1514
- //#endregion
1515
- //#region src/cli/package.ts
1516
- const textEncoder = new TextEncoder();
1517
- const FRAMEWORK_LIVE_UNSUPPORTED_MESSAGE = "deploy: void/live is currently supported in Void apps only.";
1518
- /**
1519
- * Strip dev-only / internal-only fields from each rule before shipping the
1520
- * manifest. The resulting entries contain only `{ source, destination, status }`.
1521
- *
1522
- * - `origin` is used to build the `X-Void-Routing` trace header during local
1523
- * dev; the deployed platform doesn't consume it.
1524
- * - `force` is used internally by the CLI/router to split rules into the
1525
- * `redirectRules` (pre-asset, always-fires) and `fallbackRules` (post-asset,
1526
- * only when the asset resolver 404s) buckets and to label the trace header
1527
- * on `node`/`bun`/`deno` targets. The platform dispatch worker decides phase
1528
- * purely from which array the rule lives in, so shipping `force` is dead weight.
1529
- *
1530
- * Keep in sync with `stripOriginForProd` in `router/compile.ts`, which strips
1531
- * only `origin` because the Node/Bun/Deno-target generated code still reads
1532
- * `force` at runtime to label the dev trace header.
1533
- */
1534
- function stripDevOnlyRuleFields(rules) {
1535
- if (!rules) return rules;
1536
- return rules.map(({ origin: _origin, force: _force, ...rest }) => rest);
1537
- }
1538
- /**
1539
- * Layer user-defined `routing.fallbacks` with the SPA app type's implicit
1540
- * `/index.html` fallback. Decision: user fallbacks take precedence; if none
1541
- * matches, the SPA default kicks in.
1542
- *
1543
- * The platform's `not_found_handling: 'single-page-application'` is a global
1544
- * switch — set once per deployment — so we can't have the platform evaluate
1545
- * user fallbacks *before* applying it. Instead, when the user has configured
1546
- * fallbacks on a SPA app, we disable the platform-level SPA setting and
1547
- * append a synthetic catch-all fallback rule (`/*` → `/index.html`) after
1548
- * the user's rules. The dispatch worker evaluates routing rules with
1549
- * first-match-wins semantics, so placing the synthetic rule last means any
1550
- * user rule that matches a more specific pattern fires first; the synthetic
1551
- * rule only catches paths no user rule matched.
1552
- *
1553
- * When the user has no fallbacks configured, the platform-level SPA switch
1554
- * is left intact (zero worker-side overhead for the common case).
1555
- */
1556
- function resolveSpaFallbackChain(appType, fallbackRules) {
1557
- const userRules = fallbackRules && fallbackRules.length > 0 ? fallbackRules : void 0;
1558
- if (appType !== "spa") return {
1559
- assetConfig: { not_found_handling: appType === "static" ? "404-page" : "none" },
1560
- fallbackRules: userRules
1561
- };
1562
- if (!userRules) return {
1563
- assetConfig: { not_found_handling: "single-page-application" },
1564
- fallbackRules: void 0
1565
- };
1566
- if (userRules.some((r) => r.host === void 0 && r.source === "/*" && r.destination === "/index.html")) return {
1567
- assetConfig: { not_found_handling: "none" },
1568
- fallbackRules: userRules
1569
- };
1570
- const syntheticSpaFallback = {
1571
- source: "/*",
1572
- destination: "/index.html",
1573
- status: 200,
1574
- force: false,
1575
- origin: "spa-default"
1576
- };
1577
- return {
1578
- assetConfig: { not_found_handling: "none" },
1579
- fallbackRules: [...userRules, syntheticSpaFallback]
1580
- };
1581
- }
1582
- const PROGRESS_CHUNK = 50;
1583
- /**
1584
- * Yield to the event loop every PROGRESS_CHUNK iterations so spinner ticks
1585
- * can repaint, and emit a progress message via onProgress. Otherwise the
1586
- * tight sync loop blocks the spinner interval and the user sees no updates.
1587
- */
1588
- async function tickProgress(i, total, onProgress, label) {
1589
- if (!onProgress) return;
1590
- if (i % PROGRESS_CHUNK !== 0 && i !== total) return;
1591
- onProgress(`${label} (${i}/${total})`);
1592
- await new Promise((resolve) => setImmediate(resolve));
1593
- }
1594
- /**
1595
- * Compute a CF-compatible asset hash: BLAKE3 of base64(content) + extension,
1596
- * first 32 hex chars. Matches Wrangler's hashFile algorithm exactly.
1597
- * Must match the API's computeAssetHash algorithm exactly.
1598
- */
1599
- function computeAssetHash(content, filePath) {
1600
- const base64 = content.toString("base64");
1601
- const ext = extname(filePath).substring(1);
1602
- const input = textEncoder.encode(base64 + ext);
1603
- return Buffer.from(hash(input)).toString("hex").slice(0, 32);
1604
- }
1605
- /**
1606
- * Map of common asset file extensions → content type. Mirrors
1607
- * `packages/dispatch/src/static.ts`'s MIME_TYPES table so the value the CLI
1608
- * declares for the R2 PUT signature matches what dispatch will serve later
1609
- * (dispatch reads `obj.httpMetadata?.contentType` first, falling back to
1610
- * path-based inference). Keeping the tables aligned avoids a needless
1611
- * mismatch where R2-stored content-type and serve-time content-type
1612
- * disagree on the same asset.
1613
- */
1614
- const ASSET_MIME_TYPES = {
1615
- html: "text/html; charset=utf-8",
1616
- htm: "text/html; charset=utf-8",
1617
- css: "text/css; charset=utf-8",
1618
- js: "text/javascript; charset=utf-8",
1619
- mjs: "text/javascript; charset=utf-8",
1620
- json: "application/json; charset=utf-8",
1621
- xml: "application/xml; charset=utf-8",
1622
- svg: "image/svg+xml; charset=utf-8",
1623
- txt: "text/plain; charset=utf-8",
1624
- csv: "text/csv; charset=utf-8",
1625
- md: "text/markdown; charset=utf-8",
1626
- markdown: "text/markdown; charset=utf-8",
1627
- yaml: "text/yaml; charset=utf-8",
1628
- yml: "text/yaml; charset=utf-8",
1629
- toml: "application/toml",
1630
- png: "image/png",
1631
- jpg: "image/jpeg",
1632
- jpeg: "image/jpeg",
1633
- gif: "image/gif",
1634
- ico: "image/x-icon",
1635
- webp: "image/webp",
1636
- avif: "image/avif",
1637
- woff: "font/woff",
1638
- woff2: "font/woff2",
1639
- ttf: "font/ttf",
1640
- otf: "font/otf",
1641
- eot: "application/vnd.ms-fontobject",
1642
- pdf: "application/pdf",
1643
- wasm: "application/wasm",
1644
- webmanifest: "application/manifest+json; charset=utf-8",
1645
- map: "application/json; charset=utf-8",
1646
- mp4: "video/mp4",
1647
- webm: "video/webm",
1648
- mp3: "audio/mpeg",
1649
- ogg: "audio/ogg"
1650
- };
1651
- /**
1652
- * Resolve the content-type to declare on a direct-to-R2 PUT for `path`.
1653
- * The value is part of the signed Content-Type header on the presigned
1654
- * URL — if the bytes are uploaded with a different content-type, R2
1655
- * rejects the PUT (signature mismatch). Defaults to
1656
- * `application/octet-stream` for unknown extensions.
1657
- */
1658
- function getAssetContentType(path) {
1659
- const ext = path.split(".").pop()?.toLowerCase() ?? "";
1660
- return ASSET_MIME_TYPES[ext] ?? "application/octet-stream";
1661
- }
1662
- /**
1663
- * Compute MD5 of raw content as a 32-char lowercase hex string.
1664
- *
1665
- * Used as the wire-level upload-integrity primitive on direct-to-R2 PUTs:
1666
- * R2's S3 compatibility layer enforces `Content-MD5` (RFC 1864) on PutObject,
1667
- * so the upload itself fails if uploaded bytes don't match the declared MD5.
1668
- * MD5 collision resistance is acceptable here because (a) this hash is purely
1669
- * an upload-integrity primitive (BLAKE3 remains the canonical content key),
1670
- * and (b) project-scoped storage (`assets/${projectId}/${blake3}`) means any
1671
- * collision attack is exploitable only against the attacker's own project.
1672
- */
1673
- function computeMd5(content) {
1674
- return createHash("md5").update(content).digest("hex");
1675
- }
1676
- /**
1677
- * Compute both BLAKE3 (CF-compatible asset hash) and MD5 (wire-level R2
1678
- * upload-integrity checksum) from a single in-memory buffer. Returned in
1679
- * the v2 manifest entry shape for direct-to-R2 deploys.
1680
- *
1681
- * Single-pass: callers read the file once into `content`; this function
1682
- * then derives both digests from that buffer. The file is never read twice.
1683
- */
1684
- function computeAssetHashes(content, filePath) {
1685
- return {
1686
- blake3: computeAssetHash(content, filePath),
1687
- md5: computeMd5(content)
1688
- };
1689
- }
1690
- /**
1691
- * Create a filter function that excludes .assetsignore, _headers, _redirects,
1692
- * OS metadata files, dotenv files, deploy-internal patterns, and any patterns
1693
- * listed in a .assetsignore file.
1694
- * Matches Wrangler's createAssetsIgnoreFunction from workers-shared/utils/helpers.ts.
1695
- */
1696
- function createAssetsIgnoreFunction(dir, extraPatterns = []) {
1697
- const patterns = [
1698
- "/.assetsignore",
1699
- "/_redirects",
1700
- "/_headers",
1701
- ".DS_Store",
1702
- ".env",
1703
- ".env.*",
1704
- ...extraPatterns
1705
- ];
1706
- const assetsIgnorePath = join(dir, ".assetsignore");
1707
- let assetsIgnoreFilePresent = false;
1708
- if (existsSync(assetsIgnorePath)) {
1709
- assetsIgnoreFilePresent = true;
1710
- const content = readFileSync(assetsIgnorePath, "utf-8");
1711
- patterns.push(...content.split("\n"));
1712
- }
1713
- const ig = ignore().add(patterns);
1714
- const isIgnored = (relativePath) => {
1715
- const normalized = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
1716
- return ig.test(normalized).ignored;
1717
- };
1718
- return {
1719
- isIgnored,
1720
- assetsIgnoreFilePresent
1721
- };
1722
- }
1723
- /**
1724
- * Guard against accidentally uploading _worker.js as a static asset.
1725
- * Matches Wrangler's errorOnLegacyPagesWorkerJSAsset behavior:
1726
- * throws only when no .assetsignore file exists (if .assetsignore is present
1727
- * but doesn't exclude _worker.js, the user intentionally wants to upload it).
1728
- */
1729
- function checkWorkerJsAsset(relativePath, assetsIgnoreFilePresent) {
1730
- if (assetsIgnoreFilePresent) return;
1731
- const name = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
1732
- if (name === "_worker.js" || name.startsWith("_worker.js/")) throw new Error(`deploy: Refusing to upload _worker.js ${name === "_worker.js" ? "file" : "directory"} as an asset. This could expose private server-side code to the public Internet. If this is unintended, remove it or add a .assetsignore file containing "_worker.js". If intentional, add an empty .assetsignore file to suppress this error.`);
1733
- }
1734
- /**
1735
- * Collect all files in a directory, compute hashes, and return both the
1736
- * manifest (for preflight) and the raw file map (for selective packaging).
1737
- *
1738
- * Each file is read from disk exactly once. Both BLAKE3 (storage key) and
1739
- * MD5 (R2 upload-integrity) digests are derived from that single buffer.
1740
- */
1741
- async function collectAndHashAssets(dir, onProgress, opts = {}) {
1742
- const assetManifest = {};
1743
- const assetFiles = /* @__PURE__ */ new Map();
1744
- try {
1745
- const { isIgnored, assetsIgnoreFilePresent } = createAssetsIgnoreFunction(dir, opts.ignorePatterns);
1746
- const files = collectAssetFiles(dir, isIgnored);
1747
- let processed = 0;
1748
- for (const filePath of files) {
1749
- processed++;
1750
- await tickProgress(processed, files.length, onProgress, "Hashing assets");
1751
- const relativePath = "/" + toSlash(relative(dir, filePath));
1752
- if (isIgnored(relativePath)) continue;
1753
- checkWorkerJsAsset(relativePath, assetsIgnoreFilePresent);
1754
- const content = readFileSync(filePath);
1755
- const { blake3, md5 } = computeAssetHashes(content, filePath);
1756
- assetManifest[relativePath] = {
1757
- blake3,
1758
- md5,
1759
- size: content.length,
1760
- hash: blake3
1761
- };
1762
- assetFiles.set(relativePath, content);
1763
- }
1764
- } catch (err) {
1765
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {} else throw err;
1766
- }
1767
- return {
1768
- assetManifest,
1769
- assetFiles
1770
- };
1771
- }
1772
- function toWebSocketManifest(routes) {
1773
- if (!routes || routes.length === 0) return;
1774
- return {
1775
- routes: routes.map((route) => ({
1776
- pattern: route.pattern,
1777
- filePath: route.filePath,
1778
- className: route.className,
1779
- mode: route.mode,
1780
- hasAuthGate: route.hasAuthGate
1781
- })),
1782
- classes: routes.map((route) => route.className)
1783
- };
1784
- }
1785
- function toSandboxManifest(sandbox) {
1786
- return {
1787
- bindingName: sandbox.binding,
1788
- className: sandbox.className,
1789
- containerName: sandbox.containerName,
1790
- image: sandbox.platformImage,
1791
- ...sandbox.instanceType && { instanceType: sandbox.instanceType },
1792
- ...sandbox.maxInstances != null && { maxInstances: sandbox.maxInstances }
1793
- };
1794
- }
1795
- function toLiveManifest(live) {
1796
- const bindingName = live?.bindingName ?? "VOID_LIVE";
1797
- const className = live?.className ?? "VoidLiveStreamDurableObject";
1798
- return {
1799
- bindingName,
1800
- className,
1801
- storage: "sqlite",
1802
- classes: [className],
1803
- migrations: [{
1804
- tag: LIVE_MIGRATION_TAG,
1805
- newSqliteClasses: [className]
1806
- }]
1807
- };
1808
- }
1809
- /**
1810
- * If the user configured `routing.fallbacks` on a non-SPA app, the platform
1811
- * still applies the rules but no `/* -> /index.html` SPA shell is synthesised
1812
- * — unmatched paths fall through to the platform 404. Returns the warning
1813
- * message string when that mismatch is detected, or `null` otherwise. Pure
1814
- * function; callers decide whether/how to surface the message (CLI progress,
1815
- * `console.warn`, etc.).
1816
- */
1817
- function detectNonSpaFallbackWarning(assetConfig, fallbackRules) {
1818
- if (assetConfig?.not_found_handling === "single-page-application") return null;
1819
- if (!fallbackRules || fallbackRules.length === 0) return null;
1820
- return "routing.fallbacks is set but appType is not \"spa\"; fallbacks will be applied, but no \"/* -> /index.html\" SPA shell fallback is synthesised — unmatched paths fall through to the platform 404.";
1821
- }
1822
- /**
1823
- * Build a Void-app deploy manifest from the inferred bindings + per-feature
1824
- * inputs. Pure shape transformation: reads no files, has no I/O.
1825
- *
1826
- * Note: callers are responsible for surfacing the non-SPA-fallbacks warning
1827
- * (see `detectNonSpaFallbackWarning`) before invoking this builder. The
1828
- * builder itself stays I/O-free.
1829
- */
1830
- function buildVoidManifest(opts) {
1831
- const { bindings, migrations, schedules, ssr, framework, revalidate, vars, queues, prerender, assetConfig, options, hashedAssetsPrefix, headerRules, redirectRules, fallbackRules, dialect, webSockets } = opts;
1832
- const manifest = {
1833
- version: 3,
1834
- assetManifestVersion: 2,
1835
- bindings: {}
1836
- };
1837
- if (bindings.needsAuth) manifest.auth = true;
1838
- const hasMigrations = migrations && migrations.length > 0;
1839
- if (bindings.needsD1 || bindings.needsAuth || hasMigrations) if (dialect === "postgresql") manifest.bindings.hyperdrive = ["HYPERDRIVE"];
1840
- else manifest.bindings.d1 = [options?.bindingNames?.d1 ?? "DB"];
1841
- if (bindings.needsKV) manifest.bindings.kv = [options?.bindingNames?.kv ?? "KV"];
1842
- if (bindings.needsR2) manifest.bindings.r2 = [options?.bindingNames?.r2 ?? "STORAGE"];
1843
- if (bindings.needsAI) manifest.bindings.ai = true;
1844
- if (bindings.needsSandbox) {
1845
- if (!options?.sandbox) throw new Error("deploy: Sandbox bindings require sandbox package options.");
1846
- manifest.sandbox = toSandboxManifest({
1847
- ...options.sandbox,
1848
- binding: options.bindingNames?.sandbox ?? options.sandbox.binding
1849
- });
1850
- }
1851
- if (bindings.needsLive) manifest.live = toLiveManifest(options?.live);
1852
- if (migrations && migrations.length > 0) {
1853
- const planBundle = dialect === "postgresql" ? void 0 : buildMigrationPlanBundle(migrations);
1854
- manifest.migrations = {
1855
- dialect: dialect ?? "sqlite",
1856
- ...planBundle ? {
1857
- classifierVersion: planBundle.classifierVersion,
1858
- migrationListHash: planBundle.migrationListHash,
1859
- protocolVersion: planBundle.protocolVersion,
1860
- stability: planBundle.plans.some((plan) => plan.stability === "legacy") ? "legacy" : "stable"
1861
- } : {},
1862
- pending: migrations.length
1863
- };
1864
- }
1865
- if (schedules && schedules.length > 0) manifest.schedules = schedules;
1866
- if (queues && queues.length > 0) manifest.queues = queues;
1867
- if (ssr) manifest.ssr = true;
1868
- if (framework) manifest.framework = framework;
1869
- manifest.type = framework ? "framework" : "void";
1870
- if (revalidate != null && revalidate !== 0) manifest.revalidate = revalidate;
1871
- if (options?.revalidateQueryAllowlist && Object.keys(options.revalidateQueryAllowlist).length > 0) manifest.revalidateQueryAllowlist = options.revalidateQueryAllowlist;
1872
- if (prerender && prerender.length > 0) manifest.prerender = prerender;
1873
- let resolvedAssetConfig = assetConfig;
1874
- let resolvedFallbackRules = fallbackRules;
1875
- if (assetConfig?.not_found_handling === "single-page-application") {
1876
- const chain = resolveSpaFallbackChain("spa", fallbackRules);
1877
- resolvedAssetConfig = {
1878
- ...assetConfig,
1879
- ...chain.assetConfig
1880
- };
1881
- resolvedFallbackRules = chain.fallbackRules;
1882
- }
1883
- if (resolvedAssetConfig) manifest.assetConfig = resolvedAssetConfig;
1884
- if (vars && Object.keys(vars).length > 0) manifest.vars = vars;
1885
- if (options?.workerMain) manifest.workerMain = options.workerMain;
1886
- if (options?.compatibilityDate) manifest.compatibilityDate = options.compatibilityDate;
1887
- if (options?.compatibilityFlags && options.compatibilityFlags.length > 0) manifest.compatibilityFlags = options.compatibilityFlags;
1888
- if (hashedAssetsPrefix) manifest.hashedAssetsPrefix = hashedAssetsPrefix;
1889
- if (headerRules && headerRules.length > 0) manifest.headerRules = headerRules;
1890
- if (redirectRules && redirectRules.length > 0) manifest.redirectRules = stripDevOnlyRuleFields(redirectRules);
1891
- if (resolvedFallbackRules && resolvedFallbackRules.length > 0) manifest.fallbackRules = stripDevOnlyRuleFields(resolvedFallbackRules);
1892
- const websocketManifest = toWebSocketManifest(webSockets);
1893
- if (websocketManifest) manifest.websocket = websocketManifest;
1894
- return manifest;
1895
- }
1896
- /**
1897
- * Read every worker module under `workerDir` once and return them as a map
1898
- * keyed by relative path. Skipped files (build metadata, wrangler config,
1899
- * non-module extensions) are filtered out by `isWorkerModule`. Reads each
1900
- * file from disk exactly once — buffer threading downstream is the caller's
1901
- * responsibility.
1902
- */
1903
- function collectWorkerFiles(workerDir) {
1904
- const out = /* @__PURE__ */ new Map();
1905
- let workerPaths;
1906
- try {
1907
- workerPaths = collectFiles(workerDir);
1908
- } catch (err) {
1909
- if (err instanceof Error && "code" in err && err.code === "ENOENT") return out;
1910
- throw err;
1911
- }
1912
- for (const filePath of workerPaths) {
1913
- const relativePath = toSlash(relative(workerDir, filePath));
1914
- if (!isWorkerModule(relativePath)) continue;
1915
- out.set(relativePath, readFileSync(filePath));
1916
- }
1917
- return out;
1918
- }
1919
- /**
1920
- * Build a static-deploy manifest. Pure shape transformation — no I/O.
1921
- */
1922
- function buildStaticManifest(opts) {
1923
- const { appType, hashedAssetsPrefix, headerRules, redirectRules, fallbackRules } = opts;
1924
- const { assetConfig, fallbackRules: resolvedFallbackRules } = resolveSpaFallbackChain(appType, fallbackRules);
1925
- const manifest = {
1926
- version: 3,
1927
- assetManifestVersion: 2,
1928
- bindings: {},
1929
- type: appType,
1930
- assetConfig
1931
- };
1932
- if (hashedAssetsPrefix) manifest.hashedAssetsPrefix = hashedAssetsPrefix;
1933
- if (headerRules && headerRules.length > 0) manifest.headerRules = headerRules;
1934
- if (redirectRules && redirectRules.length > 0) manifest.redirectRules = stripDevOnlyRuleFields(redirectRules);
1935
- if (resolvedFallbackRules && resolvedFallbackRules.length > 0) manifest.fallbackRules = stripDevOnlyRuleFields(resolvedFallbackRules);
1936
- return manifest;
1937
- }
1938
- /**
1939
- * Build a framework-deploy manifest. Pure shape transformation — no I/O.
1940
- */
1941
- function buildFrameworkManifest(opts) {
1942
- const po = opts.packageOptions;
1943
- const manifest = {
1944
- version: 3,
1945
- assetManifestVersion: 2,
1946
- bindings: {}
1947
- };
1948
- const hasMigrations = opts.migrations && opts.migrations.length > 0;
1949
- if (opts.bindings.needsD1 || opts.bindings.needsAuth || hasMigrations) if (opts.dialect === "postgresql") manifest.bindings.hyperdrive = ["HYPERDRIVE"];
1950
- else manifest.bindings.d1 = [po?.bindingNames?.d1 ?? "DB"];
1951
- if (opts.bindings.needsKV) manifest.bindings.kv = [po?.bindingNames?.kv ?? "KV"];
1952
- if (opts.bindings.needsR2) manifest.bindings.r2 = [po?.bindingNames?.r2 ?? "STORAGE"];
1953
- if (opts.bindings.needsAI) manifest.bindings.ai = true;
1954
- if (opts.bindings.needsLive) throw new Error(FRAMEWORK_LIVE_UNSUPPORTED_MESSAGE);
1955
- if (opts.migrations && opts.migrations.length > 0) {
1956
- const planBundle = opts.dialect === "postgresql" ? void 0 : buildMigrationPlanBundle(opts.migrations);
1957
- manifest.migrations = {
1958
- dialect: opts.dialect ?? "sqlite",
1959
- ...planBundle ? {
1960
- classifierVersion: planBundle.classifierVersion,
1961
- migrationListHash: planBundle.migrationListHash,
1962
- protocolVersion: planBundle.protocolVersion,
1963
- stability: planBundle.plans.some((plan) => plan.stability === "legacy") ? "legacy" : "stable"
1964
- } : {},
1965
- pending: opts.migrations.length
1966
- };
1967
- }
1968
- if (opts.schedules && opts.schedules.length > 0) manifest.schedules = opts.schedules;
1969
- if (opts.queues && opts.queues.length > 0) manifest.queues = opts.queues;
1970
- if (opts.revalidate != null && opts.revalidate !== 0) manifest.revalidate = opts.revalidate;
1971
- if (opts.revalidateQueryAllowlist && Object.keys(opts.revalidateQueryAllowlist).length > 0) manifest.revalidateQueryAllowlist = opts.revalidateQueryAllowlist;
1972
- if (opts.prerender && opts.prerender.length > 0) manifest.prerender = opts.prerender;
1973
- if (opts.assetConfig) manifest.assetConfig = opts.assetConfig;
1974
- if (opts.vars && Object.keys(opts.vars).length > 0) manifest.vars = opts.vars;
1975
- manifest.ssr = true;
1976
- manifest.framework = opts.frameworkName;
1977
- manifest.type = "framework";
1978
- manifest.workerMain = po?.workerMain ?? opts.workerMain;
1979
- if (po?.compatibilityDate) manifest.compatibilityDate = po.compatibilityDate;
1980
- if (po?.compatibilityFlags && po.compatibilityFlags.length > 0) manifest.compatibilityFlags = po.compatibilityFlags;
1981
- if (opts.hashedAssetsPrefix) manifest.hashedAssetsPrefix = opts.hashedAssetsPrefix;
1982
- if (opts.headerRules && opts.headerRules.length > 0) manifest.headerRules = opts.headerRules;
1983
- if (opts.redirectRules && opts.redirectRules.length > 0) manifest.redirectRules = stripDevOnlyRuleFields(opts.redirectRules);
1984
- if (opts.fallbackRules && opts.fallbackRules.length > 0) manifest.fallbackRules = stripDevOnlyRuleFields(opts.fallbackRules);
1985
- const websocketManifest = toWebSocketManifest(opts.webSockets);
1986
- if (websocketManifest) manifest.websocket = websocketManifest;
1987
- return manifest;
1988
- }
1989
- /**
1990
- * Check if a file (by relative path) should be included as a worker module.
1991
- * Skips build metadata and files with extensions CF Workers can't handle as modules.
1992
- */
1993
- function isWorkerModule(relativePath) {
1994
- const norm = relativePath.replace(/\\/g, "/");
1995
- if (norm.includes("/.vite/") || norm.startsWith(".vite/")) return false;
1996
- if (norm === "wrangler.json" || norm === "wrangler.jsonc") return false;
1997
- return /\.(js|mjs|cjs|wasm|map|txt)$/.test(norm);
1998
- }
1999
- /**
2000
- * Recursively collect all file paths in a directory.
2001
- */
2002
- function collectFiles(dir) {
2003
- const files = [];
2004
- const entries = readdirSync(dir, { withFileTypes: true });
2005
- for (const entry of entries) {
2006
- const fullPath = join(dir, entry.name);
2007
- if (entry.isDirectory()) files.push(...collectFiles(fullPath));
2008
- else files.push(fullPath);
2009
- }
2010
- return files;
2011
- }
2012
- /**
2013
- * Recursively collect static asset file paths, refusing symlinks so deploys
2014
- * cannot publish bytes from outside the intended asset root.
2015
- */
2016
- function collectAssetFiles(root, isIgnored, dir = root) {
2017
- const files = [];
2018
- const entries = readdirSync(dir, { withFileTypes: true });
2019
- for (const entry of entries) {
2020
- const fullPath = join(dir, entry.name);
2021
- const relativePath = "/" + toSlash(relative(root, fullPath));
2022
- if (isIgnored(relativePath)) continue;
2023
- if (entry.isSymbolicLink()) throw new Error(`deploy: Refusing to upload symlink as an asset: ${relativePath}`);
2024
- if (entry.isDirectory()) files.push(...collectAssetFiles(root, isIgnored, fullPath));
2025
- else files.push(fullPath);
2026
- }
2027
- return files;
2028
- }
2029
- //#endregion
2030
- //#region src/cli/wrapper.ts
2031
- const BUNDLED_ENTRY_RE = /^index\.(?:m?js|cjs)$/;
2032
- function selectBundledEntryFilename(outFiles) {
2033
- const entryFilename = outFiles.find((file) => BUNDLED_ENTRY_RE.test(file));
2034
- if (!entryFilename) throw new Error(`deploy: Bundled worker entry not found. Expected index.js, index.mjs, or index.cjs in bundle output. Found: ${outFiles.join(", ") || "(none)"}`);
2035
- return entryFilename;
2036
- }
2037
- /**
2038
- * Determine whether a wrapper is needed based on the features.
2039
- */
2040
- function needsWrapper(features) {
2041
- return features.crons.length > 0 || features.queues.length > 0 || !!features.hasMigrations;
2042
- }
2043
- /**
2044
- * Generate a wrapper entry in-place in the worker directory.
2045
- *
2046
- * - Renames the original entry (e.g. `index.js` → `__original.js`)
2047
- * - Compiles cron/queue handlers via a small Vite build
2048
- * - Writes a wrapper module as the original entry name that imports
2049
- * the renamed original + compiled handlers
2050
- *
2051
- * The original worker code is never re-bundled.
2052
- */
2053
- async function generateWrapper(workerDir, workerMain, root, features) {
2054
- const renamedName = `__original${extname(workerMain) || ".js"}`;
2055
- renameSync(join(workerDir, workerMain), join(workerDir, renamedName));
2056
- let handlerFilename = "__void_handlers.mjs";
2057
- if (features.crons.length > 0 || features.queues.length > 0) {
2058
- const tmpDir = join(root, ".void", "handlers");
2059
- mkdirSync(tmpDir, { recursive: true });
2060
- const handlersSource = generateHandlersSource(root, features);
2061
- const handlersEntry = join(tmpDir, "handlers.ts");
2062
- writeFileSync(handlersEntry, handlersSource);
2063
- const outDir = join(tmpDir, "out");
2064
- await build({
2065
- root: tmpDir,
2066
- logLevel: "silent",
2067
- build: {
2068
- outDir,
2069
- emptyOutDir: true,
2070
- lib: {
2071
- entry: handlersEntry,
2072
- formats: ["es"],
2073
- fileName: "__void_handlers"
2074
- },
2075
- rollupOptions: { external: [/^cloudflare:/, /^node:/] },
2076
- minify: false,
2077
- target: "esnext"
2078
- }
2079
- });
2080
- handlerFilename = readdirSync(outDir).filter((f) => f.startsWith("__void_handlers"))[0] ?? "__void_handlers.mjs";
2081
- copyFileSync(join(outDir, handlerFilename), join(workerDir, handlerFilename));
2082
- }
2083
- let migrations;
2084
- let migrationPlanBundle;
2085
- if (features.hasMigrations) {
2086
- const { collectMigrations } = await import("./validate-DT7nFMlf.mjs").then((n) => n.l);
2087
- const { buildMigrationPlanBundle } = await import("./validate-DT7nFMlf.mjs").then((n) => n.s);
2088
- const migrationsDir = readProjectPaths(root).migrationsDir;
2089
- migrations = collectMigrations(migrationsDir);
2090
- migrationPlanBundle = features.migrationDialect === "sqlite" ? buildMigrationPlanBundle(migrations) : void 0;
2091
- const handlerName = features.migrationDialect === "postgresql" ? "migration-handler-pg" : "migration-handler";
2092
- const handlerFilename_ = `${handlerName}.mjs`;
2093
- let handlerPath = join(import.meta.dirname, "..", "runtime", `${handlerName}.ts`);
2094
- if (!existsSync(handlerPath)) handlerPath = join(import.meta.dirname, "runtime", handlerFilename_);
2095
- if (!existsSync(handlerPath)) handlerPath = join(import.meta.dirname, "..", "runtime", handlerFilename_);
2096
- if (!existsSync(handlerPath)) handlerPath = join(import.meta.dirname, "..", "..", "dist", "runtime", handlerFilename_);
2097
- if (!existsSync(handlerPath)) throw new Error(`Migration handler not found: ${handlerFilename_}. Run "pnpm -C packages/void build" first.`);
2098
- await bundleMigrationHandler(root, workerDir, handlerPath, features.migrationDialect ?? "sqlite");
2099
- }
2100
- const wrapperSource = generateWrapperSource(renamedName, handlerFilename, features, migrations, migrationPlanBundle);
2101
- writeFileSync(join(workerDir, workerMain), wrapperSource);
2102
- }
2103
- async function bundleMigrationHandler(root, workerDir, handlerPath, dialect) {
2104
- const tmpDir = join(root, ".void", "migration-handler");
2105
- mkdirSync(tmpDir, { recursive: true });
2106
- const outDir = join(tmpDir, "out");
2107
- const isPostgresql = dialect === "postgresql";
2108
- await build({
2109
- root: tmpDir,
2110
- logLevel: "silent",
2111
- resolve: isPostgresql ? {
2112
- alias: {
2113
- "pg-cloudflare": resolvePgCloudflareWorkerdEntry(handlerPath),
2114
- "pg-pool": resolvePgCjsDependency(handlerPath, "pg-pool")
2115
- },
2116
- conditions: [
2117
- "workerd",
2118
- "worker",
2119
- "browser",
2120
- "module",
2121
- "import",
2122
- "default"
2123
- ]
2124
- } : void 0,
2125
- ssr: isPostgresql ? {
2126
- noExternal: ["pg", "pg-cloudflare"],
2127
- resolve: { conditions: [
2128
- "workerd",
2129
- "worker",
2130
- "browser",
2131
- "module",
2132
- "import",
2133
- "default"
2134
- ] }
2135
- } : void 0,
2136
- build: {
2137
- outDir,
2138
- emptyOutDir: true,
2139
- minify: false,
2140
- ssr: handlerPath,
2141
- target: "esnext",
2142
- rollupOptions: {
2143
- input: handlerPath,
2144
- external: [
2145
- /^cloudflare:/,
2146
- /^node:/,
2147
- /^pgpass$/
2148
- ],
2149
- output: {
2150
- entryFileNames: "__void_migration_handler.mjs",
2151
- chunkFileNames: "__void_migration_handler-[hash].mjs"
2152
- }
2153
- }
2154
- }
2155
- });
2156
- if (isPostgresql) patchPgWorkerBundle(outDir);
2157
- for (const file of readdirSync(outDir)) copyFileSync(join(outDir, file), join(workerDir, file));
2158
- }
2159
- function patchPgWorkerBundle(outDir) {
2160
- for (const file of readdirSync(outDir)) {
2161
- if (!file.endsWith(".mjs") && !file.endsWith(".js")) continue;
2162
- const path = join(outDir, file);
2163
- let code = readFileSync(path, "utf-8");
2164
- if (!code.includes("__require(\"pgpass\")") && !code.includes("createRequire(import.meta.url)")) continue;
2165
- code = code.replaceAll("__require(\"pgpass\")", "__voidPgpassNoop").replace("import { createRequire } from \"node:module\";", "").replace("var __require = /* @__PURE__ */ createRequire(import.meta.url);", pgWorkerRequireShim()).replace("var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();", pgWorkerRequireShim());
2166
- code = `const __voidPgpassNoop = Object.assign(function pgpass(_connectionInfo, callback) { callback(undefined); }, { warnTo() {} });\n` + code;
2167
- writeFileSync(path, code);
2168
- }
2169
- }
2170
- function createPgPackageRequire(fromPath) {
2171
- return createRequire(createRequire(fromPath).resolve("pg/package.json"));
2172
- }
2173
- function resolvePgCjsDependency(fromPath, specifier) {
2174
- return createPgPackageRequire(fromPath).resolve(specifier);
2175
- }
2176
- function resolvePgCloudflareWorkerdEntry(fromPath) {
2177
- return join(dirname(createPgPackageRequire(fromPath).resolve("pg-cloudflare/package.json")), "dist", "index.js");
2178
- }
2179
- function pgWorkerRequireShim() {
2180
- return [
2181
- "import * as __voidNodeCrypto from \"node:crypto\";",
2182
- "import * as __voidNodeDns from \"node:dns\";",
2183
- "import * as __voidNodeEvents from \"node:events\";",
2184
- "import * as __voidNodeFs from \"node:fs\";",
2185
- "import * as __voidNodeNet from \"node:net\";",
2186
- "import * as __voidNodeTls from \"node:tls\";",
2187
- "import * as __voidNodeUtil from \"node:util\";",
2188
- "import * as __voidNodeUtilTypes from \"node:util/types\";",
2189
- "var __voidNodeRequireModules = {",
2190
- " crypto: __voidNodeCrypto,",
2191
- " dns: __voidNodeDns,",
2192
- " events: __voidNodeEvents,",
2193
- " fs: __voidNodeFs,",
2194
- " net: __voidNodeNet,",
2195
- " tls: __voidNodeTls,",
2196
- " util: __voidNodeUtil,",
2197
- " \"util/types\": __voidNodeUtilTypes,",
2198
- "};",
2199
- "var __require = (id) => {",
2200
- " if (id.startsWith(\"node:\")) id = id.slice(5);",
2201
- " const mod = __voidNodeRequireModules[id];",
2202
- " if (mod) return mod;",
2203
- " throw new Error(`Unsupported require(\"${id}\") in Void PG migration bundle`);",
2204
- "};"
2205
- ].join("\n");
2206
- }
2207
- /**
2208
- * Bundle a worker entry into a self-contained module.
2209
- *
2210
- * Used for frameworks whose build output has unbundled bare imports
2211
- * (e.g. SvelteKit's `_worker.js` imports from `@sveltejs/kit`).
2212
- */
2213
- async function bundleEntry(root, entry) {
2214
- const tmpDir = join(root, ".void", "bundle");
2215
- mkdirSync(tmpDir, { recursive: true });
2216
- const outDir = join(tmpDir, "out");
2217
- await build({
2218
- root: tmpDir,
2219
- logLevel: "silent",
2220
- build: {
2221
- outDir,
2222
- emptyOutDir: true,
2223
- lib: {
2224
- entry,
2225
- formats: ["es"],
2226
- fileName: "index"
2227
- },
2228
- rollupOptions: {
2229
- external: [/^cloudflare:/, /^node:/],
2230
- output: { banner: [
2231
- "const __noopCache = {",
2232
- " match() { return Promise.resolve(undefined); },",
2233
- " put() { return Promise.resolve(); },",
2234
- " delete() { return Promise.resolve(false); },",
2235
- "};",
2236
- "globalThis.caches = { default: __noopCache, open() { return Promise.resolve(__noopCache); } };"
2237
- ].join("\n") }
2238
- },
2239
- minify: false,
2240
- target: "esnext"
2241
- }
2242
- });
2243
- return {
2244
- entryFilename: selectBundledEntryFilename(readdirSync(outDir)),
2245
- outputDir: outDir
2246
- };
2247
- }
2248
- /**
2249
- * Clean up generated wrapper/bundle files. Preserves `dev-trigger-token`
2250
- * across the wipe so that subsequent `void dev` sessions keep using the
2251
- * same UUID — invalidating it on every deploy would force users to update
2252
- * any saved curl scripts that exercise `/__void/{scheduled,queue}`.
2253
- */
2254
- function cleanupWrapper(root) {
2255
- const voidDir = join(root, ".void");
2256
- const tokenPath = join(voidDir, "dev-trigger-token");
2257
- let preservedToken;
2258
- if (existsSync(tokenPath)) try {
2259
- preservedToken = readFileSync(tokenPath, "utf-8");
2260
- } catch {}
2261
- rmSync(voidDir, {
2262
- recursive: true,
2263
- force: true
2264
- });
2265
- if (preservedToken !== void 0) {
2266
- mkdirSync(voidDir, { recursive: true });
2267
- writeFileSync(tokenPath, preservedToken);
2268
- }
2269
- }
2270
- function generateWrapperSource(renamedEntry, handlersModule, features, migrations, migrationPlanBundle) {
2271
- const lines = [];
2272
- lines.push(`import original from "./${renamedEntry}";`);
2273
- if (features.crons.length > 0) lines.push(`import { handleScheduled } from "./${handlersModule}";`);
2274
- if (features.queues.length > 0) lines.push(`import { handleQueue } from "./${handlersModule}";`);
2275
- if (migrations && migrations.length > 0) {
2276
- lines.push(`import { createMigrationHandler } from "./__void_migration_handler.mjs";`);
2277
- lines.push(`const __migrations = ${JSON.stringify(migrations)};`);
2278
- lines.push(`const __migrationPlanBundle = ${JSON.stringify(migrationPlanBundle)};`);
2279
- lines.push(`const __migrationDialect = ${JSON.stringify(features.migrationDialect ?? "sqlite")};`);
2280
- }
2281
- lines.push("");
2282
- const needsFetchWrap = features.crons.length > 0 || migrations && migrations.length > 0;
2283
- if (needsFetchWrap) {
2284
- lines.push("async function wrappedFetch(request, env, ctx) {");
2285
- lines.push(" const url = new URL(request.url);");
2286
- lines.push(" if (request.method === \"GET\" && url.pathname === \"/__void/ready\") {");
2287
- lines.push(" return Response.json({ ready: true });");
2288
- lines.push(" }");
2289
- if (migrations && migrations.length > 0) {
2290
- lines.push(" if (request.method === \"POST\" && url.pathname === \"/__void/migrate\") {");
2291
- lines.push(" const handler = createMigrationHandler(env, __migrations, __migrationDialect);");
2292
- lines.push(" return handler(request);");
2293
- lines.push(" }");
2294
- lines.push(" if (request.method === \"GET\" && url.pathname === \"/__void/migrate/capabilities\") {");
2295
- lines.push(" const handler = createMigrationHandler(env, __migrations, __migrationDialect, undefined, __migrationPlanBundle);");
2296
- lines.push(" return handler(request);");
2297
- lines.push(" }");
2298
- lines.push(" if (request.method === \"POST\" && url.pathname === \"/__void/migrate/app\") {");
2299
- lines.push(" const handler = createMigrationHandler(env, __migrations, __migrationDialect, undefined, __migrationPlanBundle);");
2300
- lines.push(" return handler(request);");
2301
- lines.push(" }");
2302
- lines.push(" if (request.method === \"GET\" && url.pathname.startsWith(\"/__void/migrate/app/attempt/\")) {");
2303
- lines.push(" const handler = createMigrationHandler(env, __migrations, __migrationDialect, undefined, __migrationPlanBundle);");
2304
- lines.push(" return handler(request);");
2305
- lines.push(" }");
2306
- }
2307
- if (features.crons.length > 0) {
2308
- lines.push(" if (request.method === \"POST\" && url.pathname === \"/__void/scheduled\") {");
2309
- lines.push(" const __token = request.headers.get(\"x-void-internal\");");
2310
- lines.push(" const __expected = env?.__VOID_PROXY_TOKEN || env?.CRON_SECRET;");
2311
- lines.push(" if (__expected && __token !== __expected) {");
2312
- lines.push(" return new Response(JSON.stringify({ error: 'unauthorized' }), {");
2313
- lines.push(" status: 401,");
2314
- lines.push(" headers: { \"Content-Type\": \"application/json\" },");
2315
- lines.push(" });");
2316
- lines.push(" }");
2317
- lines.push(" try {");
2318
- lines.push(" const { cron, scheduledTime } = await request.json();");
2319
- lines.push(" const controller = { cron, scheduledTime, noRetry() {} };");
2320
- lines.push(" await handleScheduled(controller, env, ctx);");
2321
- lines.push(" return new Response(JSON.stringify({ ok: true }), {");
2322
- lines.push(" headers: { \"Content-Type\": \"application/json\" },");
2323
- lines.push(" });");
2324
- lines.push(" } catch (err) {");
2325
- lines.push(" return new Response(JSON.stringify({ error: String(err) }), {");
2326
- lines.push(" status: 500,");
2327
- lines.push(" headers: { \"Content-Type\": \"application/json\" },");
2328
- lines.push(" });");
2329
- lines.push(" }");
2330
- lines.push(" }");
2331
- }
2332
- lines.push(" return original.fetch(request, env, ctx);");
2333
- lines.push("}");
2334
- lines.push("");
2335
- }
2336
- lines.push("export default {");
2337
- if (needsFetchWrap) lines.push(" fetch: wrappedFetch,");
2338
- else lines.push(" fetch: original.fetch,");
2339
- if (features.crons.length > 0) lines.push(" scheduled: handleScheduled,");
2340
- if (features.queues.length > 0) lines.push(" queue: handleQueue,");
2341
- lines.push("};");
2342
- return lines.join("\n");
2343
- }
2344
- function generateHandlersSource(root, features) {
2345
- const lines = [];
2346
- const paths = readProjectPaths(root);
2347
- if (features.crons.length > 0) {
2348
- for (let i = 0; i < features.crons.length; i++) {
2349
- const absPath = join(paths.cronsDir, features.crons[i].filePath);
2350
- lines.push(`import cronHandler${i} from "${absPath}";`);
2351
- }
2352
- lines.push("");
2353
- lines.push("export async function handleScheduled(controller, env, ctx) {");
2354
- lines.push(" switch (controller.cron) {");
2355
- for (let i = 0; i < features.crons.length; i++) {
2356
- for (const expr of features.crons[i].crons) lines.push(` case ${JSON.stringify(expr)}:`);
2357
- lines.push(` if (typeof cronHandler${i} === "function") {`);
2358
- lines.push(` await cronHandler${i}(controller, env, ctx);`);
2359
- lines.push(` } else if (cronHandler${i} && typeof cronHandler${i}.default === "function") {`);
2360
- lines.push(` await cronHandler${i}.default(controller, env, ctx);`);
2361
- lines.push(` }`);
2362
- lines.push(` return;`);
2363
- }
2364
- lines.push(" }");
2365
- lines.push("}");
2366
- }
2367
- if (features.queues.length > 0) {
2368
- lines.push("");
2369
- for (let i = 0; i < features.queues.length; i++) {
2370
- const absPath = join(paths.queuesDir, features.queues[i].filePath);
2371
- lines.push(`import queueHandler${i} from "${absPath}";`);
2372
- }
2373
- lines.push("");
2374
- lines.push("export async function handleQueue(batch, env, ctx) {");
2375
- lines.push(" const queueSuffix = batch.queue;");
2376
- lines.push(" const messages = [...batch.messages].map(m => ({");
2377
- lines.push(" id: m.id,");
2378
- lines.push(" timestamp: m.timestamp.toISOString(),");
2379
- lines.push(" body: m.body,");
2380
- lines.push(" attempts: m.attempts,");
2381
- lines.push(" }));");
2382
- lines.push(" const payload = { queue: queueSuffix, messages };");
2383
- for (let i = 0; i < features.queues.length; i++) {
2384
- const name = features.queues[i].name;
2385
- lines.push("");
2386
- lines.push(` if (queueSuffix === ${JSON.stringify(name)} || queueSuffix.endsWith("-" + ${JSON.stringify(name)})) {`);
2387
- lines.push(` if (typeof queueHandler${i} === "function") {`);
2388
- lines.push(` await queueHandler${i}(payload, env);`);
2389
- lines.push(` } else if (queueHandler${i} && typeof queueHandler${i}.default === "function") {`);
2390
- lines.push(` await queueHandler${i}.default(payload, env);`);
2391
- lines.push(` }`);
2392
- lines.push(` return;`);
2393
- lines.push(` }`);
2394
- }
2395
- lines.push("}");
2396
- }
2397
- return lines.join("\n");
2398
- }
2399
- //#endregion
2400
- //#region src/cli/deploy.ts
2401
- var deploy_exports = /* @__PURE__ */ __exportAll({
2402
- applyDeployEvent: () => applyDeployEvent,
2403
- buildEnv: () => buildEnv,
2404
- ensurePreviouslyDeployedLiveBindingConfig: () => ensurePreviouslyDeployedLiveBindingConfig,
2405
- generateDeployWrapperIfNeeded: () => generateDeployWrapperIfNeeded,
2406
- hasDeploySchemaDrift: () => hasDeploySchemaDrift,
2407
- loadDeployEnv: () => loadDeployEnv,
2408
- promptDeployProjectConfig: () => promptDeployProjectConfig,
2409
- resolveDeployLiveBindingConfig: () => resolveDeployLiveBindingConfig,
2410
- resolveDeployWorkerMain: () => resolveDeployWorkerMain,
2411
- resolveDeployWranglerCompat: () => resolveDeployWranglerCompat,
2412
- resolveDistDir: () => resolveDistDir,
2413
- resolveStaticBuildCommand: () => resolveStaticBuildCommand,
2414
- resolveStaticDeployPreset: () => resolveStaticDeployPreset,
2415
- resolveWorkerDirName: () => resolveWorkerDirName,
2416
- runDeploy: () => runDeploy,
2417
- runStaticDeploy: () => runStaticDeploy,
2418
- streamDeployEvents: () => streamDeployEvents,
2419
- warnRedundantForceOn3xx: () => warnRedundantForceOn3xx,
2420
- warnUnpublishedRouting: () => warnUnpublishedRouting
2421
- });
2422
- const drizzleKitBin = join(fileURLToPath(import.meta.resolve("drizzle-kit")), "..", "bin.cjs");
2423
- const deployDriftCheckDir = join(".void", "deploy-drift-check");
2424
- var DeployEventError = class extends Error {};
2425
- async function generateDeployWrapperIfNeeded(root, workerDir, workerMain, features) {
2426
- if (!needsWrapper(features)) return false;
2427
- await generateWrapper(workerDir, workerMain, root, features);
2428
- return true;
2429
- }
2430
- const EMPTY_BINDINGS = {
2431
- needsD1: false,
2432
- needsKV: false,
2433
- needsR2: false,
2434
- needsAuth: false,
2435
- needsQueues: false,
2436
- needsAI: false,
2437
- needsSandbox: false
2438
- };
2439
- function getErrorMessage(error) {
2440
- return error instanceof Error ? error.message : String(error);
2441
- }
2442
- function readLiveBindingConfigFromDeployment(deployment) {
2443
- if (!deployment?.manifest) return;
2444
- let manifest;
2445
- try {
2446
- manifest = JSON.parse(deployment.manifest);
2447
- } catch {
2448
- return;
2449
- }
2450
- if (typeof manifest.live?.bindingName !== "string" || typeof manifest.live.className !== "string") return;
2451
- return {
2452
- bindingName: manifest.live.bindingName,
2453
- className: manifest.live.className
2454
- };
2455
- }
2456
- async function ensurePreviouslyDeployedLiveBindingConfig(root, client, projectId, cliLog) {
2457
- if (readExistingLiveBindingConfig(root)) return false;
2458
- const { deployments, activeDeploymentId } = await client.listDeployments(projectId);
2459
- const live = readLiveBindingConfigFromDeployment(deployments.find((deployment) => deployment.id === activeDeploymentId));
2460
- if (!live) return false;
2461
- const synced = syncWranglerBindings(root, {
2462
- ...EMPTY_BINDINGS,
2463
- needsLive: true
2464
- }, { live });
2465
- if (synced) cliLog?.info("live_binding_restored", {
2466
- bindingName: live.bindingName,
2467
- className: live.className
2468
- });
2469
- return synced;
2470
- }
2471
- async function resolveDeployLiveBindingConfig(root, client, projectId, opts) {
2472
- if (opts.isFrameworkMode || !opts.needsLive) return;
2473
- await ensurePreviouslyDeployedLiveBindingConfig(root, client, projectId, opts.cliLog);
2474
- return resolveLiveBindingConfig(root, {
2475
- reservedBindings: opts.webSockets.map((route) => route.bindingName),
2476
- reservedClasses: [...opts.webSockets.map((route) => route.className), opts.sandboxClassName]
2477
- });
2478
- }
2479
- function getExitCode(error) {
2480
- if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return error.status;
2481
- return 1;
2482
- }
2483
- function resolveDeployWranglerCompat(root, config, buildOutputDir, requirements) {
2484
- if (isNodeTarget(config.target)) return;
2485
- const compat = ensureWranglerCompat(root, config, buildOutputDir);
2486
- const compatibilityFlags = normalizeWranglerCompatibilityFlags(compat.compatibilityFlags, { needsNodejsCompat: requirements?.authEnabled === true || requirements?.dialect === "postgresql" || requirements?.needsSandbox === true });
2487
- return compatibilityFlags === compat.compatibilityFlags ? compat : {
2488
- ...compat,
2489
- compatibilityFlags
2490
- };
2491
- }
2492
- function resolveDeployWorkerMain(config, buildOutputDir) {
2493
- if (isNodeTarget(config.target)) return;
2494
- try {
2495
- const wrangler = JSON.parse(readFileSync(join(buildOutputDir, "wrangler.json"), "utf-8"));
2496
- return typeof wrangler.main === "string" && wrangler.main.length > 0 ? wrangler.main : void 0;
2497
- } catch {
2498
- return;
2499
- }
2500
- }
2501
- function assertWorkerMainUploaded(workerFiles, workerMain) {
2502
- if (!workerMain || workerFiles.size === 0) return;
2503
- if (!workerFiles.has(workerMain)) throw new Error(`deploy: Worker entry '${workerMain}' from wrangler.json was not found in uploaded worker modules.`);
2504
- }
2505
- /**
2506
- * Run the full deploy pipeline:
2507
- * 1. Check auth
2508
- * 2. Resolve project + deploy mode
2509
- * 3. Build (if needed)
2510
- * 4. Package output
2511
- * 5. Upload to platform
2512
- * 6. Print live URL
2513
- */
2514
- async function runDeploy(root, options) {
2515
- console.log();
2516
- intro(cliTitle("deploy"));
2517
- const cliLog = openDeployLog({ mirrorToStderr: options?.debug === true || process.env.VOID_DEPLOY_DEBUG === "1" });
2518
- cliLog.info("deploy_start", {
2519
- cwd: root,
2520
- projectSlug: options?.projectSlug,
2521
- dir: options?.dir,
2522
- spa: options?.spa === true ? true : void 0,
2523
- skipBuild: options?.skipBuild === true ? true : void 0,
2524
- nodeVersion: process.version,
2525
- platform: process.platform,
2526
- arch: process.arch
2527
- });
2528
- try {
2529
- await runDeployInner(root, options, cliLog);
2530
- cliLog.info("deploy_end", { ok: true });
2531
- } catch (err) {
2532
- cliLog.error("deploy_end", err, { ok: false });
2533
- throw err;
2534
- } finally {
2535
- await cliLog.close().catch(() => {});
2536
- }
2537
- }
2538
- /**
2539
- * Build a child-process environment with the CF Access service-token PAIR
2540
- * removed.
2541
- *
2542
- * The deploy flow spawns UNTRUSTED user build code (`vite build`, framework
2543
- * preset builds, drizzle-kit loading the user schema) which inherits the
2544
- * spawn env. A shared, long-lived CF Access service token is needed only by
2545
- * this CLI's own API client (which captures the Access headers once at
2546
- * construction and reuses them for every request) — never by the build — so it
2547
- * must not survive into those children, or it would leak to arbitrary repo
2548
- * build scripts/plugins.
2549
- *
2550
- * We scrub it on the CHILD env rather than mutating `process.env`: deleting it
2551
- * from the parent also breaks interactive expired-token re-auth, which reads
2552
- * `cfAccessHeaders(process.env)` / the cloudflared pair-guard AFTER the build
2553
- * step. Only the service-token pair is scrubbed; the cloudflared
2554
- * `CF_ACCESS_TOKEN` JWT (dev path) is left untouched.
2555
- */
2556
- function buildEnv(extra) {
2557
- const env = {
2558
- ...process.env,
2559
- ...extra
2560
- };
2561
- delete env.CF_ACCESS_CLIENT_ID;
2562
- delete env.CF_ACCESS_CLIENT_SECRET;
2563
- return env;
2564
- }
2565
- async function runDeployInner(root, options, cliLog) {
2566
- let reauthenticated = false;
2567
- while (true) try {
2568
- let token = process.env.VOID_TOKEN ?? null;
2569
- if (!token && isGithubActionsOidcAvailable()) {
2570
- const slug = getRequestedProjectSlug(options) ?? readProjectConfig(root)?.slug;
2571
- if (!slug) throw new Error("deploy: running in GitHub Actions but the project is unknown — pass --project <slug> or link the project.");
2572
- cliLog.info("github_oidc_exchange_start", { slug });
2573
- token = await exchangeGithubOidcForDeployToken(getApiUrl(root), slug);
2574
- maskGithubActionsSecret(token);
2575
- process.env.VOID_TOKEN = token;
2576
- cliLog.info("github_oidc_exchange_success", { slug });
2577
- }
2578
- token ??= getToken(root);
2579
- if (!token) {
2580
- log.error("No auth token found. Run `void auth login` first.");
2581
- process.exit(1);
2582
- }
2583
- const client = new PlatformClient(token, {
2584
- root,
2585
- cliLog
2586
- });
2587
- const requestedProjectSlug = getRequestedProjectSlug(options);
2588
- let config = null;
2589
- if (requestedProjectSlug) {
2590
- config = resolveProjectBySlug(await client.listProjects(), requestedProjectSlug);
2591
- if (!config) if (!process.stdin.isTTY) config = await createProjectFromSlug(root, client, requestedProjectSlug, cliLog);
2592
- else {
2593
- const shouldCreate = await confirm({ message: `Project "${requestedProjectSlug}" does not exist. Create it?` });
2594
- if (isCancel(shouldCreate) || !shouldCreate) {
2595
- log.info("Deploy cancelled.");
2596
- process.exit(0);
2597
- }
2598
- config = await createProjectFromSlug(root, client, requestedProjectSlug, cliLog);
2599
- }
2600
- } else config = readProjectConfig(root);
2601
- if (!config && !process.stdin.isTTY) {
2602
- log.error("No project specified. Set `VOID_PROJECT`, pass `--project <slug>`, or commit `.void/project.json`.");
2603
- process.exit(1);
2604
- }
2605
- if (!config) config = await promptDeployProjectConfig(root, client, cliLog);
2606
- cliLog.info("project_resolved", {
2607
- projectId: config.projectId,
2608
- slug: config.slug
2609
- });
2610
- const voidConfig = readConfig(root);
2611
- cliLog.info("config_resolved", {
2612
- target: voidConfig.target,
2613
- appType: voidConfig.inference?.appType,
2614
- output: voidConfig.output
2615
- });
2616
- let preset = resolveStaticDeployPreset(root, voidConfig, {
2617
- dir: options?.dir,
2618
- spa: options?.spa
2619
- });
2620
- const detected = detectFramework(root);
2621
- if (!preset && detected) {
2622
- cliLog.info("framework_detected", {
2623
- name: detected.name,
2624
- class: detected.class
2625
- });
2626
- const gitMetadata = collectGitDeployMetadata(root, options?.skipBuild ? null : getGitCommit(root));
2627
- const fwPreset = FRAMEWORK_PRESETS[detected.name];
2628
- if (detected.class === "b" || detected.class === "c") {
2629
- if (!fwPreset) {
2630
- log.error(`No deploy preset found for framework "${detected.name}".`);
2631
- process.exit(1);
2632
- }
2633
- await runFrameworkDeploy(root, config, client, detected, fwPreset, voidConfig, options?.skipBuild, gitMetadata, cliLog);
2634
- } else await runFullDeploy(root, config, client, fwPreset, options?.skipBuild, gitMetadata, detected, cliLog);
2635
- return;
2636
- }
2637
- if (!preset && !voidConfig.inference?.appType) preset = detectPreset(root, { spa: options?.spa });
2638
- const gitMetadata = collectGitDeployMetadata(root, options?.skipBuild ? null : getGitCommit(root));
2639
- if (preset) {
2640
- preset = {
2641
- ...preset,
2642
- buildCommand: resolveStaticBuildCommand(preset, voidConfig.inference?.build, Boolean(options?.dir))
2643
- };
2644
- await runStaticDeploy(root, config, client, preset, options?.skipBuild, gitMetadata, void 0, void 0, void 0, void 0, voidConfig.routing, cliLog);
2645
- } else await runFullDeploy(root, config, client, void 0, options?.skipBuild, gitMetadata, void 0, cliLog);
2646
- return;
2647
- } catch (error) {
2648
- if (isExpiredTokenError(error)) {
2649
- if (getTokenSource() === "ci") throw new Error("`VOID_TOKEN` is invalid or expired. Generate a new token and try again.");
2650
- if (!process.stdin.isTTY || reauthenticated) throw new Error("Auth token expired. Run `void auth login` and try again.");
2651
- log.warn("Auth token expired. Re-authenticating...");
2652
- await promptForLoginToken(root);
2653
- log.success("Re-authenticated. Retrying deploy...");
2654
- reauthenticated = true;
2655
- continue;
2656
- }
2657
- throw error;
2658
- }
2659
- }
2660
- async function promptDeployProjectConfig(root, client, cliLog) {
2661
- const projects = await client.listProjects();
2662
- if (await promptProjectSetupAction("Set up a Void project for deploy:", { includeLink: projects.length > 0 }) === "create") return promptAndCreateProject(root, client);
2663
- const result = await promptProjectSelection(projects, "Select a project to deploy to:", root, {
2664
- confirmExisting: true,
2665
- includeCreate: false
2666
- });
2667
- if (result.action === "create") throw new Error("Unexpected project selection result.");
2668
- const config = {
2669
- projectId: result.project.id,
2670
- slug: result.project.slug
2671
- };
2672
- writeProjectConfig(root, config);
2673
- log.step(`Linked to ${import_picocolors.default.blue(config.slug)}`);
2674
- cliLog?.info("project_linked", {
2675
- projectId: config.projectId,
2676
- slug: config.slug
2677
- });
2678
- return config;
2679
- }
2680
- function resolveStaticBuildCommand(preset, buildOverride, hasExplicitDir = false) {
2681
- if (hasExplicitDir) return null;
2682
- return buildOverride ?? preset.buildCommand;
2683
- }
2684
- function resolveStaticDeployPreset(root, voidConfig, options) {
2685
- if (options?.dir) return detectPreset(root, {
2686
- dir: options.dir,
2687
- spa: options.spa
2688
- });
2689
- const appType = voidConfig.inference?.appType;
2690
- if (appType === "static" || appType === "spa") {
2691
- const outputDir = voidConfig.inference?.outputDir;
2692
- const preset = {
2693
- buildCommand: null,
2694
- outputDir: outputDir ? resolve(root, outputDir) : join(root, "dist"),
2695
- appType
2696
- };
2697
- const detectedPreset = detectPreset(root, { spa: options?.spa });
2698
- if (detectedPreset) {
2699
- preset.buildCommand = detectedPreset.buildCommand;
2700
- if (!outputDir) preset.outputDir = detectedPreset.outputDir;
2701
- }
2702
- return preset;
2703
- }
2704
- return null;
2705
- }
2706
- /**
2707
- * Apply the visible side effects of a streamed deploy event (spinner
2708
- * message or log warning). Returns a discriminated result so the caller
2709
- * advances its own state (deploymentId capture, final return, error
2710
- * throw). Pure function of (event, s) so the switch stays unit-testable.
2711
- */
2712
- function applyDeployEvent(event, s) {
2713
- switch (event.event) {
2714
- case "start":
2715
- s.message("Deploying...");
2716
- return {
2717
- kind: "start",
2718
- deploymentId: event.deploymentId
2719
- };
2720
- case "heartbeat": return { kind: "continue" };
2721
- case "asset_upload":
2722
- if (event.new > 0) s.message(`Uploading assets (${event.new} new, ${event.cached} cached)`);
2723
- else s.message(`Uploading assets (${event.total} cached)`);
2724
- return { kind: "continue" };
2725
- case "provisioning":
2726
- s.message(`Provisioning ${event.bindings.join(", ")}`);
2727
- return { kind: "continue" };
2728
- case "worker_readiness":
2729
- s.message("Waiting for worker to become reachable...");
2730
- return { kind: "continue" };
2731
- case "migration_start":
2732
- s.message(`Running ${event.pending} migration${event.pending > 1 ? "s" : ""}...`);
2733
- return { kind: "continue" };
2734
- case "migration":
2735
- s.message(`Applied migration: ${event.name} (${event.durationMs}ms)`);
2736
- return { kind: "continue" };
2737
- case "worker_upload":
2738
- s.message("Uploading worker...");
2739
- return { kind: "continue" };
2740
- case "queue_setup":
2741
- s.message(`Configuring queue consumers: ${event.queues.join(", ")}`);
2742
- return { kind: "continue" };
2743
- case "queue_consumer_conflict":
2744
- log.warn(`Queue consumer conflict on "${event.queue}": owned by "${event.ownerScript}". Messages for this queue will not be delivered to this deployment until the owning script is removed.`);
2745
- return { kind: "continue" };
2746
- case "prerender":
2747
- s.message(`Prerendering ${event.pages} pages...`);
2748
- return { kind: "continue" };
2749
- case "done":
2750
- if (event.published === false) s.stop("Deployed, but routing was not published — the project may be unreachable or still serving the previous deploy. Re-run `void deploy` to publish routing.");
2751
- else s.stop("Deployed!");
2752
- return {
2753
- kind: "done",
2754
- event
2755
- };
2756
- case "error":
2757
- s.stop("Deploy failed");
2758
- return {
2759
- kind: "error",
2760
- message: event.message
2761
- };
2762
- }
2763
- return { kind: "continue" };
2764
- }
2765
- /**
2766
- * Apply a streamed `DeployEvent` iterator to the spinner / cliLog and
2767
- * resolve with the terminal `done` event (or throw on terminal error /
2768
- * connection loss).
2769
- */
2770
- async function streamDeployEvents(projectId, events, s, cliLog) {
2771
- const startedAt = Date.now();
2772
- let deploymentId = null;
2773
- let interrupted = false;
2774
- cliLog?.info("stream_start", { projectId });
2775
- const onSigint = () => {
2776
- interrupted = true;
2777
- s.stop("Deploy interrupted");
2778
- if (deploymentId) log.info(`Deployment: ${deploymentId}`);
2779
- else log.info("Deployment id was not received before the interrupt.");
2780
- cliLog?.warn("stream_interrupted", { deploymentId });
2781
- cliLog?.close().catch(() => {});
2782
- process.exit(130);
2783
- };
2784
- process.once("SIGINT", onSigint);
2785
- try {
2786
- for await (const event of events) {
2787
- const result = applyDeployEvent(event, s);
2788
- if (result.kind === "start") {
2789
- deploymentId = result.deploymentId;
2790
- cliLog?.info("stream_deployment_id", { deploymentId });
2791
- } else if (result.kind === "done") {
2792
- const degraded = result.event.published === false;
2793
- cliLog?.info(degraded ? "deploy_degraded" : "deploy_success", {
2794
- deploymentId,
2795
- url: result.event.url,
2796
- assets: result.event.assets,
2797
- workers: result.event.workers,
2798
- durationMs: Date.now() - startedAt,
2799
- degraded
2800
- });
2801
- return result.event;
2802
- } else if (result.kind === "error") {
2803
- cliLog?.error("deploy_server_error", void 0, {
2804
- deploymentId,
2805
- message: result.message
2806
- });
2807
- throw new DeployEventError(formatDeployFailureMessage(result.message, deploymentId, cliLog?.path ?? null));
2808
- }
2809
- }
2810
- } catch (err) {
2811
- if (err instanceof DeployEventError) throw err;
2812
- if (interrupted) throw err;
2813
- cliLog?.error("stream_deploy_caught", err, { deploymentId });
2814
- s.stop("Deploy failed");
2815
- throw formatStreamDeployError(err, deploymentId, cliLog?.path ?? null);
2816
- } finally {
2817
- process.removeListener("SIGINT", onSigint);
2818
- }
2819
- cliLog?.warn("stream_no_terminal_event", { deploymentId });
2820
- s.stop("Deploy failed");
2821
- throw new Error(formatConnectionLostMessage(deploymentId, cliLog?.path ?? null));
2822
- }
2823
- /**
2824
- * Direct-to-R2 deploy orchestration. Issues presigned PUT URLs for each
2825
- * needed asset, uploads bytes directly to R2, then calls the manifest-only
2826
- * deploy endpoint to finalize.
2827
- *
2828
- * Key invariants:
2829
- * 1. No needed assets → skip presign + R2 PUT phase entirely (saves a
2830
- * round-trip when every asset was already present from a prior deploy).
2831
- * 2. Asset bytes come exclusively from the in-memory `assetFiles` cache
2832
- * computed by `collectAndHashAssets` — files are NEVER re-read from
2833
- * disk during this flow (Codex Finding #2 in design doc 0068 review).
2834
- * 3. R2 PUT failures (4xx, 5xx, network) are surfaced with the asset
2835
- * path + hash so the user can correlate against the JSONL trace.
2836
- */
2837
- async function streamDirectR2Deploy(client, projectId, manifest, assetManifest, assetFiles, needed, gitMetadata, preflightUsed, workerFiles, s, cliLog) {
2838
- if (needed.length > 0) {
2839
- const neededPaths = new Set(needed);
2840
- const preBufferCount = assetFiles.size;
2841
- const allPaths = Array.from(assetFiles.keys());
2842
- for (const path of allPaths) if (!neededPaths.has(path)) assetFiles.delete(path);
2843
- if (assetFiles.size !== preBufferCount) cliLog?.info("asset_cache_pruned", {
2844
- before: preBufferCount,
2845
- after: assetFiles.size
2846
- });
2847
- s.message(`Requesting upload URLs for ${needed.length} asset(s)...`);
2848
- const seenBlake3 = /* @__PURE__ */ new Set();
2849
- const uniqueNeeded = [];
2850
- for (const path of needed) {
2851
- const entry = assetManifest[path];
2852
- if (!entry) throw new Error(`internal: preflight returned ${path} but assetManifest has no entry`);
2853
- if (seenBlake3.has(entry.blake3)) continue;
2854
- seenBlake3.add(entry.blake3);
2855
- uniqueNeeded.push(path);
2856
- }
2857
- const assetsToUpload = uniqueNeeded.map((path) => {
2858
- const entry = assetManifest[path];
2859
- return {
2860
- blake3: entry.blake3,
2861
- md5: entry.md5,
2862
- size: entry.size,
2863
- contentType: getAssetContentType(path)
2864
- };
2865
- });
2866
- cliLog?.info("presign_request_start", {
2867
- assetCount: assetsToUpload.length,
2868
- dedupedFrom: needed.length
2869
- });
2870
- const presignStartedAt = Date.now();
2871
- const urls = await client.requestUploadUrls(projectId, assetsToUpload);
2872
- cliLog?.info("presign_request_end", {
2873
- urlsReceived: Object.keys(urls).length,
2874
- durationMs: Date.now() - presignStartedAt
2875
- });
2876
- const items = uniqueNeeded.map((path) => {
2877
- const entry = assetManifest[path];
2878
- const presigned = urls[entry.blake3];
2879
- if (!presigned) throw new Error(`Platform did not return an upload URL for ${path} (blake3=${entry.blake3}). This indicates a server-side bug or a stale client.`);
2880
- const body = assetFiles.get(path);
2881
- if (!body) throw new Error(`internal: asset ${path} declared in manifest but missing from buffer cache`);
2882
- return {
2883
- path,
2884
- hash: entry.blake3,
2885
- body,
2886
- url: presigned.url,
2887
- headers: presigned.headers
2888
- };
2889
- });
2890
- s.message(`Uploading ${items.length} asset(s) to R2...`);
2891
- await uploadAssetsToR2(items, cliLog, void 0, (item) => {
2892
- assetFiles.delete(item.path);
2893
- });
2894
- } else cliLog?.info("presign_skipped", { reason: "no_needed_assets" });
2895
- const payload = {
2896
- manifest,
2897
- assetManifest,
2898
- source: getTokenSource(),
2899
- commit: gitMetadata?.commit ?? null,
2900
- repositoryUrl: gitMetadata?.repositoryUrl ?? null,
2901
- commitUrl: gitMetadata?.commitUrl ?? null,
2902
- gitRef: gitMetadata?.gitRef ?? null,
2903
- preflight: preflightUsed ? "true" : null
2904
- };
2905
- if (workerFiles && workerFiles.size > 0) {
2906
- const encoded = {};
2907
- for (const [relPath, buf] of workerFiles) encoded[relPath] = buf.toString("base64");
2908
- payload.workerFiles = encoded;
2909
- }
2910
- s.message("Finalizing deploy...");
2911
- cliLog?.info("finalize_start", {
2912
- workerFileCount: workerFiles?.size ?? 0,
2913
- assetCount: Object.keys(assetManifest).length
2914
- });
2915
- const finalizeStartedAt = Date.now();
2916
- try {
2917
- return await streamDeployEvents(projectId, client.finalizeDeploy(projectId, payload), s, cliLog);
2918
- } finally {
2919
- cliLog?.info("finalize_end", { durationMs: Date.now() - finalizeStartedAt });
2920
- }
2921
- }
2922
- function formatKnownAssetSummary(result, skipped) {
2923
- const uploaded = result.assets - skipped;
2924
- return skipped > 0 ? `${result.assets} static asset(s) (${skipped} unchanged, ${uploaded} uploaded)` : `${result.assets} static asset(s)`;
2925
- }
2926
- async function runStaticDeploy(root, config, client, preset, skipBuild, gitMetadata, hashedAssetsPrefix, headerRules, redirectRules, fallbackRules, routing, cliLog) {
2927
- const typeLabel = preset.appType === "spa" ? "Static SPA" : "Static Site";
2928
- log.info(`${typeLabel} deploy`);
2929
- cliLog?.info("deploy_mode", {
2930
- mode: "static",
2931
- appType: preset.appType
2932
- });
2933
- if (preset.buildCommand && !skipBuild) {
2934
- log.step("Building...");
2935
- cliLog?.info("build_start", { command: preset.buildCommand });
2936
- const buildStartedAt = Date.now();
2937
- execSync(preset.buildCommand, {
2938
- cwd: root,
2939
- stdio: "inherit",
2940
- env: buildEnv()
2941
- });
2942
- cliLog?.info("build_end", { durationMs: Date.now() - buildStartedAt });
2943
- } else if (skipBuild) cliLog?.info("build_skipped", { reason: "skipBuild" });
2944
- if (!existsSync(preset.outputDir)) {
2945
- log.error(`deploy: Output directory '${preset.outputDir}' not found. Run the build first.`);
2946
- process.exit(1);
2947
- }
2948
- if (routing) {
2949
- const merged = mergeRoutingRules({
2950
- dir: preset.outputDir,
2951
- routing
2952
- });
2953
- warnRedundantForceOn3xx(merged.redundantForceOn3xx);
2954
- lintDuplicateSources(merged.redirectRules, "redirects/rewrites (pre-asset)");
2955
- lintDuplicateSources(merged.fallbackRules, "fallbacks (post-asset)");
2956
- lintDestinationSplats(merged.redirectRules, "redirects/rewrites (pre-asset)");
2957
- lintDestinationSplats(merged.fallbackRules, "fallbacks (post-asset)");
2958
- headerRules = merged.headerRules;
2959
- redirectRules = merged.redirectRules;
2960
- fallbackRules = merged.fallbackRules;
2961
- }
2962
- const s = createSpinner();
2963
- const onProgress = (msg) => s.message(msg);
2964
- s.start("Checking for changes...");
2965
- cliLog?.info("preflight_start", { dir: preset.outputDir });
2966
- const { assetManifest, assetFiles } = await collectAndHashAssets(preset.outputDir, onProgress);
2967
- const { needed, skipped } = await client.preflight(config.projectId, assetManifest);
2968
- s.stop("Checked for changes");
2969
- cliLog?.info("preflight_end", {
2970
- total: Object.keys(assetManifest).length,
2971
- needed: needed.length,
2972
- skipped
2973
- });
2974
- if (skipped > 0) log.info(`Skipping ${skipped} unchanged asset(s)`);
2975
- s.start("Packaging...");
2976
- cliLog?.info("package_start", { mode: "static" });
2977
- const packageStartedAt = Date.now();
2978
- const neededSet = skipped > 0 ? new Set(needed) : null;
2979
- const manifest = buildStaticManifest({
2980
- appType: preset.appType,
2981
- hashedAssetsPrefix: hashedAssetsPrefix ?? "assets",
2982
- headerRules,
2983
- redirectRules,
2984
- fallbackRules
2985
- });
2986
- cliLog?.info("package_end", {
2987
- mode: "static",
2988
- durationMs: Date.now() - packageStartedAt
2989
- });
2990
- const result = await streamDirectR2Deploy(client, config.projectId, manifest, assetManifest, assetFiles, needed, gitMetadata, neededSet !== null, void 0, s, cliLog);
2991
- note([`${typeLabel} — ${formatKnownAssetSummary(result, skipped)}`].join("\n"), result.url);
2992
- warnUnpublishedRouting(result);
2993
- outro("Done!");
2994
- }
2995
- /**
2996
- * Surface a persistent warning when a deploy committed live but its routing
2997
- * KV was not published. Uses `log.warn` (not the spinner) so the warning
2998
- * survives the following `note`/`outro` calls and the user sees it after the
2999
- * deploy summary. The deploy is still committed — this is a warning, not a
3000
- * failure, so the process exit code is unaffected.
3001
- */
3002
- function warnUnpublishedRouting(result) {
3003
- if (result.published !== false) return;
3004
- log.warn("Routing was not published for this deploy. The project may be unreachable (first deploy) or still serving the previous deploy. Re-run `void deploy` to publish routing.");
3005
- }
3006
- /**
3007
- * Emit a single aggregated lint warning when a `_redirects` file contains
3008
- * one or more `!` force suffixes on 3xx entries (e.g. `301!`, `302!`).
3009
- * The `!` is a no-op on redirects — we parse them as if the `!` weren't
3010
- * there — but we nudge the user to clean up for clarity. Silenced when
3011
- * `count === 0`.
3012
- */
3013
- function warnRedundantForceOn3xx(count) {
3014
- if (count === 0) return;
3015
- log.warn(`Found ${count} redundant "!" on 3xx entries in _redirects (these always force by nature; safe to remove).`);
3016
- }
3017
- /**
3018
- * Class B/C framework deploy: build with framework CLI, package with preset output paths.
3019
- */
3020
- async function runFrameworkDeploy(root, config, client, detected, fwPreset, voidConfig, skipBuild, gitMetadata, cliLog) {
3021
- log.info(`${detected.name} framework deploy`);
3022
- cliLog?.info("deploy_mode", {
3023
- mode: "framework",
3024
- framework: detected.name
3025
- });
3026
- const dialect = getDatabaseDialect(voidConfig);
3027
- const buildCmd = voidConfig.inference?.build ?? formatProjectCommand(root, fwPreset.buildCommand);
3028
- if (!skipBuild) {
3029
- log.step(`Building (${buildCmd})...`);
3030
- cliLog?.info("build_start", { command: buildCmd });
3031
- const buildStartedAt = Date.now();
3032
- execSync(voidConfig.inference?.build ?? resolveProjectCommand(root, fwPreset.buildCommand), {
3033
- cwd: root,
3034
- stdio: "inherit",
3035
- env: buildEnv()
3036
- });
3037
- cliLog?.info("build_end", { durationMs: Date.now() - buildStartedAt });
3038
- } else cliLog?.info("build_skipped", { reason: "skipBuild" });
3039
- const workerDir = join(root, fwPreset.workerDir);
3040
- const assetsDir = join(root, fwPreset.assetsDir);
3041
- const frameworkAssetIgnorePatterns = fwPreset.workerDir === fwPreset.assetsDir && fwPreset.workerMain === "_worker.js" ? [`/${fwPreset.workerMain}`, "/_routes.json"] : [];
3042
- const { headerRules, redirectRules, fallbackRules, redundantForceOn3xx } = mergeRoutingRules({
3043
- dir: assetsDir,
3044
- routing: voidConfig.routing
3045
- });
3046
- warnRedundantForceOn3xx(redundantForceOn3xx);
3047
- lintDuplicateSources(redirectRules, "redirects/rewrites (pre-asset)");
3048
- lintDuplicateSources(fallbackRules, "fallbacks (post-asset)");
3049
- lintDestinationSplats(redirectRules, "redirects/rewrites (pre-asset)");
3050
- lintDestinationSplats(fallbackRules, "fallbacks (post-asset)");
3051
- if (!existsSync(workerDir)) {
3052
- log.error(`deploy: Worker output '${fwPreset.workerDir}' not found. Run '${buildCmd}' first.`);
3053
- process.exit(1);
3054
- }
3055
- const inferenceOptions = voidConfig.inference?.bindings ? { bindings: voidConfig.inference.bindings } : void 0;
3056
- const paths = resolveProjectPaths(root, voidConfig);
3057
- const scanDirs = voidConfig.inference?.scanDirs ?? FRAMEWORK_SCAN_DIRS;
3058
- const bindings = inferProjectBindings(root, {
3059
- ...inferenceOptions,
3060
- paths
3061
- }, scanDirs);
3062
- if (bindings.needsAuth || findVoidAuthConfig(root, paths) !== null || voidConfig.auth !== void 0) {
3063
- log.error("deploy: Void-managed Better Auth is only supported in Void apps. Use Better Auth's official integration for this framework instead.");
3064
- process.exit(1);
3065
- }
3066
- if (bindings.needsLive) {
3067
- log.error("deploy: void/live is currently supported in Void apps only.");
3068
- process.exit(1);
3069
- }
3070
- const migrationsDir = paths.migrationsDir;
3071
- assertDeploySchemaDriftFree(root, dialect);
3072
- const rawMigrations = collectMigrations(migrationsDir);
3073
- let validatedMigrations;
3074
- try {
3075
- assertJournalCoherence(migrationsDir, rawMigrations);
3076
- } catch (err) {
3077
- log.error(getErrorMessage(err));
3078
- process.exit(1);
3079
- }
3080
- if (rawMigrations.length > 0) try {
3081
- validatedMigrations = validateMigrations(rawMigrations);
3082
- log.info(`Found ${validatedMigrations.length} migration(s)`);
3083
- } catch (err) {
3084
- log.error(getErrorMessage(err));
3085
- process.exit(1);
3086
- }
3087
- if (dialect === "postgresql" && (bindings.needsD1 || rawMigrations.length > 0)) {
3088
- let resources = null;
3089
- try {
3090
- resources = await client.getResources(config.projectId);
3091
- } catch {}
3092
- if (resources) {
3093
- if (!resources.some((resource) => resource.type === "hyperdrive")) {
3094
- const { text: promptText, isCancel: isCancelPrompt } = await import("./agents-Bmr5tFFb.mjs").then((n) => n.C);
3095
- const connectionString = await promptText({
3096
- message: "Your project uses PostgreSQL. Enter your connection string:",
3097
- placeholder: "postgresql://user:pass@host:5432/dbname",
3098
- validate(value) {
3099
- if (!value?.startsWith("postgres://") && !value?.startsWith("postgresql://")) return "Must start with \"postgres://\" or \"postgresql://\"";
3100
- }
3101
- });
3102
- if (isCancelPrompt(connectionString)) {
3103
- log.info("Deploy cancelled.");
3104
- process.exit(0);
3105
- }
3106
- await client.provisionDatabase(config.projectId, connectionString);
3107
- log.success("Hyperdrive provisioned.");
3108
- }
3109
- }
3110
- }
3111
- const rawJobs = scanJobsSync(root, paths.cronsDir);
3112
- const rawQueues = scanQueuesSync(root, paths.queuesDir);
3113
- const originalEntry = join(workerDir, fwPreset.workerMain);
3114
- const wrapperFeatures = {
3115
- auth: null,
3116
- crons: rawJobs,
3117
- queues: rawQueues,
3118
- hasMigrations: validatedMigrations != null && validatedMigrations.length > 0,
3119
- migrationDialect: dialect
3120
- };
3121
- let actualWorkerDir = workerDir;
3122
- let actualWorkerMain = fwPreset.workerMain;
3123
- if (fwPreset.bundle) {
3124
- log.step("Bundling worker...");
3125
- const bundleResult = await bundleEntry(root, originalEntry);
3126
- actualWorkerDir = bundleResult.outputDir;
3127
- actualWorkerMain = bundleResult.entryFilename;
3128
- log.info("Worker bundled");
3129
- }
3130
- if (needsWrapper(wrapperFeatures)) {
3131
- log.step("Generating wrapper...");
3132
- await generateDeployWrapperIfNeeded(root, actualWorkerDir, actualWorkerMain, wrapperFeatures);
3133
- log.info("Wrapper generated");
3134
- }
3135
- const wranglerCompat = resolveDeployWranglerCompat(root, voidConfig, workerDir, { dialect });
3136
- const revalidate = voidConfig.routing?.revalidate;
3137
- const prerender = voidConfig.routing?.prerender;
3138
- const schedules = rawJobs.flatMap((job) => job.crons.map((cron) => ({
3139
- name: job.name,
3140
- cron
3141
- })));
3142
- const queues = rawQueues.map((q) => ({
3143
- name: q.name,
3144
- bindingName: q.bindingName,
3145
- ...q.maxBatchSize != null && { maxBatchSize: q.maxBatchSize },
3146
- ...q.maxBatchTimeout != null && { maxBatchTimeout: q.maxBatchTimeout }
3147
- }));
3148
- const envVars = loadDeployEnv(root);
3149
- await assertEnvSchemaValid(root, client, config.projectId);
3150
- await applySchemaDefaultsToVars(root, envVars);
3151
- const s = createSpinner();
3152
- const onProgress = (msg) => s.message(msg);
3153
- s.start("Checking for changes...");
3154
- cliLog?.info("preflight_start", { dir: assetsDir });
3155
- const { assetManifest, assetFiles } = await collectAndHashAssets(assetsDir, onProgress, { ignorePatterns: frameworkAssetIgnorePatterns });
3156
- const { needed, skipped } = await client.preflight(config.projectId, assetManifest, true);
3157
- s.stop("Checked for changes");
3158
- cliLog?.info("preflight_end", {
3159
- total: Object.keys(assetManifest).length,
3160
- needed: needed.length,
3161
- skipped
3162
- });
3163
- if (skipped > 0) log.info(`Skipping ${skipped} unchanged asset(s)`);
3164
- s.start("Packaging...");
3165
- cliLog?.info("package_start", {
3166
- mode: "framework",
3167
- framework: detected.name
3168
- });
3169
- const packageStartedAt = Date.now();
3170
- const neededSet = skipped > 0 ? new Set(needed) : null;
3171
- const packageOptions = {
3172
- bindingNames: resolveBindingNames(voidConfig.inference?.bindings),
3173
- workerMain: actualWorkerMain,
3174
- compatibilityDate: wranglerCompat?.compatibilityDate,
3175
- compatibilityFlags: wranglerCompat?.compatibilityFlags
3176
- };
3177
- const manifest = buildFrameworkManifest({
3178
- frameworkName: detected.name,
3179
- workerMain: actualWorkerMain,
3180
- bindings,
3181
- migrations: validatedMigrations,
3182
- schedules,
3183
- queues,
3184
- revalidate,
3185
- revalidateQueryAllowlist: voidConfig.routing?.revalidateQueryAllowlist,
3186
- prerender,
3187
- assetConfig: { not_found_handling: "none" },
3188
- vars: envVars,
3189
- packageOptions,
3190
- hashedAssetsPrefix: fwPreset.hashedAssetsPrefix ?? "assets",
3191
- headerRules,
3192
- redirectRules,
3193
- fallbackRules,
3194
- dialect
3195
- });
3196
- const workerFiles = collectWorkerFiles(actualWorkerDir);
3197
- cliLog?.info("package_end", {
3198
- mode: "framework",
3199
- durationMs: Date.now() - packageStartedAt
3200
- });
3201
- const result = await streamDirectR2Deploy(client, config.projectId, manifest, assetManifest, assetFiles, needed, gitMetadata, neededSet !== null, workerFiles, s, cliLog);
3202
- const summary = [`${result.workers} worker module(s), ${formatKnownAssetSummary(result, skipped)}`];
3203
- if (result.migrations) summary.push(`${result.migrations} migration(s) applied`);
3204
- if (schedules.length > 0) summary.push(`${schedules.length} cron job(s) scheduled`);
3205
- if (queues.length > 0) summary.push(`${queues.length} queue(s) configured`);
3206
- if (prerender && prerender.length > 0) summary.push(`${prerender.length} page(s) prerendered`);
3207
- if (revalidate) if (typeof revalidate === "number") summary.push(`ISR revalidate: ${revalidate}s`);
3208
- else summary.push(`ISR revalidate: ${Object.keys(revalidate).length} path rule(s)`);
3209
- summary.push(`Framework: ${detected.name}`);
3210
- note(summary.join("\n"), result.url);
3211
- cleanupWrapper(root);
3212
- warnUnpublishedRouting(result);
3213
- outro("Done!");
3214
- }
3215
- async function runFullDeploy(root, config, client, fwPreset, skipBuild, gitMetadata, detected, cliLog) {
3216
- cliLog?.info("deploy_mode", {
3217
- mode: "full",
3218
- framework: detected?.name
3219
- });
3220
- if (!skipBuild) {
3221
- const buildCmd = formatProjectCommand(root, "vite build");
3222
- log.step(`Building (${buildCmd})...`);
3223
- cliLog?.info("build_start", { command: buildCmd });
3224
- const buildStartedAt = Date.now();
3225
- try {
3226
- execSync(resolveProjectCommand(root, "vite build"), {
3227
- cwd: root,
3228
- stdio: "inherit",
3229
- env: buildEnv({ VOID_DEPLOY_PROJECT_ID: config.projectId })
3230
- });
3231
- cliLog?.info("build_end", { durationMs: Date.now() - buildStartedAt });
3232
- } catch (err) {
3233
- cliLog?.error("build_failed", err, { durationMs: Date.now() - buildStartedAt });
3234
- process.exit(getExitCode(err));
3235
- }
3236
- } else cliLog?.info("build_skipped", { reason: "skipBuild" });
3237
- const frameworkBuildCmd = fwPreset ? formatProjectCommand(root, fwPreset.buildCommand) : void 0;
3238
- const distDir = resolveDistDir(root, fwPreset, frameworkBuildCmd);
3239
- let assetsPrefix = "assets";
3240
- const clientManifestPath = join(distDir, "client", ".vite", "manifest.json");
3241
- if (existsSync(clientManifestPath)) {
3242
- const manifest = JSON.parse(readFileSync(clientManifestPath, "utf-8"));
3243
- for (const entry of Object.values(manifest)) if (entry.file?.includes("/")) {
3244
- const dir = entry.file.slice(0, entry.file.indexOf("/"));
3245
- if (dir !== "assets") assetsPrefix = dir;
3246
- break;
3247
- }
3248
- }
3249
- const isFrameworkMode = detected != null;
3250
- const deployConfig = readConfig(root);
3251
- const paths = resolveProjectPaths(root, deployConfig);
3252
- const dialect = getDatabaseDialect(deployConfig);
3253
- const deployScanDirs = deployConfig.inference?.scanDirs ?? (isFrameworkMode ? FRAMEWORK_SCAN_DIRS : void 0);
3254
- const bindings = inferProjectBindings(root, { paths }, deployScanDirs);
3255
- if (isSandboxEnabled(deployConfig)) bindings.needsSandbox = true;
3256
- const authEnabled = bindings.needsAuth || findVoidAuthConfig(root, paths) !== null || deployConfig.auth !== void 0;
3257
- const effectiveBindings = authEnabled ? {
3258
- ...bindings,
3259
- needsAuth: true,
3260
- needsD1: true
3261
- } : bindings;
3262
- if (isFrameworkMode && authEnabled) {
3263
- log.error("Void-managed Better Auth is only supported in Void apps. Use Better Auth's official integration for this framework instead.");
3264
- process.exit(1);
3265
- }
3266
- if (isFrameworkMode && bindings.needsSandbox) {
3267
- log.error("deploy: Cloudflare Sandboxes are currently supported in Void apps only.");
3268
- process.exit(1);
3269
- }
3270
- if (isFrameworkMode && bindings.needsLive) {
3271
- log.error("deploy: void/live is currently supported in Void apps only.");
3272
- process.exit(1);
3273
- }
3274
- const sandboxConfig = bindings.needsSandbox ? resolveSandboxConfig(deployConfig, root) : void 0;
3275
- if (bindings.needsSandbox && requiresSandboxPlatformImage(deployConfig)) {
3276
- log.error("deploy: `sandbox.image` points at a local Dockerfile. Add `sandbox.platformImage` with a pushed registry image for `void deploy`, or deploy this sandbox directly with Wrangler.");
3277
- process.exit(1);
3278
- }
3279
- const clientDir = join(distDir, "client");
3280
- const { headerRules, redirectRules, fallbackRules, redundantForceOn3xx } = mergeRoutingRules({
3281
- dir: clientDir,
3282
- routing: deployConfig.routing
3283
- });
3284
- warnRedundantForceOn3xx(redundantForceOn3xx);
3285
- lintDuplicateSources(redirectRules, "redirects/rewrites (pre-asset)");
3286
- lintDuplicateSources(fallbackRules, "fallbacks (post-asset)");
3287
- lintDestinationSplats(redirectRules, "redirects/rewrites (pre-asset)");
3288
- lintDestinationSplats(fallbackRules, "fallbacks (post-asset)");
3289
- const migrationsDir = paths.migrationsDir;
3290
- assertDeploySchemaDriftFree(root, dialect);
3291
- const rawMigrations = collectMigrations(migrationsDir);
3292
- let validatedMigrations;
3293
- try {
3294
- assertJournalCoherence(migrationsDir, rawMigrations);
3295
- } catch (err) {
3296
- log.error(getErrorMessage(err));
3297
- process.exit(1);
3298
- }
3299
- if (rawMigrations.length > 0) try {
3300
- validatedMigrations = validateMigrations(rawMigrations);
3301
- log.info(`Found ${validatedMigrations.length} migration(s)`);
3302
- } catch (err) {
3303
- log.error(getErrorMessage(err));
3304
- process.exit(1);
3305
- }
3306
- if (dialect === "postgresql" && (effectiveBindings.needsD1 || rawMigrations.length > 0)) {
3307
- let resources = null;
3308
- try {
3309
- resources = await client.getResources(config.projectId);
3310
- } catch {}
3311
- if (resources) {
3312
- if (!resources.some((resource) => resource.type === "hyperdrive")) {
3313
- const { text: promptText, isCancel: isCancelPrompt } = await import("./agents-Bmr5tFFb.mjs").then((n) => n.C);
3314
- const connectionString = await promptText({
3315
- message: "Your project uses PostgreSQL. Enter your connection string:",
3316
- placeholder: "postgresql://user:pass@host:5432/dbname",
3317
- validate(value) {
3318
- if (!value?.startsWith("postgres://") && !value?.startsWith("postgresql://")) return "Must start with \"postgres://\" or \"postgresql://\"";
3319
- }
3320
- });
3321
- if (isCancelPrompt(connectionString)) {
3322
- log.info("Deploy cancelled.");
3323
- process.exit(0);
3324
- }
3325
- await client.provisionDatabase(config.projectId, connectionString);
3326
- log.success("Hyperdrive provisioned.");
3327
- }
3328
- }
3329
- }
3330
- const workerDirName = resolveWorkerDirName(distDir, fwPreset, frameworkBuildCmd);
3331
- const rawJobs = scanJobsSync(root, paths.cronsDir);
3332
- const rawQueues = scanQueuesSync(root, paths.queuesDir);
3333
- const schedules = rawJobs.flatMap((job) => job.crons.map((cron) => ({
3334
- name: job.name,
3335
- cron
3336
- })));
3337
- const routeScan = await scanRoutes(root, {
3338
- output: deployConfig.output,
3339
- paths
3340
- });
3341
- const webSockets = routeScan.websockets ?? scanWebSocketRoutesSync(root, paths);
3342
- const liveConfig = await resolveDeployLiveBindingConfig(root, client, config.projectId, {
3343
- isFrameworkMode,
3344
- needsLive: effectiveBindings.needsLive,
3345
- webSockets,
3346
- sandboxClassName: sandboxConfig?.className,
3347
- cliLog
3348
- });
3349
- const queues = rawQueues.map((q) => ({
3350
- name: q.name,
3351
- bindingName: q.bindingName,
3352
- ...q.maxBatchSize != null && { maxBatchSize: q.maxBatchSize },
3353
- ...q.maxBatchTimeout != null && { maxBatchTimeout: q.maxBatchTimeout }
3354
- }));
3355
- const hasPages = existsSync(paths.pagesDir);
3356
- const hasRoutes = routeScan.routes.length > 0;
3357
- const hasApiRoutes$1 = hasApiRoutes(routeScan.routes) || hasApiRoutes(webSockets) || authEnabled;
3358
- const hasDocumentRoutes$1 = hasDocumentRoutes(routeScan.routes);
3359
- const hasMiddleware = routeScan.middleware.length > 0;
3360
- const hasWebSockets = webSockets.length > 0;
3361
- const isSsr = isFrameworkMode ? true : Boolean(validateSsrEntry(root, paths)) || hasPages && deployConfig.output !== "static";
3362
- const pageScan = existsSync(paths.pagesDir) ? routeScan.pages.pages.length > 0 ? routeScan.pages : await scanPages(root, {
3363
- output: deployConfig.output,
3364
- paths
3365
- }) : null;
3366
- const revalidate = collectRevalidate(deployConfig, pageScan);
3367
- if (deployConfig.output === "static" && pageScan && !isSsr) {
3368
- if (pageScan.pages.every((p) => p.shouldPrerender && (p.params.length === 0 && !p.catchAll || p.hasGetPrerenderPaths)) && !(hasRoutes || hasMiddleware || hasWebSockets || authEnabled || liveConfig !== void 0) && schedules.length === 0 && queues.length === 0) {
3369
- log.info("All pages prerendered — deploying as static site");
3370
- cliLog?.info("full_to_static_redirect", { reason: "all_pages_prerendered" });
3371
- return runStaticDeploy(root, config, client, {
3372
- buildCommand: null,
3373
- outputDir: clientDir,
3374
- appType: "static"
3375
- }, true, gitMetadata, assetsPrefix, headerRules, redirectRules, fallbackRules, void 0, cliLog);
3376
- }
3377
- }
3378
- const uniquePrerenderPaths = deployConfig.output === "static" || isFrameworkMode ? [] : isNodeTarget(deployConfig.target) ? await collectPrerenderPathsNode({
3379
- root,
3380
- ssrDir: join(distDir, "ssr"),
3381
- clientDir
3382
- }) : await collectPrerenderPaths({
3383
- root,
3384
- workerDir: join(distDir, workerDirName),
3385
- clientDir,
3386
- persistDir: join(root, ".void"),
3387
- remoteMode: resolveRemoteMode(deployConfig.remote),
3388
- projectId: config.projectId
3389
- });
3390
- const wranglerCompat = resolveDeployWranglerCompat(root, deployConfig, join(distDir, workerDirName), {
3391
- authEnabled,
3392
- dialect,
3393
- needsSandbox: effectiveBindings.needsSandbox
3394
- });
3395
- const workerMain = resolveDeployWorkerMain(deployConfig, join(distDir, workerDirName));
3396
- const envVars = loadDeployEnv(root);
3397
- await assertEnvSchemaValid(root, client, config.projectId);
3398
- await applySchemaDefaultsToVars(root, envVars);
3399
- const s = createSpinner();
3400
- const onProgress = (msg) => s.message(msg);
3401
- const assetConfig = resolveAssetConfig({
3402
- isSsr,
3403
- hasPages,
3404
- hasApiRoutes: hasApiRoutes$1,
3405
- hasDocumentRoutes: hasDocumentRoutes$1,
3406
- hasMiddleware,
3407
- hasWebSockets: hasDocumentRoutes(webSockets),
3408
- authEnabled,
3409
- liveEnabled: liveConfig !== void 0
3410
- });
3411
- s.start("Checking for changes...");
3412
- cliLog?.info("preflight_start", { dir: clientDir });
3413
- const { assetManifest, assetFiles } = await collectAndHashAssets(clientDir, onProgress);
3414
- const { needed, skipped } = await client.preflight(config.projectId, assetManifest, true);
3415
- s.stop("Checked for changes");
3416
- cliLog?.info("preflight_end", {
3417
- total: Object.keys(assetManifest).length,
3418
- needed: needed.length,
3419
- skipped
3420
- });
3421
- if (skipped > 0) log.info(`Skipping ${skipped} unchanged asset(s)`);
3422
- s.start("Packaging...");
3423
- cliLog?.info("package_start", {
3424
- mode: "full",
3425
- isSsr,
3426
- hasRoutes,
3427
- hasMiddleware,
3428
- hasWebSockets,
3429
- authEnabled,
3430
- framework: detected?.name
3431
- });
3432
- const packageStartedAt = Date.now();
3433
- const neededSet = skipped > 0 ? new Set(needed) : null;
3434
- const voidPackageOptions = {
3435
- compatibilityDate: wranglerCompat?.compatibilityDate,
3436
- compatibilityFlags: wranglerCompat?.compatibilityFlags,
3437
- ...workerMain && { workerMain },
3438
- ...sandboxConfig && { sandbox: sandboxConfig },
3439
- ...liveConfig && { live: liveConfig },
3440
- revalidateQueryAllowlist: deployConfig.routing?.revalidateQueryAllowlist
3441
- };
3442
- const fallbackWarning = detectNonSpaFallbackWarning(assetConfig, fallbackRules);
3443
- if (fallbackWarning) {
3444
- onProgress?.(fallbackWarning);
3445
- console.warn(`[void] ${fallbackWarning}`);
3446
- }
3447
- const manifest = buildVoidManifest({
3448
- bindings: liveConfig ? {
3449
- ...effectiveBindings,
3450
- needsLive: true
3451
- } : effectiveBindings,
3452
- migrations: validatedMigrations,
3453
- schedules,
3454
- ssr: isSsr || hasRoutes || hasMiddleware || hasWebSockets || authEnabled,
3455
- framework: isFrameworkMode ? detected.name : void 0,
3456
- revalidate,
3457
- vars: envVars,
3458
- queues,
3459
- prerender: uniquePrerenderPaths,
3460
- assetConfig,
3461
- options: voidPackageOptions,
3462
- hashedAssetsPrefix: assetsPrefix,
3463
- headerRules,
3464
- redirectRules,
3465
- fallbackRules,
3466
- dialect,
3467
- webSockets
3468
- });
3469
- const workerDir = join(distDir, workerDirName);
3470
- if (isFrameworkMode) {
3471
- const wrapperFeatures = {
3472
- auth: null,
3473
- crons: rawJobs,
3474
- queues: rawQueues,
3475
- hasMigrations: validatedMigrations != null && validatedMigrations.length > 0,
3476
- migrationDialect: dialect
3477
- };
3478
- if (needsWrapper(wrapperFeatures)) {
3479
- log.step("Generating wrapper...");
3480
- await generateDeployWrapperIfNeeded(root, workerDir, fwPreset?.workerMain ?? "index.js", wrapperFeatures);
3481
- log.info("Wrapper generated");
3482
- }
3483
- }
3484
- const workerFiles = collectWorkerFiles(workerDir);
3485
- assertWorkerMainUploaded(workerFiles, workerMain);
3486
- cliLog?.info("package_end", {
3487
- mode: "full",
3488
- durationMs: Date.now() - packageStartedAt
3489
- });
3490
- const result = await streamDirectR2Deploy(client, config.projectId, manifest, assetManifest, assetFiles, needed, gitMetadata, neededSet !== null, workerFiles, s, cliLog);
3491
- const summary = [`${result.workers} worker module(s), ${formatKnownAssetSummary(result, skipped)}`];
3492
- if (result.migrations) summary.push(`${result.migrations} migration(s) applied`);
3493
- if (schedules.length > 0) summary.push(`${schedules.length} cron job(s) scheduled`);
3494
- if (queues.length > 0) summary.push(`${queues.length} queue(s) configured`);
3495
- if (uniquePrerenderPaths.length > 0) summary.push(`${uniquePrerenderPaths.length} page(s) prerendered`);
3496
- if (isSsr) summary.push("SSR enabled");
3497
- if (revalidate) if (typeof revalidate === "number") summary.push(`ISR revalidate: ${revalidate}s`);
3498
- else summary.push(`ISR revalidate: ${Object.keys(revalidate).length} path rule(s)`);
3499
- if (isFrameworkMode) summary.push("Framework mode");
3500
- note(summary.join("\n"), result.url);
3501
- warnUnpublishedRouting(result);
3502
- outro("Done!");
3503
- }
3504
- async function createProjectFromSlug(root, client, slug, cliLog) {
3505
- log.step(`Creating project ${import_picocolors.default.blue(slug)}...`);
3506
- cliLog?.info("project_create_start", { slug });
3507
- const project = await client.createProject(slug);
3508
- const config = {
3509
- projectId: project.id,
3510
- slug: project.slug
3511
- };
3512
- writeProjectConfig(root, config);
3513
- log.success(`Project created: ${import_picocolors.default.blue(config.slug)}`);
3514
- cliLog?.info("project_create_success", {
3515
- projectId: config.projectId,
3516
- slug: config.slug
3517
- });
3518
- return config;
3519
- }
3520
- function getGitCommit(root) {
3521
- try {
3522
- const githubSha = process.env.GITHUB_SHA?.trim();
3523
- if (githubSha && /^[0-9a-f]{7,64}$/i.test(githubSha)) return githubSha;
3524
- const gitEnv = buildEnv();
3525
- execSync("git rev-parse --is-inside-work-tree", {
3526
- cwd: root,
3527
- stdio: "ignore",
3528
- env: gitEnv
3529
- });
3530
- if (execSync("git status --porcelain", {
3531
- cwd: root,
3532
- encoding: "utf-8",
3533
- env: gitEnv
3534
- }).trim() !== "") return null;
3535
- return execSync("git rev-parse HEAD", {
3536
- cwd: root,
3537
- encoding: "utf-8",
3538
- env: gitEnv
3539
- }).trim();
3540
- } catch {
3541
- return null;
3542
- }
3543
- }
3544
- function assertDeploySchemaDriftFree(root, dialect) {
3545
- try {
3546
- if (!hasDeploySchemaDrift(root, dialect)) return;
3547
- } catch (error) {
3548
- log.error(getErrorMessage(error));
3549
- process.exit(1);
3550
- }
3551
- log.error(getDeploySchemaDriftMessage());
3552
- process.exit(1);
3553
- }
3554
- function hasDeploySchemaDrift(root, dialect) {
3555
- if (!discoverSchema(root, readProjectPaths(root))) return false;
3556
- const driftRoot = mkDeployDriftCheckRoot(root);
3557
- const driftProjectRoot = join(driftRoot, "project");
3558
- createDeployDriftProject(root, driftProjectRoot);
3559
- try {
3560
- const migrationsDir = readProjectPaths(driftProjectRoot).migrationsDir;
3561
- const before = snapshotDir(migrationsDir);
3562
- const output = runDrizzleKitGenerate(driftProjectRoot, writeDrizzleConfig(driftProjectRoot, dialect === "postgresql" ? "postgresql://dummy" : ":memory:", dialect));
3563
- const snapshotsEqual = migrationSnapshotsEqual(before, snapshotDir(migrationsDir));
3564
- if (snapshotsEqual && !output.includes("No schema changes, nothing to migrate")) throw new Error(output || "drizzle-kit generate finished without reporting schema status.");
3565
- return !snapshotsEqual;
3566
- } catch (error) {
3567
- throw new Error(formatDeploySchemaDriftCheckError(error));
3568
- } finally {
3569
- rmSync(driftRoot, {
3570
- recursive: true,
3571
- force: true
3572
- });
3573
- cleanupDeployDriftCheckRoot(root);
3574
- }
3575
- }
3576
- function runDrizzleKitGenerate(root, configPath) {
3577
- const result = spawnSync(process.execPath, [
3578
- drizzleKitBin,
3579
- "generate",
3580
- "--config",
3581
- configPath
3582
- ], {
3583
- cwd: root,
3584
- encoding: "utf-8",
3585
- env: buildEnv()
3586
- });
3587
- const stderr = bufferToString(result.stderr);
3588
- const output = [stderr, bufferToString(result.stdout)].filter(Boolean).join("\n").trim();
3589
- if (result.error) throw result.error;
3590
- if (stderr) throw new Error(output);
3591
- if (result.status !== 0) throw new Error(output || `drizzle-kit generate exited with code ${result.status}.`);
3592
- return output;
3593
- }
3594
- function getDeploySchemaDriftMessage() {
3595
- return ["Schema drift detected between your Drizzle schema and db/migrations/.", "Run `void db generate`, review the generated migration, commit it yourself, then rerun `void deploy`."].join("\n");
3596
- }
3597
- function formatDeploySchemaDriftCheckError(error) {
3598
- const detail = extractExecErrorOutput(error);
3599
- return [
3600
- "Failed to check Drizzle schema drift during deploy.",
3601
- "Run `void db generate` locally, fix any reported issues, review the generated migration, commit it yourself, then rerun `void deploy`.",
3602
- ...detail ? [detail] : []
3603
- ].join("\n\n");
3604
- }
3605
- function extractExecErrorOutput(error) {
3606
- if (!error || typeof error !== "object") return "";
3607
- const details = [];
3608
- const stderr = "stderr" in error ? bufferToString(error.stderr) : "";
3609
- const stdout = "stdout" in error ? bufferToString(error.stdout) : "";
3610
- if (stderr) details.push(stderr);
3611
- if (stdout) details.push(stdout);
3612
- const message = "message" in error ? bufferToString(error.message) : "";
3613
- if (details.length === 0 && message) details.push(message);
3614
- return details.join("\n").trim();
3615
- }
3616
- function bufferToString(value) {
3617
- if (typeof value === "string") return value.trim();
3618
- if (value instanceof Buffer) return value.toString("utf-8").trim();
3619
- return "";
3620
- }
3621
- function createDeployDriftProject(root, driftProjectRoot) {
3622
- mkdirSync(driftProjectRoot, { recursive: true });
3623
- for (const entry of readdirSync(root, { withFileTypes: true })) {
3624
- if (entry.name === ".git" || entry.name === ".void" || entry.name === "node_modules") continue;
3625
- if (entry.name === "dist" || entry.name === "build" || entry.name === ".wrangler" || entry.name === ".turbo" || entry.name === ".next" || entry.name === ".svelte-kit" || entry.name === ".output") continue;
3626
- cpSync(join(root, entry.name), join(driftProjectRoot, entry.name), { recursive: true });
3627
- }
3628
- }
3629
- function mkDeployDriftCheckRoot(root) {
3630
- const driftCheckDir = join(root, deployDriftCheckDir);
3631
- rmSync(driftCheckDir, {
3632
- recursive: true,
3633
- force: true
3634
- });
3635
- mkdirSync(driftCheckDir, { recursive: true });
3636
- return mkdtempSync(join(driftCheckDir, "check-"));
3637
- }
3638
- function cleanupDeployDriftCheckRoot(root) {
3639
- const driftCheckDir = join(root, deployDriftCheckDir);
3640
- try {
3641
- if (existsSync(driftCheckDir) && readdirSync(driftCheckDir).length === 0) rmSync(driftCheckDir, {
3642
- recursive: true,
3643
- force: true
3644
- });
3645
- } catch {}
3646
- }
3647
- function snapshotDir(root) {
3648
- if (!existsSync(root)) return {};
3649
- const snapshot = {};
3650
- collectSnapshotFiles(root, root, snapshot);
3651
- return snapshot;
3652
- }
3653
- function collectSnapshotFiles(current, root, snapshot) {
3654
- for (const entry of readdirSync(current, { withFileTypes: true })) {
3655
- const fullPath = join(current, entry.name);
3656
- if (entry.isDirectory()) {
3657
- collectSnapshotFiles(fullPath, root, snapshot);
3658
- continue;
3659
- }
3660
- snapshot[relative(root, fullPath)] = readFileSync(fullPath, "utf-8");
3661
- }
3662
- }
3663
- function migrationSnapshotsEqual(left, right) {
3664
- const leftEntries = Object.entries(left).sort(([a], [b]) => a.localeCompare(b));
3665
- const rightEntries = Object.entries(right).sort(([a], [b]) => a.localeCompare(b));
3666
- if (leftEntries.length !== rightEntries.length) return false;
3667
- return leftEntries.every(([leftPath, leftContent], index) => {
3668
- const [rightPath, rightContent] = rightEntries[index] ?? [];
3669
- return leftPath === rightPath && leftContent === rightContent;
3670
- });
3671
- }
3672
- /**
3673
- * Overlay schema `.default(value)` entries onto the deploy manifest vars so
3674
- * the deployed worker sees the same fallback as the typed `env.X` proxy at
3675
- * runtime. User-provided `.env*` values always win — defaults only fill
3676
- * gaps. No-op when the project has no `env.ts`.
3677
- */
3678
- async function applySchemaDefaultsToVars(root, envVars) {
3679
- const defaults = await getDeployEnvDefaults(root);
3680
- for (const [key, value] of Object.entries(defaults)) if (envVars[key] === void 0) envVars[key] = value;
3681
- }
3682
- /**
3683
- * If the project has an `env.ts`, validate its schema against the union of
3684
- * production env files and remote secret names. Hard-error before upload so
3685
- * deploys never ship missing required env vars.
3686
- */
3687
- async function assertEnvSchemaValid(root, client, projectId) {
3688
- if (findEnvFile(root) == null) return;
3689
- const { hasSchema, report } = await validateProdEnv(root, {
3690
- productionOnly: true,
3691
- remoteSecrets: await fetchRemoteSecretNames(client, projectId)
3692
- });
3693
- if (!hasSchema || report.valid) return;
3694
- const formatted = formatEnvReport(report);
3695
- log.error(`Env validation failed:\n${formatted}`);
3696
- if (report.missing.length > 0) log.info(`Add the missing values to .env / .env.production, or upload them with \`void secret put <NAME>\`.`);
3697
- throw new DeployEventError("deploy: Aborting because env.ts schema is not satisfied.");
3698
- }
3699
- /**
3700
- * Load env vars for deploy — only .env and .env.production (not .local files).
3701
- * Later files win on conflict.
3702
- */
3703
- function loadDeployEnv(root) {
3704
- const { sanitized, stripped } = stripInternalEnvKeys(mergeAndExpand([".env", ".env.production"].map((file) => parseDotenvFile(join(root, file), { expand: false })), { onWarn: (msg) => voidWarn(msg) }));
3705
- if (stripped.length > 0) voidWarn(`Ignoring internal env keys from .env files: ${stripped.join(", ")}. These are reserved for Void runtime use and will not be shipped in the deploy manifest.`);
3706
- return sanitized;
3707
- }
3708
- /**
3709
- * Resolve the top-level build output directory.
3710
- *
3711
- * When a framework preset is available, uses its workerDir to derive the
3712
- * parent directory (e.g. "build/server" → "build"). This gives framework-
3713
- * specific error messages when the output is missing.
3714
- *
3715
- * For non-framework (Void app) deploys, defaults to "dist".
3716
- */
3717
- function resolveDistDir(root, fwPreset, buildCommand) {
3718
- if (fwPreset) {
3719
- const topDir = fwPreset.workerDir.split("/")[0];
3720
- const distDir = join(root, topDir);
3721
- if (!existsSync(distDir)) throw new Error(`deploy: Build output directory '${topDir}' not found. Run '${buildCommand ?? fwPreset.buildCommand}' first.`);
3722
- return distDir;
3723
- }
3724
- return join(root, "dist");
3725
- }
3726
- /**
3727
- * Resolve the worker output subdirectory name within the build dir.
3728
- *
3729
- * When a framework preset is available, uses the preset's workerDir to
3730
- * derive the subdirectory name and gives a framework-specific error.
3731
- *
3732
- * For non-framework (Void app) deploys, probes for known subdirectories.
3733
- */
3734
- function resolveWorkerDirName(distDir, fwPreset, buildCommand) {
3735
- if (fwPreset) {
3736
- const subDir = fwPreset.workerDir.split("/").slice(1).join("/");
3737
- if (!existsSync(join(distDir, subDir))) throw new Error(`deploy: Worker output '${fwPreset.workerDir}' not found. Run '${buildCommand ?? fwPreset.buildCommand}' first.`);
3738
- return subDir;
3739
- }
3740
- for (const name of [
3741
- "ssr",
3742
- "rsc",
3743
- "server"
3744
- ]) if (existsSync(join(distDir, name))) return name;
3745
- throw new Error("deploy: Worker output not found. Expected dist/ssr/ or dist/rsc/.");
3746
- }
3747
- //#endregion
3748
- export { isSandboxEnabled as C, resolveAssetRoutingPolicy as D, hasDocumentRoutes as E, SANDBOX_MIGRATION_TAG as S, hasApiRoutes as T, normalizeWranglerCompatibilityFlags as _, prerenderPagesNode as a, resolveLiveBindingConfig as b, filterLoadedEnv as c, LIVE_MIGRATION_TAG as d, ensureWranglerCompat as f, nextDurableObjectMigrationTag as g, hasDurableObjectSqliteMigration as h, prerenderPages as i, resolveRemoteMode as l, ensureWranglerNodejsAlsFlag as m, resolveDistDir as n, collectRevalidate as o, ensureWranglerCompatibilityDate as p, resolveWorkerDirName as r, hasRevalidate as s, deploy_exports as t, stripInternalEnvKeys as u, readExistingLiveBindingConfig as v, resolveSandboxConfig as w, syncWranglerBindings as x, readWranglerCompat as y };