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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,10 @@ This package provides Cloudflare-specific cache and image backends for vinext:
7
7
 
8
8
  - **`kvDataAdapter()`** (`@vinext/cloudflare/cache/kv-data-adapter`) — backs the
9
9
  data cache (`fetch`, `"use cache"`, `unstable_cache`) with a Workers KV
10
- namespace. Also used for ISR in the absence of a CDN adapter.
10
+ namespace.
11
+ - **`cdnAdapter()`** (`@vinext/cloudflare/cache/cdn-adapter`) — delegates
12
+ page-level ISR serving and revalidation to Cloudflare Workers Cache. This is
13
+ opt-in and requires Workers Cache to be enabled in Wrangler config.
11
14
  - **`imagesOptimizer()`** (`@vinext/cloudflare/images/images-optimizer`) — backs
12
15
  `next/image` transformations with a Cloudflare Images binding.
13
16
 
@@ -32,6 +35,25 @@ export default defineConfig({
32
35
  });
33
36
  ```
34
37
 
38
+ ### Workers Cache
39
+
40
+ `cdnAdapter()` is optional. Only configure it when Workers Cache is enabled in
41
+ `wrangler.jsonc`:
42
+
43
+ ```jsonc
44
+ {
45
+ "cache": {
46
+ "enabled": true,
47
+ },
48
+ }
49
+ ```
50
+
51
+ ```ts
52
+ import { cdnAdapter } from "@vinext/cloudflare/cache/cdn-adapter";
53
+
54
+ vinext({ cache: { cdn: cdnAdapter() } });
55
+ ```
56
+
35
57
  ## Deploy
36
58
 
37
59
  Deploy Cloudflare Workers projects with the package CLI:
@@ -11,6 +11,7 @@ declare class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
11
11
  /** No-op: the platform caches the response via its headers, not an origin store. */
12
12
  set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
13
13
  buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
14
+ hasExplicitNonCacheableResponsePolicy(headers: Headers): boolean;
14
15
  /** Purge edge-cached responses by tag via the request context's `cache.purge`. */
15
16
  revalidateTag(tags: string | string[], _durations?: {
16
17
  expire?: number;
@@ -1,5 +1,25 @@
1
1
  import { getRequestExecutionContext } from "vinext/shims/request-context";
2
2
  //#region src/cache/cdn-adapter.runtime.ts
3
+ const NON_CACHEABLE_DIRECTIVE_RE = /\b(?:private|no-store|no-cache)\b/i;
4
+ const CACHEABLE_EDGE_DIRECTIVE_RE = /(?:^|,)\s*(?:s-maxage|max-age)\s*=/i;
5
+ const EDGE_POLICY_HEADERS = ["CDN-Cache-Control", "Cloudflare-CDN-Cache-Control"];
6
+ /** Remove every response header whose cache semantics are owned by Cloudflare. */
7
+ function clearCloudflareCdnResponseHeaders(cacheControl) {
8
+ return {
9
+ "Cache-Control": cacheControl,
10
+ "CDN-Cache-Control": null,
11
+ "Cloudflare-CDN-Cache-Control": null,
12
+ "Cache-Tag": null
13
+ };
14
+ }
15
+ /** Interpret Cloudflare's edge policy before core applies a replacement policy. */
16
+ function hasExplicitCloudflareNonCacheableResponsePolicy(headers) {
17
+ const edgePolicies = EDGE_POLICY_HEADERS.map((name) => headers.get(name));
18
+ if (edgePolicies.some((value) => value && NON_CACHEABLE_DIRECTIVE_RE.test(value))) return true;
19
+ const hasCacheableEdgePolicy = edgePolicies.some((value) => value && CACHEABLE_EDGE_DIRECTIVE_RE.test(value));
20
+ const browserPolicy = headers.get("Cache-Control");
21
+ return Boolean(!hasCacheableEdgePolicy && browserPolicy && NON_CACHEABLE_DIRECTIVE_RE.test(browserPolicy));
22
+ }
3
23
  function getWorkersCache() {
4
24
  const cache = getRequestExecutionContext()?.cache;
5
25
  if (cache && typeof cache.purge === "function") return cache;
@@ -72,22 +92,17 @@ var CloudflareCdnCacheAdapter = class {
72
92
  /** No-op: the platform caches the response via its headers, not an origin store. */
73
93
  async set(_key, _data, _ctx) {}
74
94
  buildResponseHeaders(input) {
75
- if (!input.cacheControl) return { "Cache-Control": NO_STORE };
76
- if (/\b(?:no-store|no-cache|private)\b/.test(input.cacheControl)) return {
77
- "Cache-Control": input.cacheControl,
78
- "CDN-Cache-Control": null,
79
- "Cloudflare-CDN-Cache-Control": null,
80
- "Cache-Tag": null
81
- };
82
- const headers = {
95
+ if (!input.cacheControl) return clearCloudflareCdnResponseHeaders(NO_STORE);
96
+ if (/\b(?:no-store|no-cache|private)\b/.test(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
97
+ return {
83
98
  "Cache-Control": BROWSER_REVALIDATE,
84
- "CDN-Cache-Control": toEdgeCacheControl(input.cacheControl)
99
+ "CDN-Cache-Control": toEdgeCacheControl(input.cacheControl),
100
+ "Cloudflare-CDN-Cache-Control": null,
101
+ "Cache-Tag": input.tags ? formatCacheTag(input.tags) : null
85
102
  };
86
- if (input.tags && input.tags.length > 0) {
87
- const cacheTag = formatCacheTag(input.tags);
88
- if (cacheTag) headers["Cache-Tag"] = cacheTag;
89
- }
90
- return headers;
103
+ }
104
+ hasExplicitNonCacheableResponsePolicy(headers) {
105
+ return hasExplicitCloudflareNonCacheableResponsePolicy(headers);
91
106
  }
92
107
  /** Purge edge-cached responses by tag via the request context's `cache.purge`. */
93
108
  async revalidateTag(tags, _durations) {
@@ -1,11 +1,11 @@
1
1
  import { createKvKeySpace } from "./cache/kv-key.js";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
- import { getRenderedAppRoutes, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
4
+ import { getRenderedAppRoutes, getRenderedMetadataRoutes, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
5
5
  import { appIsrCacheKey } from "vinext/internal/server/isr-cache";
6
- import { buildAppPageCacheTags } from "vinext/internal/server/app-page-cache";
6
+ import { buildAppPageCacheTags, buildAppRouteCacheTags } from "vinext/internal/server/app-page-cache";
7
7
  import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
8
- import { getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
8
+ import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
9
9
  //#region src/prerender-kv-populate.ts
10
10
  /**
11
11
  * Deploy-time KV population for App Router prerendered artifacts.
@@ -111,6 +111,38 @@ function buildPrerenderKVPairs(serverDir, options) {
111
111
  }
112
112
  routeCount++;
113
113
  }
114
+ for (const route of getRenderedMetadataRoutes(manifest.routes)) {
115
+ const pathname = route.path ?? route.route;
116
+ let artifactPath;
117
+ try {
118
+ artifactPath = resolveContainedFile(prerenderDir, getAppRouteOutputPath(pathname));
119
+ } catch (error) {
120
+ console.warn(`[vinext] Skipping metadata prerender KV seed for ${pathname}: ${formatUnknownError(error)}`);
121
+ continue;
122
+ }
123
+ if (!fs.existsSync(artifactPath)) continue;
124
+ const cachePathname = normalizePregeneratedPathname(pathname);
125
+ if (typeof route.revalidate === "number" && route.revalidate <= 0) continue;
126
+ const revalidateSeconds = typeof route.revalidate === "number" ? route.revalidate : void 0;
127
+ const expireSeconds = typeof route.expire === "number" ? route.expire : void 0;
128
+ const staleSeconds = typeof route.stale === "number" && route.stale >= 0 ? route.stale : void 0;
129
+ const expirationTtl = revalidateSeconds === void 0 ? void 0 : ttlSeconds;
130
+ const tags = buildAppRouteCacheTags(cachePathname, route.tags ?? [], route.routeSegments ?? []);
131
+ const metadata = buildMetadata(tags);
132
+ const routeKey = appIsrCacheKey(cachePathname, "route", manifest.buildId);
133
+ pairs.push({
134
+ key: keySpace.entryKey(routeKey),
135
+ value: buildCacheEntry({
136
+ kind: "APP_ROUTE",
137
+ body: fs.readFileSync(artifactPath).toString("base64"),
138
+ headers: route.headers ?? {},
139
+ status: route.responseStatus ?? 200
140
+ }, tags, now, revalidateSeconds, expireSeconds, staleSeconds),
141
+ ...expirationTtl !== void 0 ? { expiration_ttl: expirationTtl } : {},
142
+ ...metadata ? { metadata } : {}
143
+ });
144
+ routeCount++;
145
+ }
114
146
  return {
115
147
  routeCount,
116
148
  pairs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinext/cloudflare",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "Cloudflare adapters for vinext (KV data cache and Images-backed image optimization).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,10 +54,10 @@
54
54
  "@types/node": "^25.9.2",
55
55
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
56
56
  "vite-plus": "0.2.6",
57
- "vinext": "1.0.0-beta.5"
57
+ "vinext": "1.0.0-beta.6"
58
58
  },
59
59
  "peerDependencies": {
60
- "vinext": "^1.0.0-beta.5"
60
+ "vinext": "^1.0.0-beta.6"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22"