@vinext/cloudflare 0.2.1 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/kv-data-adapter.d.ts +2 -1
- package/dist/cache/kv-data-adapter.js +2 -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 +329 -35
- 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 +10 -2
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
17
|
import { loadNextConfig, resolveNextConfig } from "vinext/internal/config/next-config";
|
|
12
|
-
import { formatVinextPrerenderLabel, loadVinextPrerenderConfigFromViteConfig, resolveVinextPrerenderDecision } from "vinext/internal/config/prerender";
|
|
18
|
+
import { formatVinextPrerenderLabel, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, 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"]),
|
|
@@ -131,6 +171,27 @@ async function runBuild(info, env) {
|
|
|
131
171
|
await (await createBuilder({ root: info.root })).buildApp();
|
|
132
172
|
});
|
|
133
173
|
}
|
|
174
|
+
async function populateKVCacheFromPrerenderedArtifacts(root, buildEnv, wranglerEnv) {
|
|
175
|
+
if (!viteConfigHasCacheAdapter(root)) return;
|
|
176
|
+
const vite = await loadProjectViteApi(root);
|
|
177
|
+
const kvConfig = resolveKvDataAdapterConfig(await withCloudflareEnv(buildEnv, () => loadVinextCacheConfigFromViteConfig(vite, root)));
|
|
178
|
+
if (!kvConfig) return;
|
|
179
|
+
const { routeCount, pairs } = buildPrerenderKVPairs(path.join(root, "dist", "server"), {
|
|
180
|
+
appPrefix: kvConfig.appPrefix,
|
|
181
|
+
ttlSeconds: kvConfig.ttlSeconds
|
|
182
|
+
});
|
|
183
|
+
if (pairs.length === 0) {
|
|
184
|
+
console.log(" KV cache: Skipping prerender upload (no App Router prerendered cache entries found).");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await runWranglerKVBulkPut(root, {
|
|
188
|
+
binding: kvConfig.binding,
|
|
189
|
+
env: wranglerEnv,
|
|
190
|
+
pairs
|
|
191
|
+
});
|
|
192
|
+
console.log(` KV cache: Uploaded ${pairs.length} entr${pairs.length === 1 ? "y" : "ies"} for ${routeCount} prerendered route${routeCount === 1 ? "" : "s"}.`);
|
|
193
|
+
}
|
|
194
|
+
const KV_BULK_PUT_CHUNK_SIZE = 25;
|
|
134
195
|
function validateWranglerEnvName(env) {
|
|
135
196
|
if (env.includes("\0")) throw new Error("Wrangler environment names cannot contain null bytes.");
|
|
136
197
|
return env;
|
|
@@ -138,6 +199,25 @@ function validateWranglerEnvName(env) {
|
|
|
138
199
|
function buildWranglerDeployArgs(options) {
|
|
139
200
|
const args = ["deploy"];
|
|
140
201
|
const env = options.env || (options.preview ? "preview" : void 0);
|
|
202
|
+
if (options.config) args.push("--config", options.config);
|
|
203
|
+
if (options.name) args.push("--name", options.name);
|
|
204
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
205
|
+
return {
|
|
206
|
+
args,
|
|
207
|
+
env
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function buildWranglerKVBulkPutArgs(options) {
|
|
211
|
+
const env = options.env || void 0;
|
|
212
|
+
const args = [
|
|
213
|
+
"kv",
|
|
214
|
+
"bulk",
|
|
215
|
+
"put",
|
|
216
|
+
options.filePath,
|
|
217
|
+
"--binding",
|
|
218
|
+
options.binding,
|
|
219
|
+
"--remote"
|
|
220
|
+
];
|
|
141
221
|
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
142
222
|
return {
|
|
143
223
|
args,
|
|
@@ -179,21 +259,196 @@ function buildWranglerInvocation(root, options, nodeExecutable = process.execPat
|
|
|
179
259
|
env
|
|
180
260
|
};
|
|
181
261
|
}
|
|
182
|
-
function
|
|
183
|
-
const
|
|
262
|
+
async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutable = process.execPath) {
|
|
263
|
+
const tempDir = fs.mkdtempSync(path.join(options.tempDir ?? os.tmpdir(), "vinext-kv-bulk-"));
|
|
264
|
+
try {
|
|
265
|
+
const wranglerBin = resolveWranglerBin(root);
|
|
266
|
+
const totalChunks = Math.ceil(options.pairs.length / KV_BULK_PUT_CHUNK_SIZE);
|
|
267
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
268
|
+
const filePath = path.join(tempDir, `prerender-kv-${i}.json`);
|
|
269
|
+
const chunk = options.pairs.slice(i * KV_BULK_PUT_CHUNK_SIZE, (i + 1) * KV_BULK_PUT_CHUNK_SIZE);
|
|
270
|
+
fs.writeFileSync(filePath, JSON.stringify(chunk), "utf-8");
|
|
271
|
+
const { args } = buildWranglerKVBulkPutArgs({
|
|
272
|
+
binding: options.binding,
|
|
273
|
+
env: options.env,
|
|
274
|
+
filePath
|
|
275
|
+
});
|
|
276
|
+
const invocation = buildNodeCliInvocation(wranglerBin, args, nodeExecutable);
|
|
277
|
+
const child = execute(invocation.file, invocation.args, {
|
|
278
|
+
cwd: root,
|
|
279
|
+
stdio: "inherit",
|
|
280
|
+
shell: false
|
|
281
|
+
});
|
|
282
|
+
await new Promise((resolve, reject) => {
|
|
283
|
+
child.once("error", reject);
|
|
284
|
+
child.once("close", (code, signal) => {
|
|
285
|
+
if (code === 0) {
|
|
286
|
+
resolve();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
|
290
|
+
reject(/* @__PURE__ */ new Error(`Wrangler KV bulk put failed with ${exitReason}.`));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
fs.rmSync(tempDir, {
|
|
296
|
+
recursive: true,
|
|
297
|
+
force: true
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function runWranglerDeploy(root, options, execute = spawn) {
|
|
302
|
+
const spawnOptions = {
|
|
184
303
|
cwd: root,
|
|
185
|
-
stdio:
|
|
186
|
-
|
|
304
|
+
stdio: [
|
|
305
|
+
"inherit",
|
|
306
|
+
"pipe",
|
|
307
|
+
"pipe"
|
|
308
|
+
],
|
|
187
309
|
shell: false
|
|
188
310
|
};
|
|
189
311
|
const { file, args, env } = buildWranglerInvocation(root, options);
|
|
190
312
|
if (env) console.log(`\n Deploying to env: ${env}...`);
|
|
191
313
|
else console.log("\n Deploying to production...");
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
314
|
+
const child = execute(file, args, spawnOptions);
|
|
315
|
+
let output = "";
|
|
316
|
+
child.stdout?.on("data", (chunk) => {
|
|
317
|
+
const text = chunk.toString();
|
|
318
|
+
output += text;
|
|
319
|
+
process.stdout.write(text);
|
|
320
|
+
});
|
|
321
|
+
child.stderr?.on("data", (chunk) => {
|
|
322
|
+
const text = chunk.toString();
|
|
323
|
+
output += text;
|
|
324
|
+
process.stderr.write(text);
|
|
325
|
+
});
|
|
326
|
+
await new Promise((resolve, reject) => {
|
|
327
|
+
child.once("error", reject);
|
|
328
|
+
child.once("close", (code, signal) => {
|
|
329
|
+
if (code === 0) {
|
|
330
|
+
resolve();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
|
334
|
+
reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
return parseWorkersDevUrl(output) ?? "(URL not detected in wrangler output)";
|
|
338
|
+
}
|
|
339
|
+
async function deployWithCdnWarmup(root, paths, options) {
|
|
340
|
+
const upload = runWranglerVersionUpload(root, options);
|
|
341
|
+
const wranglerConfig = parseWranglerConfig(root, options.config);
|
|
342
|
+
const stagingTraffic = getZeroPercentStagingTraffic(readWranglerDeploymentStatus(root, options), upload.versionId);
|
|
343
|
+
let staged = null;
|
|
344
|
+
let triggersDeployedUrl = null;
|
|
345
|
+
let warmedBeforePromotion = false;
|
|
346
|
+
let triggersApplied = false;
|
|
347
|
+
function applyTriggers() {
|
|
348
|
+
if (triggersApplied) return;
|
|
349
|
+
triggersDeployedUrl = runWranglerTriggersDeploy(root, options).deployedUrl;
|
|
350
|
+
triggersApplied = true;
|
|
351
|
+
}
|
|
352
|
+
if (stagingTraffic) {
|
|
353
|
+
staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
|
|
354
|
+
try {
|
|
355
|
+
applyTriggers();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw withStagedVersionCleanupNote(error);
|
|
358
|
+
}
|
|
359
|
+
const targetUrl = resolveCdnWarmupTargetUrl(root, staged.deployedUrl ?? triggersDeployedUrl, options);
|
|
360
|
+
const headers = buildVersionOverrideHeaders(resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
|
|
361
|
+
if (targetUrl && headers) {
|
|
362
|
+
try {
|
|
363
|
+
await warmCdnCache({
|
|
364
|
+
targetUrl,
|
|
365
|
+
paths,
|
|
366
|
+
headers,
|
|
367
|
+
concurrency: options.warmCdnConcurrency,
|
|
368
|
+
timeoutMs: options.warmCdnTimeout,
|
|
369
|
+
retries: options.warmCdnRetries,
|
|
370
|
+
strict: options.warmCdnStrict
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw withStagedVersionCleanupNote(error);
|
|
374
|
+
}
|
|
375
|
+
warmedBeforePromotion = true;
|
|
376
|
+
} 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());
|
|
377
|
+
} else console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not a single 100% version.");
|
|
378
|
+
const deployed = runWranglerVersionDeploy(root, [{
|
|
379
|
+
versionId: upload.versionId,
|
|
380
|
+
percentage: 100
|
|
381
|
+
}], options, warmedBeforePromotion ? "promote-warmed" : "promote-uploaded");
|
|
382
|
+
if (!warmedBeforePromotion) {
|
|
383
|
+
try {
|
|
384
|
+
applyTriggers();
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw withPromotedVersionTriggerNote(error);
|
|
387
|
+
}
|
|
388
|
+
const targetUrl = resolveCdnWarmupTargetUrl(root, deployed.deployedUrl ?? triggersDeployedUrl, options);
|
|
389
|
+
if (targetUrl) await warmCdnCache({
|
|
390
|
+
targetUrl,
|
|
391
|
+
paths,
|
|
392
|
+
concurrency: options.warmCdnConcurrency,
|
|
393
|
+
timeoutMs: options.warmCdnTimeout,
|
|
394
|
+
retries: options.warmCdnRetries,
|
|
395
|
+
strict: options.warmCdnStrict
|
|
396
|
+
});
|
|
397
|
+
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.");
|
|
398
|
+
else console.warn(" CDN warmup skipped: no production URL could be inferred from wrangler config or output.");
|
|
399
|
+
}
|
|
400
|
+
return deployed.deployedUrl ?? triggersDeployedUrl ?? staged?.deployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
|
|
401
|
+
}
|
|
402
|
+
function resolveCdnWarmupTargetUrl(root, deployedUrl, options) {
|
|
403
|
+
const config = parseWranglerConfig(root, options?.config);
|
|
404
|
+
const env = getWranglerTargetEnv(options ?? {});
|
|
405
|
+
const customDomain = (env ? config?.env?.[env]?.customDomain : void 0) ?? config?.customDomain;
|
|
406
|
+
if (customDomain) return `https://${customDomain}`;
|
|
407
|
+
return deployedUrl;
|
|
408
|
+
}
|
|
409
|
+
function readWranglerDeploymentStatus(root, options) {
|
|
410
|
+
try {
|
|
411
|
+
return runWranglerDeploymentStatus(root, options);
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function getZeroPercentStagingTraffic(deployment, versionId) {
|
|
417
|
+
const current = deployment?.versions ?? [];
|
|
418
|
+
if (current.length !== 1 || current[0].percentage !== 100) return null;
|
|
419
|
+
if (current[0].versionId === versionId) return null;
|
|
420
|
+
return [current[0], {
|
|
421
|
+
versionId,
|
|
422
|
+
percentage: 0
|
|
423
|
+
}];
|
|
424
|
+
}
|
|
425
|
+
function getWranglerTargetEnv(options) {
|
|
426
|
+
return options.env || (options.preview ? "preview" : void 0);
|
|
427
|
+
}
|
|
428
|
+
function resolveWorkerNameForVersionOverride(config, options) {
|
|
429
|
+
if (options.name) return options.name;
|
|
430
|
+
const env = getWranglerTargetEnv(options);
|
|
431
|
+
if (!env) return config?.name;
|
|
432
|
+
if (config?.legacyEnv === false) return config.name;
|
|
433
|
+
return config?.env?.[env]?.name ?? (config?.name ? `${config.name}-${env}` : void 0);
|
|
434
|
+
}
|
|
435
|
+
function quoteStructuredHeaderString(value) {
|
|
436
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
437
|
+
}
|
|
438
|
+
function buildVersionOverrideHeaders(workerName, versionId) {
|
|
439
|
+
if (!workerName) return void 0;
|
|
440
|
+
return { "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}` };
|
|
441
|
+
}
|
|
442
|
+
function withStagedVersionCleanupNote(error) {
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
|
|
445
|
+
}
|
|
446
|
+
function getStagedVersionCleanupNote() {
|
|
447
|
+
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.";
|
|
448
|
+
}
|
|
449
|
+
function withPromotedVersionTriggerNote(error) {
|
|
450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
451
|
+
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
452
|
}
|
|
198
453
|
async function deploy(options) {
|
|
199
454
|
const deployEnv = validateWranglerEnvName(options.env || (options.preview ? "preview" : "production"));
|
|
@@ -228,45 +483,84 @@ async function deploy(options) {
|
|
|
228
483
|
console.log("\n Dry run complete. No build or deploy performed.\n");
|
|
229
484
|
return;
|
|
230
485
|
}
|
|
486
|
+
const nextConfig = await resolveNextConfig(await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
|
|
231
487
|
const buildEnv = deployEnv === "production" && !options.env ? void 0 : deployEnv;
|
|
488
|
+
const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? await withCloudflareEnv(buildEnv, async () => {
|
|
489
|
+
return loadVinextPrerenderConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
|
|
490
|
+
}) : null;
|
|
491
|
+
const prerenderDecision = resolveVinextPrerenderDecision({
|
|
492
|
+
prerenderAllFlag: options.prerenderAll,
|
|
493
|
+
vinextPrerenderConfig,
|
|
494
|
+
nextOutput: nextConfig.output
|
|
495
|
+
});
|
|
496
|
+
const shouldEmitPrerenderPathManifest = options.warmCdnCache || !options.skipBuild && prerenderDecision;
|
|
232
497
|
if (!options.skipBuild) await runBuild(info, buildEnv);
|
|
233
498
|
else console.log("\n Skipping build (--skip-build)");
|
|
234
|
-
{
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
return loadVinextPrerenderConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
|
|
238
|
-
}) : null;
|
|
239
|
-
const prerenderDecision = resolveVinextPrerenderDecision({
|
|
240
|
-
prerenderAllFlag: options.prerenderAll,
|
|
241
|
-
vinextPrerenderConfig,
|
|
242
|
-
nextOutput: nextConfig.output
|
|
499
|
+
if (shouldEmitPrerenderPathManifest) {
|
|
500
|
+
const routeRootConfig = await withCloudflareEnv(buildEnv, async () => {
|
|
501
|
+
return loadVinextRouteRootConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
|
|
243
502
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
503
|
+
await emitPrerenderPathManifest({
|
|
504
|
+
root: info.root,
|
|
505
|
+
nextConfigOverride: nextConfig,
|
|
506
|
+
routeRootConfig
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
let ranPrerender = false;
|
|
510
|
+
if (prerenderDecision) {
|
|
511
|
+
console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
|
|
512
|
+
if (nextConfig.enablePrerenderSourceMaps) {
|
|
513
|
+
process.setSourceMapsEnabled(true);
|
|
514
|
+
Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
|
|
254
515
|
}
|
|
516
|
+
await runPrerender({
|
|
517
|
+
root: info.root,
|
|
518
|
+
concurrency: options.prerenderConcurrency
|
|
519
|
+
});
|
|
520
|
+
ranPrerender = true;
|
|
521
|
+
}
|
|
522
|
+
if (ranPrerender) try {
|
|
523
|
+
await populateKVCacheFromPrerenderedArtifacts(root, buildEnv, deployEnv === "production" && !options.env ? void 0 : deployEnv);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.log(` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`);
|
|
255
526
|
}
|
|
256
527
|
if (options.experimentalTPR) {
|
|
257
528
|
console.log();
|
|
258
529
|
const tprResult = await runTPR({
|
|
259
530
|
root,
|
|
531
|
+
config: options.config,
|
|
260
532
|
coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
|
|
261
533
|
limit: Math.max(1, options.tprLimit ?? 1e3),
|
|
262
534
|
window: Math.max(1, options.tprWindow ?? 24)
|
|
263
535
|
});
|
|
264
536
|
if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
|
|
265
537
|
}
|
|
266
|
-
const
|
|
538
|
+
const wranglerOptions = {
|
|
539
|
+
env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
|
|
540
|
+
name: options.name,
|
|
541
|
+
config: options.config
|
|
542
|
+
};
|
|
543
|
+
let url;
|
|
544
|
+
if (options.warmCdnCache) {
|
|
545
|
+
const warmPaths = readPrerenderWarmPaths(root, {
|
|
546
|
+
includeFallbackShells: options.warmCdnIncludeFallbacks,
|
|
547
|
+
strict: options.warmCdnStrict
|
|
548
|
+
});
|
|
549
|
+
if (warmPaths.length > 0) url = await deployWithCdnWarmup(root, warmPaths, {
|
|
550
|
+
...wranglerOptions,
|
|
551
|
+
warmCdnConcurrency: options.warmCdnConcurrency,
|
|
552
|
+
warmCdnTimeout: options.warmCdnTimeout,
|
|
553
|
+
warmCdnRetries: options.warmCdnRetries,
|
|
554
|
+
warmCdnStrict: options.warmCdnStrict
|
|
555
|
+
});
|
|
556
|
+
else {
|
|
557
|
+
console.log("\n CDN warmup skipped: no build-discovered paths found.");
|
|
558
|
+
url = await runWranglerDeploy(root, wranglerOptions);
|
|
559
|
+
}
|
|
560
|
+
} else url = await runWranglerDeploy(root, wranglerOptions);
|
|
267
561
|
console.log("\n ─────────────────────────────────────────");
|
|
268
562
|
console.log(` Deployed to: ${url}`);
|
|
269
563
|
console.log(" ─────────────────────────────────────────\n");
|
|
270
564
|
}
|
|
271
565
|
//#endregion
|
|
272
|
-
export { buildNodeCliInvocation, buildWranglerDeployArgs, buildWranglerInvocation, deploy, parseDeployArgs, resolveWranglerBin, runWranglerDeploy, validateWranglerEnvName, withCloudflareEnv };
|
|
566
|
+
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 };
|