@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.
- package/README.md +165 -37
- package/dist/cache/browser-cache-policy.d.ts +7 -0
- package/dist/cache/browser-cache-policy.js +39 -0
- package/dist/cache/cdn-adapter-config.d.ts +2 -1
- package/dist/cache/cdn-adapter-config.js +13 -5
- package/dist/cache/cdn-adapter.d.ts +5 -56
- package/dist/cache/cdn-adapter.js +3 -61
- package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
- package/dist/cache/cdn-adapter.runtime.js +33 -19
- package/dist/cache/cdn-adapter.worker.d.ts +1 -1
- package/dist/cache/cdn-adapter.worker.js +10 -35
- package/dist/cache/cdn-build-id.d.ts +1 -1
- package/dist/cache/cdn-build-id.js +1 -1
- package/dist/cache/config.d.ts +137 -0
- package/dist/cache/config.js +87 -0
- package/dist/cache/kv-data-adapter.d.ts +1 -1
- package/dist/cache/kv-data-adapter.js +1 -1
- package/dist/cache/kv-data-adapter.runtime.js +17 -15
- package/dist/cache/kv-key.d.ts +7 -4
- package/dist/cache/kv-key.js +17 -6
- package/dist/cache/response-store-adapter.d.ts +9 -6
- package/dist/cache/response-store-adapter.js +20 -1
- package/dist/cache/response-store-adapter.worker.js +121 -48
- package/dist/cache/response-store-cdn.runtime.js +1 -0
- package/dist/cache/response-store-data.runtime.js +53 -31
- package/dist/cache/static-assets-adapter.build.d.ts +6 -0
- package/dist/cache/static-assets-adapter.build.js +102 -0
- package/dist/cache/static-assets-adapter.d.ts +27 -0
- package/dist/cache/static-assets-adapter.js +26 -0
- package/dist/cache/static-assets-adapter.runtime.d.ts +27 -0
- package/dist/cache/static-assets-adapter.runtime.js +94 -0
- package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
- package/dist/cache/static-assets-adapter.shared.js +4 -0
- package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
- package/dist/cache/workers-cache-cdn-adapter.js +65 -0
- package/dist/cacheability-artifact.d.ts +7 -1
- package/dist/cacheability-artifact.js +60 -21
- package/dist/cacheability-probe.d.ts +2 -0
- package/dist/cacheability-probe.js +107 -13
- package/dist/cdn-warm.d.ts +1 -0
- package/dist/cdn-warm.js +4 -3
- package/dist/deploy-config.d.ts +3 -8
- package/dist/deploy-config.js +5 -23
- package/dist/deploy-help.js +47 -42
- package/dist/deploy.d.ts +18 -16
- package/dist/deploy.js +355 -216
- package/dist/tpr.d.ts +4 -0
- package/dist/tpr.js +16 -7
- package/dist/version-deploy.d.ts +27 -0
- package/dist/version-deploy.js +121 -3
- package/dist/wrangler-version-metadata.js +1 -1
- package/package.json +16 -5
- package/dist/prerender-kv-populate.d.ts +0 -24
- 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
|
-
- **`
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
`
|
|
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.
|
|
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 {
|
|
55
|
+
import { workersCacheCdnAdapter } from "@vinext/cloudflare/cache/workers-cache-cdn-adapter";
|
|
53
56
|
|
|
54
|
-
vinext({ cache: { cdn:
|
|
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 `
|
|
60
|
-
custom binding
|
|
80
|
+
the same `versionMetadataBinding` to `workersCacheCdnAdapter()` and
|
|
81
|
+
`createWorkersCacheConfig()` only when the deployment needs a custom binding
|
|
82
|
+
name.
|
|
61
83
|
|
|
62
|
-
Use `--
|
|
63
|
-
makes one final fill request per admitted identity. Add `--warm-
|
|
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
|
|
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 `
|
|
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`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
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
|
-
|
|
5
|
-
const
|
|
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.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
112
|
-
|
|
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
|
|
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
|
}
|