@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/tpr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ENTRY_PREFIX } from "./cache/kv-data-adapter.runtime.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { VINEXT_REVALIDATE_HEADER } from "vinext/internal/server/headers";
|
|
@@ -33,13 +33,24 @@ import { buildAppPageCacheTags } from "vinext/internal/server/app-page-cache";
|
|
|
33
33
|
* Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:
|
|
34
34
|
* account_id, VINEXT_KV_CACHE KV namespace ID, and custom domain.
|
|
35
35
|
*/
|
|
36
|
-
function parseWranglerConfig(root) {
|
|
36
|
+
function parseWranglerConfig(root, configPath) {
|
|
37
|
+
if (configPath) {
|
|
38
|
+
const filepath = path.resolve(root, configPath);
|
|
39
|
+
if (!fs.existsSync(filepath)) return null;
|
|
40
|
+
const content = fs.readFileSync(filepath, "utf-8");
|
|
41
|
+
if (filepath.endsWith(".toml")) return extractFromTOML(content);
|
|
42
|
+
try {
|
|
43
|
+
return extractFromJSON(JSON.parse(stripJsonCommentsAndTrailingCommas(content)));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
for (const filename of ["wrangler.jsonc", "wrangler.json"]) {
|
|
38
49
|
const filepath = path.join(root, filename);
|
|
39
50
|
if (fs.existsSync(filepath)) {
|
|
40
51
|
const content = fs.readFileSync(filepath, "utf-8");
|
|
41
52
|
try {
|
|
42
|
-
return extractFromJSON(JSON.parse(
|
|
53
|
+
return extractFromJSON(JSON.parse(stripJsonCommentsAndTrailingCommas(content)));
|
|
43
54
|
} catch {
|
|
44
55
|
continue;
|
|
45
56
|
}
|
|
@@ -50,10 +61,10 @@ function parseWranglerConfig(root) {
|
|
|
50
61
|
return null;
|
|
51
62
|
}
|
|
52
63
|
/**
|
|
53
|
-
* Strip single-line (//)
|
|
54
|
-
* preserving strings that contain
|
|
64
|
+
* Strip single-line (//), multi-line comments, and trailing commas from JSONC
|
|
65
|
+
* while preserving strings that contain comment-like text or commas.
|
|
55
66
|
*/
|
|
56
|
-
function
|
|
67
|
+
function stripJsonCommentsAndTrailingCommas(str) {
|
|
57
68
|
let result = "";
|
|
58
69
|
let inString = false;
|
|
59
70
|
let inSingleLine = false;
|
|
@@ -106,12 +117,41 @@ function stripJsonComments(str) {
|
|
|
106
117
|
i++;
|
|
107
118
|
continue;
|
|
108
119
|
}
|
|
120
|
+
if (!inString && ch === "," && isJsonTrailingComma(str, i + 1)) continue;
|
|
109
121
|
result += ch;
|
|
110
122
|
}
|
|
111
123
|
return result;
|
|
112
124
|
}
|
|
125
|
+
function isJsonTrailingComma(str, start) {
|
|
126
|
+
for (let i = start; i < str.length; i++) {
|
|
127
|
+
const ch = str[i];
|
|
128
|
+
const next = str[i + 1];
|
|
129
|
+
if (ch === void 0) return false;
|
|
130
|
+
if (/\s/.test(ch)) continue;
|
|
131
|
+
if (ch === "/" && next === "/") {
|
|
132
|
+
i += 2;
|
|
133
|
+
while (i < str.length && str[i] !== "\n") i++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === "/" && next === "*") {
|
|
137
|
+
i += 2;
|
|
138
|
+
while (i < str.length) {
|
|
139
|
+
if (str[i] === "*" && str[i + 1] === "/") {
|
|
140
|
+
i++;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
return ch === "}" || ch === "]";
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
113
151
|
function extractFromJSON(config) {
|
|
114
152
|
const result = {};
|
|
153
|
+
if (typeof config.name === "string" && config.name.length > 0) result.name = config.name;
|
|
154
|
+
if (typeof config.legacy_env === "boolean") result.legacyEnv = config.legacy_env;
|
|
115
155
|
if (typeof config.account_id === "string") result.accountId = config.account_id;
|
|
116
156
|
if (Array.isArray(config.kv_namespaces)) {
|
|
117
157
|
const vinextKV = config.kv_namespaces.find((ns) => ns && typeof ns === "object" && (ns.binding === "VINEXT_KV_CACHE" || ns.binding === "VINEXT_CACHE"));
|
|
@@ -119,6 +159,25 @@ function extractFromJSON(config) {
|
|
|
119
159
|
}
|
|
120
160
|
const domain = extractDomainFromRoutes(config.routes) ?? extractDomainFromCustomDomains(config);
|
|
121
161
|
if (domain) result.customDomain = domain;
|
|
162
|
+
const env = extractEnvConfigs(config.env);
|
|
163
|
+
if (env) result.env = env;
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function extractEnvConfigs(envs) {
|
|
167
|
+
if (!envs || typeof envs !== "object" || Array.isArray(envs)) return void 0;
|
|
168
|
+
const result = {};
|
|
169
|
+
for (const [envName, rawConfig] of Object.entries(envs)) {
|
|
170
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) continue;
|
|
171
|
+
const envConfig = extractEnvironmentConfig(rawConfig);
|
|
172
|
+
if (envConfig.name || envConfig.customDomain) result[envName] = envConfig;
|
|
173
|
+
}
|
|
174
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
175
|
+
}
|
|
176
|
+
function extractEnvironmentConfig(config) {
|
|
177
|
+
const result = {};
|
|
178
|
+
if (typeof config.name === "string" && config.name.length > 0) result.name = config.name;
|
|
179
|
+
const domain = extractDomainFromRoutes(config.routes) ?? extractDomainFromCustomDomains(config);
|
|
180
|
+
if (domain) result.customDomain = domain;
|
|
122
181
|
return result;
|
|
123
182
|
}
|
|
124
183
|
function extractDomainFromRoutes(routes) {
|
|
@@ -152,6 +211,10 @@ function cleanDomain(raw) {
|
|
|
152
211
|
*/
|
|
153
212
|
function extractFromTOML(content) {
|
|
154
213
|
const result = {};
|
|
214
|
+
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
215
|
+
if (nameMatch) result.name = nameMatch[1];
|
|
216
|
+
const legacyEnvMatch = content.match(/^legacy_env\s*=\s*(true|false)\s*$/m);
|
|
217
|
+
if (legacyEnvMatch) result.legacyEnv = legacyEnvMatch[1] === "true";
|
|
155
218
|
const accountMatch = content.match(/^account_id\s*=\s*"([^"]+)"/m);
|
|
156
219
|
if (accountMatch) result.accountId = accountMatch[1];
|
|
157
220
|
const kvBlocks = content.split(/\[\[kv_namespaces\]\]/);
|
|
@@ -179,8 +242,83 @@ function extractFromTOML(content) {
|
|
|
179
242
|
}
|
|
180
243
|
}
|
|
181
244
|
}
|
|
245
|
+
const env = extractEnvConfigsFromTOML(content);
|
|
246
|
+
if (env) result.env = env;
|
|
182
247
|
return result;
|
|
183
248
|
}
|
|
249
|
+
function extractEnvConfigsFromTOML(content) {
|
|
250
|
+
const result = {};
|
|
251
|
+
for (const section of getTomlSections(content)) {
|
|
252
|
+
const envName = section.header.match(/^env\.([^.]+)$/)?.[1];
|
|
253
|
+
if (envName) {
|
|
254
|
+
const envConfig = result[envName] ?? {};
|
|
255
|
+
const nameMatch = section.body.match(/^name\s*=\s*"([^"]+)"/m);
|
|
256
|
+
if (nameMatch) envConfig.name = nameMatch[1];
|
|
257
|
+
const domain = extractTomlScalarRouteDomain(section.body) ?? extractTomlRoutesArrayDomain(section.body);
|
|
258
|
+
if (domain) envConfig.customDomain = domain;
|
|
259
|
+
if (envConfig.name || envConfig.customDomain) result[envName] = envConfig;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const routesEnvName = section.header.match(/^env\.([^.]+)\.routes$/)?.[1];
|
|
263
|
+
if (routesEnvName) {
|
|
264
|
+
const envConfig = result[routesEnvName] ?? {};
|
|
265
|
+
const domain = extractTomlRouteBlockDomain(section.body);
|
|
266
|
+
if (domain) envConfig.customDomain = domain;
|
|
267
|
+
if (envConfig.name || envConfig.customDomain) result[routesEnvName] = envConfig;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
271
|
+
}
|
|
272
|
+
function getTomlSections(content) {
|
|
273
|
+
const sections = [];
|
|
274
|
+
let currentHeader = null;
|
|
275
|
+
let currentBody = [];
|
|
276
|
+
for (const line of content.split("\n")) {
|
|
277
|
+
const header = parseTomlSectionHeader(line);
|
|
278
|
+
if (header) {
|
|
279
|
+
if (currentHeader) sections.push({
|
|
280
|
+
header: currentHeader,
|
|
281
|
+
body: currentBody.join("\n")
|
|
282
|
+
});
|
|
283
|
+
currentHeader = header;
|
|
284
|
+
currentBody = [];
|
|
285
|
+
} else if (currentHeader) currentBody.push(line);
|
|
286
|
+
}
|
|
287
|
+
if (currentHeader) sections.push({
|
|
288
|
+
header: currentHeader,
|
|
289
|
+
body: currentBody.join("\n")
|
|
290
|
+
});
|
|
291
|
+
return sections;
|
|
292
|
+
}
|
|
293
|
+
function parseTomlSectionHeader(line) {
|
|
294
|
+
const trimmed = line.trim();
|
|
295
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
296
|
+
const isArrayHeader = trimmed.startsWith("[[") && trimmed.endsWith("]]");
|
|
297
|
+
const start = isArrayHeader ? 2 : 1;
|
|
298
|
+
const end = isArrayHeader ? trimmed.length - 2 : trimmed.length - 1;
|
|
299
|
+
const header = trimmed.slice(start, end).trim();
|
|
300
|
+
return header.length > 0 ? header : null;
|
|
301
|
+
}
|
|
302
|
+
function extractTomlScalarRouteDomain(section) {
|
|
303
|
+
const routeMatch = section.match(/^route\s*=\s*"([^"]+)"/m);
|
|
304
|
+
if (!routeMatch) return null;
|
|
305
|
+
const domain = cleanDomain(routeMatch[1]);
|
|
306
|
+
return domain && !domain.includes("workers.dev") ? domain : null;
|
|
307
|
+
}
|
|
308
|
+
function extractTomlRoutesArrayDomain(section) {
|
|
309
|
+
const routesMatch = section.match(/^routes\s*=\s*\[([\s\S]*?)\]/m);
|
|
310
|
+
if (!routesMatch) return null;
|
|
311
|
+
const patternMatch = (routesMatch[1] ?? "").match(/(?:pattern\s*=\s*)?"([^"]+)"/);
|
|
312
|
+
if (!patternMatch) return null;
|
|
313
|
+
const domain = cleanDomain(patternMatch[1]);
|
|
314
|
+
return domain && !domain.includes("workers.dev") ? domain : null;
|
|
315
|
+
}
|
|
316
|
+
function extractTomlRouteBlockDomain(section) {
|
|
317
|
+
const patternMatch = section.match(/^(?:pattern|zone_name)\s*=\s*"([^"]+)"/m);
|
|
318
|
+
if (!patternMatch) return null;
|
|
319
|
+
const domain = cleanDomain(patternMatch[1]);
|
|
320
|
+
return domain && !domain.includes("workers.dev") ? domain : null;
|
|
321
|
+
}
|
|
184
322
|
/**
|
|
185
323
|
* Generate zone lookup candidates from shortest (2-part) to longest.
|
|
186
324
|
* Tries the most common case first (e.g., "example.com") and progressively
|
|
@@ -306,6 +444,25 @@ const PRERENDER_PORT = 19384;
|
|
|
306
444
|
const SERVER_STARTUP_TIMEOUT = 3e4;
|
|
307
445
|
/** Max concurrent fetch requests during pre-rendering. */
|
|
308
446
|
const FETCH_CONCURRENCY = 10;
|
|
447
|
+
const NON_CACHEABLE_CACHE_CONTROL_RE = /\b(?:no-store|no-cache|private)\b/i;
|
|
448
|
+
function getTprHeader(headers, name) {
|
|
449
|
+
const normalizedName = name.toLowerCase();
|
|
450
|
+
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === normalizedName) return value;
|
|
451
|
+
}
|
|
452
|
+
function hasNonCacheableCacheControl(headers) {
|
|
453
|
+
const cacheControl = getTprHeader(headers, "cache-control");
|
|
454
|
+
return cacheControl ? NON_CACHEABLE_CACHE_CONTROL_RE.test(cacheControl) : false;
|
|
455
|
+
}
|
|
456
|
+
function readTprRevalidateHeader(headers) {
|
|
457
|
+
const revalidateHeader = getTprHeader(headers, VINEXT_REVALIDATE_HEADER);
|
|
458
|
+
if (revalidateHeader === void 0) return void 0;
|
|
459
|
+
return Number(revalidateHeader);
|
|
460
|
+
}
|
|
461
|
+
function isTprCacheable(headers) {
|
|
462
|
+
if (hasNonCacheableCacheControl(headers)) return false;
|
|
463
|
+
const revalidate = readTprRevalidateHeader(headers);
|
|
464
|
+
return revalidate === void 0 || Number.isFinite(revalidate) && revalidate > 0;
|
|
465
|
+
}
|
|
309
466
|
/**
|
|
310
467
|
* Start a local production server, fetch each route to produce HTML,
|
|
311
468
|
* and return the results. Pages that fail to render are skipped.
|
|
@@ -333,11 +490,12 @@ async function prerenderRoutes(routes, root, hostDomain) {
|
|
|
333
490
|
redirect: "manual"
|
|
334
491
|
});
|
|
335
492
|
if (response.status < 400) {
|
|
336
|
-
const html = await response.text();
|
|
337
493
|
const headers = {};
|
|
338
494
|
response.headers.forEach((value, key) => {
|
|
339
495
|
if (key === "content-type" || key === "cache-control" || key === VINEXT_REVALIDATE_HEADER || key === "location") headers[key] = value;
|
|
340
496
|
});
|
|
497
|
+
if (!isTprCacheable(headers)) return;
|
|
498
|
+
const html = await response.text();
|
|
341
499
|
results.set(routePath, {
|
|
342
500
|
html,
|
|
343
501
|
status: response.status,
|
|
@@ -427,10 +585,11 @@ function buildTprKVPairs(entries, buildId, defaultRevalidateSeconds) {
|
|
|
427
585
|
const now = Date.now();
|
|
428
586
|
const pairs = [];
|
|
429
587
|
for (const [routePath, result] of entries) {
|
|
430
|
-
|
|
431
|
-
const revalidateSeconds =
|
|
432
|
-
|
|
433
|
-
const
|
|
588
|
+
if (!isTprCacheable(result.headers)) continue;
|
|
589
|
+
const revalidateSeconds = readTprRevalidateHeader(result.headers) ?? defaultRevalidateSeconds;
|
|
590
|
+
if (!Number.isFinite(revalidateSeconds) || revalidateSeconds <= 0) continue;
|
|
591
|
+
const revalidateAt = now + revalidateSeconds * 1e3;
|
|
592
|
+
const kvTtl = MAX_KV_TTL_SECONDS;
|
|
434
593
|
const tags = buildAppPageCacheTags(routePath, []);
|
|
435
594
|
const entry = {
|
|
436
595
|
value: {
|
|
@@ -486,7 +645,7 @@ const DEFAULT_REVALIDATE_SECONDS = 3600;
|
|
|
486
645
|
*/
|
|
487
646
|
async function runTPR(options) {
|
|
488
647
|
const startTime = Date.now();
|
|
489
|
-
const { root, coverage, limit, window: windowHours } = options;
|
|
648
|
+
const { root, config, coverage, limit, window: windowHours } = options;
|
|
490
649
|
const skip = (reason) => ({
|
|
491
650
|
totalPaths: 0,
|
|
492
651
|
prerenderedCount: 0,
|
|
@@ -496,7 +655,7 @@ async function runTPR(options) {
|
|
|
496
655
|
});
|
|
497
656
|
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
498
657
|
if (!apiToken) return skip("no CLOUDFLARE_API_TOKEN set");
|
|
499
|
-
const wranglerConfig = parseWranglerConfig(root);
|
|
658
|
+
const wranglerConfig = parseWranglerConfig(root, config);
|
|
500
659
|
if (!wranglerConfig) return skip("could not parse wrangler config");
|
|
501
660
|
if (!wranglerConfig.customDomain) return skip("no custom domain — zone analytics unavailable");
|
|
502
661
|
if (!wranglerConfig.kvNamespaceId) return skip("no VINEXT_KV_CACHE KV namespace configured");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseWorkersDevUrl } from "./workers-dev-url.js";
|
|
2
|
+
import { DeployOptions } from "./deploy.js";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
//#region src/version-deploy.d.ts
|
|
6
|
+
type WranglerVersionUploadResult = {
|
|
7
|
+
versionId: string;
|
|
8
|
+
previewUrl: string | null;
|
|
9
|
+
output: string;
|
|
10
|
+
};
|
|
11
|
+
type WranglerVersionDeployResult = {
|
|
12
|
+
deployedUrl: string | null;
|
|
13
|
+
output: string;
|
|
14
|
+
};
|
|
15
|
+
type WranglerVersionTraffic = {
|
|
16
|
+
versionId: string;
|
|
17
|
+
percentage: number;
|
|
18
|
+
};
|
|
19
|
+
type WranglerDeploymentStatus = {
|
|
20
|
+
versions: WranglerVersionTraffic[];
|
|
21
|
+
output: string;
|
|
22
|
+
};
|
|
23
|
+
type WranglerVersionArgs = {
|
|
24
|
+
args: string[];
|
|
25
|
+
env: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
declare function parseVersionId(output: string): string | null;
|
|
28
|
+
declare function parseWranglerVersionUploadOutput(output: string): WranglerVersionUploadResult;
|
|
29
|
+
declare function buildWranglerVersionUploadArgs(options: Pick<DeployOptions, "preview" | "env" | "name" | "config"> & {
|
|
30
|
+
previewAlias?: string;
|
|
31
|
+
}): WranglerVersionArgs;
|
|
32
|
+
declare function buildWranglerVersionDeployArgs(versionTraffic: readonly WranglerVersionTraffic[], options: Pick<DeployOptions, "preview" | "env" | "name" | "config">): WranglerVersionArgs;
|
|
33
|
+
declare function buildWranglerDeploymentsStatusArgs(options: Pick<DeployOptions, "preview" | "env" | "name" | "config">): WranglerVersionArgs;
|
|
34
|
+
declare function buildWranglerTriggersDeployArgs(options: Pick<DeployOptions, "preview" | "env" | "name" | "config">): WranglerVersionArgs;
|
|
35
|
+
declare function parseWranglerDeploymentStatusOutput(output: string): WranglerDeploymentStatus;
|
|
36
|
+
declare function runWranglerVersionUpload(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config"> & {
|
|
37
|
+
previewAlias?: string;
|
|
38
|
+
}, execute?: typeof execFileSync): WranglerVersionUploadResult;
|
|
39
|
+
declare function runWranglerVersionDeploy(root: string, versionTraffic: readonly WranglerVersionTraffic[], options: Pick<DeployOptions, "preview" | "env" | "name" | "config">, phase?: "stage" | "promote-warmed" | "promote-uploaded", execute?: typeof execFileSync): WranglerVersionDeployResult;
|
|
40
|
+
declare function runWranglerDeploymentStatus(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config">, execute?: typeof execFileSync): WranglerDeploymentStatus;
|
|
41
|
+
declare function runWranglerTriggersDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config">, execute?: typeof execFileSync): WranglerVersionDeployResult;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { WranglerDeploymentStatus, WranglerVersionDeployResult, WranglerVersionTraffic, WranglerVersionUploadResult, buildWranglerDeploymentsStatusArgs, buildWranglerTriggersDeployArgs, buildWranglerVersionDeployArgs, buildWranglerVersionUploadArgs, parseVersionId, parseWorkersDevUrl, parseWranglerDeploymentStatusOutput, parseWranglerVersionUploadOutput, runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { parseWorkersDevUrl } from "./workers-dev-url.js";
|
|
2
|
+
import { buildNodeCliInvocation, resolveWranglerBin, validateWranglerEnvName } from "./deploy.js";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
//#region src/version-deploy.ts
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function parseJsonObject(output) {
|
|
9
|
+
const trimmed = output.trim();
|
|
10
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(trimmed);
|
|
13
|
+
return isRecord(parsed) || Array.isArray(parsed) ? parsed : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function findStringInRecord(value, keys) {
|
|
19
|
+
if (!isRecord(value)) return null;
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
const field = value[key];
|
|
22
|
+
if (typeof field === "string" && field.length > 0) return field;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function findVersionIdInUploadJson(parsed) {
|
|
27
|
+
if (!isRecord(parsed)) return null;
|
|
28
|
+
return findStringInRecord(parsed, [
|
|
29
|
+
"version_id",
|
|
30
|
+
"versionId",
|
|
31
|
+
"id"
|
|
32
|
+
]) ?? findStringInRecord(parsed.version, [
|
|
33
|
+
"version_id",
|
|
34
|
+
"versionId",
|
|
35
|
+
"id"
|
|
36
|
+
]) ?? findStringInRecord(parsed.result, [
|
|
37
|
+
"version_id",
|
|
38
|
+
"versionId",
|
|
39
|
+
"id"
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
function findPreviewUrlInUploadJson(parsed) {
|
|
43
|
+
if (!isRecord(parsed)) return null;
|
|
44
|
+
return findStringInRecord(parsed, [
|
|
45
|
+
"preview_url",
|
|
46
|
+
"previewUrl",
|
|
47
|
+
"url"
|
|
48
|
+
]) ?? findStringInRecord(parsed.version, [
|
|
49
|
+
"preview_url",
|
|
50
|
+
"previewUrl",
|
|
51
|
+
"url"
|
|
52
|
+
]) ?? findStringInRecord(parsed.result, [
|
|
53
|
+
"preview_url",
|
|
54
|
+
"previewUrl",
|
|
55
|
+
"url"
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
function parseVersionId(output) {
|
|
59
|
+
return output.match(/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/)?.[0] ?? null;
|
|
60
|
+
}
|
|
61
|
+
function parseWranglerVersionUploadOutput(output) {
|
|
62
|
+
const parsed = parseJsonObject(output);
|
|
63
|
+
const versionId = findVersionIdInUploadJson(parsed) ?? parseVersionId(output);
|
|
64
|
+
const previewUrl = findPreviewUrlInUploadJson(parsed) ?? parseWorkersDevUrl(output);
|
|
65
|
+
if (!versionId) throw new Error("Could not detect Worker version ID from `wrangler versions upload` output.");
|
|
66
|
+
return {
|
|
67
|
+
versionId,
|
|
68
|
+
previewUrl,
|
|
69
|
+
output
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function buildWranglerVersionUploadArgs(options) {
|
|
73
|
+
const args = ["versions", "upload"];
|
|
74
|
+
const env = options.env || (options.preview ? "preview" : void 0);
|
|
75
|
+
if (options.config) args.push("--config", options.config);
|
|
76
|
+
if (options.name) args.push("--name", options.name);
|
|
77
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
78
|
+
if (options.previewAlias) args.push("--preview-alias", options.previewAlias);
|
|
79
|
+
return {
|
|
80
|
+
args,
|
|
81
|
+
env
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function buildWranglerVersionDeployArgs(versionTraffic, options) {
|
|
85
|
+
const args = [
|
|
86
|
+
"versions",
|
|
87
|
+
"deploy",
|
|
88
|
+
...versionTraffic.map(({ versionId, percentage }) => `${versionId}@${percentage}%`),
|
|
89
|
+
"--yes"
|
|
90
|
+
];
|
|
91
|
+
const env = options.env || (options.preview ? "preview" : void 0);
|
|
92
|
+
if (options.config) args.push("--config", options.config);
|
|
93
|
+
if (options.name) args.push("--name", options.name);
|
|
94
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
95
|
+
return {
|
|
96
|
+
args,
|
|
97
|
+
env
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildWranglerDeploymentsStatusArgs(options) {
|
|
101
|
+
const args = [
|
|
102
|
+
"deployments",
|
|
103
|
+
"status",
|
|
104
|
+
"--json"
|
|
105
|
+
];
|
|
106
|
+
const env = options.env || (options.preview ? "preview" : void 0);
|
|
107
|
+
if (options.config) args.push("--config", options.config);
|
|
108
|
+
if (options.name) args.push("--name", options.name);
|
|
109
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
110
|
+
return {
|
|
111
|
+
args,
|
|
112
|
+
env
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildWranglerTriggersDeployArgs(options) {
|
|
116
|
+
const args = ["triggers", "deploy"];
|
|
117
|
+
const env = options.env || (options.preview ? "preview" : void 0);
|
|
118
|
+
if (options.config) args.push("--config", options.config);
|
|
119
|
+
if (options.name) args.push("--name", options.name);
|
|
120
|
+
if (env) args.push("--env", validateWranglerEnvName(env));
|
|
121
|
+
return {
|
|
122
|
+
args,
|
|
123
|
+
env
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function runWranglerCommand(root, args, execute = execFileSync) {
|
|
127
|
+
const invocation = buildNodeCliInvocation(resolveWranglerBin(root), args);
|
|
128
|
+
const execOpts = {
|
|
129
|
+
cwd: root,
|
|
130
|
+
stdio: "pipe",
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
shell: false
|
|
133
|
+
};
|
|
134
|
+
const output = execute(invocation.file, invocation.args, execOpts);
|
|
135
|
+
if (output.trim()) for (const line of output.trim().split("\n")) console.log(` ${line}`);
|
|
136
|
+
return output;
|
|
137
|
+
}
|
|
138
|
+
function errorText(error) {
|
|
139
|
+
if (!error || typeof error !== "object") return String(error);
|
|
140
|
+
const parts = error instanceof Error ? [error.message] : [];
|
|
141
|
+
const record = error;
|
|
142
|
+
for (const field of [
|
|
143
|
+
"stdout",
|
|
144
|
+
"stderr",
|
|
145
|
+
"output"
|
|
146
|
+
]) {
|
|
147
|
+
const value = record[field];
|
|
148
|
+
if (Array.isArray(value)) parts.push(...value.filter((entry) => typeof entry === "string" || Buffer.isBuffer(entry)).map((entry) => entry.toString()));
|
|
149
|
+
else if (typeof value === "string" || Buffer.isBuffer(value)) parts.push(value.toString());
|
|
150
|
+
}
|
|
151
|
+
return parts.join("\n");
|
|
152
|
+
}
|
|
153
|
+
function isMissingWorkerVersionUploadError(error) {
|
|
154
|
+
return /cannot upload a new version of a Worker that does not yet exist/i.test(errorText(error));
|
|
155
|
+
}
|
|
156
|
+
function withInitialDeployRequiredMessage() {
|
|
157
|
+
return /* @__PURE__ */ new Error("CDN pre-warm needs an existing Cloudflare Worker before it can upload a new Worker version. Run `vinext-cloudflare deploy` once without `--experimental-warm-cdn-cache` to create the Worker, then rerun your pre-warm deploy.");
|
|
158
|
+
}
|
|
159
|
+
function parseDeploymentVersions(value) {
|
|
160
|
+
const deployment = Array.isArray(value) ? value.at(-1) : value;
|
|
161
|
+
if (!isRecord(deployment) || !Array.isArray(deployment.versions)) return [];
|
|
162
|
+
const versions = [];
|
|
163
|
+
for (const version of deployment.versions) {
|
|
164
|
+
if (!isRecord(version)) continue;
|
|
165
|
+
const versionId = version.version_id;
|
|
166
|
+
const percentage = version.percentage;
|
|
167
|
+
if (typeof versionId !== "string" || typeof percentage !== "number") continue;
|
|
168
|
+
versions.push({
|
|
169
|
+
versionId,
|
|
170
|
+
percentage
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return versions;
|
|
174
|
+
}
|
|
175
|
+
function parseWranglerDeploymentStatusOutput(output) {
|
|
176
|
+
const parsed = parseJsonObject(output);
|
|
177
|
+
if (!parsed) throw new Error("Could not parse `wrangler deployments status --json` output.");
|
|
178
|
+
return {
|
|
179
|
+
versions: parseDeploymentVersions(parsed),
|
|
180
|
+
output
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function runWranglerVersionUpload(root, options, execute = execFileSync) {
|
|
184
|
+
const { args, env } = buildWranglerVersionUploadArgs(options);
|
|
185
|
+
if (env) console.log(`\n Uploading Worker version for env: ${env}...`);
|
|
186
|
+
else console.log("\n Uploading Worker version for production...");
|
|
187
|
+
try {
|
|
188
|
+
return parseWranglerVersionUploadOutput(runWranglerCommand(root, args, execute));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (isMissingWorkerVersionUploadError(error)) throw withInitialDeployRequiredMessage();
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function runWranglerVersionDeploy(root, versionTraffic, options, phase = "promote-uploaded", execute = execFileSync) {
|
|
195
|
+
const { args, env } = buildWranglerVersionDeployArgs(versionTraffic, options);
|
|
196
|
+
const target = env ? `env: ${env}` : "production";
|
|
197
|
+
if (phase === "stage") console.log(`\n Staging uploaded Worker version at 0% for CDN warmup in ${target}...`);
|
|
198
|
+
else if (phase === "promote-warmed") console.log(`\n Promoting warmed Worker version to ${target}...`);
|
|
199
|
+
else console.log(`\n Promoting uploaded Worker version to ${target}...`);
|
|
200
|
+
const output = runWranglerCommand(root, args, execute);
|
|
201
|
+
return {
|
|
202
|
+
deployedUrl: parseWorkersDevUrl(output),
|
|
203
|
+
output
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function runWranglerDeploymentStatus(root, options, execute = execFileSync) {
|
|
207
|
+
const { args, env } = buildWranglerDeploymentsStatusArgs(options);
|
|
208
|
+
if (env) console.log(`\n Reading current Worker deployment for env: ${env}...`);
|
|
209
|
+
else console.log("\n Reading current Worker deployment...");
|
|
210
|
+
return parseWranglerDeploymentStatusOutput(runWranglerCommand(root, args, execute));
|
|
211
|
+
}
|
|
212
|
+
function runWranglerTriggersDeploy(root, options, execute = execFileSync) {
|
|
213
|
+
const { args, env } = buildWranglerTriggersDeployArgs(options);
|
|
214
|
+
if (env) console.log(`\n Applying Worker triggers for env: ${env}...`);
|
|
215
|
+
else console.log("\n Applying Worker triggers...");
|
|
216
|
+
const output = runWranglerCommand(root, args, execute);
|
|
217
|
+
return {
|
|
218
|
+
deployedUrl: parseWorkersDevUrl(output),
|
|
219
|
+
output
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { buildWranglerDeploymentsStatusArgs, buildWranglerTriggersDeployArgs, buildWranglerVersionDeployArgs, buildWranglerVersionUploadArgs, parseVersionId, parseWorkersDevUrl, parseWranglerDeploymentStatusOutput, parseWranglerVersionUploadOutput, runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/workers-dev-url.ts
|
|
2
|
+
function parseWorkersDevUrl(output) {
|
|
3
|
+
for (const rawToken of splitWhitespace(output)) {
|
|
4
|
+
const candidate = trimUrlPunctuation(rawToken);
|
|
5
|
+
if (!candidate.startsWith("https://")) continue;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(candidate);
|
|
8
|
+
if (url.protocol !== "https:") continue;
|
|
9
|
+
if (url.hostname !== "workers.dev" && !url.hostname.endsWith(".workers.dev")) continue;
|
|
10
|
+
if (url.pathname === "/" && url.search === "" && url.hash === "") return url.origin;
|
|
11
|
+
return url.toString();
|
|
12
|
+
} catch {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function splitWhitespace(value) {
|
|
19
|
+
const tokens = [];
|
|
20
|
+
let tokenStart = null;
|
|
21
|
+
for (let i = 0; i < value.length; i++) {
|
|
22
|
+
const char = value[i];
|
|
23
|
+
if (char === void 0) continue;
|
|
24
|
+
if (char.trim() === "") {
|
|
25
|
+
if (tokenStart !== null) {
|
|
26
|
+
tokens.push(value.slice(tokenStart, i));
|
|
27
|
+
tokenStart = null;
|
|
28
|
+
}
|
|
29
|
+
} else if (tokenStart === null) tokenStart = i;
|
|
30
|
+
}
|
|
31
|
+
if (tokenStart !== null) tokens.push(value.slice(tokenStart));
|
|
32
|
+
return tokens;
|
|
33
|
+
}
|
|
34
|
+
function trimUrlPunctuation(value) {
|
|
35
|
+
let start = 0;
|
|
36
|
+
let end = value.length;
|
|
37
|
+
while (start < end && "\"'(<".includes(value[start])) start++;
|
|
38
|
+
while (end > start && "\"')>.,;".includes(value[end - 1])) end--;
|
|
39
|
+
return value.slice(start, end);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { parseWorkersDevUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinext/cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
4
|
"description": "Cloudflare adapters for vinext (KV data cache and Images-backed image optimization).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
"types": "./dist/tpr.d.ts",
|
|
34
34
|
"import": "./dist/tpr.js"
|
|
35
35
|
},
|
|
36
|
+
"./internal/cdn-warm": {
|
|
37
|
+
"types": "./dist/cdn-warm.d.ts",
|
|
38
|
+
"import": "./dist/cdn-warm.js"
|
|
39
|
+
},
|
|
40
|
+
"./internal/version-deploy": {
|
|
41
|
+
"types": "./dist/version-deploy.d.ts",
|
|
42
|
+
"import": "./dist/version-deploy.js"
|
|
43
|
+
},
|
|
36
44
|
"./cache/*": {
|
|
37
45
|
"types": "./dist/cache/*.d.ts",
|
|
38
46
|
"import": "./dist/cache/*.js"
|
|
@@ -45,7 +53,7 @@
|
|
|
45
53
|
"devDependencies": {
|
|
46
54
|
"@types/node": "^25.9.2",
|
|
47
55
|
"vite-plus": "0.2.1",
|
|
48
|
-
"vinext": "0.
|
|
56
|
+
"vinext": "1.0.0-beta.0"
|
|
49
57
|
},
|
|
50
58
|
"peerDependencies": {
|
|
51
59
|
"vinext": ">=0.0.0"
|