void 0.10.8 → 0.10.11

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