@vinext/cloudflare 1.0.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +165 -37
  2. package/dist/cache/cdn-adapter-config.d.ts +2 -1
  3. package/dist/cache/cdn-adapter-config.js +13 -5
  4. package/dist/cache/cdn-adapter.d.ts +5 -56
  5. package/dist/cache/cdn-adapter.js +3 -61
  6. package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
  7. package/dist/cache/cdn-adapter.runtime.js +17 -3
  8. package/dist/cache/cdn-build-id.d.ts +1 -1
  9. package/dist/cache/cdn-build-id.js +1 -1
  10. package/dist/cache/config.d.ts +137 -0
  11. package/dist/cache/config.js +87 -0
  12. package/dist/cache/kv-data-adapter.d.ts +1 -1
  13. package/dist/cache/kv-data-adapter.js +1 -1
  14. package/dist/cache/kv-data-adapter.runtime.js +13 -9
  15. package/dist/cache/kv-key.d.ts +7 -4
  16. package/dist/cache/kv-key.js +16 -5
  17. package/dist/cache/response-store-adapter.d.ts +9 -6
  18. package/dist/cache/response-store-adapter.js +20 -1
  19. package/dist/cache/response-store-adapter.worker.js +100 -43
  20. package/dist/cache/response-store-cdn.runtime.js +1 -0
  21. package/dist/cache/response-store-data.runtime.js +53 -31
  22. package/dist/cache/static-assets-adapter.build.d.ts +6 -0
  23. package/dist/cache/static-assets-adapter.build.js +65 -0
  24. package/dist/cache/static-assets-adapter.d.ts +27 -0
  25. package/dist/cache/static-assets-adapter.js +26 -0
  26. package/dist/cache/static-assets-adapter.runtime.d.ts +26 -0
  27. package/dist/cache/static-assets-adapter.runtime.js +77 -0
  28. package/dist/cache/static-assets-adapter.shared.d.ts +12 -0
  29. package/dist/cache/static-assets-adapter.shared.js +4 -0
  30. package/dist/cache/workers-cache-cdn-adapter.d.ts +58 -0
  31. package/dist/cache/workers-cache-cdn-adapter.js +65 -0
  32. package/dist/cacheability-artifact.d.ts +7 -1
  33. package/dist/cacheability-artifact.js +60 -21
  34. package/dist/cacheability-probe.d.ts +2 -0
  35. package/dist/cacheability-probe.js +107 -13
  36. package/dist/cdn-warm.d.ts +1 -0
  37. package/dist/cdn-warm.js +2 -1
  38. package/dist/deploy-config.d.ts +3 -8
  39. package/dist/deploy-config.js +5 -23
  40. package/dist/deploy-help.js +47 -42
  41. package/dist/deploy.d.ts +18 -16
  42. package/dist/deploy.js +355 -216
  43. package/dist/tpr.d.ts +4 -0
  44. package/dist/tpr.js +16 -7
  45. package/dist/version-deploy.d.ts +27 -0
  46. package/dist/version-deploy.js +121 -3
  47. package/dist/wrangler-version-metadata.js +1 -1
  48. package/package.json +16 -5
  49. package/dist/prerender-kv-populate.d.ts +0 -24
  50. package/dist/prerender-kv-populate.js +0 -153
@@ -1,5 +1,6 @@
1
1
  import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
2
- import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
2
+ import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER } from "vinext/internal/server/headers";
3
+ import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
3
4
  import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
5
  import { loadVinextRequestStage } from "vinext/server/request-stage";
5
6
  import { loadVinextResponseStage } from "vinext/server/response-stage";
@@ -9,7 +10,7 @@ const ROUTE_REVALIDATOR_ID = "vinext:response";
9
10
  const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
10
11
  const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
11
12
  const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
12
- const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase());
13
+ const REPLAY_REQUEST_HEADERS = VINEXT_RSC_VARY_HEADER$1.split(",").map((name) => name.trim().toLowerCase());
13
14
  const CACHE_REQUEST_VARY_HEADERS = REPLAY_REQUEST_HEADERS.map((name) => [name, "vinext-keyed"]);
14
15
  function stageContext(ctx, env) {
15
16
  const assets = Reflect.get(env, "ASSETS");
@@ -42,15 +43,18 @@ function isReplayableInvocation(request, props) {
42
43
  if (request.headers.has("authorization") || request.headers.has("cookie")) return false;
43
44
  return !(props && typeof props === "object" && ["draftModeCookie", "middlewareCookieOverlay"].some((name) => name in props && Reflect.get(props, name) !== null));
44
45
  }
45
- function serializeInvocation(request, props) {
46
- return JSON.stringify({
46
+ function prepareInvocation(request, props) {
47
+ return {
47
48
  props: safeProps(props),
48
49
  request: {
49
50
  headers: replayHeaders(request),
50
51
  method: request.method,
51
52
  url: request.url
52
53
  }
53
- });
54
+ };
55
+ }
56
+ function serializeInvocation(request, props) {
57
+ return JSON.stringify(prepareInvocation(request, props));
54
58
  }
55
59
  function parseInvocation(value) {
56
60
  if (typeof value !== "string") throw new TypeError("Invalid vinext response-store invocation");
@@ -68,17 +72,23 @@ async function invokeRequestStage(request, env, ctx) {
68
72
  const { handleRequestStage } = await loadVinextRequestStage();
69
73
  return handleRequestStage(request, env, stageContext(ctx, env), (request, props, options) => invokeResponseStage(request, props, env, ctx, options.cache));
70
74
  }
71
- async function invokeResponseStage(request, props, env, ctx, cache, capture) {
75
+ async function invokeResponseStage(request, props, env, ctx, cache, capture, invocation) {
72
76
  const context = stageContext(ctx, env);
73
77
  const dispatchRequestStage = (request) => invokeRequestStage(request, env, ctx);
74
78
  const { handleResponseStage } = await loadVinextResponseStage();
75
- const serialized = serializeInvocation(request, props);
76
- return runWithResponseStoreInvocation(serialized, isReplayableInvocation(request, props), () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
79
+ const storedInvocation = invocation ?? {
80
+ replayable: isReplayableInvocation(request, props),
81
+ serialized: serializeInvocation(request, props)
82
+ };
83
+ return runWithResponseStoreInvocation(storedInvocation.serialized, storedInvocation.replayable, () => handleResponseStage(request, env, context, props, dispatchRequestStage, { cache }), capture);
77
84
  }
78
85
  function createVinextResponseStoreOptions(configuration) {
86
+ const locationHint = configuration?.locationHint;
87
+ if (locationHint !== void 0 && typeof locationHint !== "string") throw new TypeError("Workers Response Store locationHint must be a string");
79
88
  const shards = configuration?.shards;
80
89
  if (shards !== void 0 && typeof shards !== "number") throw new TypeError("Workers Response Store shards must be a number");
81
90
  return {
91
+ ...locationHint === void 0 ? {} : { locationHint },
82
92
  ...shards === void 0 ? {} : { shards },
83
93
  async regenerate(input, { env, ctx }) {
84
94
  if (input.id === ROUTE_REVALIDATOR_ID) {
@@ -88,7 +98,7 @@ function createVinextResponseStoreOptions(configuration) {
88
98
  await response.body?.cancel().catch(() => {});
89
99
  throw new Error("Vinext response-stage regeneration was not cacheable");
90
100
  }
91
- return response;
101
+ return withoutRequestScopedHeaders(response, invocation.props);
92
102
  }
93
103
  if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
94
104
  const invocation = JSON.parse(input.args[1]);
@@ -109,27 +119,56 @@ function createVinextResponseStoreOptions(configuration) {
109
119
  }
110
120
  };
111
121
  }
112
- async function cacheRequest(request, props) {
122
+ async function cacheRequest(invocation) {
113
123
  const identity = JSON.stringify([
114
- request.method,
115
- request.url,
116
- safeProps(props),
117
- replayHeaders(request)
124
+ invocation.request.method,
125
+ invocation.request.url,
126
+ invocation.props,
127
+ invocation.request.headers
118
128
  ]);
119
129
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity)));
120
- const url = new URL(request.url);
130
+ const url = new URL(invocation.request.url);
121
131
  url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
122
132
  return new Request(url, {
123
133
  headers: CACHE_REQUEST_VARY_HEADERS,
124
134
  method: "GET"
125
135
  });
126
136
  }
137
+ /**
138
+ * The request stage recomposes the routed params and path on every App page
139
+ * RSC response, HITs included, so such an entry shared across queries must not
140
+ * carry the values of the request that filled it. Every other response kind
141
+ * (route handlers, metadata routes, HTML, Pages) keeps its headers as rendered.
142
+ */
143
+ function withoutRequestScopedHeaders(response, responseStageProps) {
144
+ if (responseStageProps === null || typeof responseStageProps !== "object" || Reflect.get(responseStageProps, "kind") !== "app-page" || Reflect.get(responseStageProps, "isRscRequest") !== true) return response;
145
+ const headers = new Headers(response.headers);
146
+ headers.delete(VINEXT_PARAMS_HEADER);
147
+ headers.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
148
+ return new Response(response.body, {
149
+ headers,
150
+ status: response.status,
151
+ statusText: response.statusText
152
+ });
153
+ }
127
154
  function isCacheable(response) {
128
155
  const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
129
156
  return response.status >= 200 && response.status < 400 && policy !== null && !isNonCacheableCacheControl(policy);
130
157
  }
131
- function isResponseStoreMiss(response) {
132
- return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS";
158
+ async function readStoredResponse(key) {
159
+ try {
160
+ const response = await responseStore.fetch(key);
161
+ if (response.status >= 200 && response.status < 400) return response;
162
+ response.body?.cancel().catch(() => {});
163
+ if (response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS") return null;
164
+ throw new Error(`Workers Response Store returned ${response.status}`);
165
+ } catch (error) {
166
+ console.error(JSON.stringify({
167
+ message: "Vinext response-store response lookup failed; treating as a cache miss",
168
+ error: error instanceof Error ? error.message : String(error)
169
+ }));
170
+ return null;
171
+ }
133
172
  }
134
173
  function publicResponse(response, cacheStatus, responseStageProps) {
135
174
  const headers = new Headers(response.headers);
@@ -170,42 +209,49 @@ function createVinextResponseStoreHandler(store) {
170
209
  const handler = { async fetch(request, env, ctx) {
171
210
  const context = stageContext(ctx, env);
172
211
  const dispatchResponseStage = async (stageRequest, props, options) => {
173
- const invocation = serializeInvocation(stageRequest, props);
174
212
  if (options.cache === "bypass" || stageRequest.method !== "GET" && stageRequest.method !== "HEAD") return publicResponse(await invokeResponseStage(stageRequest, props, env, ctx, "bypass"), "BYPASS", props);
213
+ const identityRequest = options.cacheIdentity?.request ?? stageRequest;
214
+ const identityProps = options.cacheIdentity?.props ?? props;
175
215
  const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
176
216
  const rscSeed = isWarmup && props !== null && typeof props === "object" && Reflect.get(props, "kind") === "app-page" && Reflect.get(props, "isRscRequest") === false && Reflect.get(props, "matchKind") === "request" && Reflect.get(props, "interceptionContext") === null && Reflect.get(props, "interceptionId") === null && Reflect.get(props, "mountedSlotsHeader") === null ? {
177
217
  props: {
178
- ...props,
218
+ ...identityProps,
179
219
  isRscRequest: true,
180
220
  renderMode: "navigation"
181
221
  },
182
- request: new Request(new URL(createCanonicalRscRequestUrl(stageRequest.url), stageRequest.url), { headers: createCanonicalRscRequestHeaders() })
222
+ request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
183
223
  } : void 0;
184
- const rscKey = rscSeed ? await cacheRequest(rscSeed.request, rscSeed.props) : void 0;
185
- const key = await cacheRequest(stageRequest, props);
186
- const stored = await responseStore.fetch(key);
187
- if (!isResponseStoreMiss(stored)) {
224
+ const invocation = prepareInvocation(identityRequest, identityProps);
225
+ const rscInvocation = rscSeed ? prepareInvocation(rscSeed.request, rscSeed.props) : void 0;
226
+ const rscKey = rscInvocation ? await cacheRequest(rscInvocation) : void 0;
227
+ const key = await cacheRequest(invocation);
228
+ const stored = await readStoredResponse(key);
229
+ if (stored) {
188
230
  if (!rscKey) return publicResponse(stored, "HIT", props);
189
- const storedRsc = await responseStore.fetch(rscKey);
190
- if (!isResponseStoreMiss(storedRsc)) {
191
- await storedRsc.body?.cancel();
231
+ const storedRsc = await readStoredResponse(rscKey);
232
+ if (storedRsc) {
233
+ storedRsc.body?.cancel().catch(() => {});
192
234
  return publicResponse(stored, "HIT", props);
193
235
  }
194
- await Promise.all([stored.body?.cancel(), storedRsc.body?.cancel()]);
236
+ stored.body?.cancel().catch(() => {});
195
237
  }
196
238
  const capture = rscSeed ? { captureRscData: true } : isWarmup ? {} : { streamResponse: true };
197
- const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture);
239
+ const serializedInvocation = JSON.stringify(invocation);
240
+ const rendered = await invokeResponseStage(stageRequest, props, env, ctx, "shared", capture, {
241
+ replayable: isReplayableInvocation(stageRequest, props),
242
+ serialized: options.cacheIdentity ? serializeInvocation(stageRequest, props) : serializedInvocation
243
+ });
198
244
  if (capture.admittedResponse) {
199
245
  ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
200
246
  if (!isCacheable(admitted)) {
201
247
  await admitted.body?.cancel().catch(() => {});
202
248
  return;
203
249
  }
204
- await responseStore.put(key, admitted, {
250
+ await responseStore.put(key, withoutRequestScopedHeaders(admitted, props), {
205
251
  coalesce: true,
206
252
  revalidator: {
207
253
  id: ROUTE_REVALIDATOR_ID,
208
- args: [invocation]
254
+ args: [serializedInvocation]
209
255
  }
210
256
  });
211
257
  }).catch((error) => {
@@ -225,25 +271,36 @@ const handler = { async fetch(request, env, ctx) {
225
271
  throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
226
272
  }
227
273
  const [foreground, cacheBody] = rendered.body ? rendered.body.tee() : [null, null];
228
- const cacheResponse = new Response(cacheBody, rendered);
229
- await responseStore.put(key, cacheResponse, {
230
- coalesce: true,
231
- revalidator: {
232
- id: ROUTE_REVALIDATOR_ID,
233
- args: [invocation]
234
- }
235
- });
236
- if (rscSeed && rscKey && capture?.rscData) {
274
+ const cacheResponse = withoutRequestScopedHeaders(new Response(cacheBody, rendered), props);
275
+ try {
276
+ await responseStore.put(key, cacheResponse, {
277
+ coalesce: true,
278
+ revalidator: {
279
+ id: ROUTE_REVALIDATOR_ID,
280
+ args: [serializedInvocation]
281
+ }
282
+ });
283
+ } catch (error) {
284
+ if (isWarmup) throw error;
285
+ cacheResponse.body?.cancel().catch(() => {});
286
+ console.error(JSON.stringify({
287
+ message: "Vinext response-store response fill failed; serving the rendered response",
288
+ error: error instanceof Error ? error.message : String(error)
289
+ }));
290
+ }
291
+ if (rscSeed && rscInvocation && rscKey && capture?.rscData) {
237
292
  const rscData = await capture.rscData;
238
293
  const rscHeaders = new Headers(rendered.headers);
239
294
  rscHeaders.delete("Content-Length");
240
295
  rscHeaders.delete("Link");
241
296
  rscHeaders.delete("X-Vinext-Response-Store-Replayable");
297
+ rscHeaders.delete(VINEXT_PARAMS_HEADER);
298
+ rscHeaders.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
242
299
  rscHeaders.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
243
- rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER);
300
+ rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER$1);
244
301
  applyRscCompatibilityIdHeader(rscHeaders);
245
302
  applyRscDeploymentIdHeader(rscHeaders);
246
- const rscInvocation = serializeInvocation(rscSeed.request, rscSeed.props);
303
+ const serializedRscInvocation = JSON.stringify(rscInvocation);
247
304
  await responseStore.put(rscKey, new Response(rscData, {
248
305
  headers: rscHeaders,
249
306
  status: 200
@@ -251,7 +308,7 @@ const handler = { async fetch(request, env, ctx) {
251
308
  coalesce: true,
252
309
  revalidator: {
253
310
  id: ROUTE_REVALIDATOR_ID,
254
- args: [rscInvocation]
311
+ args: [serializedRscInvocation]
255
312
  }
256
313
  });
257
314
  }
@@ -6,6 +6,7 @@ var ResponseStoreCdnCacheAdapter = class {
6
6
  headers;
7
7
  ownsBackgroundRevalidation = false;
8
8
  requiresCompletedResponseAdmission = true;
9
+ responseStageCacheIdentity = "query-free";
9
10
  responsePolicy;
10
11
  constructor(headers) {
11
12
  this.headers = headers;
@@ -192,39 +192,61 @@ var WorkersResponseStoreCacheHandler = class {
192
192
  async get(key, context) {
193
193
  if (regenerationStorage.getStore()?.targetKey === key) return null;
194
194
  const request = await cacheRequest(key);
195
- const response = await this.store.fetch(request);
196
- if (response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS") return null;
197
- if (!response.ok) throw new Error(`Workers Response Store returned ${response.status}`);
198
- const entry = deserialize(await response.text());
199
- if (!entry) {
200
- await this.store.purge({ pathPrefixes: [new URL(request.url).pathname] });
201
- return null;
202
- }
203
- const softTags = [...new Set(readStringArrayField(context, "softTags").map(encodeCloudflareCacheTag))].sort();
204
- if (softTags.length) {
205
- const key = softTags.join(",");
206
- const expirations = this.tagExpirations();
207
- let expiration = expirations.get(key);
208
- if (!expiration) {
209
- expiration = this.store.getTagExpiration(softTags);
210
- expirations.set(key, expiration);
195
+ try {
196
+ const response = await this.store.fetch(request);
197
+ if (!response.ok) {
198
+ response.body?.cancel().catch(() => {});
199
+ if (response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS") return null;
200
+ throw new Error(`Workers Response Store returned ${response.status}`);
211
201
  }
212
- if (await expiration >= entry.lastModified) return null;
202
+ const softTags = [...new Set(readStringArrayField(context, "softTags").map(encodeCloudflareCacheTag))].sort();
203
+ const getTagExpiration = () => {
204
+ const key = softTags.join(",");
205
+ const expirations = this.tagExpirations();
206
+ let expiration = expirations.get(key);
207
+ if (!expiration) {
208
+ expiration = Promise.resolve().then(() => this.store.getTagExpiration(softTags));
209
+ expirations.set(key, expiration);
210
+ expiration.catch(() => {
211
+ if (expirations.get(key) === expiration) expirations.delete(key);
212
+ });
213
+ }
214
+ return expiration;
215
+ };
216
+ const storeStatus = response.headers.get("X-Workers-Response-Store");
217
+ const body = response.text();
218
+ const eagerExpiration = softTags.length && (storeStatus === "BLOB-FRESH" || storeStatus === "BLOB-STALE") ? getTagExpiration().then((value) => ({ value }), (error) => ({ error })) : void 0;
219
+ const entry = deserialize(await body);
220
+ if (!entry) {
221
+ await this.store.purge({ pathPrefixes: [new URL(request.url).pathname] });
222
+ return null;
223
+ }
224
+ if (softTags.length) {
225
+ const expiration = eagerExpiration ? await eagerExpiration : { value: await getTagExpiration() };
226
+ if ("error" in expiration) throw expiration.error;
227
+ if (expiration.value >= entry.lastModified) return null;
228
+ }
229
+ const age = Date.now() - entry.lastModified;
230
+ const requestedRevalidate = readCacheControlField(context, "revalidate");
231
+ const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
232
+ let cacheState;
233
+ if (response.headers.get(REPLAYABLE_HEADER) === "1") {
234
+ if (requestedStale) cacheState = "stale";
235
+ } else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
236
+ else if (requestedStale || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
237
+ return {
238
+ lastModified: entry.lastModified,
239
+ value: entry.value,
240
+ ...cacheState ? { cacheState } : {},
241
+ ...entry.cacheControl ? { cacheControl: entry.cacheControl } : {}
242
+ };
243
+ } catch (error) {
244
+ console.error(JSON.stringify({
245
+ message: "Vinext response-store data lookup failed; treating as a cache miss",
246
+ error: error instanceof Error ? error.message : String(error)
247
+ }));
248
+ return null;
213
249
  }
214
- const age = Date.now() - entry.lastModified;
215
- const requestedRevalidate = readCacheControlField(context, "revalidate");
216
- const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
217
- let cacheState;
218
- if (response.headers.get(REPLAYABLE_HEADER) === "1") {
219
- if (requestedStale) cacheState = "stale";
220
- } else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
221
- else if (requestedStale || typeof entry.cacheControl?.revalidate === "number" && entry.cacheControl.revalidate > 0 && age > entry.cacheControl.revalidate * 1e3) cacheState = "stale";
222
- return {
223
- lastModified: entry.lastModified,
224
- value: entry.value,
225
- ...cacheState ? { cacheState } : {},
226
- ...entry.cacheControl ? { cacheControl: entry.cacheControl } : {}
227
- };
228
250
  }
229
251
  async set(key, value, context) {
230
252
  let revalidate = readCacheControlField(context, "revalidate");
@@ -0,0 +1,6 @@
1
+ //#region src/cache/static-assets-adapter.build.d.ts
2
+ /** Package App Router prerender output as immutable Workers Static Assets. */
3
+ export declare function finalizeStaticAssetsPrerenderOutput(root: string, options?: {
4
+ clientOutDir?: string;
5
+ }): number;
6
+ //#endregion
@@ -0,0 +1,65 @@
1
+ import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
6
+ import { appIsrCacheKey } from "vinext/internal/server/isr-cache";
7
+ import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
8
+ import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
9
+ //#region src/cache/static-assets-adapter.build.ts
10
+ function cacheAssetId(key) {
11
+ return createHash("sha256").update(key).digest("hex");
12
+ }
13
+ function cacheControl(route) {
14
+ if (route.revalidate === void 0) return void 0;
15
+ return {
16
+ revalidate: route.revalidate,
17
+ ...route.expire === void 0 ? {} : { expire: route.expire },
18
+ ...route.stale === void 0 ? {} : { stale: route.stale }
19
+ };
20
+ }
21
+ function writeCacheAsset(outputDir, index, key, kind, sourcePath, route) {
22
+ if (!fs.existsSync(sourcePath)) return false;
23
+ const id = cacheAssetId(key);
24
+ const extension = kind === "html" ? "html" : kind === "rsc" ? "rsc" : "route";
25
+ const policy = cacheControl(route);
26
+ const metadata = {
27
+ kind,
28
+ lastModified: fs.statSync(sourcePath).mtimeMs,
29
+ ...policy ? { cacheControl: policy } : {},
30
+ ...kind !== "rsc" && route.headers ? { headers: route.headers } : {},
31
+ ...kind !== "rsc" && route.responseStatus !== void 0 ? { status: route.responseStatus } : {}
32
+ };
33
+ fs.mkdirSync(outputDir, { recursive: true });
34
+ fs.copyFileSync(sourcePath, path.join(outputDir, `${id}.${extension}`));
35
+ index[id] = metadata;
36
+ return true;
37
+ }
38
+ /** Package App Router prerender output as immutable Workers Static Assets. */
39
+ function finalizeStaticAssetsPrerenderOutput(root, options = {}) {
40
+ const serverDir = path.join(root, "dist", "server");
41
+ const prerenderDir = path.join(serverDir, "prerendered-routes");
42
+ const outputDir = path.join(options.clientOutDir ?? path.join(root, "dist", "client"), STATIC_ASSET_CACHE_PATH.replace(/^\//, ""));
43
+ fs.rmSync(outputDir, {
44
+ recursive: true,
45
+ force: true
46
+ });
47
+ const manifest = readPrerenderManifest(path.join(serverDir, "vinext-prerender.json"));
48
+ if (!manifest?.buildId || !manifest.routes || !fs.existsSync(prerenderDir)) return 0;
49
+ const index = {};
50
+ let count = 0;
51
+ for (const route of manifest.routes) {
52
+ if (route.status !== "rendered") continue;
53
+ if (typeof route.revalidate === "number" && route.revalidate <= 0) continue;
54
+ const pathname = route.path ?? route.route;
55
+ const cachePathname = normalizePregeneratedPathname(pathname);
56
+ if (route.router === "app") {
57
+ count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "html", manifest.buildId), "html", path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false)), route));
58
+ count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "rsc", manifest.buildId), "rsc", path.join(prerenderDir, getRscOutputPath(pathname)), route));
59
+ } else if (route.router === "metadata") count += Number(writeCacheAsset(outputDir, index, appIsrCacheKey(cachePathname, "route", manifest.buildId), "route", path.join(prerenderDir, getAppRouteOutputPath(pathname)), route));
60
+ }
61
+ if (count > 0) fs.writeFileSync(path.join(outputDir, "index.json"), JSON.stringify(index));
62
+ return count;
63
+ }
64
+ //#endregion
65
+ export { finalizeStaticAssetsPrerenderOutput };
@@ -0,0 +1,27 @@
1
+ //#region src/cache/static-assets-adapter.d.ts
2
+ export type StaticAssetsAdapterOptions = {
3
+ /** Workers Static Assets binding name. @default "ASSETS" */
4
+ binding?: string;
5
+ };
6
+ /**
7
+ * Read-only page cache backed by Workers Static Assets.
8
+ *
9
+ * Locally prerendered App Router HTML, RSC, and metadata responses are copied
10
+ * into `dist/client` after the build. Runtime cache writes and invalidations are
11
+ * intentionally no-ops; a new deployment replaces the cache contents.
12
+ */
13
+ export declare function staticAssetsAdapter(options?: StaticAssetsAdapterOptions): {
14
+ adapter: string;
15
+ options: StaticAssetsAdapterOptions | undefined;
16
+ output: {
17
+ finalizePrerenderOutput({ root, clientOutDir }: {
18
+ root: string;
19
+ clientOutDir: string;
20
+ }): void;
21
+ };
22
+ capabilities: {
23
+ buildIdentity: "response-header";
24
+ warmup: "data-cache";
25
+ };
26
+ };
27
+ //#endregion
@@ -0,0 +1,26 @@
1
+ import { finalizeStaticAssetsPrerenderOutput } from "./static-assets-adapter.build.js";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/cache/static-assets-adapter.ts
4
+ /**
5
+ * Read-only page cache backed by Workers Static Assets.
6
+ *
7
+ * Locally prerendered App Router HTML, RSC, and metadata responses are copied
8
+ * into `dist/client` after the build. Runtime cache writes and invalidations are
9
+ * intentionally no-ops; a new deployment replaces the cache contents.
10
+ */
11
+ function staticAssetsAdapter(options) {
12
+ if (options?.binding !== void 0 && (typeof options.binding !== "string" || options.binding.length === 0)) throw new TypeError("[vinext] staticAssetsAdapter({ binding }) must be a non-empty string binding name.");
13
+ return {
14
+ adapter: fileURLToPath(import.meta.resolve("./static-assets-adapter.runtime.js")),
15
+ options,
16
+ output: { finalizePrerenderOutput({ root, clientOutDir }) {
17
+ finalizeStaticAssetsPrerenderOutput(root, { clientOutDir });
18
+ } },
19
+ capabilities: {
20
+ buildIdentity: "response-header",
21
+ warmup: "data-cache"
22
+ }
23
+ };
24
+ }
25
+ //#endregion
26
+ export { staticAssetsAdapter };
@@ -0,0 +1,26 @@
1
+ import { StaticAssetsAdapterOptions } from "./static-assets-adapter.js";
2
+ import { CdnCacheAdapter, CdnCacheableHeaderInput, CdnResponseHeaders } from "vinext/shims/cdn-cache";
3
+ import { CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache-handler";
4
+ //#region src/cache/static-assets-adapter.runtime.d.ts
5
+ type AssetFetcher = {
6
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
7
+ };
8
+ export declare class StaticAssetsCacheAdapter implements CdnCacheAdapter {
9
+ private readonly assets;
10
+ readonly ownsBackgroundRevalidation = false;
11
+ private indexPromise;
12
+ constructor(assets: AssetFetcher);
13
+ private loadIndex;
14
+ get(key: string): Promise<CacheHandlerValue | null>;
15
+ set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
16
+ buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
17
+ buildResponseIdentityHeaders(): CdnResponseHeaders;
18
+ revalidateTag(_tags: string | string[], _durations?: {
19
+ expire?: number;
20
+ }): Promise<void>;
21
+ }
22
+ export default function createStaticAssetsCacheAdapter({ env, options }?: {
23
+ env?: Record<string, unknown>;
24
+ options?: StaticAssetsAdapterOptions;
25
+ }): CdnCacheAdapter;
26
+ //#endregion
@@ -0,0 +1,77 @@
1
+ import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
2
+ import { DefaultCdnCacheAdapter } from "vinext/shims/cdn-cache";
3
+ //#region src/cache/static-assets-adapter.runtime.ts
4
+ async function cacheAssetId(key) {
5
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
6
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
7
+ }
8
+ function isMetadata(value) {
9
+ return value !== null && typeof value === "object" && "kind" in value && (value.kind === "html" || value.kind === "rsc" || value.kind === "route") && "lastModified" in value && typeof value.lastModified === "number";
10
+ }
11
+ var StaticAssetsCacheAdapter = class {
12
+ assets;
13
+ ownsBackgroundRevalidation = false;
14
+ indexPromise;
15
+ constructor(assets) {
16
+ this.assets = assets;
17
+ }
18
+ loadIndex() {
19
+ return this.indexPromise ??= this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/index.json`).then(async (response) => response.ok ? await response.json() : null).then((value) => value !== null && typeof value === "object" ? value : null);
20
+ }
21
+ async get(key) {
22
+ const id = await cacheAssetId(key);
23
+ const metadata = (await this.loadIndex())?.[id];
24
+ if (!isMetadata(metadata)) return null;
25
+ const extension = metadata.kind === "html" ? "html" : metadata.kind === "rsc" ? "rsc" : "route";
26
+ const bodyResponse = await this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/${id}.${extension}`);
27
+ if (!bodyResponse.ok) return null;
28
+ let value;
29
+ if (metadata.kind === "html") value = {
30
+ kind: "APP_PAGE",
31
+ html: await bodyResponse.text(),
32
+ rscData: void 0,
33
+ headers: metadata.headers,
34
+ postponed: void 0,
35
+ prerendered: true,
36
+ status: metadata.status
37
+ };
38
+ else if (metadata.kind === "rsc") value = {
39
+ kind: "APP_PAGE",
40
+ html: "",
41
+ rscData: await bodyResponse.arrayBuffer(),
42
+ headers: void 0,
43
+ postponed: void 0,
44
+ prerendered: true,
45
+ status: void 0
46
+ };
47
+ else value = {
48
+ kind: "APP_ROUTE",
49
+ body: await bodyResponse.arrayBuffer(),
50
+ headers: metadata.headers ?? {},
51
+ status: metadata.status ?? 200
52
+ };
53
+ return {
54
+ lastModified: metadata.lastModified,
55
+ ...metadata.cacheControl ? { cacheControl: metadata.cacheControl } : {},
56
+ value
57
+ };
58
+ }
59
+ async set(_key, _data, _ctx) {}
60
+ buildResponseHeaders(input) {
61
+ return { "Cache-Control": input.pendingDynamicCheck ? "no-store, must-revalidate" : input.cacheControl };
62
+ }
63
+ buildResponseIdentityHeaders() {
64
+ const buildId = process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID;
65
+ return buildId ? { "X-Vinext-Build-Id": buildId } : {};
66
+ }
67
+ async revalidateTag(_tags, _durations) {}
68
+ };
69
+ function createStaticAssetsCacheAdapter({ env, options } = {}) {
70
+ if (process.env.VINEXT_PRERENDER === "1") return new DefaultCdnCacheAdapter(process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID);
71
+ const binding = options?.binding ?? "ASSETS";
72
+ const assets = env?.[binding];
73
+ if (!assets || typeof assets !== "object" || !("fetch" in assets) || typeof assets.fetch !== "function") throw new Error(`[vinext] Static Assets binding \`${binding}\` is not configured.`);
74
+ return new StaticAssetsCacheAdapter(assets);
75
+ }
76
+ //#endregion
77
+ export { StaticAssetsCacheAdapter, createStaticAssetsCacheAdapter as default };
@@ -0,0 +1,12 @@
1
+ import { CacheControlMetadata } from "vinext/shims/cache-handler";
2
+ //#region src/cache/static-assets-adapter.shared.d.ts
3
+ export declare const STATIC_ASSET_CACHE_PATH = "/_vinext/static-cache";
4
+ export type StaticAssetCacheMetadata = {
5
+ kind: "html" | "rsc" | "route";
6
+ lastModified: number;
7
+ cacheControl?: CacheControlMetadata;
8
+ headers?: Record<string, string | string[]>;
9
+ status?: number;
10
+ };
11
+ export type StaticAssetCacheIndex = Record<string, StaticAssetCacheMetadata>;
12
+ //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/cache/static-assets-adapter.shared.ts
2
+ const STATIC_ASSET_CACHE_PATH = "/_vinext/static-cache";
3
+ //#endregion
4
+ export { STATIC_ASSET_CACHE_PATH };