@vinext/cloudflare 1.0.0-beta.6 → 1.0.0-beta.7

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/README.md CHANGED
@@ -45,6 +45,9 @@ export default defineConfig({
45
45
  "cache": {
46
46
  "enabled": true,
47
47
  },
48
+ "version_metadata": {
49
+ "binding": "CF_VERSION_METADATA",
50
+ },
48
51
  }
49
52
  ```
50
53
 
@@ -54,6 +57,20 @@ import { cdnAdapter } from "@vinext/cloudflare/cache/cdn-adapter";
54
57
  vinext({ cache: { cdn: cdnAdapter() } });
55
58
  ```
56
59
 
60
+ The version metadata binding lets staged warmup prove that every discovery,
61
+ probe, and fill request reached the uploaded Worker version. Wrangler named
62
+ environments do not inherit `version_metadata`; repeat the binding inside every
63
+ `env.<name>` used with CDN warming.
64
+
65
+ Use `--experimental-warm-cdn-cache` for the two-stage deploy. The default flow
66
+ makes one final fill request per admitted identity. Add `--warm-cdn-certify`
67
+ only when you want an opt-in second, header-only request that must prove every
68
+ planned entry reusable before promotion.
69
+
70
+ Do not configure a Cache Rule that ignores or normalizes query strings for
71
+ these responses. The two-stage cacheability manifest authorizes the full
72
+ pathname + query identity, but an edge HIT happens before Worker admission.
73
+
57
74
  ## Deploy
58
75
 
59
76
  Deploy Cloudflare Workers projects with the package CLI:
@@ -0,0 +1,22 @@
1
+ //#region src/cache/cdn-adapter-config.d.ts
2
+ type WranglerOutputConfig = {
3
+ version_metadata?: unknown;
4
+ [key: string]: unknown;
5
+ };
6
+ type VersionMetadataOptions = {
7
+ binding: string;
8
+ bindingIsExplicit: boolean;
9
+ };
10
+ declare function configureCdnVersionMetadata(config: WranglerOutputConfig, options: VersionMetadataOptions): WranglerOutputConfig;
11
+ /**
12
+ * Add the CDN adapter's version metadata binding to the Cloudflare Vite
13
+ * plugin's primary generated deployment config.
14
+ */
15
+ declare function finalizeCdnAdapterBuildOutput({ outDir, isPrimaryServerOutput, binding, bindingIsExplicit }: {
16
+ outDir: string;
17
+ isPrimaryServerOutput: boolean;
18
+ binding: string;
19
+ bindingIsExplicit: boolean;
20
+ }): Promise<void>;
21
+ //#endregion
22
+ export { configureCdnVersionMetadata, finalizeCdnAdapterBuildOutput };
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ //#region src/cache/cdn-adapter-config.ts
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function configureCdnVersionMetadata(config, options) {
8
+ if (!Object.hasOwn(config, "version_metadata")) return {
9
+ ...config,
10
+ version_metadata: { binding: options.binding }
11
+ };
12
+ if (!isRecord(config.version_metadata)) throw new Error("[vinext] The generated Wrangler config has an invalid version_metadata value.");
13
+ const existingBinding = config.version_metadata.binding;
14
+ if (typeof existingBinding !== "string" || existingBinding.length === 0) throw new Error("[vinext] The generated Wrangler config has an invalid version_metadata.binding value.");
15
+ if (existingBinding === options.binding) return config;
16
+ if (!options.bindingIsExplicit) throw new Error(`[vinext] cdnAdapter() uses the default version metadata binding ${JSON.stringify(options.binding)}, but the generated Wrangler config already declares ${JSON.stringify(existingBinding)}. Align the Wrangler binding or configure cdnAdapter({ versionMetadataBinding: ${JSON.stringify(existingBinding)} }).`);
17
+ return {
18
+ ...config,
19
+ version_metadata: { binding: options.binding }
20
+ };
21
+ }
22
+ /**
23
+ * Add the CDN adapter's version metadata binding to the Cloudflare Vite
24
+ * plugin's primary generated deployment config.
25
+ */
26
+ async function finalizeCdnAdapterBuildOutput({ outDir, isPrimaryServerOutput, binding, bindingIsExplicit }) {
27
+ if (!isPrimaryServerOutput) return;
28
+ await configureGeneratedCdnVersionMetadata(path.resolve(outDir, "wrangler.json"), {
29
+ binding,
30
+ bindingIsExplicit
31
+ });
32
+ }
33
+ async function configureGeneratedCdnVersionMetadata(generatedConfigPath, options) {
34
+ let generatedConfig;
35
+ try {
36
+ const parsed = JSON.parse(await fs.readFile(generatedConfigPath, "utf8"));
37
+ if (!isRecord(parsed)) throw new TypeError("the root value must be an object");
38
+ generatedConfig = parsed;
39
+ } catch (cause) {
40
+ throw new Error(`[vinext] Could not read the generated Wrangler config at ${generatedConfigPath}.`, { cause });
41
+ }
42
+ const configured = configureCdnVersionMetadata(generatedConfig, options);
43
+ if (configured === generatedConfig) return;
44
+ await fs.writeFile(generatedConfigPath, `${JSON.stringify(configured, null, 2)}\n`);
45
+ }
46
+ //#endregion
47
+ export { configureCdnVersionMetadata, finalizeCdnAdapterBuildOutput };
@@ -1,4 +1,10 @@
1
1
  //#region src/cache/cdn-adapter.d.ts
2
+ declare const DEFAULT_CDN_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
3
+ /** Options accepted by {@link cdnAdapter}, forwarded to the runtime factory. */
4
+ type CdnAdapterOptions = {
5
+ /** Version metadata binding used to verify version-overridden requests. */
6
+ versionMetadataBinding?: string;
7
+ };
2
8
  /**
3
9
  * Cloudflare CDN cache adapter - edge-managed page-level ISR backed by the
4
10
  * Cloudflare Workers Cache.
@@ -14,12 +20,37 @@
14
20
  * "cache": {
15
21
  * "enabled": true,
16
22
  * },
23
+ * "version_metadata": {
24
+ * "binding": "CF_VERSION_METADATA"
25
+ * }
17
26
  * }
18
27
  * ```
28
+ * Wrangler does not inherit `version_metadata` into named environments. Repeat
29
+ * the binding in every `env.<name>` used for CDN warmup.
30
+ *
31
+ * Cache Rules must preserve the full query string in the incoming cache key.
32
+ * Two-stage admission certifies exact pathname + search identities, while an
33
+ * edge HIT is served before the Worker can reject a differently keyed request.
19
34
  */
20
- declare function cdnAdapter(options?: Record<string, never>): {
35
+ declare function cdnAdapter(options?: CdnAdapterOptions): {
21
36
  adapter: string;
22
- options: Record<string, never> | undefined;
37
+ options: CdnAdapterOptions | undefined;
38
+ output: {
39
+ matchesBuild({ plugins }: {
40
+ plugins: readonly {
41
+ name?: string;
42
+ }[];
43
+ }): boolean;
44
+ finalizeBuildOutput({ outDir, isPrimaryServerOutput }: {
45
+ outDir: string;
46
+ isPrimaryServerOutput: boolean;
47
+ }): Promise<void>;
48
+ };
49
+ capabilities: {
50
+ buildIdentity: "response-header";
51
+ responseVary: "verbatim";
52
+ routeCacheability: "probe-manifest";
53
+ };
23
54
  };
24
55
  //#endregion
25
- export { cdnAdapter };
56
+ export { CdnAdapterOptions, DEFAULT_CDN_VERSION_METADATA_BINDING, cdnAdapter };
@@ -1,5 +1,7 @@
1
+ import { finalizeCdnAdapterBuildOutput } from "./cdn-adapter-config.js";
1
2
  import { fileURLToPath } from "node:url";
2
3
  //#region src/cache/cdn-adapter.ts
4
+ const DEFAULT_CDN_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
3
5
  /**
4
6
  * Cloudflare CDN cache adapter - edge-managed page-level ISR backed by the
5
7
  * Cloudflare Workers Cache.
@@ -15,14 +17,44 @@ import { fileURLToPath } from "node:url";
15
17
  * "cache": {
16
18
  * "enabled": true,
17
19
  * },
20
+ * "version_metadata": {
21
+ * "binding": "CF_VERSION_METADATA"
22
+ * }
18
23
  * }
19
24
  * ```
25
+ * Wrangler does not inherit `version_metadata` into named environments. Repeat
26
+ * the binding in every `env.<name>` used for CDN warmup.
27
+ *
28
+ * Cache Rules must preserve the full query string in the incoming cache key.
29
+ * Two-stage admission certifies exact pathname + search identities, while an
30
+ * edge HIT is served before the Worker can reject a differently keyed request.
20
31
  */
21
32
  function cdnAdapter(options) {
33
+ if (options?.versionMetadataBinding !== void 0 && (typeof options.versionMetadataBinding !== "string" || options.versionMetadataBinding.length === 0)) throw new TypeError("[vinext] cdnAdapter({ versionMetadataBinding }) must be a non-empty string binding name.");
34
+ const versionMetadataBinding = options?.versionMetadataBinding ?? "CF_VERSION_METADATA";
35
+ const bindingIsExplicit = options?.versionMetadataBinding !== void 0;
22
36
  return {
23
37
  adapter: fileURLToPath(import.meta.resolve("./cdn-adapter.runtime.js")),
24
- options
38
+ options,
39
+ output: {
40
+ matchesBuild({ plugins }) {
41
+ return plugins.some(({ name }) => name === "vite-plugin-cloudflare" || name?.startsWith("vite-plugin-cloudflare:"));
42
+ },
43
+ finalizeBuildOutput({ outDir, isPrimaryServerOutput }) {
44
+ return finalizeCdnAdapterBuildOutput({
45
+ outDir,
46
+ isPrimaryServerOutput,
47
+ binding: versionMetadataBinding,
48
+ bindingIsExplicit
49
+ });
50
+ }
51
+ },
52
+ capabilities: {
53
+ buildIdentity: "response-header",
54
+ responseVary: "verbatim",
55
+ routeCacheability: "probe-manifest"
56
+ }
25
57
  };
26
58
  }
27
59
  //#endregion
28
- export { cdnAdapter };
60
+ export { DEFAULT_CDN_VERSION_METADATA_BINDING, cdnAdapter };
@@ -1,7 +1,19 @@
1
1
  import { CdnCacheAdapter, CdnCacheableHeaderInput, CdnResponseHeaders } from "vinext/shims/cdn-cache";
2
2
  import { CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache";
3
3
  //#region src/cache/cdn-adapter.runtime.d.ts
4
+ type WorkerVersionMetadata = {
5
+ id: string;
6
+ };
7
+ type CdnAdapterOptions = {
8
+ versionMetadataBinding?: string;
9
+ };
4
10
  declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
11
+ private readonly versionMetadata?;
12
+ private readonly versionMetadataBinding;
13
+ readonly requiresCompletedResponseAdmission = true;
14
+ readonly responseVary: "verbatim";
15
+ constructor(versionMetadata?: WorkerVersionMetadata | undefined, versionMetadataBinding?: string);
16
+ validateRequest(request: Request): Response | null;
5
17
  readonly ownsBackgroundRevalidation = false;
6
18
  /**
7
19
  * The origin keeps no page store — return null so the request renders fresh.
@@ -10,6 +22,7 @@ declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
10
22
  get(): Promise<CacheHandlerValue | null>;
11
23
  /** No-op: the platform caches the response via its headers, not an origin store. */
12
24
  set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
25
+ buildResponseIdentityHeaders(): CdnResponseHeaders;
13
26
  buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
14
27
  hasExplicitNonCacheableResponsePolicy(headers: Headers): boolean;
15
28
  /** Purge edge-cached responses by tag via the request context's `cache.purge`. */
@@ -17,6 +30,9 @@ declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
17
30
  expire?: number;
18
31
  }): Promise<void>;
19
32
  }
20
- declare const createCloudflareCdnCacheAdapter: () => CdnCacheAdapter;
33
+ declare const createCloudflareCdnCacheAdapter: ({ env, options }?: {
34
+ env?: Record<string, unknown>;
35
+ options?: CdnAdapterOptions;
36
+ }) => CdnCacheAdapter;
21
37
  //#endregion
22
38
  export { CloudflareCdnCacheAdapter, createCloudflareCdnCacheAdapter as default };
@@ -1,11 +1,61 @@
1
+ import { VINEXT_CDN_BUILD_ID_HEADER } from "./cdn-build-id.js";
2
+ import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "../version-headers.js";
3
+ import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
1
4
  import { getRequestExecutionContext } from "vinext/shims/request-context";
2
5
  //#region src/cache/cdn-adapter.runtime.ts
3
- const NON_CACHEABLE_DIRECTIVE_RE = /\b(?:private|no-store|no-cache)\b/i;
6
+ /**
7
+ * Cloudflare CDN cache adapter — edge-managed page-level ISR backed by the
8
+ * Cloudflare Workers Cache (`ctx.cache`).
9
+ *
10
+ * Unlike the origin-managed default adapter (which stores rendered artifacts in
11
+ * the data cache and serves HIT/STALE itself), this adapter delegates serving
12
+ * to Cloudflare's edge:
13
+ *
14
+ * - The origin never serves from a store — `get` returns `null`, so any
15
+ * request that reaches the Worker renders fresh. The edge absorbs HIT/STALE
16
+ * traffic and revalidates in the background (the `UPDATING` cache status).
17
+ * - `set` is a no-op: the platform caches the *response* based on its
18
+ * cache headers, so there is nothing to persist at the origin.
19
+ * - `buildResponseHeaders` emits the SWR policy as `CDN-Cache-Control`
20
+ * (`public, max-age=…, stale-while-revalidate=…`) so the edge caches and
21
+ * revalidates, while the browser-facing `Cache-Control` is
22
+ * `public, max-age=0, must-revalidate` so a browser never serves a stored copy
23
+ * without revalidating against the edge. A `Cache-Tag` header lets entries be
24
+ * purged by tag. Note the edge directive uses `max-age` (not `s-maxage`):
25
+ * the framework computes the policy with `s-maxage` for shared caches, but
26
+ * `CDN-Cache-Control` is already CDN-scoped so `max-age` is the correct knob
27
+ * for the edge to honor max-age + stale-while-revalidate.
28
+ * - `revalidateTag` purges the edge via the request context's `cache.purge({ tags })`.
29
+ *
30
+ * Tag alignment: the tags emitted in `Cache-Tag` come from the page's render
31
+ * tags (already canonicalised via `encodeCacheTag`), and the framework's
32
+ * `revalidateTag` / `revalidatePath` pass the same canonical form to this
33
+ * adapter's `revalidateTag`, so a purge targets exactly the responses that
34
+ * carried the tag.
35
+ *
36
+ * The default export is the adapter factory the generated
37
+ * `virtual:vinext-cache-adapters` registration imports; configure it from
38
+ * vite.config via the {@link cdnAdapter} builder in `./cdn-adapter.ts` (which
39
+ * `require.resolve`s this file).
40
+ */
41
+ const DEFAULT_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
42
+ const WORKER_VERSION_OVERRIDE_HEADER = "Cloudflare-Workers-Version-Overrides";
43
+ function versionValidationFailure(message, status = 500) {
44
+ return Response.json({ error: message }, {
45
+ status,
46
+ headers: { "Cache-Control": "no-store" }
47
+ });
48
+ }
4
49
  const CACHEABLE_EDGE_DIRECTIVE_RE = /(?:^|,)\s*(?:s-maxage|max-age)\s*=/i;
5
50
  const EDGE_POLICY_HEADERS = ["CDN-Cache-Control", "Cloudflare-CDN-Cache-Control"];
51
+ function getBuildIdentityResponseHeader() {
52
+ const buildId = process.env.__VINEXT_RSC_BUILD_IDENTITY ?? process.env.__VINEXT_BUILD_ID;
53
+ return buildId ? { [VINEXT_CDN_BUILD_ID_HEADER]: buildId } : {};
54
+ }
6
55
  /** Remove every response header whose cache semantics are owned by Cloudflare. */
7
56
  function clearCloudflareCdnResponseHeaders(cacheControl) {
8
57
  return {
58
+ ...getBuildIdentityResponseHeader(),
9
59
  "Cache-Control": cacheControl,
10
60
  "CDN-Cache-Control": null,
11
61
  "Cloudflare-CDN-Cache-Control": null,
@@ -15,10 +65,10 @@ function clearCloudflareCdnResponseHeaders(cacheControl) {
15
65
  /** Interpret Cloudflare's edge policy before core applies a replacement policy. */
16
66
  function hasExplicitCloudflareNonCacheableResponsePolicy(headers) {
17
67
  const edgePolicies = EDGE_POLICY_HEADERS.map((name) => headers.get(name));
18
- if (edgePolicies.some((value) => value && NON_CACHEABLE_DIRECTIVE_RE.test(value))) return true;
68
+ if (edgePolicies.some((value) => value && isNonCacheableCacheControl(value))) return true;
19
69
  const hasCacheableEdgePolicy = edgePolicies.some((value) => value && CACHEABLE_EDGE_DIRECTIVE_RE.test(value));
20
70
  const browserPolicy = headers.get("Cache-Control");
21
- return Boolean(!hasCacheableEdgePolicy && browserPolicy && NON_CACHEABLE_DIRECTIVE_RE.test(browserPolicy));
71
+ return Boolean(!hasCacheableEdgePolicy && browserPolicy && isNonCacheableCacheControl(browserPolicy));
22
72
  }
23
73
  function getWorkersCache() {
24
74
  const cache = getRequestExecutionContext()?.cache;
@@ -81,6 +131,22 @@ function formatCacheTag(tags) {
81
131
  return parts.length > 0 ? parts.join(",") : null;
82
132
  }
83
133
  var CloudflareCdnCacheAdapter = class {
134
+ versionMetadata;
135
+ versionMetadataBinding;
136
+ requiresCompletedResponseAdmission = true;
137
+ responseVary = "verbatim";
138
+ constructor(versionMetadata, versionMetadataBinding = DEFAULT_VERSION_METADATA_BINDING) {
139
+ this.versionMetadata = versionMetadata;
140
+ this.versionMetadataBinding = versionMetadataBinding;
141
+ }
142
+ validateRequest(request) {
143
+ const expectedVersionId = request.headers.get(VINEXT_EXPECTED_WORKER_VERSION_HEADER);
144
+ if (expectedVersionId === null) return null;
145
+ if (!request.headers.has(WORKER_VERSION_OVERRIDE_HEADER)) return versionValidationFailure(`received ${VINEXT_EXPECTED_WORKER_VERSION_HEADER} without ${WORKER_VERSION_OVERRIDE_HEADER}.`);
146
+ if (!this.versionMetadata) return versionValidationFailure(`Cloudflare CDN prewarming requires the \`${this.versionMetadataBinding}\` version metadata binding. Add "version_metadata": { "binding": "${this.versionMetadataBinding}" } to the active Wrangler environment. Named environments do not inherit this binding from the top level.`);
147
+ if (expectedVersionId !== this.versionMetadata.id) return versionValidationFailure(`Cloudflare invoked Worker version ${this.versionMetadata.id}, but vinext warmup expected ${expectedVersionId}.`, 503);
148
+ return null;
149
+ }
84
150
  ownsBackgroundRevalidation = false;
85
151
  /**
86
152
  * The origin keeps no page store — return null so the request renders fresh.
@@ -91,10 +157,15 @@ var CloudflareCdnCacheAdapter = class {
91
157
  }
92
158
  /** No-op: the platform caches the response via its headers, not an origin store. */
93
159
  async set(_key, _data, _ctx) {}
160
+ buildResponseIdentityHeaders() {
161
+ return getBuildIdentityResponseHeader();
162
+ }
94
163
  buildResponseHeaders(input) {
164
+ if (input.pendingDynamicCheck) return clearCloudflareCdnResponseHeaders(NO_STORE);
95
165
  if (!input.cacheControl) return clearCloudflareCdnResponseHeaders(NO_STORE);
96
- if (/\b(?:no-store|no-cache|private)\b/.test(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
166
+ if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
97
167
  return {
168
+ ...getBuildIdentityResponseHeader(),
98
169
  "Cache-Control": BROWSER_REVALIDATE,
99
170
  "CDN-Cache-Control": toEdgeCacheControl(input.cacheControl),
100
171
  "Cloudflare-CDN-Cache-Control": null,
@@ -113,6 +184,10 @@ var CloudflareCdnCacheAdapter = class {
113
184
  await cache.purge({ tags: tagList });
114
185
  }
115
186
  };
116
- const createCloudflareCdnCacheAdapter = () => new CloudflareCdnCacheAdapter();
187
+ const createCloudflareCdnCacheAdapter = ({ env, options } = {}) => {
188
+ const binding = options?.versionMetadataBinding ?? DEFAULT_VERSION_METADATA_BINDING;
189
+ const candidate = env?.[binding];
190
+ return new CloudflareCdnCacheAdapter(candidate && typeof candidate === "object" && "id" in candidate && typeof candidate.id === "string" ? { id: candidate.id } : void 0, binding);
191
+ };
117
192
  //#endregion
118
193
  export { CloudflareCdnCacheAdapter, createCloudflareCdnCacheAdapter as default };
@@ -0,0 +1,5 @@
1
+ //#region src/cache/cdn-build-id.d.ts
2
+ /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
+ declare const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
+ //#endregion
5
+ export { VINEXT_CDN_BUILD_ID_HEADER };
@@ -0,0 +1,5 @@
1
+ //#region src/cache/cdn-build-id.ts
2
+ /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
+ const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
+ //#endregion
5
+ export { VINEXT_CDN_BUILD_ID_HEADER };
@@ -1,7 +1,7 @@
1
1
  import { ENTRY_PREFIX, createKvKeySpace } from "./kv-key.js";
2
2
  import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
3
- import { getRequestExecutionContext } from "vinext/shims/request-context";
4
3
  import { Buffer } from "node:buffer";
4
+ import { getRequestExecutionContext } from "vinext/shims/request-context";
5
5
  //#region src/cache/kv-data-adapter.runtime.ts
6
6
  /**
7
7
  * Cloudflare KV data cache — implementation + runtime factory.
@@ -0,0 +1,10 @@
1
+ import { CacheabilityManifest } from "vinext/internal/server/cacheability-manifest";
2
+ //#region src/cacheability-artifact.d.ts
3
+ /**
4
+ * Write the version-specific manifest into the built Worker artifact.
5
+ * The application build already imports this stable module asset, so the
6
+ * completed dist directory remains the exact input to the final upload.
7
+ */
8
+ declare function writeCacheabilityManifestArtifact(root: string, configuredPath: string | undefined, manifest: CacheabilityManifest): string;
9
+ //#endregion
10
+ export { writeCacheabilityManifestArtifact };
@@ -0,0 +1,165 @@
1
+ import { cacheabilityManifestByteLimitError } from "./cacheability-manifest-limits.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Buffer } from "node:buffer";
5
+ import { CACHEABILITY_MANIFEST_MODULE } from "vinext/internal/server/cacheability-manifest";
6
+ //#region src/cacheability-artifact.ts
7
+ function tokenizeStaticImports(source) {
8
+ const tokens = [];
9
+ let index = 0;
10
+ const canStartRegularExpression = () => {
11
+ const previous = tokens.at(-1);
12
+ if (!previous) return true;
13
+ if (previous.kind === "punctuator" && "({[=,:;!?&|+-*%^~<>".includes(previous.value)) return true;
14
+ return previous.kind === "identifier" && /^(?:await|case|delete|do|else|in|instanceof|of|return|throw|typeof|void|yield)$/.test(previous.value);
15
+ };
16
+ while (index < source.length) {
17
+ const character = source[index];
18
+ if (/\s/.test(character)) {
19
+ index++;
20
+ continue;
21
+ }
22
+ if (character === "/" && source[index + 1] === "/") {
23
+ index = source.indexOf("\n", index + 2);
24
+ if (index === -1) break;
25
+ continue;
26
+ }
27
+ if (character === "/" && source[index + 1] === "*") {
28
+ const end = source.indexOf("*/", index + 2);
29
+ index = end === -1 ? source.length : end + 2;
30
+ continue;
31
+ }
32
+ if (character === "/" && canStartRegularExpression()) {
33
+ let inCharacterClass = false;
34
+ index++;
35
+ while (index < source.length) {
36
+ if (source[index] === "\\") {
37
+ index += 2;
38
+ continue;
39
+ }
40
+ if (source[index] === "[") inCharacterClass = true;
41
+ if (source[index] === "]") inCharacterClass = false;
42
+ if (source[index++] === "/" && !inCharacterClass) break;
43
+ }
44
+ while (index < source.length && /[A-Za-z]/.test(source[index])) index++;
45
+ continue;
46
+ }
47
+ if (character === "`") {
48
+ index++;
49
+ while (index < source.length) if (source[index] === "\\") index += 2;
50
+ else if (source[index++] === "`") break;
51
+ continue;
52
+ }
53
+ if (character === "\"" || character === "'") {
54
+ const quote = character;
55
+ const start = ++index;
56
+ while (index < source.length && source[index] !== quote) index += source[index] === "\\" ? 2 : 1;
57
+ tokens.push({
58
+ kind: "string",
59
+ value: source.slice(start, index)
60
+ });
61
+ index++;
62
+ continue;
63
+ }
64
+ if (/[A-Za-z_$]/.test(character)) {
65
+ const start = index++;
66
+ while (index < source.length && /[\w$]/.test(source[index])) index++;
67
+ tokens.push({
68
+ kind: "identifier",
69
+ value: source.slice(start, index)
70
+ });
71
+ continue;
72
+ }
73
+ tokens.push({
74
+ kind: "punctuator",
75
+ value: character
76
+ });
77
+ index++;
78
+ }
79
+ return tokens;
80
+ }
81
+ /**
82
+ * Check only the generated entry module's static dependency declarations. This
83
+ * deliberately avoids an AST parse or module-graph walk on the deploy path,
84
+ * while rejecting comments, strings, templates, and dynamic imports that only
85
+ * happen to mention the manifest filename.
86
+ */
87
+ function hasStaticModuleSpecifier(source, expected) {
88
+ const tokens = tokenizeStaticImports(source);
89
+ let braceDepth = 0;
90
+ for (let index = 0; index < tokens.length; index++) {
91
+ const token = tokens[index];
92
+ if (token.value === "{") {
93
+ braceDepth++;
94
+ continue;
95
+ }
96
+ if (token.value === "}") {
97
+ braceDepth = Math.max(0, braceDepth - 1);
98
+ continue;
99
+ }
100
+ if (braceDepth !== 0 || token.kind !== "identifier") continue;
101
+ if (token.value === "import") {
102
+ const next = tokens[index + 1];
103
+ if (next?.kind === "string" && next.value === expected) return true;
104
+ if (next?.value === "(" || next?.value === ".") continue;
105
+ } else if (token.value !== "export") continue;
106
+ for (let cursor = index + 1; cursor < tokens.length; cursor++) {
107
+ const candidate = tokens[cursor];
108
+ if (candidate.value === ";") break;
109
+ if (candidate.kind === "identifier" && candidate.value === "from") {
110
+ const specifier = tokens[cursor + 1];
111
+ if (specifier?.kind === "string" && specifier.value === expected) return true;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+ function resolveGeneratedServerConfig(root, configuredPath) {
119
+ const distDirectory = path.join(root, "dist");
120
+ const configPath = path.resolve(root, configuredPath ?? "dist/server/wrangler.json");
121
+ const relativeConfigPath = path.relative(distDirectory, configPath);
122
+ if (relativeConfigPath.startsWith("..") || path.isAbsolute(relativeConfigPath)) throw new Error("Two-stage CDN warming requires a generated Wrangler config under dist/. Pass --config dist/server/wrangler.json (or another generated dist config).");
123
+ if (!fs.existsSync(configPath) || !fs.lstatSync(configPath).isFile()) throw new Error(`Two-stage CDN warming requires the generated Wrangler config at ${path.relative(root, configPath)}. Rebuild the app before deploying.`);
124
+ return configPath;
125
+ }
126
+ function assertManifestModuleReachable(configPath) {
127
+ const serverDirectory = path.dirname(configPath);
128
+ let config;
129
+ try {
130
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
131
+ } catch (cause) {
132
+ throw new Error("Two-stage CDN warming could not parse the generated Wrangler config.", { cause });
133
+ }
134
+ const main = config && typeof config === "object" && !Array.isArray(config) ? config.main : void 0;
135
+ if (typeof main !== "string" || main.length === 0) throw new Error("Two-stage CDN warming requires a generated Wrangler main module.");
136
+ const mainPath = path.resolve(serverDirectory, main);
137
+ if (!fs.existsSync(mainPath) || !fs.lstatSync(mainPath).isFile()) throw new Error("Two-stage CDN warming could not find the generated Wrangler main module.");
138
+ if (!hasStaticModuleSpecifier(fs.readFileSync(mainPath, "utf8"), `./${CACHEABILITY_MANIFEST_MODULE}`)) throw new Error(`Two-stage CDN warming requires the generated Worker main module to import ${CACHEABILITY_MANIFEST_MODULE}.`);
139
+ }
140
+ /**
141
+ * Write the version-specific manifest into the built Worker artifact.
142
+ * The application build already imports this stable module asset, so the
143
+ * completed dist directory remains the exact input to the final upload.
144
+ */
145
+ function writeCacheabilityManifestArtifact(root, configuredPath, manifest) {
146
+ const configPath = resolveGeneratedServerConfig(root, configuredPath);
147
+ assertManifestModuleReachable(configPath);
148
+ const serverDirectory = path.dirname(configPath);
149
+ const manifestPath = path.join(serverDirectory, CACHEABILITY_MANIFEST_MODULE);
150
+ if (!fs.existsSync(manifestPath) || !fs.lstatSync(manifestPath).isFile()) throw new Error(`Two-stage CDN warming requires ${CACHEABILITY_MANIFEST_MODULE} in the generated Worker artifact. Rebuild the app before deploying.`);
151
+ const serializedManifest = JSON.stringify(manifest);
152
+ const manifestBytes = Buffer.byteLength(serializedManifest);
153
+ if (manifestBytes > 2097152) throw cacheabilityManifestByteLimitError(manifestBytes);
154
+ const manifestSource = `export default ${JSON.stringify(serializedManifest)};\n`;
155
+ const pendingManifestPath = `${manifestPath}.${process.pid}.tmp`;
156
+ try {
157
+ fs.writeFileSync(pendingManifestPath, manifestSource, "utf8");
158
+ fs.renameSync(pendingManifestPath, manifestPath);
159
+ } finally {
160
+ if (fs.existsSync(pendingManifestPath)) fs.unlinkSync(pendingManifestPath);
161
+ }
162
+ return path.relative(root, configPath);
163
+ }
164
+ //#endregion
165
+ export { writeCacheabilityManifestArtifact };
@@ -0,0 +1,5 @@
1
+ //#region src/cacheability-manifest-limits.d.ts
2
+ declare const MAX_CACHEABILITY_MANIFEST_BYTES: number;
3
+ declare function cacheabilityManifestByteLimitError(manifestBytes: number, limit?: number): Error;
4
+ //#endregion
5
+ export { MAX_CACHEABILITY_MANIFEST_BYTES, cacheabilityManifestByteLimitError };
@@ -0,0 +1,7 @@
1
+ //#region src/cacheability-manifest-limits.ts
2
+ const MAX_CACHEABILITY_MANIFEST_BYTES = 2 * 1024 * 1024;
3
+ function cacheabilityManifestByteLimitError(manifestBytes, limit = MAX_CACHEABILITY_MANIFEST_BYTES) {
4
+ return /* @__PURE__ */ new Error(`Two-stage CDN warming produced a ${manifestBytes}-byte route-pattern cacheability manifest; the limit is ${limit} bytes. Reduce the number or length of route patterns, or split the deployment before retrying.`);
5
+ }
6
+ //#endregion
7
+ export { MAX_CACHEABILITY_MANIFEST_BYTES, cacheabilityManifestByteLimitError };
@@ -0,0 +1,50 @@
1
+ import { CdnWarmTarget } from "./cdn-warm.js";
2
+ import { CacheabilityManifest } from "vinext/internal/server/cacheability-manifest";
3
+ import { PrerenderRoutePattern } from "vinext/internal/build/prerender-paths";
4
+ //#region src/cacheability-probe.d.ts
5
+ declare const DEFAULT_CACHEABILITY_PROBE_PHASE_TIMEOUT_MS = 120000;
6
+ declare const DEFAULT_CACHEABILITY_PROBE_RETRIES = 2;
7
+ declare const DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS = 1000;
8
+ type CacheabilityProbeProgress = {
9
+ completed: number;
10
+ dynamic: number;
11
+ failed: number;
12
+ probed: number;
13
+ skipped: number;
14
+ static: number;
15
+ total: number;
16
+ };
17
+ type CacheabilityProbeResult = {
18
+ cacheableTargets: CdnWarmTarget[];
19
+ classified: number;
20
+ dynamic: number;
21
+ failures: string[];
22
+ manifest: CacheabilityManifest;
23
+ probed: number;
24
+ skipped: number;
25
+ /** Paired representations admitted only if their own final warm render remains cacheable. */
26
+ speculativeTargets: CdnWarmTarget[];
27
+ };
28
+ declare function readPrerenderSecret(root: string): string;
29
+ declare function probeStagedWorkerCacheability(options: {
30
+ buildId: string;
31
+ concurrency?: number;
32
+ expectedResponseBuildId?: string;
33
+ fallbackRoutePatterns?: readonly PrerenderRoutePattern[];
34
+ fetchImpl?: typeof fetch;
35
+ headers?: HeadersInit;
36
+ retries?: number;
37
+ retryDelayMs?: number;
38
+ root: string;
39
+ targets: readonly CdnWarmTarget[];
40
+ targetUrl: string;
41
+ timeoutMs?: number;
42
+ phaseTimeoutMs?: number;
43
+ onProgress?: (progress: CacheabilityProbeProgress) => void;
44
+ /** @internal Apply stricter artifact bounds for focused coordinator tests. */
45
+ manifestLimits?: {
46
+ maxBytes?: number;
47
+ };
48
+ }): Promise<CacheabilityProbeResult>;
49
+ //#endregion
50
+ export { CacheabilityProbeProgress, CacheabilityProbeResult, DEFAULT_CACHEABILITY_PROBE_PHASE_TIMEOUT_MS, DEFAULT_CACHEABILITY_PROBE_RETRIES, DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS, probeStagedWorkerCacheability, readPrerenderSecret };