@vinext/cloudflare 0.2.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/kv-data-adapter.d.ts +2 -1
- package/dist/cache/kv-data-adapter.js +2 -1
- package/dist/cache/kv-data-adapter.runtime.js +1 -1
- package/dist/cdn-warm.d.ts +37 -0
- package/dist/cdn-warm.js +183 -0
- package/dist/cli.js +7 -1
- package/dist/deploy-config.d.ts +9 -1
- package/dist/deploy-config.js +21 -2
- package/dist/deploy-help.js +16 -0
- package/dist/deploy.d.ts +46 -7
- package/dist/deploy.js +339 -37
- package/dist/prerender-kv-populate.d.ts +25 -0
- package/dist/prerender-kv-populate.js +120 -0
- package/dist/tpr.d.ts +11 -3
- package/dist/tpr.js +146 -8
- package/dist/version-deploy.d.ts +43 -0
- package/dist/version-deploy.js +223 -0
- package/dist/workers-dev-url.d.ts +4 -0
- package/dist/workers-dev-url.js +42 -0
- package/package.json +12 -3
package/dist/deploy.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readPrerenderWarmPaths, warmCdnCache } from "./cdn-warm.js";
|
|
2
|
+
import { parseWranglerConfig, runTPR } from "./tpr.js";
|
|
3
|
+
import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
|
|
4
|
+
import { parseWorkersDevUrl } from "./workers-dev-url.js";
|
|
5
|
+
import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
|
|
6
|
+
import { buildPrerenderKVPairs } from "./prerender-kv-populate.js";
|
|
3
7
|
import { createRequire } from "node:module";
|
|
4
|
-
import fs from "node:fs";
|
|
5
8
|
import path from "node:path";
|
|
6
|
-
import
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
7
13
|
import { parseArgs } from "node:util";
|
|
8
14
|
import { pathToFileURL } from "node:url";
|
|
9
15
|
import { runPrerender } from "vinext/internal/build/run-prerender";
|
|
10
16
|
import { loadDotenv } from "vinext/internal/config/dotenv";
|
|
11
|
-
import { loadNextConfig, resolveNextConfig } from "vinext/internal/config/next-config";
|
|
12
|
-
import {
|
|
17
|
+
import { findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "vinext/internal/config/next-config";
|
|
18
|
+
import { findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, resolveVinextPrerenderDecision } from "vinext/internal/config/prerender";
|
|
13
19
|
import { detectProject, findInNodeModules, formatMissingCloudflarePluginError, getMissingDeps } from "vinext/internal/utils/project";
|
|
20
|
+
import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
|
|
14
21
|
//#region src/deploy.ts
|
|
15
22
|
/**
|
|
16
23
|
* vinext-cloudflare deploy — one-command Cloudflare Workers deployment.
|
|
@@ -27,6 +34,16 @@ function parsePositiveIntegerArg(raw, flag) {
|
|
|
27
34
|
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} expects a positive integer, but got "${raw}".`);
|
|
28
35
|
return parsed;
|
|
29
36
|
}
|
|
37
|
+
function parseNonNegativeIntegerArg(raw, flag) {
|
|
38
|
+
if (raw === "") throw new Error(`${flag} requires a value, but none was provided.`);
|
|
39
|
+
const parsed = Number(raw);
|
|
40
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
function formatUnknownError(error) {
|
|
44
|
+
if (error instanceof Error && error.message) return error.message;
|
|
45
|
+
return String(error);
|
|
46
|
+
}
|
|
30
47
|
/** Deploy command flag definitions for util.parseArgs. */
|
|
31
48
|
const deployArgOptions = {
|
|
32
49
|
help: {
|
|
@@ -40,6 +57,7 @@ const deployArgOptions = {
|
|
|
40
57
|
},
|
|
41
58
|
env: { type: "string" },
|
|
42
59
|
name: { type: "string" },
|
|
60
|
+
config: { type: "string" },
|
|
43
61
|
"skip-build": {
|
|
44
62
|
type: "boolean",
|
|
45
63
|
default: false
|
|
@@ -53,6 +71,21 @@ const deployArgOptions = {
|
|
|
53
71
|
default: false
|
|
54
72
|
},
|
|
55
73
|
"prerender-concurrency": { type: "string" },
|
|
74
|
+
"experimental-warm-cdn-cache": {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
default: false
|
|
77
|
+
},
|
|
78
|
+
"warm-cdn-concurrency": { type: "string" },
|
|
79
|
+
"warm-cdn-timeout": { type: "string" },
|
|
80
|
+
"warm-cdn-retries": { type: "string" },
|
|
81
|
+
"warm-cdn-strict": {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
default: false
|
|
84
|
+
},
|
|
85
|
+
"warm-cdn-include-fallbacks": {
|
|
86
|
+
type: "boolean",
|
|
87
|
+
default: false
|
|
88
|
+
},
|
|
56
89
|
"experimental-tpr": {
|
|
57
90
|
type: "boolean",
|
|
58
91
|
default: false
|
|
@@ -81,10 +114,17 @@ function parseDeployArgs(args) {
|
|
|
81
114
|
preview: values.preview,
|
|
82
115
|
env: values.env?.trim() || void 0,
|
|
83
116
|
name: values.name?.trim() || void 0,
|
|
117
|
+
config: values.config?.trim() || void 0,
|
|
84
118
|
skipBuild: values["skip-build"],
|
|
85
119
|
dryRun: values["dry-run"],
|
|
86
120
|
prerenderAll: values["prerender-all"],
|
|
87
121
|
prerenderConcurrency: values["prerender-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["prerender-concurrency"], "--prerender-concurrency"),
|
|
122
|
+
warmCdnCache: values["experimental-warm-cdn-cache"],
|
|
123
|
+
warmCdnConcurrency: values["warm-cdn-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-concurrency"], "--warm-cdn-concurrency"),
|
|
124
|
+
warmCdnTimeout: values["warm-cdn-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-timeout"], "--warm-cdn-timeout"),
|
|
125
|
+
warmCdnRetries: values["warm-cdn-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-retries"], "--warm-cdn-retries"),
|
|
126
|
+
warmCdnStrict: values["warm-cdn-strict"],
|
|
127
|
+
warmCdnIncludeFallbacks: values["warm-cdn-include-fallbacks"],
|
|
88
128
|
experimentalTPR: values["experimental-tpr"],
|
|
89
129
|
tprCoverage: parseIntArg("tpr-coverage", values["tpr-coverage"]),
|
|
90
130
|
tprLimit: parseIntArg("tpr-limit", values["tpr-limit"]),
|
|
@@ -124,6 +164,18 @@ async function loadProjectViteApi(root) {
|
|
|
124
164
|
}
|
|
125
165
|
return await (vitePath === "vite" ? import(vitePath) : import(pathToFileURL(vitePath).href));
|
|
126
166
|
}
|
|
167
|
+
async function loadDeployViteConfigMetadata(root) {
|
|
168
|
+
const plugins = (await (await loadProjectViteApi(root)).loadConfigFromFile({
|
|
169
|
+
command: "build",
|
|
170
|
+
mode: "production"
|
|
171
|
+
}, void 0, root))?.config.plugins;
|
|
172
|
+
return {
|
|
173
|
+
cacheConfig: viteConfigHasCacheAdapter(root) ? await findVinextCacheConfigInPlugins(plugins) : null,
|
|
174
|
+
nextConfig: await findVinextNextConfigInPlugins(plugins),
|
|
175
|
+
prerenderConfig: await findVinextPrerenderConfigInPlugins(plugins),
|
|
176
|
+
routeRootConfig: await findVinextRouteRootConfigInPlugins(plugins)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
127
179
|
async function runBuild(info, env) {
|
|
128
180
|
console.log("\n Building for Cloudflare Workers...\n");
|
|
129
181
|
const { createBuilder } = await loadProjectViteApi(info.root);
|
|
@@ -131,6 +183,25 @@ async function runBuild(info, env) {
|
|
|
131
183
|
await (await createBuilder({ root: info.root })).buildApp();
|
|
132
184
|
});
|
|
133
185
|
}
|
|
186
|
+
async function populateKVCacheFromPrerenderedArtifacts(root, wranglerEnv, cacheConfig) {
|
|
187
|
+
const kvConfig = resolveKvDataAdapterConfig(cacheConfig);
|
|
188
|
+
if (!kvConfig) return;
|
|
189
|
+
const { routeCount, pairs } = buildPrerenderKVPairs(path.join(root, "dist", "server"), {
|
|
190
|
+
appPrefix: kvConfig.appPrefix,
|
|
191
|
+
ttlSeconds: kvConfig.ttlSeconds
|
|
192
|
+
});
|
|
193
|
+
if (pairs.length === 0) {
|
|
194
|
+
console.log(" KV cache: Skipping prerender upload (no App Router prerendered cache entries found).");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await runWranglerKVBulkPut(root, {
|
|
198
|
+
binding: kvConfig.binding,
|
|
199
|
+
env: wranglerEnv,
|
|
200
|
+
pairs
|
|
201
|
+
});
|
|
202
|
+
console.log(` KV cache: Uploaded ${pairs.length} entr${pairs.length === 1 ? "y" : "ies"} for ${routeCount} prerendered route${routeCount === 1 ? "" : "s"}.`);
|
|
203
|
+
}
|
|
204
|
+
const KV_BULK_PUT_CHUNK_SIZE = 25;
|
|
134
205
|
function validateWranglerEnvName(env) {
|
|
135
206
|
if (env.includes("\0")) throw new Error("Wrangler environment names cannot contain null bytes.");
|
|
136
207
|
return env;
|
|
@@ -138,6 +209,25 @@ function validateWranglerEnvName(env) {
|
|
|
138
209
|
function buildWranglerDeployArgs(options) {
|
|
139
210
|
const args = ["deploy"];
|
|
140
211
|
const env = options.env || (options.preview ? "preview" : void 0);
|
|
212
|
+
if (options.config) args.push("--config", options.config);
|
|
213
|
+
if (options.name) args.push("--name", options.name);
|
|
214
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
215
|
+
return {
|
|
216
|
+
args,
|
|
217
|
+
env
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function buildWranglerKVBulkPutArgs(options) {
|
|
221
|
+
const env = options.env || void 0;
|
|
222
|
+
const args = [
|
|
223
|
+
"kv",
|
|
224
|
+
"bulk",
|
|
225
|
+
"put",
|
|
226
|
+
options.filePath,
|
|
227
|
+
"--binding",
|
|
228
|
+
options.binding,
|
|
229
|
+
"--remote"
|
|
230
|
+
];
|
|
141
231
|
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
142
232
|
return {
|
|
143
233
|
args,
|
|
@@ -179,21 +269,196 @@ function buildWranglerInvocation(root, options, nodeExecutable = process.execPat
|
|
|
179
269
|
env
|
|
180
270
|
};
|
|
181
271
|
}
|
|
182
|
-
function
|
|
183
|
-
const
|
|
272
|
+
async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutable = process.execPath) {
|
|
273
|
+
const tempDir = fs.mkdtempSync(path.join(options.tempDir ?? os.tmpdir(), "vinext-kv-bulk-"));
|
|
274
|
+
try {
|
|
275
|
+
const wranglerBin = resolveWranglerBin(root);
|
|
276
|
+
const totalChunks = Math.ceil(options.pairs.length / KV_BULK_PUT_CHUNK_SIZE);
|
|
277
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
278
|
+
const filePath = path.join(tempDir, `prerender-kv-${i}.json`);
|
|
279
|
+
const chunk = options.pairs.slice(i * KV_BULK_PUT_CHUNK_SIZE, (i + 1) * KV_BULK_PUT_CHUNK_SIZE);
|
|
280
|
+
fs.writeFileSync(filePath, JSON.stringify(chunk), "utf-8");
|
|
281
|
+
const { args } = buildWranglerKVBulkPutArgs({
|
|
282
|
+
binding: options.binding,
|
|
283
|
+
env: options.env,
|
|
284
|
+
filePath
|
|
285
|
+
});
|
|
286
|
+
const invocation = buildNodeCliInvocation(wranglerBin, args, nodeExecutable);
|
|
287
|
+
const child = execute(invocation.file, invocation.args, {
|
|
288
|
+
cwd: root,
|
|
289
|
+
stdio: "inherit",
|
|
290
|
+
shell: false
|
|
291
|
+
});
|
|
292
|
+
await new Promise((resolve, reject) => {
|
|
293
|
+
child.once("error", reject);
|
|
294
|
+
child.once("close", (code, signal) => {
|
|
295
|
+
if (code === 0) {
|
|
296
|
+
resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
|
300
|
+
reject(/* @__PURE__ */ new Error(`Wrangler KV bulk put failed with ${exitReason}.`));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
fs.rmSync(tempDir, {
|
|
306
|
+
recursive: true,
|
|
307
|
+
force: true
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function runWranglerDeploy(root, options, execute = spawn) {
|
|
312
|
+
const spawnOptions = {
|
|
184
313
|
cwd: root,
|
|
185
|
-
stdio:
|
|
186
|
-
|
|
314
|
+
stdio: [
|
|
315
|
+
"inherit",
|
|
316
|
+
"pipe",
|
|
317
|
+
"pipe"
|
|
318
|
+
],
|
|
187
319
|
shell: false
|
|
188
320
|
};
|
|
189
321
|
const { file, args, env } = buildWranglerInvocation(root, options);
|
|
190
322
|
if (env) console.log(`\n Deploying to env: ${env}...`);
|
|
191
323
|
else console.log("\n Deploying to production...");
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
324
|
+
const child = execute(file, args, spawnOptions);
|
|
325
|
+
let output = "";
|
|
326
|
+
child.stdout?.on("data", (chunk) => {
|
|
327
|
+
const text = chunk.toString();
|
|
328
|
+
output += text;
|
|
329
|
+
process.stdout.write(text);
|
|
330
|
+
});
|
|
331
|
+
child.stderr?.on("data", (chunk) => {
|
|
332
|
+
const text = chunk.toString();
|
|
333
|
+
output += text;
|
|
334
|
+
process.stderr.write(text);
|
|
335
|
+
});
|
|
336
|
+
await new Promise((resolve, reject) => {
|
|
337
|
+
child.once("error", reject);
|
|
338
|
+
child.once("close", (code, signal) => {
|
|
339
|
+
if (code === 0) {
|
|
340
|
+
resolve();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
|
344
|
+
reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
return parseWorkersDevUrl(output) ?? "(URL not detected in wrangler output)";
|
|
348
|
+
}
|
|
349
|
+
async function deployWithCdnWarmup(root, paths, options) {
|
|
350
|
+
const upload = runWranglerVersionUpload(root, options);
|
|
351
|
+
const wranglerConfig = parseWranglerConfig(root, options.config);
|
|
352
|
+
const stagingTraffic = getZeroPercentStagingTraffic(readWranglerDeploymentStatus(root, options), upload.versionId);
|
|
353
|
+
let staged = null;
|
|
354
|
+
let triggersDeployedUrl = null;
|
|
355
|
+
let warmedBeforePromotion = false;
|
|
356
|
+
let triggersApplied = false;
|
|
357
|
+
function applyTriggers() {
|
|
358
|
+
if (triggersApplied) return;
|
|
359
|
+
triggersDeployedUrl = runWranglerTriggersDeploy(root, options).deployedUrl;
|
|
360
|
+
triggersApplied = true;
|
|
361
|
+
}
|
|
362
|
+
if (stagingTraffic) {
|
|
363
|
+
staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
|
|
364
|
+
try {
|
|
365
|
+
applyTriggers();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw withStagedVersionCleanupNote(error);
|
|
368
|
+
}
|
|
369
|
+
const targetUrl = resolveCdnWarmupTargetUrl(root, staged.deployedUrl ?? triggersDeployedUrl, options);
|
|
370
|
+
const headers = buildVersionOverrideHeaders(resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
|
|
371
|
+
if (targetUrl && headers) {
|
|
372
|
+
try {
|
|
373
|
+
await warmCdnCache({
|
|
374
|
+
targetUrl,
|
|
375
|
+
paths,
|
|
376
|
+
headers,
|
|
377
|
+
concurrency: options.warmCdnConcurrency,
|
|
378
|
+
timeoutMs: options.warmCdnTimeout,
|
|
379
|
+
retries: options.warmCdnRetries,
|
|
380
|
+
strict: options.warmCdnStrict
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
throw withStagedVersionCleanupNote(error);
|
|
384
|
+
}
|
|
385
|
+
warmedBeforePromotion = true;
|
|
386
|
+
} else if (options.warmCdnStrict) throw new Error("CDN warmup failed: pre-traffic warmup needs a production URL and Worker name for version overrides. Configure a route/custom domain and Worker name, or rerun without --warm-cdn-strict. " + getStagedVersionCleanupNote());
|
|
387
|
+
} else console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not a single 100% version.");
|
|
388
|
+
const deployed = runWranglerVersionDeploy(root, [{
|
|
389
|
+
versionId: upload.versionId,
|
|
390
|
+
percentage: 100
|
|
391
|
+
}], options, warmedBeforePromotion ? "promote-warmed" : "promote-uploaded");
|
|
392
|
+
if (!warmedBeforePromotion) {
|
|
393
|
+
try {
|
|
394
|
+
applyTriggers();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw withPromotedVersionTriggerNote(error);
|
|
397
|
+
}
|
|
398
|
+
const targetUrl = resolveCdnWarmupTargetUrl(root, deployed.deployedUrl ?? triggersDeployedUrl, options);
|
|
399
|
+
if (targetUrl) await warmCdnCache({
|
|
400
|
+
targetUrl,
|
|
401
|
+
paths,
|
|
402
|
+
concurrency: options.warmCdnConcurrency,
|
|
403
|
+
timeoutMs: options.warmCdnTimeout,
|
|
404
|
+
retries: options.warmCdnRetries,
|
|
405
|
+
strict: options.warmCdnStrict
|
|
406
|
+
});
|
|
407
|
+
else if (options.warmCdnStrict) throw new Error("CDN warmup failed: no production URL could be inferred from wrangler config or output. Configure a route/custom domain, ensure Wrangler prints a workers.dev URL, or rerun without --warm-cdn-strict.");
|
|
408
|
+
else console.warn(" CDN warmup skipped: no production URL could be inferred from wrangler config or output.");
|
|
409
|
+
}
|
|
410
|
+
return deployed.deployedUrl ?? triggersDeployedUrl ?? staged?.deployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
|
|
411
|
+
}
|
|
412
|
+
function resolveCdnWarmupTargetUrl(root, deployedUrl, options) {
|
|
413
|
+
const config = parseWranglerConfig(root, options?.config);
|
|
414
|
+
const env = getWranglerTargetEnv(options ?? {});
|
|
415
|
+
const customDomain = (env ? config?.env?.[env]?.customDomain : void 0) ?? config?.customDomain;
|
|
416
|
+
if (customDomain) return `https://${customDomain}`;
|
|
417
|
+
return deployedUrl;
|
|
418
|
+
}
|
|
419
|
+
function readWranglerDeploymentStatus(root, options) {
|
|
420
|
+
try {
|
|
421
|
+
return runWranglerDeploymentStatus(root, options);
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function getZeroPercentStagingTraffic(deployment, versionId) {
|
|
427
|
+
const current = deployment?.versions ?? [];
|
|
428
|
+
if (current.length !== 1 || current[0].percentage !== 100) return null;
|
|
429
|
+
if (current[0].versionId === versionId) return null;
|
|
430
|
+
return [current[0], {
|
|
431
|
+
versionId,
|
|
432
|
+
percentage: 0
|
|
433
|
+
}];
|
|
434
|
+
}
|
|
435
|
+
function getWranglerTargetEnv(options) {
|
|
436
|
+
return options.env || (options.preview ? "preview" : void 0);
|
|
437
|
+
}
|
|
438
|
+
function resolveWorkerNameForVersionOverride(config, options) {
|
|
439
|
+
if (options.name) return options.name;
|
|
440
|
+
const env = getWranglerTargetEnv(options);
|
|
441
|
+
if (!env) return config?.name;
|
|
442
|
+
if (config?.legacyEnv === false) return config.name;
|
|
443
|
+
return config?.env?.[env]?.name ?? (config?.name ? `${config.name}-${env}` : void 0);
|
|
444
|
+
}
|
|
445
|
+
function quoteStructuredHeaderString(value) {
|
|
446
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
447
|
+
}
|
|
448
|
+
function buildVersionOverrideHeaders(workerName, versionId) {
|
|
449
|
+
if (!workerName) return void 0;
|
|
450
|
+
return { "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}` };
|
|
451
|
+
}
|
|
452
|
+
function withStagedVersionCleanupNote(error) {
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
|
|
455
|
+
}
|
|
456
|
+
function getStagedVersionCleanupNote() {
|
|
457
|
+
return "The uploaded version may remain staged at 0% with the previous version still serving 100% traffic; rerun deploy to promote it or use `wrangler versions deploy` to choose the desired version split.";
|
|
458
|
+
}
|
|
459
|
+
function withPromotedVersionTriggerNote(error) {
|
|
460
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
461
|
+
return new Error(`${message} The uploaded version may already be promoted to 100%, but Worker triggers/routes may not be updated; rerun deploy or \`wrangler triggers deploy\` after fixing the trigger error.`, { cause: error });
|
|
197
462
|
}
|
|
198
463
|
async function deploy(options) {
|
|
199
464
|
const deployEnv = validateWranglerEnvName(options.env || (options.preview ? "preview" : "production"));
|
|
@@ -229,44 +494,81 @@ async function deploy(options) {
|
|
|
229
494
|
return;
|
|
230
495
|
}
|
|
231
496
|
const buildEnv = deployEnv === "production" && !options.env ? void 0 : deployEnv;
|
|
497
|
+
const viteConfigMetadata = await withCloudflareEnv(buildEnv, () => loadDeployViteConfigMetadata(info.root));
|
|
498
|
+
const nextConfig = await withCloudflareEnv(buildEnv, async () => {
|
|
499
|
+
const inlineNextConfig = viteConfigMetadata.nextConfig;
|
|
500
|
+
return resolveNextConfig(inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
|
|
501
|
+
});
|
|
502
|
+
const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? viteConfigMetadata.prerenderConfig : null;
|
|
503
|
+
const prerenderDecision = resolveVinextPrerenderDecision({
|
|
504
|
+
prerenderAllFlag: options.prerenderAll,
|
|
505
|
+
vinextPrerenderConfig,
|
|
506
|
+
nextOutput: nextConfig.output
|
|
507
|
+
});
|
|
508
|
+
const shouldEmitPrerenderPathManifest = options.warmCdnCache || !options.skipBuild && prerenderDecision;
|
|
232
509
|
if (!options.skipBuild) await runBuild(info, buildEnv);
|
|
233
510
|
else console.log("\n Skipping build (--skip-build)");
|
|
234
|
-
{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
|
|
246
|
-
if (nextConfig.enablePrerenderSourceMaps) {
|
|
247
|
-
process.setSourceMapsEnabled(true);
|
|
248
|
-
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
|
|
249
|
-
}
|
|
250
|
-
await runPrerender({
|
|
251
|
-
root: info.root,
|
|
252
|
-
concurrency: options.prerenderConcurrency
|
|
253
|
-
});
|
|
511
|
+
if (shouldEmitPrerenderPathManifest) await emitPrerenderPathManifest({
|
|
512
|
+
root: info.root,
|
|
513
|
+
nextConfig,
|
|
514
|
+
routeRootConfig: viteConfigMetadata.routeRootConfig
|
|
515
|
+
});
|
|
516
|
+
let ranPrerender = false;
|
|
517
|
+
if (prerenderDecision) {
|
|
518
|
+
console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
|
|
519
|
+
if (nextConfig.enablePrerenderSourceMaps) {
|
|
520
|
+
process.setSourceMapsEnabled(true);
|
|
521
|
+
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
|
|
254
522
|
}
|
|
523
|
+
await runPrerender({
|
|
524
|
+
root: info.root,
|
|
525
|
+
concurrency: options.prerenderConcurrency,
|
|
526
|
+
nextConfig
|
|
527
|
+
});
|
|
528
|
+
ranPrerender = true;
|
|
529
|
+
}
|
|
530
|
+
if (ranPrerender) try {
|
|
531
|
+
await populateKVCacheFromPrerenderedArtifacts(root, deployEnv === "production" && !options.env ? void 0 : deployEnv, viteConfigMetadata.cacheConfig);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.log(` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`);
|
|
255
534
|
}
|
|
256
535
|
if (options.experimentalTPR) {
|
|
257
536
|
console.log();
|
|
258
537
|
const tprResult = await runTPR({
|
|
259
538
|
root,
|
|
539
|
+
config: options.config,
|
|
260
540
|
coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
|
|
261
541
|
limit: Math.max(1, options.tprLimit ?? 1e3),
|
|
262
542
|
window: Math.max(1, options.tprWindow ?? 24)
|
|
263
543
|
});
|
|
264
544
|
if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
|
|
265
545
|
}
|
|
266
|
-
const
|
|
546
|
+
const wranglerOptions = {
|
|
547
|
+
env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
|
|
548
|
+
name: options.name,
|
|
549
|
+
config: options.config
|
|
550
|
+
};
|
|
551
|
+
let url;
|
|
552
|
+
if (options.warmCdnCache) {
|
|
553
|
+
const warmPaths = readPrerenderWarmPaths(root, {
|
|
554
|
+
includeFallbackShells: options.warmCdnIncludeFallbacks,
|
|
555
|
+
strict: options.warmCdnStrict
|
|
556
|
+
});
|
|
557
|
+
if (warmPaths.length > 0) url = await deployWithCdnWarmup(root, warmPaths, {
|
|
558
|
+
...wranglerOptions,
|
|
559
|
+
warmCdnConcurrency: options.warmCdnConcurrency,
|
|
560
|
+
warmCdnTimeout: options.warmCdnTimeout,
|
|
561
|
+
warmCdnRetries: options.warmCdnRetries,
|
|
562
|
+
warmCdnStrict: options.warmCdnStrict
|
|
563
|
+
});
|
|
564
|
+
else {
|
|
565
|
+
console.log("\n CDN warmup skipped: no build-discovered paths found.");
|
|
566
|
+
url = await runWranglerDeploy(root, wranglerOptions);
|
|
567
|
+
}
|
|
568
|
+
} else url = await runWranglerDeploy(root, wranglerOptions);
|
|
267
569
|
console.log("\n ─────────────────────────────────────────");
|
|
268
570
|
console.log(` Deployed to: ${url}`);
|
|
269
571
|
console.log(" ─────────────────────────────────────────\n");
|
|
270
572
|
}
|
|
271
573
|
//#endregion
|
|
272
|
-
export { buildNodeCliInvocation, buildWranglerDeployArgs, buildWranglerInvocation, deploy, parseDeployArgs, resolveWranglerBin, runWranglerDeploy, validateWranglerEnvName, withCloudflareEnv };
|
|
574
|
+
export { buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, parseDeployArgs, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, validateWranglerEnvName, withCloudflareEnv };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/prerender-kv-populate.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Deploy-time KV population for App Router prerendered artifacts.
|
|
4
|
+
*
|
|
5
|
+
* Reads `dist/server/vinext-prerender.json` and `dist/server/prerendered-routes/*`,
|
|
6
|
+
* converts rendered App Router HTML/RSC artifacts into the same serialized
|
|
7
|
+
* KVCacheEntry shape written by KVCacheHandler.set(), and returns Wrangler KV
|
|
8
|
+
* bulk import records for deploy-time upload.
|
|
9
|
+
*/
|
|
10
|
+
type KVBulkPair = {
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
expiration_ttl?: number;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
declare function buildPrerenderKVPairs(serverDir: string, options?: {
|
|
17
|
+
appPrefix?: string;
|
|
18
|
+
now?: number;
|
|
19
|
+
ttlSeconds?: number;
|
|
20
|
+
}): {
|
|
21
|
+
routeCount: number;
|
|
22
|
+
pairs: KVBulkPair[];
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { KVBulkPair, buildPrerenderKVPairs };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ENTRY_PREFIX } from "./cache/kv-data-adapter.runtime.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { getRenderedAppRoutes, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
5
|
+
import { appIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
6
|
+
import { buildAppPageCacheTags } from "vinext/internal/server/app-page-cache";
|
|
7
|
+
import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
|
|
8
|
+
import { getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
|
|
9
|
+
//#region src/prerender-kv-populate.ts
|
|
10
|
+
/**
|
|
11
|
+
* Deploy-time KV population for App Router prerendered artifacts.
|
|
12
|
+
*
|
|
13
|
+
* Reads `dist/server/vinext-prerender.json` and `dist/server/prerendered-routes/*`,
|
|
14
|
+
* converts rendered App Router HTML/RSC artifacts into the same serialized
|
|
15
|
+
* KVCacheEntry shape written by KVCacheHandler.set(), and returns Wrangler KV
|
|
16
|
+
* bulk import records for deploy-time upload.
|
|
17
|
+
*/
|
|
18
|
+
/** Default KV expiration TTL used by KVCacheHandler for revalidating entries. */
|
|
19
|
+
const DEFAULT_KV_TTL_SECONDS = 720 * 3600;
|
|
20
|
+
function resolveContainedFile(rootDir, relativePath) {
|
|
21
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
22
|
+
const resolvedFile = path.resolve(resolvedRoot, relativePath);
|
|
23
|
+
const relative = path.relative(resolvedRoot, resolvedFile);
|
|
24
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`[vinext] Refusing to read prerender artifact outside ${resolvedRoot}`);
|
|
25
|
+
return resolvedFile;
|
|
26
|
+
}
|
|
27
|
+
function formatUnknownError(error) {
|
|
28
|
+
if (error instanceof Error && error.message) return error.message;
|
|
29
|
+
return String(error);
|
|
30
|
+
}
|
|
31
|
+
function buildKVKey(appPrefix, cacheKey) {
|
|
32
|
+
return `${appPrefix ? `${appPrefix}:` : ""}${ENTRY_PREFIX}${cacheKey}`;
|
|
33
|
+
}
|
|
34
|
+
function buildCacheEntry(value, tags, now, revalidateSeconds, expireSeconds) {
|
|
35
|
+
const cacheControl = revalidateSeconds === void 0 ? void 0 : expireSeconds === void 0 ? { revalidate: revalidateSeconds } : {
|
|
36
|
+
revalidate: revalidateSeconds,
|
|
37
|
+
expire: expireSeconds
|
|
38
|
+
};
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
value,
|
|
41
|
+
tags,
|
|
42
|
+
lastModified: now,
|
|
43
|
+
revalidateAt: revalidateSeconds === void 0 ? null : now + revalidateSeconds * 1e3,
|
|
44
|
+
expireAt: expireSeconds === void 0 ? null : now + expireSeconds * 1e3,
|
|
45
|
+
...cacheControl ? { cacheControl } : {}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function buildMetadata(tags) {
|
|
49
|
+
const metadata = { tags };
|
|
50
|
+
return JSON.stringify(metadata).length <= 1024 ? metadata : void 0;
|
|
51
|
+
}
|
|
52
|
+
function buildPrerenderKVPairs(serverDir, options) {
|
|
53
|
+
const manifest = readPrerenderManifest(path.join(serverDir, "vinext-prerender.json"));
|
|
54
|
+
if (!manifest?.buildId || !Array.isArray(manifest.routes)) return {
|
|
55
|
+
routeCount: 0,
|
|
56
|
+
pairs: []
|
|
57
|
+
};
|
|
58
|
+
const prerenderDir = path.join(serverDir, "prerendered-routes");
|
|
59
|
+
if (!fs.existsSync(prerenderDir)) return {
|
|
60
|
+
routeCount: 0,
|
|
61
|
+
pairs: []
|
|
62
|
+
};
|
|
63
|
+
const pairs = [];
|
|
64
|
+
const now = options?.now ?? Date.now();
|
|
65
|
+
const ttlSeconds = options?.ttlSeconds ?? DEFAULT_KV_TTL_SECONDS;
|
|
66
|
+
const trailingSlash = manifest.trailingSlash ?? false;
|
|
67
|
+
let routeCount = 0;
|
|
68
|
+
for (const route of getRenderedAppRoutes(manifest.routes)) {
|
|
69
|
+
const artifactPathname = route.path ?? route.route;
|
|
70
|
+
const cachePathname = normalizePregeneratedPathname(artifactPathname);
|
|
71
|
+
let htmlPath;
|
|
72
|
+
let rscPath;
|
|
73
|
+
try {
|
|
74
|
+
htmlPath = resolveContainedFile(prerenderDir, getOutputPath(artifactPathname, trailingSlash));
|
|
75
|
+
rscPath = resolveContainedFile(prerenderDir, getRscOutputPath(artifactPathname));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn(`[vinext] Skipping prerender KV seed for ${artifactPathname}: ${formatUnknownError(error)}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!fs.existsSync(htmlPath)) continue;
|
|
81
|
+
if (typeof route.revalidate === "number" && route.revalidate <= 0) continue;
|
|
82
|
+
const revalidateSeconds = typeof route.revalidate === "number" ? route.revalidate : void 0;
|
|
83
|
+
const expireSeconds = typeof route.expire === "number" ? route.expire : void 0;
|
|
84
|
+
const expirationTtl = revalidateSeconds === void 0 ? void 0 : ttlSeconds;
|
|
85
|
+
const tags = buildAppPageCacheTags(cachePathname, []);
|
|
86
|
+
const metadata = buildMetadata(tags);
|
|
87
|
+
const htmlKey = appIsrCacheKey(cachePathname, "html", manifest.buildId);
|
|
88
|
+
const rscKey = appIsrCacheKey(cachePathname, "rsc", manifest.buildId);
|
|
89
|
+
pairs.push({
|
|
90
|
+
key: buildKVKey(options?.appPrefix, htmlKey),
|
|
91
|
+
value: buildCacheEntry({
|
|
92
|
+
kind: "APP_PAGE",
|
|
93
|
+
html: fs.readFileSync(htmlPath, "utf-8"),
|
|
94
|
+
headers: route.headers
|
|
95
|
+
}, tags, now, revalidateSeconds, expireSeconds),
|
|
96
|
+
...expirationTtl !== void 0 ? { expiration_ttl: expirationTtl } : {},
|
|
97
|
+
...metadata ? { metadata } : {}
|
|
98
|
+
});
|
|
99
|
+
if (fs.existsSync(rscPath)) {
|
|
100
|
+
const rscData = fs.readFileSync(rscPath).toString("base64");
|
|
101
|
+
pairs.push({
|
|
102
|
+
key: buildKVKey(options?.appPrefix, rscKey),
|
|
103
|
+
value: buildCacheEntry({
|
|
104
|
+
kind: "APP_PAGE",
|
|
105
|
+
html: "",
|
|
106
|
+
rscData
|
|
107
|
+
}, tags, now, revalidateSeconds, expireSeconds),
|
|
108
|
+
...expirationTtl !== void 0 ? { expiration_ttl: expirationTtl } : {},
|
|
109
|
+
...metadata ? { metadata } : {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
routeCount++;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
routeCount,
|
|
116
|
+
pairs
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { buildPrerenderKVPairs };
|
package/dist/tpr.d.ts
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* or no KV namespace is configured.
|
|
22
22
|
*/
|
|
23
23
|
type TPROptions = {
|
|
24
|
-
/** Project root directory. */root: string; /**
|
|
24
|
+
/** Project root directory. */root: string; /** Wrangler config path, relative to root unless absolute. */
|
|
25
|
+
config?: string; /** Traffic coverage percentage (0–100). Default: 90. */
|
|
25
26
|
coverage: number; /** Hard cap on number of pages to pre-render. Default: 1000. */
|
|
26
27
|
limit: number; /** Analytics lookback window in hours. Default: 24. */
|
|
27
28
|
window: number;
|
|
@@ -42,12 +43,19 @@ type WranglerConfig = {
|
|
|
42
43
|
accountId?: string;
|
|
43
44
|
kvNamespaceId?: string;
|
|
44
45
|
customDomain?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
legacyEnv?: boolean;
|
|
48
|
+
env?: Record<string, WranglerEnvironmentConfig>;
|
|
49
|
+
};
|
|
50
|
+
type WranglerEnvironmentConfig = {
|
|
51
|
+
customDomain?: string;
|
|
52
|
+
name?: string;
|
|
45
53
|
};
|
|
46
54
|
/**
|
|
47
55
|
* Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:
|
|
48
56
|
* account_id, VINEXT_KV_CACHE KV namespace ID, and custom domain.
|
|
49
57
|
*/
|
|
50
|
-
declare function parseWranglerConfig(root: string): WranglerConfig | null;
|
|
58
|
+
declare function parseWranglerConfig(root: string, configPath?: string): WranglerConfig | null;
|
|
51
59
|
/**
|
|
52
60
|
* Generate zone lookup candidates from shortest (2-part) to longest.
|
|
53
61
|
* Tries the most common case first (e.g., "example.com") and progressively
|
|
@@ -80,4 +88,4 @@ declare function buildTprKVPairs(entries: Map<string, PrerenderResult>, buildId:
|
|
|
80
88
|
*/
|
|
81
89
|
declare function runTPR(options: TPROptions): Promise<TPRResult>;
|
|
82
90
|
//#endregion
|
|
83
|
-
export { TPROptions, TPRResult, buildTprKVPairs, domainCandidates, parseWranglerConfig, resolveVinextProdServerPath, runTPR };
|
|
91
|
+
export { TPROptions, TPRResult, WranglerEnvironmentConfig, buildTprKVPairs, domainCandidates, parseWranglerConfig, resolveVinextProdServerPath, runTPR };
|