@vinext/cloudflare 1.0.0-beta.9 → 1.0.1

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 (54) hide show
  1. package/README.md +165 -37
  2. package/dist/cache/browser-cache-policy.d.ts +7 -0
  3. package/dist/cache/browser-cache-policy.js +39 -0
  4. package/dist/cache/cdn-adapter-config.d.ts +2 -1
  5. package/dist/cache/cdn-adapter-config.js +13 -5
  6. package/dist/cache/cdn-adapter.d.ts +5 -56
  7. package/dist/cache/cdn-adapter.js +3 -61
  8. package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
  9. package/dist/cache/cdn-adapter.runtime.js +33 -19
  10. package/dist/cache/cdn-adapter.worker.d.ts +1 -1
  11. package/dist/cache/cdn-adapter.worker.js +10 -35
  12. package/dist/cache/cdn-build-id.d.ts +1 -1
  13. package/dist/cache/cdn-build-id.js +1 -1
  14. package/dist/cache/config.d.ts +137 -0
  15. package/dist/cache/config.js +87 -0
  16. package/dist/cache/kv-data-adapter.d.ts +1 -1
  17. package/dist/cache/kv-data-adapter.js +1 -1
  18. package/dist/cache/kv-data-adapter.runtime.js +17 -15
  19. package/dist/cache/kv-key.d.ts +7 -4
  20. package/dist/cache/kv-key.js +17 -6
  21. package/dist/cache/response-store-adapter.d.ts +9 -6
  22. package/dist/cache/response-store-adapter.js +20 -1
  23. package/dist/cache/response-store-adapter.worker.js +121 -48
  24. package/dist/cache/response-store-cdn.runtime.js +1 -0
  25. package/dist/cache/response-store-data.runtime.js +53 -31
  26. package/dist/cache/static-assets-adapter.build.d.ts +6 -0
  27. package/dist/cache/static-assets-adapter.build.js +102 -0
  28. package/dist/cache/static-assets-adapter.d.ts +27 -0
  29. package/dist/cache/static-assets-adapter.js +26 -0
  30. package/dist/cache/static-assets-adapter.runtime.d.ts +27 -0
  31. package/dist/cache/static-assets-adapter.runtime.js +94 -0
  32. package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
  33. package/dist/cache/static-assets-adapter.shared.js +4 -0
  34. package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
  35. package/dist/cache/workers-cache-cdn-adapter.js +65 -0
  36. package/dist/cacheability-artifact.d.ts +7 -1
  37. package/dist/cacheability-artifact.js +60 -21
  38. package/dist/cacheability-probe.d.ts +2 -0
  39. package/dist/cacheability-probe.js +107 -13
  40. package/dist/cdn-warm.d.ts +1 -0
  41. package/dist/cdn-warm.js +4 -3
  42. package/dist/deploy-config.d.ts +3 -8
  43. package/dist/deploy-config.js +5 -23
  44. package/dist/deploy-help.js +47 -42
  45. package/dist/deploy.d.ts +18 -16
  46. package/dist/deploy.js +355 -216
  47. package/dist/tpr.d.ts +4 -0
  48. package/dist/tpr.js +16 -7
  49. package/dist/version-deploy.d.ts +27 -0
  50. package/dist/version-deploy.js +121 -3
  51. package/dist/wrangler-version-metadata.js +1 -1
  52. package/package.json +16 -5
  53. package/dist/prerender-kv-populate.d.ts +0 -24
  54. package/dist/prerender-kv-populate.js +0 -153
package/README.md CHANGED
@@ -8,7 +8,7 @@ This package provides Cloudflare-specific cache and image backends for vinext:
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
10
  namespace.
11
- - **`cdnAdapter()`** (`@vinext/cloudflare/cache/cdn-adapter`) — delegates
11
+ - **`workersCacheCdnAdapter()`** (`@vinext/cloudflare/cache/workers-cache-cdn-adapter`) — delegates
12
12
  page-level ISR serving and revalidation to Cloudflare Workers Cache through
13
13
  an automatically generated cache-enabled response entrypoint.
14
14
  - **`responseStoreAdapter()`** (`@vinext/cloudflare/cache/response-store-adapter`) —
@@ -19,78 +19,156 @@ This package provides Cloudflare-specific cache and image backends for vinext:
19
19
 
20
20
  ## Usage
21
21
 
22
- Declare the adapters on the `vinext()` plugin in your Vite config:
22
+ These snippets extend the config generated by `vinext init`. Declare adapters on
23
+ the existing `vinext()` plugin in your Vite config:
23
24
 
24
25
  ```ts
25
26
  import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";
26
27
  import { imagesOptimizer } from "@vinext/cloudflare/images/images-optimizer";
27
28
 
28
- export default defineConfig({
29
- plugins: [
30
- vinext({
31
- cache: {
32
- data: kvDataAdapter(), // KV-backed data cache (binding: VINEXT_KV_CACHE)
33
- },
34
- images: { optimizer: imagesOptimizer() }, // Cloudflare Images binding: IMAGES
35
- }),
36
- cloudflare(),
37
- ],
29
+ vinext({
30
+ cache: { data: kvDataAdapter() },
31
+ images: { optimizer: imagesOptimizer() },
38
32
  });
39
33
  ```
40
34
 
35
+ Add the bindings to your Worker's `env` in `cloudflare.config.ts`:
36
+
37
+ ```ts
38
+ import { bindings } from "cf/config";
39
+
40
+ env: {
41
+ // ...existing bindings
42
+ VINEXT_KV_CACHE: bindings.kv(),
43
+ IMAGES: bindings.images(),
44
+ },
45
+ ```
46
+
41
47
  ### Workers Cache
42
48
 
43
- `cdnAdapter()` is optional. Configuring it asks the Cloudflare build for two
49
+ `workersCacheCdnAdapter()` is optional. Configuring it asks the Cloudflare build for two
44
50
  Worker entrypoints: the default entrypoint runs middleware and request-time
45
51
  routing with caching disabled, while `VinextCachedResponse` lazily loads the
46
- render stage with Workers Cache enabled. These settings are written to the
47
- generated `dist/server/wrangler.json`; do not enable Workers Cache on the
48
- default entrypoint in your source config. The generated config also declares
49
- the version metadata binding used for staged warmup.
52
+ render stage with Workers Cache enabled.
50
53
 
51
54
  ```ts
52
- import { cdnAdapter } from "@vinext/cloudflare/cache/cdn-adapter";
55
+ import { workersCacheCdnAdapter } from "@vinext/cloudflare/cache/workers-cache-cdn-adapter";
53
56
 
54
- vinext({ cache: { cdn: cdnAdapter() } });
57
+ vinext({ cache: { cdn: workersCacheCdnAdapter() } });
58
+ ```
59
+
60
+ Cloudflare Vite plugin v2 uses Build Output and treats `cloudflare.config.ts`
61
+ as the deployment source of truth. Declare the equivalent policies there.
62
+ In these snippets, `existingExports` is your current Worker's `exports` object,
63
+ or `{}` if it has none:
64
+
65
+ ```ts
66
+ import { createWorkersCacheConfig } from "@vinext/cloudflare/cache/config";
67
+
68
+ const workersCache = await createWorkersCacheConfig();
69
+
70
+ defineWorker({
71
+ // ...existing Worker settings
72
+ ...workersCache,
73
+ env: { /* existing bindings, */ ...workersCache.env },
74
+ exports: { ...existingExports, ...workersCache.exports },
75
+ });
55
76
  ```
56
77
 
57
78
  The generated version metadata binding lets staged warmup prove that every
58
79
  discovery, probe, and fill request reached the uploaded Worker version. Pass
59
- `versionMetadataBinding` to `cdnAdapter()` only when the deployment needs a
60
- custom binding name.
80
+ the same `versionMetadataBinding` to `workersCacheCdnAdapter()` and
81
+ `createWorkersCacheConfig()` only when the deployment needs a custom binding
82
+ name.
61
83
 
62
- Use `--experimental-warm-cdn-cache` for the two-stage deploy. The default flow
63
- makes one final fill request per admitted identity. Add `--warm-cdn-certify`
84
+ Use `--warm-cache` for the two-stage deploy. The default flow
85
+ makes one final fill request per admitted identity. Add `--warm-cache-certify`
64
86
  only when you want an opt-in second, header-only request that must prove every
65
87
  planned entry reusable before promotion.
66
88
 
67
89
  The response entrypoint hashes the complete transport identity into its
68
- Workers Cache URL, independently of zone Cache Rules, so distinct query and
69
- representation variants cannot collide.
90
+ Workers Cache URL, independently of zone Cache Rules, so distinct
91
+ representation, rewrite and interception variants cannot collide. For App
92
+ Router pages that the cacheability manifest of a staged
93
+ `--warm-cache` deploy marks `static-candidate`, the identity
94
+ leaves out the query string, so every query of the page shares one entry, as in
95
+ Next.js. Other requests, and responses that a `next.config` `headers()` rule
96
+ makes cacheable, keep the query in their identity. See
97
+ [the caching guide](https://vinext.dev/docs/guides/caching#query-strings).
70
98
 
71
99
  ### Workers Response Store
72
100
 
73
- `responseStoreAdapter()` replaces both `cdnAdapter()` and `kvDataAdapter()`.
101
+ `responseStoreAdapter()` replaces both `workersCacheCdnAdapter()` and `kvDataAdapter()`.
74
102
  It defaults to a separate cache Worker reached through the `RESPONSE_STORE`
75
- service binding. `vinext init` writes two collocated source configs:
76
- `wrangler.jsonc` for the application and `wrangler.response-store.jsonc` for the
77
- cache Worker. The latter points directly at the installed
78
- `@cloudflare/workers-response-store` implementation and owns its R2 bucket,
79
- SQLite Durable Object, Worker name, and cache settings. Edit those configs to
80
- choose or reuse names, keeping the application service binding aligned with the
81
- cache Worker name.
103
+ service binding. `vinext init` defines both Workers in `cloudflare.config.ts`.
104
+ The service-binding helper points directly at the installed
105
+ `@cloudflare/workers-response-store` implementation and configures its R2 binding,
106
+ SQLite Durable Object, Worker name, and cache settings, along with the application's
107
+ service binding and version metadata.
82
108
 
83
109
  The two Workers are deliberately deployed separately. Deploy the Response Store
84
110
  when its package or config changes, then deploy the application normally:
85
111
 
86
112
  ```sh
87
- npx wrangler deploy --config wrangler.response-store.jsonc
113
+ pnpm run build:vinext
114
+ pnpm run deploy:response-store
88
115
  npx @vinext/cloudflare deploy
89
116
  ```
90
117
 
91
118
  `vinext-cloudflare deploy` never creates, rewrites, or deploys the Response
92
119
  Store Worker.
93
120
 
121
+ `cf deploy` creates the named R2 bucket if it does not already exist. `create-vinext-app`
122
+ uses `build` instead of `build:vinext`. The source config uses the shared helper:
123
+
124
+ ```ts
125
+ import { createWorkersResponseStoreServiceBindingConfig } from "@vinext/cloudflare/cache/config";
126
+
127
+ const responseStore = await createWorkersResponseStoreServiceBindingConfig({
128
+ worker: {
129
+ name: "example-response-store",
130
+ compatibilityDate: "2026-09-15",
131
+ compatibilityFlags: ["nodejs_compat"],
132
+ },
133
+ bucket: "example-response-store-cache-bodies",
134
+ });
135
+
136
+ export const responseStoreServiceBinding = responseStore.serviceBindingWorker;
137
+ ```
138
+
139
+ Merge the helper's application settings into your existing Worker:
140
+
141
+ ```ts
142
+ defineWorker({
143
+ // ...existing Worker settings
144
+ ...responseStore.applicationWorker,
145
+ env: {
146
+ // ...existing bindings
147
+ ...responseStore.applicationWorker.env,
148
+ },
149
+ });
150
+ ```
151
+
152
+ `worker.observability` accepts the full observability configuration supported by
153
+ your installed `cf` package, including logs and traces settings.
154
+ The optional `accountId` is returned as `responseStore.accountId`: pass it to `defineConfig`,
155
+ not to either Worker. It applies to both Workers in the project.
156
+
157
+ The async config helpers import bindings and exports from your installed
158
+ `cf/config`. The `cf` package is an optional peer dependency of `@vinext/cloudflare`;
159
+ install it when using these helpers.
160
+
161
+ Add the exported auxiliary Worker to your existing Cloudflare Vite plugin options:
162
+
163
+ ```ts
164
+ import { responseStoreServiceBinding } from "./cloudflare.config";
165
+
166
+ cloudflare({
167
+ // ...existing plugin options
168
+ auxiliaryWorkers: [{ config: responseStoreServiceBinding }],
169
+ });
170
+ ```
171
+
94
172
  Metadata sharding is opt-in and works in either deployment mode:
95
173
 
96
174
  ```ts
@@ -100,6 +178,18 @@ vinext({ cache: responseStoreAdapter({ shards: 16 }) });
100
178
  Keys remain pinned to one shard while tag/path mutations fan out across every
101
179
  shard. Omit `shards` to retain the original single metadata Durable Object.
102
180
 
181
+ To place newly created metadata Durable Objects near a stable traffic and R2
182
+ region, pass a Cloudflare location hint:
183
+
184
+ ```ts
185
+ vinext({ cache: responseStoreAdapter({ locationHint: "weur" }) });
186
+ ```
187
+
188
+ The hint works in both deployment modes, is best-effort, and only affects each
189
+ Durable Object's first creation. Changing it does not relocate existing
190
+ objects; treat the change as a cache-cold deployment and align it with the R2
191
+ bucket's location.
192
+
103
193
  To deploy storage and cache entrypoints with the application instead, select
104
194
  self-contained mode:
105
195
 
@@ -109,9 +199,27 @@ import { responseStoreAdapter } from "@vinext/cloudflare/cache/response-store-ad
109
199
  vinext({ cache: responseStoreAdapter({ mode: "self-contained" }) });
110
200
  ```
111
201
 
112
- In this mode `vinext init` places the required R2, SQLite Durable Object,
113
- Workers Cache entrypoint, and version-metadata configuration in
114
- `wrangler.jsonc`; no second Wrangler config is required.
202
+ Use the matching helper in `cloudflare.config.ts`:
203
+
204
+ ```ts
205
+ import { createWorkersResponseStoreSelfContainedConfig } from "@vinext/cloudflare/cache/config";
206
+
207
+ const cache = await createWorkersResponseStoreSelfContainedConfig({
208
+ worker: "example",
209
+ bucket: "example-response-store-cache-bodies",
210
+ });
211
+
212
+ defineWorker({
213
+ // ...existing Worker settings
214
+ ...cache,
215
+ env: { /* existing bindings, */ ...cache.env },
216
+ exports: { ...existingExports, ...cache.exports },
217
+ });
218
+ ```
219
+
220
+ The adapter generates the required exports during the build. This config declares
221
+ the R2 and SQLite Durable Object bindings, Workers Cache entrypoint, and version
222
+ metadata on the application Worker; no auxiliary Worker or separate deployment is needed.
115
223
 
116
224
  ## Deploy
117
225
 
@@ -121,5 +229,25 @@ Deploy Cloudflare Workers projects with the package CLI:
121
229
  npx @vinext/cloudflare deploy
122
230
  ```
123
231
 
232
+ Cloudflare init and create-vinext-app use `cloudflare.config.ts`, Cloudflare Vite
233
+ plugin v2, and `cf` by default. The deploy command builds and deploys the generated
234
+ Cloudflare Build Output. Source config remains authoritative for bindings and
235
+ cache policies.
236
+
237
+ `vinext-cloudflare deploy` only deploys the entry Worker. It never deploys named
238
+ auxiliary Workers, including during staged warming or `--no-promote` uploads.
239
+ Deploy a configured Response Store Worker explicitly after building, when first
240
+ setting it up or when its code/config changes:
241
+
242
+ ```sh
243
+ cf deploy --prebuilt --mode production --worker example-response-store
244
+ ```
245
+
246
+ Use the mode and Worker name from your build. `cloudflare.config.ts` remains the
247
+ source of truth.
248
+
249
+ Staged cache warming uses `cf` to upload a version, read deployment
250
+ status, stage and promote traffic, and apply triggers.
251
+
124
252
  With Vite+, use `vpx @vinext/cloudflare deploy`, or
125
253
  `vp exec vinext-cloudflare deploy` when running the locally installed bin.
@@ -0,0 +1,7 @@
1
+ //#region src/cache/browser-cache-policy.d.ts
2
+ export type SharedResponseStage = {
3
+ headers: Headers;
4
+ };
5
+ export declare const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
6
+ export declare function finalizeGatewayResponse(response: Response, sharedResponses: Map<string, SharedResponseStage>, cacheStatusHeader?: string): Response;
7
+ //#endregion
@@ -0,0 +1,39 @@
1
+ import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER } from "vinext/internal/server/headers";
2
+ //#region src/cache/browser-cache-policy.ts
3
+ const CLOUDFLARE_EDGE_POLICY_HEADER = "Cloudflare-CDN-Cache-Control";
4
+ const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
5
+ const PRIVATE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
6
+ SHARED_RESPONSE_STAGE_HEADER,
7
+ CLOUDFLARE_EDGE_POLICY_HEADER.toLowerCase(),
8
+ "cdn-cache-control",
9
+ "cache-tag",
10
+ VINEXT_CACHE_HEADER.toLowerCase(),
11
+ NEXTJS_CACHE_HEADER.toLowerCase()
12
+ ]);
13
+ function finalizeGatewayResponse(response, sharedResponses, cacheStatusHeader = "CF-Cache-Status") {
14
+ const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
15
+ const sharedResponse = provenance === null ? void 0 : sharedResponses.get(provenance);
16
+ const usedSharedResponseStage = sharedResponse !== void 0;
17
+ const sharedResponseStageCollision = provenance !== null && !usedSharedResponseStage;
18
+ if (!response.headers.has(CLOUDFLARE_EDGE_POLICY_HEADER) && !usedSharedResponseStage && !sharedResponseStageCollision) return response;
19
+ const headers = new Headers(response.headers);
20
+ headers.delete(SHARED_RESPONSE_STAGE_HEADER);
21
+ headers.delete(CLOUDFLARE_EDGE_POLICY_HEADER);
22
+ if (usedSharedResponseStage || sharedResponseStageCollision) {
23
+ for (const name of PRIVATE_RESPONSE_HEADERS) headers.delete(name);
24
+ if (sharedResponseStageCollision) headers.set("Cache-Control", "no-store");
25
+ else if (!headers.has("Cache-Control")) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
26
+ }
27
+ const cacheStatus = sharedResponse?.headers.get(cacheStatusHeader);
28
+ if (cacheStatus) {
29
+ headers.set(VINEXT_CACHE_HEADER, cacheStatus);
30
+ headers.set(NEXTJS_CACHE_HEADER, cacheStatus);
31
+ }
32
+ return new Response(response.body, {
33
+ headers,
34
+ status: response.status,
35
+ statusText: response.statusText
36
+ });
37
+ }
38
+ //#endregion
39
+ export { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse };
@@ -21,7 +21,8 @@ export declare function configureCdnVersionMetadata(config: WranglerOutputConfig
21
21
  /** Add the per-entrypoint Workers Cache policy to an emitted Wrangler config. */
22
22
  export declare function configureWorkersCacheEntrypoints(config: WranglerOutputConfig): WranglerOutputConfig;
23
23
  /** Apply the complete CDN adapter policy to the primary generated config. */
24
- export declare function finalizeCdnAdapterBuildOutput({ outDir, isPrimaryServerOutput, binding, bindingIsExplicit }: {
24
+ export declare function finalizeCdnAdapterBuildOutput({ root, outDir, isPrimaryServerOutput, binding, bindingIsExplicit }: {
25
+ root: string;
25
26
  outDir: string;
26
27
  isPrimaryServerOutput: boolean;
27
28
  binding: string;
@@ -16,7 +16,7 @@ function configureCdnVersionMetadata(config, options) {
16
16
  const existingBinding = config.version_metadata.binding;
17
17
  if (typeof existingBinding !== "string" || existingBinding.length === 0) throw new Error("[vinext] The generated Wrangler config has an invalid version_metadata.binding value.");
18
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)} }).`);
19
+ if (!options.bindingIsExplicit) throw new Error(`[vinext] workersCacheCdnAdapter() 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 workersCacheCdnAdapter({ versionMetadataBinding: ${JSON.stringify(existingBinding)} }).`);
20
20
  return {
21
21
  ...config,
22
22
  version_metadata: { binding: options.binding }
@@ -26,7 +26,7 @@ function configureCdnVersionMetadata(config, options) {
26
26
  function configureWorkersCacheEntrypoints(config) {
27
27
  const configuredExports = config.exports ?? {};
28
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.");
29
+ if (compatibilityFlags.includes("disable_ctx_exports")) throw new Error("[vinext] workersCacheCdnAdapter() requires ctx.exports, but the generated Wrangler config explicitly disables it with disable_ctx_exports.");
30
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
31
  for (const name of [
32
32
  "default",
@@ -34,7 +34,7 @@ function configureWorkersCacheEntrypoints(config) {
34
34
  UNCACHED_RESPONSE_STAGE_EXPORT
35
35
  ]) {
36
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)}.`);
37
+ if (existing?.type !== void 0 && existing.type !== "worker") throw new Error(`[vinext] workersCacheCdnAdapter() cannot configure the reserved Worker export ${JSON.stringify(name)} because it is already declared as ${JSON.stringify(existing.type)}.`);
38
38
  }
39
39
  return {
40
40
  ...config,
@@ -72,11 +72,19 @@ async function readGeneratedConfig(generatedConfigPath, allowMissing) {
72
72
  async function writeGeneratedConfig(generatedConfigPath, config) {
73
73
  await fs.writeFile(generatedConfigPath, `${JSON.stringify(config, null, 2)}\n`);
74
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
+ }
75
83
  /** Apply the complete CDN adapter policy to the primary generated config. */
76
- async function finalizeCdnAdapterBuildOutput({ outDir, isPrimaryServerOutput, binding, bindingIsExplicit }) {
84
+ async function finalizeCdnAdapterBuildOutput({ root, outDir, isPrimaryServerOutput, binding, bindingIsExplicit }) {
77
85
  if (!isPrimaryServerOutput) return;
78
86
  const generatedConfigPath = path.resolve(outDir, "wrangler.json");
79
- const generatedConfig = await readGeneratedConfig(generatedConfigPath, false);
87
+ const generatedConfig = await readGeneratedConfig(generatedConfigPath, await usesCloudflareBuildOutput(root));
80
88
  if (!generatedConfig) return;
81
89
  await writeGeneratedConfig(generatedConfigPath, configureWorkersCacheEntrypoints(configureCdnVersionMetadata(generatedConfig, {
82
90
  binding,
@@ -1,57 +1,6 @@
1
+ import { CdnAdapterOptions, DEFAULT_CDN_VERSION_METADATA_BINDING, workersCacheCdnAdapter } from "./workers-cache-cdn-adapter.js";
1
2
  //#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
- };
8
- /**
9
- * Cloudflare CDN cache adapter - edge-managed page-level ISR backed by the
10
- * Cloudflare Workers Cache.
11
- *
12
- * Unlike the data adapter (which stores cache entries in a durable store and
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 emitted Wrangler configuration enables Workers Cache only for that
19
- * response-stage export, so cache hits do not start the application stage.
20
- * The generated deployment config automatically enables Workers Cache for the
21
- * cached response entrypoint and adds the version metadata binding used by
22
- * warmup. The uncached response entrypoint keeps bypass and probe renders out
23
- * of the gateway without enabling Workers Cache for them.
24
- *
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.
28
- */
29
- export declare function cdnAdapter(options?: CdnAdapterOptions): {
30
- adapter: string;
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({ outDir, isPrimaryServerOutput }: {
44
- outDir: string;
45
- isPrimaryServerOutput: boolean;
46
- }): Promise<void>;
47
- type: "multi-stage";
48
- };
49
- capabilities: {
50
- buildIdentity: "response-header";
51
- isResponsePolicyHeader: (name: string) => boolean;
52
- requestRouting: "uncached-stage";
53
- responseVary: "verbatim";
54
- routeCacheability: "probe-manifest";
55
- };
56
- };
57
- //#endregion
3
+ /** @deprecated Use {@link workersCacheCdnAdapter} from `@vinext/cloudflare/cache/workers-cache-cdn-adapter` instead. */
4
+ export declare const cdnAdapter: typeof workersCacheCdnAdapter;
5
+ //#endregion
6
+ export { type CdnAdapterOptions, DEFAULT_CDN_VERSION_METADATA_BINDING };
@@ -1,64 +1,6 @@
1
- import { finalizeCdnAdapterBuildOutput } from "./cdn-adapter-config.js";
2
- import { fileURLToPath } from "node:url";
1
+ import { DEFAULT_CDN_VERSION_METADATA_BINDING, workersCacheCdnAdapter } from "./workers-cache-cdn-adapter.js";
3
2
  //#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";
6
- /**
7
- * Cloudflare CDN cache adapter - edge-managed page-level ISR backed by the
8
- * Cloudflare Workers Cache.
9
- *
10
- * Unlike the data adapter (which stores cache entries in a durable store and
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.
15
- *
16
- * The emitted Wrangler configuration enables Workers Cache only for that
17
- * response-stage export, so cache hits do not start the application stage.
18
- * The generated deployment config automatically enables Workers Cache for the
19
- * cached response entrypoint and adds the version metadata binding used by
20
- * warmup. The uncached response entrypoint keeps bypass and probe renders out
21
- * of the 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.
26
- */
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"));
32
- return {
33
- adapter: fileURLToPath(import.meta.resolve("./cdn-adapter.runtime.js")),
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({ outDir, isPrimaryServerOutput }) {
45
- return finalizeCdnAdapterBuildOutput({
46
- outDir,
47
- isPrimaryServerOutput,
48
- binding: versionMetadataBinding,
49
- bindingIsExplicit
50
- });
51
- },
52
- type: "multi-stage"
53
- },
54
- capabilities: {
55
- buildIdentity: "response-header",
56
- isResponsePolicyHeader: (name) => name.toLowerCase() === "cdn-cache-control" || name.toLowerCase() === "cloudflare-cdn-cache-control",
57
- requestRouting: "uncached-stage",
58
- responseVary: "verbatim",
59
- routeCacheability: "probe-manifest"
60
- }
61
- };
62
- }
3
+ /** @deprecated Use {@link workersCacheCdnAdapter} from `@vinext/cloudflare/cache/workers-cache-cdn-adapter` instead. */
4
+ const cdnAdapter = workersCacheCdnAdapter;
63
5
  //#endregion
64
6
  export { DEFAULT_CDN_VERSION_METADATA_BINDING, cdnAdapter };
@@ -17,6 +17,7 @@ export declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
17
17
  readonly responsePolicy: {
18
18
  isHeader(name: string): boolean;
19
19
  readCacheControl(headers: Headers): string | null;
20
+ readCacheControlHeaderName(headers: Headers): string | null;
20
21
  hasExplicitNonCacheablePolicy(headers: Headers, baseline?: Headers): boolean;
21
22
  };
22
23
  constructor(versionMetadata?: WorkerVersionMetadata | undefined, versionMetadataBinding?: string);
@@ -1,8 +1,8 @@
1
1
  import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
2
2
  import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "../version-headers.js";
3
- import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
- import { fnv1a64 } from "vinext/internal/utils/hash";
3
+ import { isNonCacheableCacheControl, splitCacheControlDirectives } from "vinext/shims/cdn-cache";
5
4
  import { getRequestExecutionContext } from "vinext/shims/request-context";
5
+ import { fnv1a64 } from "vinext/internal/utils/hash";
6
6
  //#region src/cache/cdn-adapter.runtime.ts
7
7
  /**
8
8
  * Cloudflare CDN cache adapter — edge-managed page-level ISR backed by the
@@ -19,11 +19,9 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
19
19
  * cache headers, so there is nothing to persist at the origin.
20
20
  * - `buildResponseHeaders` emits the SWR policy as `Cloudflare-CDN-Cache-Control`
21
21
  * (`public, max-age=…, stale-while-revalidate=…`) so the edge caches and
22
- * revalidates. The inner response's `Cache-Control` is
23
- * `public, max-age=0, must-revalidate` so a browser never serves a stored copy
24
- * without revalidating against the edge; the uncached gateway changes that to
25
- * `private, max-age=0, must-revalidate` before public egress so personalized
26
- * request-stage headers cannot enter another shared cache. A `Cache-Tag`
22
+ * revalidates. Explicit endpoint Cache-Control is preserved for clients,
23
+ * independently of shared admission. Framework-managed responses default to
24
+ * `private, max-age=0, must-revalidate`. A `Cache-Tag`
27
25
  * header lets entries be purged by tag. Note the edge directive uses `max-age`
28
26
  * (not `s-maxage`):
29
27
  * the framework computes the policy with `s-maxage` for shared caches, but
@@ -37,7 +35,7 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
37
35
  *
38
36
  * The default export is the adapter factory the generated
39
37
  * `virtual:vinext-cache-adapters` registration imports; configure it from
40
- * vite.config via the {@link cdnAdapter} builder in `./cdn-adapter.ts` (which
38
+ * vite.config via the {@link workersCacheCdnAdapter} builder in `./workers-cache-cdn-adapter.ts` (which
41
39
  * `require.resolve`s this file).
42
40
  */
43
41
  const DEFAULT_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
@@ -54,13 +52,13 @@ function getBuildIdentityResponseHeader() {
54
52
  const buildId = getVinextCdnBuildIdentity();
55
53
  return buildId ? { [VINEXT_CDN_BUILD_ID_HEADER]: buildId } : {};
56
54
  }
57
- /** Remove every response header whose cache semantics are owned by Cloudflare. */
58
- function clearCloudflareCdnResponseHeaders(cacheControl) {
55
+ /** Disable shared caching while preserving an explicit client policy. */
56
+ function clearCloudflareCdnResponseHeaders(cacheControl, browserCacheControl) {
59
57
  return {
60
58
  ...getBuildIdentityResponseHeader(),
61
- "Cache-Control": cacheControl,
59
+ "Cache-Control": browserCacheControl ?? cacheControl,
62
60
  "CDN-Cache-Control": null,
63
- "Cloudflare-CDN-Cache-Control": null,
61
+ "Cloudflare-CDN-Cache-Control": browserCacheControl !== void 0 && !isNonCacheableCacheControl(browserCacheControl) ? "no-store" : null,
64
62
  "Cache-Tag": null
65
63
  };
66
64
  }
@@ -72,8 +70,19 @@ function hasExplicitCloudflareNonCacheableResponsePolicy(headers) {
72
70
  const browserPolicy = headers.get("Cache-Control");
73
71
  return Boolean(!hasCacheableEdgePolicy && browserPolicy && isNonCacheableCacheControl(browserPolicy));
74
72
  }
73
+ /** Shared-cache policy headers in the order Cloudflare honors them. */
74
+ const RESPONSE_POLICY_PRECEDENCE = [
75
+ "Cloudflare-CDN-Cache-Control",
76
+ "CDN-Cache-Control",
77
+ "Cache-Control"
78
+ ];
79
+ /** Name the highest-precedence policy header Cloudflare honors on a response. */
80
+ function readCloudflareResponsePolicyHeaderName(headers) {
81
+ return RESPONSE_POLICY_PRECEDENCE.find((name) => headers.has(name)) ?? null;
82
+ }
75
83
  function readCloudflareResponseCacheControl(headers) {
76
- return headers.get("Cloudflare-CDN-Cache-Control") ?? headers.get("CDN-Cache-Control") ?? headers.get("Cache-Control");
84
+ const name = readCloudflareResponsePolicyHeaderName(headers);
85
+ return name ? headers.get(name) : null;
77
86
  }
78
87
  function getWorkersCache() {
79
88
  const cache = getRequestExecutionContext()?.cache;
@@ -88,7 +97,7 @@ const NO_STORE = "no-store";
88
97
  * revalidate (against the edge) rather than serving a stored copy — so the user
89
98
  * always sees edge-fresh content while still permitting conditional 304s.
90
99
  */
91
- const BROWSER_REVALIDATE = "public, max-age=0, must-revalidate";
100
+ const BROWSER_REVALIDATE = "private, max-age=0, must-revalidate";
92
101
  /**
93
102
  * A concrete stale window (1 year) substituted for a value-less
94
103
  * `stale-while-revalidate`. The framework emits a bare `stale-while-revalidate`
@@ -108,8 +117,10 @@ const UNBOUNDED_SWR_SECONDS = 31536e3;
108
117
  * `public`.
109
118
  */
110
119
  function toEdgeCacheControl(cacheControl) {
111
- const withMaxAge = cacheControl.replace(/\bs-maxage=/g, "max-age=").replace(/\bstale-while-revalidate\b(?!=)/g, `stale-while-revalidate=${UNBOUNDED_SWR_SECONDS}`);
112
- return /\bpublic\b/.test(withMaxAge) ? withMaxAge : `public, ${withMaxAge}`;
120
+ const directives = splitCacheControlDirectives(cacheControl);
121
+ const hasSharedMaxAge = directives.some((directive) => /^s-maxage\s*=/i.test(directive));
122
+ const withMaxAge = directives.filter((directive) => !hasSharedMaxAge || !/^max-age\s*=/i.test(directive)).map((directive) => directive.replace(/^s-maxage\s*=/i, "max-age=").replace(/^stale-while-revalidate$/i, `stale-while-revalidate=${UNBOUNDED_SWR_SECONDS}`)).join(", ");
123
+ return directives.some((directive) => /^public$/i.test(directive)) ? withMaxAge : `public, ${withMaxAge}`;
113
124
  }
114
125
  /**
115
126
  * Cloudflare's `Cache-Tag` header budget is 16 KB total. Fixed-size digests let
@@ -143,6 +154,9 @@ var CloudflareCdnCacheAdapter = class {
143
154
  readCacheControl(headers) {
144
155
  return readCloudflareResponseCacheControl(headers);
145
156
  },
157
+ readCacheControlHeaderName(headers) {
158
+ return readCloudflareResponsePolicyHeaderName(headers);
159
+ },
146
160
  hasExplicitNonCacheablePolicy(headers, baseline) {
147
161
  if (baseline) {
148
162
  for (const name of EDGE_POLICY_HEADERS) {
@@ -183,12 +197,12 @@ var CloudflareCdnCacheAdapter = class {
183
197
  buildResponseHeaders(input) {
184
198
  if (input.pendingDynamicCheck) return clearCloudflareCdnResponseHeaders(NO_STORE);
185
199
  if (!input.cacheControl) return clearCloudflareCdnResponseHeaders(NO_STORE);
186
- if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
200
+ if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl, input.browserCacheControl);
187
201
  const cacheTag = input.tags?.length ? formatCacheTag(input.tags) : null;
188
- if (input.tags?.length && !cacheTag) return clearCloudflareCdnResponseHeaders(NO_STORE);
202
+ if (input.tags?.length && !cacheTag) return clearCloudflareCdnResponseHeaders(NO_STORE, input.browserCacheControl);
189
203
  return {
190
204
  ...getBuildIdentityResponseHeader(),
191
- "Cache-Control": BROWSER_REVALIDATE,
205
+ "Cache-Control": input.browserCacheControl ?? BROWSER_REVALIDATE,
192
206
  "CDN-Cache-Control": null,
193
207
  "Cloudflare-CDN-Cache-Control": toEdgeCacheControl(input.cacheControl),
194
208
  "Cache-Tag": cacheTag
@@ -19,7 +19,7 @@ export declare class VinextCachedResponse extends WorkerEntrypoint<unknown, unkn
19
19
  fetch(request: Request): Promise<Response>;
20
20
  purge(options: CachePurgeOptions): Promise<unknown>;
21
21
  }
22
- /** Uncached response entrypoint. Bypass and probe renders execute only here. */
22
+ /** Uncached response entrypoint retained for deployment readiness probes. */
23
23
  export declare class VinextUncachedResponse extends WorkerEntrypoint<unknown, unknown> {
24
24
  fetch(request: Request): Promise<Response>;
25
25
  }