@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.
|
|
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
|
|
76
|
-
if (/\b(?:no-store|no-cache|private)\b/.test(input.cacheControl)) return
|
|
77
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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.
|
|
57
|
+
"vinext": "1.0.0-beta.6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"vinext": "^1.0.0-beta.
|
|
60
|
+
"vinext": "^1.0.0-beta.6"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=22"
|