@vinext/cloudflare 0.2.0 → 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/cache/kv-data-adapter.runtime.js +8 -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 +330 -40
- 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 +172 -13
- 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"));
|
|
@@ -214,11 +469,7 @@ async function deploy(options) {
|
|
|
214
469
|
console.log(` Project: ${info.projectName}`);
|
|
215
470
|
console.log(` Router: ${info.isAppRouter ? "App Router" : "Pages Router"}`);
|
|
216
471
|
console.log(` ISR: ${info.hasISR ? "detected" : "none"}`);
|
|
217
|
-
const missingScaffolding = [
|
|
218
|
-
!info.hasViteConfig && "Vite config",
|
|
219
|
-
!info.hasWranglerConfig && "Wrangler config",
|
|
220
|
-
info.isPagesRouter && !info.hasWorkerEntry && "Worker entry"
|
|
221
|
-
].filter((value) => Boolean(value));
|
|
472
|
+
const missingScaffolding = [!info.hasViteConfig && "Vite config", !info.hasWranglerConfig && "Wrangler config"].filter((value) => Boolean(value));
|
|
222
473
|
if (missingScaffolding.length > 0) throw new Error(`Missing Cloudflare deployment setup: ${missingScaffolding.join(", ")}. Run \`vinext init --platform=cloudflare\` first.`);
|
|
223
474
|
const missingDeps = getMissingDeps(info);
|
|
224
475
|
if (missingDeps.length > 0) throw new Error(`Missing deployment dependencies: ${missingDeps.map((dependency) => dependency.name).join(", ")}. Run \`vinext init --platform=cloudflare\` first.`);
|
|
@@ -232,45 +483,84 @@ async function deploy(options) {
|
|
|
232
483
|
console.log("\n Dry run complete. No build or deploy performed.\n");
|
|
233
484
|
return;
|
|
234
485
|
}
|
|
486
|
+
const nextConfig = await resolveNextConfig(await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
|
|
235
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;
|
|
236
497
|
if (!options.skipBuild) await runBuild(info, buildEnv);
|
|
237
498
|
else console.log("\n Skipping build (--skip-build)");
|
|
238
|
-
{
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
return loadVinextPrerenderConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
|
|
242
|
-
}) : null;
|
|
243
|
-
const prerenderDecision = resolveVinextPrerenderDecision({
|
|
244
|
-
prerenderAllFlag: options.prerenderAll,
|
|
245
|
-
vinextPrerenderConfig,
|
|
246
|
-
nextOutput: nextConfig.output
|
|
499
|
+
if (shouldEmitPrerenderPathManifest) {
|
|
500
|
+
const routeRootConfig = await withCloudflareEnv(buildEnv, async () => {
|
|
501
|
+
return loadVinextRouteRootConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
|
|
247
502
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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);
|
|
258
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.`);
|
|
259
526
|
}
|
|
260
527
|
if (options.experimentalTPR) {
|
|
261
528
|
console.log();
|
|
262
529
|
const tprResult = await runTPR({
|
|
263
530
|
root,
|
|
531
|
+
config: options.config,
|
|
264
532
|
coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
|
|
265
533
|
limit: Math.max(1, options.tprLimit ?? 1e3),
|
|
266
534
|
window: Math.max(1, options.tprWindow ?? 24)
|
|
267
535
|
});
|
|
268
536
|
if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
|
|
269
537
|
}
|
|
270
|
-
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);
|
|
271
561
|
console.log("\n ─────────────────────────────────────────");
|
|
272
562
|
console.log(` Deployed to: ${url}`);
|
|
273
563
|
console.log(" ─────────────────────────────────────────\n");
|
|
274
564
|
}
|
|
275
565
|
//#endregion
|
|
276
|
-
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 };
|