@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.11

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.
Files changed (66) hide show
  1. package/README.md +193 -1
  2. package/dist/cache/cdn-adapter-config.d.ts +31 -0
  3. package/dist/cache/cdn-adapter-config.js +95 -0
  4. package/dist/cache/cdn-adapter.d.ts +48 -15
  5. package/dist/cache/cdn-adapter.js +50 -13
  6. package/dist/cache/cdn-adapter.runtime.d.ts +26 -4
  7. package/dist/cache/cdn-adapter.runtime.js +150 -37
  8. package/dist/cache/cdn-adapter.worker.d.ts +31 -0
  9. package/dist/cache/cdn-adapter.worker.js +373 -0
  10. package/dist/cache/cdn-build-id.d.ts +6 -0
  11. package/dist/cache/cdn-build-id.js +9 -0
  12. package/dist/cache/config.d.ts +137 -0
  13. package/dist/cache/config.js +87 -0
  14. package/dist/cache/kv-data-adapter.d.ts +32 -8
  15. package/dist/cache/kv-data-adapter.js +5 -1
  16. package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
  17. package/dist/cache/kv-data-adapter.runtime.js +132 -52
  18. package/dist/cache/kv-key.d.ts +15 -0
  19. package/dist/cache/kv-key.js +43 -0
  20. package/dist/cache/response-store-adapter.d.ts +73 -0
  21. package/dist/cache/response-store-adapter.js +64 -0
  22. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
  23. package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
  24. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
  25. package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
  26. package/dist/cache/response-store-adapter.worker.d.ts +14 -0
  27. package/dist/cache/response-store-adapter.worker.js +278 -0
  28. package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
  29. package/dist/cache/response-store-cdn.runtime.js +41 -0
  30. package/dist/cache/response-store-data.runtime.d.ts +34 -0
  31. package/dist/cache/response-store-data.runtime.js +300 -0
  32. package/dist/cacheability-artifact.d.ts +10 -0
  33. package/dist/cacheability-artifact.js +215 -0
  34. package/dist/cacheability-manifest-limits.d.ts +4 -0
  35. package/dist/cacheability-manifest-limits.js +7 -0
  36. package/dist/cacheability-probe.d.ts +49 -0
  37. package/dist/cacheability-probe.js +660 -0
  38. package/dist/cdn-warm.d.ts +110 -12
  39. package/dist/cdn-warm.js +880 -82
  40. package/dist/cli.d.ts +1 -1
  41. package/dist/cli.js +15 -1
  42. package/dist/deploy-config.d.ts +14 -11
  43. package/dist/deploy-config.js +24 -5
  44. package/dist/deploy-help.d.ts +3 -4
  45. package/dist/deploy-help.js +65 -22
  46. package/dist/deploy.d.ts +139 -42
  47. package/dist/deploy.js +946 -132
  48. package/dist/images/images-optimizer.d.ts +5 -5
  49. package/dist/images/images-optimizer.runtime.d.ts +1 -5
  50. package/dist/prerender-kv-populate.d.ts +3 -4
  51. package/dist/prerender-kv-populate.js +51 -18
  52. package/dist/tpr.d.ts +34 -70
  53. package/dist/tpr.js +50 -603
  54. package/dist/utils/cache-control-metadata.d.ts +3 -4
  55. package/dist/version-deploy.d.ts +46 -17
  56. package/dist/version-deploy.js +151 -14
  57. package/dist/version-headers.d.ts +2 -0
  58. package/dist/version-headers.js +2 -0
  59. package/dist/worker-deployment-url.d.ts +9 -0
  60. package/dist/worker-deployment-url.js +45 -0
  61. package/dist/workers-dev-url.d.ts +2 -3
  62. package/dist/wrangler-config.d.ts +19 -0
  63. package/dist/wrangler-config.js +294 -0
  64. package/dist/wrangler-version-metadata.d.ts +7 -0
  65. package/dist/wrangler-version-metadata.js +9 -0
  66. package/package.json +21 -11
package/README.md CHANGED
@@ -7,7 +7,13 @@ This package provides Cloudflare-specific cache and image backends for vinext:
7
7
 
8
8
  - **`kvDataAdapter()`** (`@vinext/cloudflare/cache/kv-data-adapter`) — backs the
9
9
  data cache (`fetch`, `"use cache"`, `unstable_cache`) with a Workers KV
10
- namespace. Also used for ISR in the absence of a CDN adapter.
10
+ namespace.
11
+ - **`cdnAdapter()`** (`@vinext/cloudflare/cache/cdn-adapter`) — delegates
12
+ page-level ISR serving and revalidation to Cloudflare Workers Cache through
13
+ an automatically generated cache-enabled response entrypoint.
14
+ - **`responseStoreAdapter()`** (`@vinext/cloudflare/cache/response-store-adapter`) —
15
+ uses Workers Response Store for both response and data caching, either in a
16
+ separate cache Worker or inside the application Worker.
11
17
  - **`imagesOptimizer()`** (`@vinext/cloudflare/images/images-optimizer`) — backs
12
18
  `next/image` transformations with a Cloudflare Images binding.
13
19
 
@@ -32,6 +38,170 @@ export default defineConfig({
32
38
  });
33
39
  ```
34
40
 
41
+ ### Workers Cache
42
+
43
+ `cdnAdapter()` is optional. Configuring it asks the Cloudflare build for two
44
+ Worker entrypoints: the default entrypoint runs middleware and request-time
45
+ routing with caching disabled, while `VinextCachedResponse` lazily loads the
46
+ render stage with Workers Cache enabled. Legacy Cloudflare Vite plugin builds
47
+ write these settings and the version metadata binding to the generated
48
+ `dist/server/wrangler.json`.
49
+
50
+ ```ts
51
+ import { cdnAdapter } from "@vinext/cloudflare/cache/cdn-adapter";
52
+
53
+ vinext({ cache: { cdn: cdnAdapter() } });
54
+ ```
55
+
56
+ Cloudflare Vite plugin v2 uses Build Output and treats `cloudflare.config.ts`
57
+ as the deployment source of truth. Declare the equivalent policies there:
58
+
59
+ ```ts
60
+ import { defineWorker } from "@cloudflare/vite-plugin/experimental-config";
61
+ import { createWorkersCacheConfig } from "@vinext/cloudflare/cache/config";
62
+
63
+ const workersCache = await createWorkersCacheConfig();
64
+
65
+ export default defineWorker({
66
+ // ...
67
+ ...workersCache,
68
+ env: {
69
+ ...workersCache.env,
70
+ // Other application bindings...
71
+ },
72
+ });
73
+ ```
74
+
75
+ The generated version metadata binding lets staged warmup prove that every
76
+ discovery, probe, and fill request reached the uploaded Worker version. Pass
77
+ the same `versionMetadataBinding` to `cdnAdapter()` and
78
+ `createWorkersCacheConfig()` only when the deployment needs a custom binding
79
+ name.
80
+
81
+ Use `--experimental-warm-cdn-cache` for the two-stage deploy. The default flow
82
+ makes one final fill request per admitted identity. Add `--warm-cdn-certify`
83
+ only when you want an opt-in second, header-only request that must prove every
84
+ planned entry reusable before promotion.
85
+
86
+ The response entrypoint hashes the complete transport identity into its
87
+ Workers Cache URL, independently of zone Cache Rules, so distinct query and
88
+ representation variants cannot collide.
89
+
90
+ ### Workers Response Store
91
+
92
+ `responseStoreAdapter()` replaces both `cdnAdapter()` and `kvDataAdapter()`.
93
+ It defaults to a separate cache Worker reached through the `RESPONSE_STORE`
94
+ service binding. `vinext init` writes two collocated source configs:
95
+ `wrangler.jsonc` for the application and `wrangler.response-store.jsonc` for the
96
+ cache Worker. The latter points directly at the installed
97
+ `@cloudflare/workers-response-store` implementation and owns its R2 bucket,
98
+ SQLite Durable Object, Worker name, and cache settings. Edit those configs to
99
+ choose or reuse names, keeping the application service binding aligned with the
100
+ cache Worker name.
101
+
102
+ The two Workers are deliberately deployed separately. Deploy the Response Store
103
+ when its package or config changes, then deploy the application normally:
104
+
105
+ ```sh
106
+ npx wrangler deploy --config wrangler.response-store.jsonc
107
+ npx @vinext/cloudflare deploy
108
+ ```
109
+
110
+ `vinext-cloudflare deploy` never creates, rewrites, or deploys the Response
111
+ Store Worker.
112
+
113
+ Cloudflare Vite plugin v2 projects can define the same service-binding setup in
114
+ `cloudflare.config.ts` without a second Wrangler config:
115
+
116
+ ```ts
117
+ import { defineConfig, defineWorker } from "@cloudflare/vite-plugin/experimental-config";
118
+ import { createWorkersResponseStoreServiceBindingConfig } from "@vinext/cloudflare/cache/config";
119
+
120
+ const responseStore = await createWorkersResponseStoreServiceBindingConfig({
121
+ accountId: "<your-account-id>",
122
+ worker: {
123
+ name: "example-response-store",
124
+ compatibilityDate: "2026-09-15",
125
+ compatibilityFlags: ["nodejs_compat"],
126
+ observability: {
127
+ enabled: true,
128
+ headSamplingRate: 1,
129
+ logs: { enabled: true, invocationLogs: true },
130
+ traces: { enabled: true, headSamplingRate: 0.1 },
131
+ },
132
+ },
133
+ bucket: "example-response-store-cache-bodies",
134
+ });
135
+
136
+ export const responseStoreServiceBinding = responseStore.serviceBindingWorker;
137
+
138
+ export default defineConfig({
139
+ accountId: responseStore.accountId,
140
+ worker: defineWorker({
141
+ ...responseStore.applicationWorker,
142
+ name: "example",
143
+ entrypoint: "./worker.ts",
144
+ }),
145
+ });
146
+ ```
147
+
148
+ `worker.observability` accepts the full observability configuration supported by
149
+ your installed Cloudflare Vite plugin, including logs and traces settings.
150
+ The optional `accountId` is returned at the top level: pass it to `defineConfig`,
151
+ not to either Worker. It applies to both Workers in the project.
152
+
153
+ The async config helpers import bindings and exports from your installed
154
+ `@cloudflare/vite-plugin/experimental-config`. The plugin is an optional peer
155
+ dependency of `@vinext/cloudflare`; these helpers require v2, but Wrangler-only
156
+ projects do not need it.
157
+
158
+ Register the exported auxiliary Worker with the Cloudflare Vite plugin so it
159
+ is built alongside the application:
160
+
161
+ ```ts
162
+ import { cloudflare } from "@cloudflare/vite-plugin";
163
+ import { responseStoreServiceBinding } from "./cloudflare.config.ts";
164
+
165
+ cloudflare({ auxiliaryWorkers: [{ config: responseStoreServiceBinding }] });
166
+ ```
167
+
168
+ Metadata sharding is opt-in and works in either deployment mode:
169
+
170
+ ```ts
171
+ vinext({ cache: responseStoreAdapter({ shards: 16 }) });
172
+ ```
173
+
174
+ Keys remain pinned to one shard while tag/path mutations fan out across every
175
+ shard. Omit `shards` to retain the original single metadata Durable Object.
176
+
177
+ To place newly created metadata Durable Objects near a stable traffic and R2
178
+ region, pass a Cloudflare location hint:
179
+
180
+ ```ts
181
+ vinext({ cache: responseStoreAdapter({ locationHint: "weur" }) });
182
+ ```
183
+
184
+ The hint works in both deployment modes, is best-effort, and only affects each
185
+ Durable Object's first creation. Changing it does not relocate existing
186
+ objects; treat the change as a cache-cold deployment and align it with the R2
187
+ bucket's location.
188
+
189
+ To deploy storage and cache entrypoints with the application instead, select
190
+ self-contained mode:
191
+
192
+ ```ts
193
+ import { responseStoreAdapter } from "@vinext/cloudflare/cache/response-store-adapter";
194
+
195
+ vinext({ cache: responseStoreAdapter({ mode: "self-contained" }) });
196
+ ```
197
+
198
+ The corresponding typed config helper is
199
+ `createWorkersResponseStoreSelfContainedConfig`.
200
+
201
+ In this mode `vinext init` places the required R2, SQLite Durable Object,
202
+ Workers Cache entrypoint, and version-metadata configuration in
203
+ `wrangler.jsonc`; no second Wrangler config is required.
204
+
35
205
  ## Deploy
36
206
 
37
207
  Deploy Cloudflare Workers projects with the package CLI:
@@ -40,5 +210,27 @@ Deploy Cloudflare Workers projects with the package CLI:
40
210
  npx @vinext/cloudflare deploy
41
211
  ```
42
212
 
213
+ Projects with `cloudflare.config.ts` opt into the experimental Cloudflare Vite
214
+ plugin v2 path and deploy their generated Build Output with `cf`. Existing
215
+ Wrangler-configured projects continue to use Wrangler. A normal typed-config
216
+ deploy needs neither `wrangler.jsonc` nor the Wrangler package.
217
+
218
+ `vinext-cloudflare deploy` only deploys the entry Worker. It never deploys named
219
+ auxiliary Workers, including during staged warming or `--no-promote` uploads.
220
+ Deploy a configured Response Store Worker explicitly after building, when first
221
+ setting it up or when its code/config changes:
222
+
223
+ ```sh
224
+ cf deploy --prebuilt --mode production --worker example-response-store
225
+ ```
226
+
227
+ Use the mode and Worker name from your build. `cloudflare.config.ts` remains the
228
+ source of truth; no auxiliary Wrangler config is required.
229
+
230
+ Experimental staged CDN warming uses `cf` to upload a version, read deployment
231
+ status, stage and promote traffic, and apply triggers. Typed-config projects do
232
+ not need an equivalent Wrangler config. Existing Wrangler-configured projects
233
+ continue to use Wrangler for this flow.
234
+
43
235
  With Vite+, use `vpx @vinext/cloudflare deploy`, or
44
236
  `vp exec vinext-cloudflare deploy` when running the locally installed bin.
@@ -0,0 +1,31 @@
1
+ //#region src/cache/cdn-adapter-config.d.ts
2
+ type WranglerWorkerExport = {
3
+ cache?: {
4
+ enabled: boolean;
5
+ };
6
+ type?: string;
7
+ [key: string]: unknown;
8
+ };
9
+ type WranglerOutputConfig = {
10
+ compatibility_date?: string;
11
+ compatibility_flags?: string[];
12
+ exports?: Record<string, WranglerWorkerExport>;
13
+ version_metadata?: unknown;
14
+ [key: string]: unknown;
15
+ };
16
+ type VersionMetadataOptions = {
17
+ binding: string;
18
+ bindingIsExplicit: boolean;
19
+ };
20
+ export declare function configureCdnVersionMetadata(config: WranglerOutputConfig, options: VersionMetadataOptions): WranglerOutputConfig;
21
+ /** Add the per-entrypoint Workers Cache policy to an emitted Wrangler config. */
22
+ export declare function configureWorkersCacheEntrypoints(config: WranglerOutputConfig): WranglerOutputConfig;
23
+ /** Apply the complete CDN adapter policy to the primary generated config. */
24
+ export declare function finalizeCdnAdapterBuildOutput({ root, outDir, isPrimaryServerOutput, binding, bindingIsExplicit }: {
25
+ root: string;
26
+ outDir: string;
27
+ isPrimaryServerOutput: boolean;
28
+ binding: string;
29
+ bindingIsExplicit: boolean;
30
+ }): Promise<void>;
31
+ //#endregion
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ //#region src/cache/cdn-adapter-config.ts
4
+ const CACHED_RESPONSE_STAGE_EXPORT = "VinextCachedResponse";
5
+ const UNCACHED_RESPONSE_STAGE_EXPORT = "VinextUncachedResponse";
6
+ const CTX_EXPORTS_DEFAULT_DATE = "2025-11-17";
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function configureCdnVersionMetadata(config, options) {
11
+ if (!Object.hasOwn(config, "version_metadata")) return {
12
+ ...config,
13
+ version_metadata: { binding: options.binding }
14
+ };
15
+ if (!isRecord(config.version_metadata)) throw new Error("[vinext] The generated Wrangler config has an invalid version_metadata value.");
16
+ const existingBinding = config.version_metadata.binding;
17
+ if (typeof existingBinding !== "string" || existingBinding.length === 0) throw new Error("[vinext] The generated Wrangler config has an invalid version_metadata.binding value.");
18
+ if (existingBinding === options.binding) return config;
19
+ 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)} }).`);
20
+ return {
21
+ ...config,
22
+ version_metadata: { binding: options.binding }
23
+ };
24
+ }
25
+ /** Add the per-entrypoint Workers Cache policy to an emitted Wrangler config. */
26
+ function configureWorkersCacheEntrypoints(config) {
27
+ const configuredExports = config.exports ?? {};
28
+ const compatibilityFlags = config.compatibility_flags ?? [];
29
+ if (compatibilityFlags.includes("disable_ctx_exports")) throw new Error("[vinext] cdnAdapter() requires ctx.exports, but the generated Wrangler config explicitly disables it with disable_ctx_exports.");
30
+ const configuredCompatibilityFlags = config.compatibility_date === void 0 || config.compatibility_date < CTX_EXPORTS_DEFAULT_DATE ? compatibilityFlags.includes("enable_ctx_exports") ? compatibilityFlags : [...compatibilityFlags, "enable_ctx_exports"] : compatibilityFlags.filter((flag) => flag !== "enable_ctx_exports");
31
+ for (const name of [
32
+ "default",
33
+ CACHED_RESPONSE_STAGE_EXPORT,
34
+ UNCACHED_RESPONSE_STAGE_EXPORT
35
+ ]) {
36
+ const existing = configuredExports[name];
37
+ if (existing?.type !== void 0 && existing.type !== "worker") throw new Error(`[vinext] cdnAdapter() cannot configure the reserved Worker export ${JSON.stringify(name)} because it is already declared as ${JSON.stringify(existing.type)}.`);
38
+ }
39
+ return {
40
+ ...config,
41
+ compatibility_flags: configuredCompatibilityFlags,
42
+ exports: {
43
+ ...configuredExports,
44
+ default: {
45
+ ...configuredExports.default,
46
+ type: "worker",
47
+ cache: { enabled: false }
48
+ },
49
+ [CACHED_RESPONSE_STAGE_EXPORT]: {
50
+ ...configuredExports[CACHED_RESPONSE_STAGE_EXPORT],
51
+ type: "worker",
52
+ cache: { enabled: true }
53
+ },
54
+ [UNCACHED_RESPONSE_STAGE_EXPORT]: {
55
+ ...configuredExports[UNCACHED_RESPONSE_STAGE_EXPORT],
56
+ type: "worker",
57
+ cache: { enabled: false }
58
+ }
59
+ }
60
+ };
61
+ }
62
+ async function readGeneratedConfig(generatedConfigPath, allowMissing) {
63
+ try {
64
+ const parsed = JSON.parse(await fs.readFile(generatedConfigPath, "utf8"));
65
+ if (!isRecord(parsed)) throw new TypeError("the root value must be an object");
66
+ return parsed;
67
+ } catch (cause) {
68
+ if (allowMissing && cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") return null;
69
+ throw new Error(`[vinext] Could not read the generated Wrangler config at ${generatedConfigPath}.`, { cause });
70
+ }
71
+ }
72
+ async function writeGeneratedConfig(generatedConfigPath, config) {
73
+ await fs.writeFile(generatedConfigPath, `${JSON.stringify(config, null, 2)}\n`);
74
+ }
75
+ async function usesCloudflareBuildOutput(root) {
76
+ try {
77
+ return (await fs.stat(path.resolve(root, "cloudflare.config.ts"))).isFile();
78
+ } catch (cause) {
79
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") return false;
80
+ throw cause;
81
+ }
82
+ }
83
+ /** Apply the complete CDN adapter policy to the primary generated config. */
84
+ async function finalizeCdnAdapterBuildOutput({ root, outDir, isPrimaryServerOutput, binding, bindingIsExplicit }) {
85
+ if (!isPrimaryServerOutput) return;
86
+ const generatedConfigPath = path.resolve(outDir, "wrangler.json");
87
+ const generatedConfig = await readGeneratedConfig(generatedConfigPath, await usesCloudflareBuildOutput(root));
88
+ if (!generatedConfig) return;
89
+ await writeGeneratedConfig(generatedConfigPath, configureWorkersCacheEntrypoints(configureCdnVersionMetadata(generatedConfig, {
90
+ binding,
91
+ bindingIsExplicit
92
+ })));
93
+ }
94
+ //#endregion
95
+ export { configureCdnVersionMetadata, configureWorkersCacheEntrypoints, finalizeCdnAdapterBuildOutput };
@@ -1,25 +1,58 @@
1
1
  //#region src/cache/cdn-adapter.d.ts
2
+ export declare const DEFAULT_CDN_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
3
+ /** Options accepted by {@link cdnAdapter}, forwarded to the runtime factory. */
4
+ export 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.
5
11
  *
6
12
  * Unlike the data adapter (which stores cache entries in a durable store and
7
- * serves HIT/STALE itself), this adapter delegates serving to Cloudflare's
8
- * edge cache.
13
+ * serves HIT/STALE itself), this adapter delegates serving to Workers Cache on
14
+ * a named Worker entrypoint. The default entrypoint always runs middleware and
15
+ * request-time routing before dispatching to cached or uncached response-stage
16
+ * entrypoints.
17
+ *
18
+ * The deployment configuration enables Workers Cache only for that response-stage
19
+ * export, so cache hits do not start the application stage. Legacy Cloudflare
20
+ * builds receive this policy in their generated Wrangler config. Cloudflare Vite
21
+ * plugin v2 builds declare the same entrypoint policy in `cloudflare.config.ts`.
22
+ * The uncached response entrypoint keeps bypass and probe renders out of the
23
+ * gateway without enabling Workers Cache for them.
9
24
  *
10
- * Workers Cache must be enabled in your Wrangler config for this to work.
11
- * ```jsonc
12
- * // wrangler.jsonc
13
- * {
14
- * "cache": {
15
- * "enabled": true,
16
- * },
17
- * }
18
- * ```
25
+ * The adapter adds a transport-only URL digest so distinct response-stage
26
+ * identities cannot collide. Workers Cache owns this key independently of
27
+ * zone Cache Rules.
19
28
  */
20
- declare function cdnAdapter(options?: Record<string, never>): {
29
+ export declare function cdnAdapter(options?: CdnAdapterOptions): {
21
30
  adapter: string;
22
- options: Record<string, never> | undefined;
31
+ options: CdnAdapterOptions | undefined;
32
+ output: {
33
+ entry: string;
34
+ matchesBuild({ plugins }: {
35
+ plugins: readonly {
36
+ name?: string;
37
+ }[];
38
+ }): boolean;
39
+ transformHostEntry({ code, id }: {
40
+ code: string;
41
+ id: string;
42
+ }): string | null;
43
+ finalizeBuildOutput({ root, outDir, isPrimaryServerOutput }: {
44
+ root: string;
45
+ outDir: string;
46
+ isPrimaryServerOutput: boolean;
47
+ }): Promise<void>;
48
+ type: "multi-stage";
49
+ };
50
+ capabilities: {
51
+ buildIdentity: "response-header";
52
+ isResponsePolicyHeader: (name: string) => boolean;
53
+ requestRouting: "uncached-stage";
54
+ responseVary: "verbatim";
55
+ routeCacheability: "probe-manifest";
56
+ };
23
57
  };
24
- //#endregion
25
- export { cdnAdapter };
58
+ //#endregion
@@ -1,28 +1,65 @@
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";
5
+ const CLOUDFLARE_WORKER_ENTRY_ID = "virtual:cloudflare/worker-entry";
3
6
  /**
4
7
  * Cloudflare CDN cache adapter - edge-managed page-level ISR backed by the
5
8
  * Cloudflare Workers Cache.
6
9
  *
7
10
  * Unlike the data adapter (which stores cache entries in a durable store and
8
- * serves HIT/STALE itself), this adapter delegates serving to Cloudflare's
9
- * edge cache.
11
+ * serves HIT/STALE itself), this adapter delegates serving to Workers Cache on
12
+ * a named Worker entrypoint. The default entrypoint always runs middleware and
13
+ * request-time routing before dispatching to cached or uncached response-stage
14
+ * entrypoints.
10
15
  *
11
- * Workers Cache must be enabled in your Wrangler config for this to work.
12
- * ```jsonc
13
- * // wrangler.jsonc
14
- * {
15
- * "cache": {
16
- * "enabled": true,
17
- * },
18
- * }
19
- * ```
16
+ * The deployment configuration enables Workers Cache only for that response-stage
17
+ * export, so cache hits do not start the application stage. Legacy Cloudflare
18
+ * builds receive this policy in their generated Wrangler config. Cloudflare Vite
19
+ * plugin v2 builds declare the same entrypoint policy in `cloudflare.config.ts`.
20
+ * The uncached response entrypoint keeps bypass and probe renders out of the
21
+ * gateway without enabling Workers Cache for them.
22
+ *
23
+ * The adapter adds a transport-only URL digest so distinct response-stage
24
+ * identities cannot collide. Workers Cache owns this key independently of
25
+ * zone Cache Rules.
20
26
  */
21
27
  function cdnAdapter(options) {
28
+ 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.");
29
+ const versionMetadataBinding = options?.versionMetadataBinding ?? "CF_VERSION_METADATA";
30
+ const bindingIsExplicit = options?.versionMetadataBinding !== void 0;
31
+ const workerEntry = fileURLToPath(import.meta.resolve("./cdn-adapter.worker.js"));
22
32
  return {
23
33
  adapter: fileURLToPath(import.meta.resolve("./cdn-adapter.runtime.js")),
24
- options
34
+ options,
35
+ output: {
36
+ entry: workerEntry,
37
+ matchesBuild({ plugins }) {
38
+ return plugins.some(({ name }) => name === "vite-plugin-cloudflare" || name?.startsWith("vite-plugin-cloudflare:"));
39
+ },
40
+ transformHostEntry({ code, id }) {
41
+ if ((id.charCodeAt(0) === 0 ? id.slice(1) : id) !== CLOUDFLARE_WORKER_ENTRY_ID) return null;
42
+ return `${code}\nexport { VinextCachedResponse, VinextUncachedResponse } from ${JSON.stringify(workerEntry)};\n`;
43
+ },
44
+ finalizeBuildOutput({ root, outDir, isPrimaryServerOutput }) {
45
+ return finalizeCdnAdapterBuildOutput({
46
+ root,
47
+ outDir,
48
+ isPrimaryServerOutput,
49
+ binding: versionMetadataBinding,
50
+ bindingIsExplicit
51
+ });
52
+ },
53
+ type: "multi-stage"
54
+ },
55
+ capabilities: {
56
+ buildIdentity: "response-header",
57
+ isResponsePolicyHeader: (name) => name.toLowerCase() === "cdn-cache-control" || name.toLowerCase() === "cloudflare-cdn-cache-control",
58
+ requestRouting: "uncached-stage",
59
+ responseVary: "verbatim",
60
+ routeCacheability: "probe-manifest"
61
+ }
25
62
  };
26
63
  }
27
64
  //#endregion
28
- export { cdnAdapter };
65
+ export { DEFAULT_CDN_VERSION_METADATA_BINDING, cdnAdapter };
@@ -1,8 +1,26 @@
1
1
  import { CdnCacheAdapter, CdnCacheableHeaderInput, CdnResponseHeaders } from "vinext/shims/cdn-cache";
2
2
  import { CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache";
3
-
4
3
  //#region src/cache/cdn-adapter.runtime.d.ts
5
- declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
4
+ type WorkerVersionMetadata = {
5
+ id: string;
6
+ };
7
+ type CdnAdapterOptions = {
8
+ versionMetadataBinding?: string;
9
+ };
10
+ /** Encode a case-sensitive Next.js tag into a fixed Workers Cache tag. */
11
+ export declare function encodeCloudflareCacheTag(tag: string): string;
12
+ export declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
13
+ private readonly versionMetadata?;
14
+ private readonly versionMetadataBinding;
15
+ readonly requiresCompletedResponseAdmission = true;
16
+ readonly responseVary: "verbatim";
17
+ readonly responsePolicy: {
18
+ isHeader(name: string): boolean;
19
+ readCacheControl(headers: Headers): string | null;
20
+ hasExplicitNonCacheablePolicy(headers: Headers, baseline?: Headers): boolean;
21
+ };
22
+ constructor(versionMetadata?: WorkerVersionMetadata | undefined, versionMetadataBinding?: string);
23
+ validateRequest(request: Request): Response | null;
6
24
  readonly ownsBackgroundRevalidation = false;
7
25
  /**
8
26
  * The origin keeps no page store — return null so the request renders fresh.
@@ -11,12 +29,16 @@ declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
11
29
  get(): Promise<CacheHandlerValue | null>;
12
30
  /** No-op: the platform caches the response via its headers, not an origin store. */
13
31
  set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
32
+ buildResponseIdentityHeaders(): CdnResponseHeaders;
14
33
  buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
15
34
  /** Purge edge-cached responses by tag via the request context's `cache.purge`. */
16
35
  revalidateTag(tags: string | string[], _durations?: {
17
36
  expire?: number;
18
37
  }): Promise<void>;
19
38
  }
20
- declare const createCloudflareCdnCacheAdapter: () => CdnCacheAdapter;
39
+ declare const createCloudflareCdnCacheAdapter: ({ env, options }?: {
40
+ env?: Record<string, unknown>;
41
+ options?: CdnAdapterOptions;
42
+ }) => CdnCacheAdapter;
21
43
  //#endregion
22
- export { CloudflareCdnCacheAdapter, createCloudflareCdnCacheAdapter as default };
44
+ export { createCloudflareCdnCacheAdapter as default };