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