vinext 0.0.0 → 0.0.2
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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/build/static-export.d.ts +78 -0
- package/dist/build/static-export.d.ts.map +1 -0
- package/dist/build/static-export.js +553 -0
- package/dist/build/static-export.js.map +1 -0
- package/dist/check.d.ts +52 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +483 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/entry.d.ts +2 -0
- package/dist/client/entry.d.ts.map +1 -0
- package/dist/client/entry.js +85 -0
- package/dist/client/entry.js.map +1 -0
- package/dist/cloudflare/index.d.ts +8 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +8 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/cloudflare/kv-cache-handler.d.ts +68 -0
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -0
- package/dist/cloudflare/kv-cache-handler.js +304 -0
- package/dist/cloudflare/kv-cache-handler.js.map +1 -0
- package/dist/cloudflare/tpr.d.ts +78 -0
- package/dist/cloudflare/tpr.d.ts.map +1 -0
- package/dist/cloudflare/tpr.js +672 -0
- package/dist/cloudflare/tpr.js.map +1 -0
- package/dist/config/config-matchers.d.ts +106 -0
- package/dist/config/config-matchers.d.ts.map +1 -0
- package/dist/config/config-matchers.js +499 -0
- package/dist/config/config-matchers.js.map +1 -0
- package/dist/config/next-config.d.ts +153 -0
- package/dist/config/next-config.d.ts.map +1 -0
- package/dist/config/next-config.js +274 -0
- package/dist/config/next-config.js.map +1 -0
- package/dist/deploy.d.ts +87 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +644 -0
- package/dist/deploy.js.map +1 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3296 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +55 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +201 -0
- package/dist/init.js.map +1 -0
- package/dist/routing/app-router.d.ts +96 -0
- package/dist/routing/app-router.d.ts.map +1 -0
- package/dist/routing/app-router.js +815 -0
- package/dist/routing/app-router.js.map +1 -0
- package/dist/routing/pages-router.d.ts +52 -0
- package/dist/routing/pages-router.d.ts.map +1 -0
- package/dist/routing/pages-router.js +239 -0
- package/dist/routing/pages-router.js.map +1 -0
- package/dist/server/api-handler.d.ts +18 -0
- package/dist/server/api-handler.d.ts.map +1 -0
- package/dist/server/api-handler.js +169 -0
- package/dist/server/api-handler.js.map +1 -0
- package/dist/server/app-dev-server.d.ts +42 -0
- package/dist/server/app-dev-server.d.ts.map +1 -0
- package/dist/server/app-dev-server.js +2718 -0
- package/dist/server/app-dev-server.js.map +1 -0
- package/dist/server/app-router-entry.d.ts +18 -0
- package/dist/server/app-router-entry.d.ts.map +1 -0
- package/dist/server/app-router-entry.js +34 -0
- package/dist/server/app-router-entry.js.map +1 -0
- package/dist/server/dev-server.d.ts +40 -0
- package/dist/server/dev-server.d.ts.map +1 -0
- package/dist/server/dev-server.js +758 -0
- package/dist/server/dev-server.js.map +1 -0
- package/dist/server/html.d.ts +22 -0
- package/dist/server/html.d.ts.map +1 -0
- package/dist/server/html.js +29 -0
- package/dist/server/html.js.map +1 -0
- package/dist/server/image-optimization.d.ts +56 -0
- package/dist/server/image-optimization.d.ts.map +1 -0
- package/dist/server/image-optimization.js +103 -0
- package/dist/server/image-optimization.js.map +1 -0
- package/dist/server/instrumentation.d.ts +68 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +90 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/isr-cache.d.ts +61 -0
- package/dist/server/isr-cache.d.ts.map +1 -0
- package/dist/server/isr-cache.js +134 -0
- package/dist/server/isr-cache.js.map +1 -0
- package/dist/server/metadata-routes.d.ts +103 -0
- package/dist/server/metadata-routes.d.ts.map +1 -0
- package/dist/server/metadata-routes.js +270 -0
- package/dist/server/metadata-routes.js.map +1 -0
- package/dist/server/middleware.d.ts +77 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +228 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/prod-server.d.ts +78 -0
- package/dist/server/prod-server.d.ts.map +1 -0
- package/dist/server/prod-server.js +712 -0
- package/dist/server/prod-server.js.map +1 -0
- package/dist/shims/amp.d.ts +17 -0
- package/dist/shims/amp.d.ts.map +1 -0
- package/dist/shims/amp.js +21 -0
- package/dist/shims/amp.js.map +1 -0
- package/dist/shims/app.d.ts +12 -0
- package/dist/shims/app.d.ts.map +1 -0
- package/dist/shims/app.js +2 -0
- package/dist/shims/app.js.map +1 -0
- package/dist/shims/cache-runtime.d.ts +68 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -0
- package/dist/shims/cache-runtime.js +437 -0
- package/dist/shims/cache-runtime.js.map +1 -0
- package/dist/shims/cache.d.ts +243 -0
- package/dist/shims/cache.d.ts.map +1 -0
- package/dist/shims/cache.js +415 -0
- package/dist/shims/cache.js.map +1 -0
- package/dist/shims/client-only.d.ts +18 -0
- package/dist/shims/client-only.d.ts.map +1 -0
- package/dist/shims/client-only.js +18 -0
- package/dist/shims/client-only.js.map +1 -0
- package/dist/shims/config.d.ts +27 -0
- package/dist/shims/config.d.ts.map +1 -0
- package/dist/shims/config.js +30 -0
- package/dist/shims/config.js.map +1 -0
- package/dist/shims/constants.d.ts +13 -0
- package/dist/shims/constants.d.ts.map +1 -0
- package/dist/shims/constants.js +13 -0
- package/dist/shims/constants.js.map +1 -0
- package/dist/shims/document.d.ts +33 -0
- package/dist/shims/document.d.ts.map +1 -0
- package/dist/shims/document.js +32 -0
- package/dist/shims/document.js.map +1 -0
- package/dist/shims/dynamic.d.ts +33 -0
- package/dist/shims/dynamic.d.ts.map +1 -0
- package/dist/shims/dynamic.js +149 -0
- package/dist/shims/dynamic.js.map +1 -0
- package/dist/shims/error-boundary.d.ts +33 -0
- package/dist/shims/error-boundary.d.ts.map +1 -0
- package/dist/shims/error-boundary.js +88 -0
- package/dist/shims/error-boundary.js.map +1 -0
- package/dist/shims/error.d.ts +16 -0
- package/dist/shims/error.d.ts.map +1 -0
- package/dist/shims/error.js +45 -0
- package/dist/shims/error.js.map +1 -0
- package/dist/shims/fetch-cache.d.ts +61 -0
- package/dist/shims/fetch-cache.d.ts.map +1 -0
- package/dist/shims/fetch-cache.js +307 -0
- package/dist/shims/fetch-cache.js.map +1 -0
- package/dist/shims/font-google.d.ts +122 -0
- package/dist/shims/font-google.d.ts.map +1 -0
- package/dist/shims/font-google.js +387 -0
- package/dist/shims/font-google.js.map +1 -0
- package/dist/shims/font-local.d.ts +61 -0
- package/dist/shims/font-local.d.ts.map +1 -0
- package/dist/shims/font-local.js +303 -0
- package/dist/shims/font-local.js.map +1 -0
- package/dist/shims/form.d.ts +30 -0
- package/dist/shims/form.d.ts.map +1 -0
- package/dist/shims/form.js +78 -0
- package/dist/shims/form.js.map +1 -0
- package/dist/shims/head-state.d.ts +11 -0
- package/dist/shims/head-state.d.ts.map +1 -0
- package/dist/shims/head-state.js +47 -0
- package/dist/shims/head-state.js.map +1 -0
- package/dist/shims/head.d.ts +28 -0
- package/dist/shims/head.d.ts.map +1 -0
- package/dist/shims/head.js +148 -0
- package/dist/shims/head.js.map +1 -0
- package/dist/shims/headers.d.ts +150 -0
- package/dist/shims/headers.d.ts.map +1 -0
- package/dist/shims/headers.js +412 -0
- package/dist/shims/headers.js.map +1 -0
- package/dist/shims/image-config.d.ts +30 -0
- package/dist/shims/image-config.d.ts.map +1 -0
- package/dist/shims/image-config.js +91 -0
- package/dist/shims/image-config.js.map +1 -0
- package/dist/shims/image.d.ts +63 -0
- package/dist/shims/image.d.ts.map +1 -0
- package/dist/shims/image.js +284 -0
- package/dist/shims/image.js.map +1 -0
- package/dist/shims/internal/api-utils.d.ts +12 -0
- package/dist/shims/internal/api-utils.d.ts.map +1 -0
- package/dist/shims/internal/api-utils.js +7 -0
- package/dist/shims/internal/api-utils.js.map +1 -0
- package/dist/shims/internal/app-router-context.d.ts +21 -0
- package/dist/shims/internal/app-router-context.d.ts.map +1 -0
- package/dist/shims/internal/app-router-context.js +15 -0
- package/dist/shims/internal/app-router-context.js.map +1 -0
- package/dist/shims/internal/cookies.d.ts +9 -0
- package/dist/shims/internal/cookies.d.ts.map +1 -0
- package/dist/shims/internal/cookies.js +9 -0
- package/dist/shims/internal/cookies.js.map +1 -0
- package/dist/shims/internal/router-context.d.ts +2 -0
- package/dist/shims/internal/router-context.d.ts.map +1 -0
- package/dist/shims/internal/router-context.js +9 -0
- package/dist/shims/internal/router-context.js.map +1 -0
- package/dist/shims/internal/utils.d.ts +48 -0
- package/dist/shims/internal/utils.d.ts.map +1 -0
- package/dist/shims/internal/utils.js +35 -0
- package/dist/shims/internal/utils.js.map +1 -0
- package/dist/shims/internal/work-unit-async-storage.d.ts +12 -0
- package/dist/shims/internal/work-unit-async-storage.d.ts.map +1 -0
- package/dist/shims/internal/work-unit-async-storage.js +13 -0
- package/dist/shims/internal/work-unit-async-storage.js.map +1 -0
- package/dist/shims/layout-segment-context.d.ts +21 -0
- package/dist/shims/layout-segment-context.d.ts.map +1 -0
- package/dist/shims/layout-segment-context.js +27 -0
- package/dist/shims/layout-segment-context.js.map +1 -0
- package/dist/shims/legacy-image.d.ts +52 -0
- package/dist/shims/legacy-image.d.ts.map +1 -0
- package/dist/shims/legacy-image.js +46 -0
- package/dist/shims/legacy-image.js.map +1 -0
- package/dist/shims/link.d.ts +48 -0
- package/dist/shims/link.d.ts.map +1 -0
- package/dist/shims/link.js +395 -0
- package/dist/shims/link.js.map +1 -0
- package/dist/shims/metadata.d.ts +184 -0
- package/dist/shims/metadata.d.ts.map +1 -0
- package/dist/shims/metadata.js +472 -0
- package/dist/shims/metadata.js.map +1 -0
- package/dist/shims/navigation-state.d.ts +14 -0
- package/dist/shims/navigation-state.d.ts.map +1 -0
- package/dist/shims/navigation-state.js +77 -0
- package/dist/shims/navigation-state.js.map +1 -0
- package/dist/shims/navigation.d.ts +201 -0
- package/dist/shims/navigation.d.ts.map +1 -0
- package/dist/shims/navigation.js +672 -0
- package/dist/shims/navigation.js.map +1 -0
- package/dist/shims/og.d.ts +20 -0
- package/dist/shims/og.d.ts.map +1 -0
- package/dist/shims/og.js +19 -0
- package/dist/shims/og.js.map +1 -0
- package/dist/shims/router-state.d.ts +11 -0
- package/dist/shims/router-state.d.ts.map +1 -0
- package/dist/shims/router-state.js +56 -0
- package/dist/shims/router-state.js.map +1 -0
- package/dist/shims/router.d.ts +103 -0
- package/dist/shims/router.d.ts.map +1 -0
- package/dist/shims/router.js +536 -0
- package/dist/shims/router.js.map +1 -0
- package/dist/shims/script.d.ts +58 -0
- package/dist/shims/script.d.ts.map +1 -0
- package/dist/shims/script.js +163 -0
- package/dist/shims/script.js.map +1 -0
- package/dist/shims/server-only.d.ts +19 -0
- package/dist/shims/server-only.d.ts.map +1 -0
- package/dist/shims/server-only.js +19 -0
- package/dist/shims/server-only.js.map +1 -0
- package/dist/shims/server.d.ts +178 -0
- package/dist/shims/server.d.ts.map +1 -0
- package/dist/shims/server.js +377 -0
- package/dist/shims/server.js.map +1 -0
- package/dist/shims/web-vitals.d.ts +24 -0
- package/dist/shims/web-vitals.d.ts.map +1 -0
- package/dist/shims/web-vitals.js +17 -0
- package/dist/shims/web-vitals.js.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +20 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/project.d.ts +36 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +112 -0
- package/dist/utils/project.js.map +1 -0
- package/dist/utils/query.d.ts +10 -0
- package/dist/utils/query.d.ts.map +1 -0
- package/dist/utils/query.js +27 -0
- package/dist/utils/query.js.map +1 -0
- package/package.json +65 -7
- package/index.js +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side hydration entry point.
|
|
3
|
+
*
|
|
4
|
+
* This module is injected as a <script type="module"> in the SSR HTML.
|
|
5
|
+
* It reads __NEXT_DATA__ from the window, dynamically imports the page
|
|
6
|
+
* component, and hydrates it onto #__next.
|
|
7
|
+
*
|
|
8
|
+
* The actual page import path is injected at serve-time by the plugin
|
|
9
|
+
* via a virtual module or inline script.
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { hydrateRoot } from "react-dom/client";
|
|
13
|
+
// Eagerly import the router shim so its module-level popstate listener is
|
|
14
|
+
// registered. Without this, browser back/forward buttons do nothing because
|
|
15
|
+
// navigateClient() is never invoked on history changes.
|
|
16
|
+
import "next/router";
|
|
17
|
+
// Read the SSR data injected by the server
|
|
18
|
+
const nextData = window.__NEXT_DATA__;
|
|
19
|
+
const { pageProps } = nextData?.props ?? { pageProps: {} };
|
|
20
|
+
const pageModulePath = nextData?.__pageModule;
|
|
21
|
+
const appModulePath = nextData?.__appModule;
|
|
22
|
+
/** Defense-in-depth: validate module paths from __NEXT_DATA__. */
|
|
23
|
+
function isValidModulePath(p) {
|
|
24
|
+
if (typeof p !== "string" || p.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
// Must start with / or ./ (relative Vite module paths)
|
|
27
|
+
if (!p.startsWith("/") && !p.startsWith("./"))
|
|
28
|
+
return false;
|
|
29
|
+
// Must not contain protocol (prevents importing from external URLs)
|
|
30
|
+
if (p.includes("://"))
|
|
31
|
+
return false;
|
|
32
|
+
// Must not traverse directories
|
|
33
|
+
if (p.includes(".."))
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
async function hydrate() {
|
|
38
|
+
if (!isValidModulePath(pageModulePath)) {
|
|
39
|
+
console.error("[vinext] Invalid or missing __pageModule in __NEXT_DATA__");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Dynamically import the page module
|
|
43
|
+
const pageModule = await import(/* @vite-ignore */ pageModulePath);
|
|
44
|
+
const PageComponent = pageModule.default;
|
|
45
|
+
if (!PageComponent) {
|
|
46
|
+
console.error("[vinext] Page module has no default export");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let element;
|
|
50
|
+
// If there's a custom _app, wrap the page with it
|
|
51
|
+
if (appModulePath) {
|
|
52
|
+
if (!isValidModulePath(appModulePath)) {
|
|
53
|
+
console.error("[vinext] Invalid __appModule in __NEXT_DATA__");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
try {
|
|
57
|
+
const appModule = await import(/* @vite-ignore */ appModulePath);
|
|
58
|
+
const AppComponent = appModule.default;
|
|
59
|
+
element = React.createElement(AppComponent, {
|
|
60
|
+
Component: PageComponent,
|
|
61
|
+
pageProps,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// No _app, render page directly
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// @ts-expect-error -- element is assigned in the _app branch above, or falls through here
|
|
70
|
+
if (!element) {
|
|
71
|
+
element = React.createElement(PageComponent, pageProps);
|
|
72
|
+
}
|
|
73
|
+
const container = document.getElementById("__next");
|
|
74
|
+
if (!container) {
|
|
75
|
+
console.error("[vinext] No #__next element found");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const root = hydrateRoot(container, element);
|
|
79
|
+
// Expose root on window so the router shim (a separate module) can
|
|
80
|
+
// re-render the tree during client-side navigation. import.meta.hot.data
|
|
81
|
+
// is module-scoped and cannot be read across module boundaries.
|
|
82
|
+
window.__VINEXT_ROOT__ = root;
|
|
83
|
+
}
|
|
84
|
+
hydrate();
|
|
85
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/client/entry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Client-side hydration entry point.\n *\n * This module is injected as a <script type=\"module\"> in the SSR HTML.\n * It reads __NEXT_DATA__ from the window, dynamically imports the page\n * component, and hydrates it onto #__next.\n *\n * The actual page import path is injected at serve-time by the plugin\n * via a virtual module or inline script.\n */\nimport React from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\n// Eagerly import the router shim so its module-level popstate listener is\n// registered. Without this, browser back/forward buttons do nothing because\n// navigateClient() is never invoked on history changes.\nimport \"next/router\";\n\n// Read the SSR data injected by the server\nconst nextData = (window as any).__NEXT_DATA__;\nconst { pageProps } = nextData?.props ?? { pageProps: {} };\nconst pageModulePath = nextData?.__pageModule;\nconst appModulePath = nextData?.__appModule;\n\n/** Defense-in-depth: validate module paths from __NEXT_DATA__. */\nfunction isValidModulePath(p: unknown): p is string {\n if (typeof p !== \"string\" || p.length === 0) return false;\n // Must start with / or ./ (relative Vite module paths)\n if (!p.startsWith(\"/\") && !p.startsWith(\"./\")) return false;\n // Must not contain protocol (prevents importing from external URLs)\n if (p.includes(\"://\")) return false;\n // Must not traverse directories\n if (p.includes(\"..\")) return false;\n return true;\n}\n\nasync function hydrate() {\n if (!isValidModulePath(pageModulePath)) {\n console.error(\"[vinext] Invalid or missing __pageModule in __NEXT_DATA__\");\n return;\n }\n\n // Dynamically import the page module\n const pageModule = await import(/* @vite-ignore */ pageModulePath);\n const PageComponent = pageModule.default;\n\n if (!PageComponent) {\n console.error(\"[vinext] Page module has no default export\");\n return;\n }\n\n let element: React.ReactElement;\n\n // If there's a custom _app, wrap the page with it\n if (appModulePath) {\n if (!isValidModulePath(appModulePath)) {\n console.error(\"[vinext] Invalid __appModule in __NEXT_DATA__\");\n } else {\n try {\n const appModule = await import(/* @vite-ignore */ appModulePath);\n const AppComponent = appModule.default;\n element = React.createElement(AppComponent, {\n Component: PageComponent,\n pageProps,\n });\n } catch {\n // No _app, render page directly\n }\n }\n }\n\n // @ts-expect-error -- element is assigned in the _app branch above, or falls through here\n if (!element) {\n element = React.createElement(PageComponent, pageProps);\n }\n\n const container = document.getElementById(\"__next\");\n if (!container) {\n console.error(\"[vinext] No #__next element found\");\n return;\n }\n\n const root = hydrateRoot(container, element);\n\n // Expose root on window so the router shim (a separate module) can\n // re-render the tree during client-side navigation. import.meta.hot.data\n // is module-scoped and cannot be read across module boundaries.\n (window as any).__VINEXT_ROOT__ = root;\n}\n\nhydrate();\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vinext/cloudflare — Cloudflare Workers integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides cache handlers and utilities for running vinext on Cloudflare Workers.
|
|
5
|
+
*/
|
|
6
|
+
export { KVCacheHandler } from "./kv-cache-handler.js";
|
|
7
|
+
export { runTPR, type TPROptions, type TPRResult } from "./tpr.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vinext/cloudflare — Cloudflare Workers integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides cache handlers and utilities for running vinext on Cloudflare Workers.
|
|
5
|
+
*/
|
|
6
|
+
export { KVCacheHandler } from "./kv-cache-handler.js";
|
|
7
|
+
export { runTPR } from "./tpr.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAmC,MAAM,UAAU,CAAC","sourcesContent":["/**\n * vinext/cloudflare — Cloudflare Workers integration.\n *\n * Provides cache handlers and utilities for running vinext on Cloudflare Workers.\n */\n\nexport { KVCacheHandler } from \"./kv-cache-handler.js\";\nexport { runTPR, type TPROptions, type TPRResult } from \"./tpr.js\";\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare KV-backed CacheHandler for vinext.
|
|
3
|
+
*
|
|
4
|
+
* Provides persistent ISR caching on Cloudflare Workers using KV as the
|
|
5
|
+
* storage backend. Supports time-based expiry (stale-while-revalidate)
|
|
6
|
+
* and tag-based invalidation.
|
|
7
|
+
*
|
|
8
|
+
* Usage in worker/index.ts:
|
|
9
|
+
*
|
|
10
|
+
* import { KVCacheHandler } from "vinext/cloudflare";
|
|
11
|
+
* import { setCacheHandler } from "vinext/shims/cache";
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* async fetch(request: Request, env: Env) {
|
|
15
|
+
* setCacheHandler(new KVCacheHandler(env.VINEXT_CACHE));
|
|
16
|
+
* // ... rest of worker handler
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
*
|
|
20
|
+
* Wrangler config (wrangler.jsonc):
|
|
21
|
+
*
|
|
22
|
+
* {
|
|
23
|
+
* "kv_namespaces": [
|
|
24
|
+
* { "binding": "VINEXT_CACHE", "id": "<your-kv-namespace-id>" }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
import type { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "../shims/cache.js";
|
|
29
|
+
interface KVNamespace {
|
|
30
|
+
get(key: string, options?: {
|
|
31
|
+
type?: string;
|
|
32
|
+
}): Promise<string | null>;
|
|
33
|
+
get(key: string, options: {
|
|
34
|
+
type: "arrayBuffer";
|
|
35
|
+
}): Promise<ArrayBuffer | null>;
|
|
36
|
+
put(key: string, value: string | ArrayBuffer | ReadableStream, options?: {
|
|
37
|
+
expirationTtl?: number;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
list(options?: {
|
|
42
|
+
prefix?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
cursor?: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
keys: Array<{
|
|
47
|
+
name: string;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
}>;
|
|
50
|
+
list_complete: boolean;
|
|
51
|
+
cursor?: string;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
export declare class KVCacheHandler implements CacheHandler {
|
|
55
|
+
private kv;
|
|
56
|
+
private prefix;
|
|
57
|
+
constructor(kvNamespace: KVNamespace, options?: {
|
|
58
|
+
appPrefix?: string;
|
|
59
|
+
});
|
|
60
|
+
get(key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
61
|
+
set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
|
|
62
|
+
revalidateTag(tags: string | string[], _durations?: {
|
|
63
|
+
expire?: number;
|
|
64
|
+
}): Promise<void>;
|
|
65
|
+
resetRequestCache(): void;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=kv-cache-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-cache-handler.d.ts","sourceRoot":"","sources":["../../src/cloudflare/kv-cache-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,mBAAmB,CAAC;AAG3B,UAAU,WAAW;IACnB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAChF,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,cAAc,EAC5C,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GACvE,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO,CAAC,EAAE;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC;QACV,IAAI,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC,CAAC;QAClE,aAAa,EAAE,OAAO,CAAC;QACvB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;CACJ;AAiCD,qBAAa,cAAe,YAAW,YAAY;IACjD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAS;gBAEX,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAKhE,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAkE9B,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,qBAAqB,GAAG,IAAI,EAClC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC;IA8DV,aAAa,CACjB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,UAAU,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/B,OAAO,CAAC,IAAI,CAAC;IAehB,iBAAiB,IAAI,IAAI;CAG1B"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare KV-backed CacheHandler for vinext.
|
|
3
|
+
*
|
|
4
|
+
* Provides persistent ISR caching on Cloudflare Workers using KV as the
|
|
5
|
+
* storage backend. Supports time-based expiry (stale-while-revalidate)
|
|
6
|
+
* and tag-based invalidation.
|
|
7
|
+
*
|
|
8
|
+
* Usage in worker/index.ts:
|
|
9
|
+
*
|
|
10
|
+
* import { KVCacheHandler } from "vinext/cloudflare";
|
|
11
|
+
* import { setCacheHandler } from "vinext/shims/cache";
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* async fetch(request: Request, env: Env) {
|
|
15
|
+
* setCacheHandler(new KVCacheHandler(env.VINEXT_CACHE));
|
|
16
|
+
* // ... rest of worker handler
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
*
|
|
20
|
+
* Wrangler config (wrangler.jsonc):
|
|
21
|
+
*
|
|
22
|
+
* {
|
|
23
|
+
* "kv_namespaces": [
|
|
24
|
+
* { "binding": "VINEXT_CACHE", "id": "<your-kv-namespace-id>" }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
/** Key prefix for tag invalidation timestamps. */
|
|
29
|
+
const TAG_PREFIX = "__tag:";
|
|
30
|
+
/** Key prefix for cache entries. */
|
|
31
|
+
const ENTRY_PREFIX = "cache:";
|
|
32
|
+
/** Max tag length to prevent KV key abuse. */
|
|
33
|
+
const MAX_TAG_LENGTH = 256;
|
|
34
|
+
/**
|
|
35
|
+
* Validate a cache tag. Returns null if invalid.
|
|
36
|
+
* Note: `:` is rejected because TAG_PREFIX and ENTRY_PREFIX use `:` as a
|
|
37
|
+
* separator — allowing `:` in user tags could cause ambiguous key lookups.
|
|
38
|
+
*/
|
|
39
|
+
function validateTag(tag) {
|
|
40
|
+
if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_TAG_LENGTH)
|
|
41
|
+
return null;
|
|
42
|
+
// Block control characters, path separators, and KV-special characters.
|
|
43
|
+
// eslint-disable-next-line no-control-regex -- intentional: reject control chars in tags
|
|
44
|
+
if (/[\x00-\x1f/\\:]/.test(tag))
|
|
45
|
+
return null;
|
|
46
|
+
return tag;
|
|
47
|
+
}
|
|
48
|
+
export class KVCacheHandler {
|
|
49
|
+
kv;
|
|
50
|
+
prefix;
|
|
51
|
+
constructor(kvNamespace, options) {
|
|
52
|
+
this.kv = kvNamespace;
|
|
53
|
+
this.prefix = options?.appPrefix ? `${options.appPrefix}:` : "";
|
|
54
|
+
}
|
|
55
|
+
async get(key, _ctx) {
|
|
56
|
+
const kvKey = this.prefix + ENTRY_PREFIX + key;
|
|
57
|
+
const raw = await this.kv.get(kvKey);
|
|
58
|
+
if (!raw)
|
|
59
|
+
return null;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Corrupted JSON — delete and treat as miss
|
|
66
|
+
await this.kv.delete(kvKey);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Validate deserialized shape before using
|
|
70
|
+
const entry = validateCacheEntry(parsed);
|
|
71
|
+
if (!entry) {
|
|
72
|
+
console.error("[vinext] Invalid cache entry shape for key:", key);
|
|
73
|
+
await this.kv.delete(kvKey);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// Restore ArrayBuffer fields that were base64-encoded for JSON storage
|
|
77
|
+
if (entry.value) {
|
|
78
|
+
const ok = restoreArrayBuffers(entry.value);
|
|
79
|
+
if (!ok) {
|
|
80
|
+
// base64 decode failed — corrupted entry, treat as miss
|
|
81
|
+
await this.kv.delete(kvKey);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check tag-based invalidation (parallel for lower latency)
|
|
86
|
+
if (entry.tags.length > 0) {
|
|
87
|
+
const tagResults = await Promise.all(entry.tags.map((tag) => this.kv.get(this.prefix + TAG_PREFIX + tag)));
|
|
88
|
+
for (let i = 0; i < entry.tags.length; i++) {
|
|
89
|
+
const tagTime = tagResults[i];
|
|
90
|
+
if (tagTime) {
|
|
91
|
+
const tagTimestamp = Number(tagTime);
|
|
92
|
+
if (Number.isNaN(tagTimestamp) || tagTimestamp >= entry.lastModified) {
|
|
93
|
+
// Tag was invalidated after this entry, or timestamp is corrupted
|
|
94
|
+
// — treat as miss to force re-render
|
|
95
|
+
await this.kv.delete(kvKey);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Check time-based expiry — return stale with cacheState
|
|
102
|
+
if (entry.revalidateAt !== null && Date.now() > entry.revalidateAt) {
|
|
103
|
+
return {
|
|
104
|
+
lastModified: entry.lastModified,
|
|
105
|
+
value: entry.value,
|
|
106
|
+
cacheState: "stale",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
lastModified: entry.lastModified,
|
|
111
|
+
value: entry.value,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async set(key, data, ctx) {
|
|
115
|
+
// Collect, validate, and dedupe tags from data and context
|
|
116
|
+
const tagSet = new Set();
|
|
117
|
+
if (data && "tags" in data && Array.isArray(data.tags)) {
|
|
118
|
+
for (const t of data.tags) {
|
|
119
|
+
const validated = validateTag(t);
|
|
120
|
+
if (validated)
|
|
121
|
+
tagSet.add(validated);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (ctx && "tags" in ctx && Array.isArray(ctx.tags)) {
|
|
125
|
+
for (const t of ctx.tags) {
|
|
126
|
+
const validated = validateTag(t);
|
|
127
|
+
if (validated)
|
|
128
|
+
tagSet.add(validated);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const tags = [...tagSet];
|
|
132
|
+
// Determine revalidation time
|
|
133
|
+
let revalidateAt = null;
|
|
134
|
+
if (ctx) {
|
|
135
|
+
const revalidate = ctx.cacheControl?.revalidate ?? ctx.revalidate;
|
|
136
|
+
if (typeof revalidate === "number" && revalidate > 0) {
|
|
137
|
+
revalidateAt = Date.now() + revalidate * 1000;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (data &&
|
|
141
|
+
"revalidate" in data &&
|
|
142
|
+
typeof data.revalidate === "number" &&
|
|
143
|
+
data.revalidate > 0) {
|
|
144
|
+
revalidateAt = Date.now() + data.revalidate * 1000;
|
|
145
|
+
}
|
|
146
|
+
// Prepare entry — convert ArrayBuffers to base64 for JSON storage
|
|
147
|
+
const serializable = data ? serializeForJSON(data) : null;
|
|
148
|
+
const entry = {
|
|
149
|
+
value: serializable,
|
|
150
|
+
tags,
|
|
151
|
+
lastModified: Date.now(),
|
|
152
|
+
revalidateAt,
|
|
153
|
+
};
|
|
154
|
+
// Calculate KV TTL — keep entries well beyond their revalidate window
|
|
155
|
+
// (10x revalidate period, clamped to 60s–30d) so stale-while-revalidate
|
|
156
|
+
// can serve stale content while background regeneration happens.
|
|
157
|
+
let expirationTtl;
|
|
158
|
+
if (revalidateAt !== null) {
|
|
159
|
+
const revalidateSeconds = Math.ceil((revalidateAt - Date.now()) / 1000);
|
|
160
|
+
// Keep in KV for 10x the revalidation period, up to 30 days
|
|
161
|
+
expirationTtl = Math.min(revalidateSeconds * 10, 30 * 24 * 3600);
|
|
162
|
+
// KV minimum TTL is 60 seconds
|
|
163
|
+
expirationTtl = Math.max(expirationTtl, 60);
|
|
164
|
+
}
|
|
165
|
+
await this.kv.put(this.prefix + ENTRY_PREFIX + key, JSON.stringify(entry), {
|
|
166
|
+
expirationTtl,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async revalidateTag(tags, _durations) {
|
|
170
|
+
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const validTags = tagList.filter((t) => validateTag(t) !== null);
|
|
173
|
+
// Store invalidation timestamp for each tag
|
|
174
|
+
// Use a long TTL (30 days) so recent invalidations are always found
|
|
175
|
+
await Promise.all(validTags.map((tag) => this.kv.put(this.prefix + TAG_PREFIX + tag, String(now), {
|
|
176
|
+
expirationTtl: 30 * 24 * 3600,
|
|
177
|
+
})));
|
|
178
|
+
}
|
|
179
|
+
resetRequestCache() {
|
|
180
|
+
// No-op — KV is stateless per request
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Validation helpers
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
const VALID_KINDS = new Set([
|
|
187
|
+
"FETCH",
|
|
188
|
+
"APP_PAGE",
|
|
189
|
+
"PAGES",
|
|
190
|
+
"APP_ROUTE",
|
|
191
|
+
"REDIRECT",
|
|
192
|
+
"IMAGE",
|
|
193
|
+
]);
|
|
194
|
+
/**
|
|
195
|
+
* Validate that a parsed JSON value has the expected KVCacheEntry shape.
|
|
196
|
+
* Returns the validated entry or null if the shape is invalid.
|
|
197
|
+
*/
|
|
198
|
+
function validateCacheEntry(raw) {
|
|
199
|
+
if (!raw || typeof raw !== "object")
|
|
200
|
+
return null;
|
|
201
|
+
const obj = raw;
|
|
202
|
+
// Required fields
|
|
203
|
+
if (typeof obj.lastModified !== "number")
|
|
204
|
+
return null;
|
|
205
|
+
if (!Array.isArray(obj.tags))
|
|
206
|
+
return null;
|
|
207
|
+
if (obj.revalidateAt !== null &&
|
|
208
|
+
typeof obj.revalidateAt !== "number")
|
|
209
|
+
return null;
|
|
210
|
+
// value must be null or a valid cache value object with a known kind
|
|
211
|
+
if (obj.value !== null) {
|
|
212
|
+
if (!obj.value || typeof obj.value !== "object")
|
|
213
|
+
return null;
|
|
214
|
+
const value = obj.value;
|
|
215
|
+
if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind))
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return raw;
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// ArrayBuffer serialization helpers
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
/**
|
|
224
|
+
* Deep-clone a cache value, converting ArrayBuffer fields to base64 strings
|
|
225
|
+
* so the entire structure can be JSON.stringify'd for KV storage.
|
|
226
|
+
*/
|
|
227
|
+
function serializeForJSON(value) {
|
|
228
|
+
if (value.kind === "APP_PAGE") {
|
|
229
|
+
return {
|
|
230
|
+
...value,
|
|
231
|
+
rscData: value.rscData
|
|
232
|
+
? arrayBufferToBase64(value.rscData)
|
|
233
|
+
: undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (value.kind === "APP_ROUTE") {
|
|
237
|
+
return {
|
|
238
|
+
...value,
|
|
239
|
+
body: arrayBufferToBase64(value.body),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (value.kind === "IMAGE") {
|
|
243
|
+
return {
|
|
244
|
+
...value,
|
|
245
|
+
buffer: arrayBufferToBase64(value.buffer),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Restore base64 strings back to ArrayBuffers after JSON.parse.
|
|
252
|
+
* Returns false if any base64 decode fails (corrupted entry).
|
|
253
|
+
*/
|
|
254
|
+
function restoreArrayBuffers(value) {
|
|
255
|
+
if (value.kind === "APP_PAGE" && typeof value.rscData === "string") {
|
|
256
|
+
const decoded = safeBase64ToArrayBuffer(value.rscData);
|
|
257
|
+
if (!decoded)
|
|
258
|
+
return false;
|
|
259
|
+
value.rscData = decoded;
|
|
260
|
+
}
|
|
261
|
+
if (value.kind === "APP_ROUTE" && typeof value.body === "string") {
|
|
262
|
+
const decoded = safeBase64ToArrayBuffer(value.body);
|
|
263
|
+
if (!decoded)
|
|
264
|
+
return false;
|
|
265
|
+
value.body = decoded;
|
|
266
|
+
}
|
|
267
|
+
if (value.kind === "IMAGE" && typeof value.buffer === "string") {
|
|
268
|
+
const decoded = safeBase64ToArrayBuffer(value.buffer);
|
|
269
|
+
if (!decoded)
|
|
270
|
+
return false;
|
|
271
|
+
value.buffer = decoded;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
function arrayBufferToBase64(buffer) {
|
|
276
|
+
const bytes = new Uint8Array(buffer);
|
|
277
|
+
let binary = "";
|
|
278
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
279
|
+
binary += String.fromCharCode(bytes[i]);
|
|
280
|
+
}
|
|
281
|
+
return btoa(binary);
|
|
282
|
+
}
|
|
283
|
+
function base64ToArrayBuffer(base64) {
|
|
284
|
+
const binary = atob(base64);
|
|
285
|
+
const bytes = new Uint8Array(binary.length);
|
|
286
|
+
for (let i = 0; i < binary.length; i++) {
|
|
287
|
+
bytes[i] = binary.charCodeAt(i);
|
|
288
|
+
}
|
|
289
|
+
return bytes.buffer;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Safely decode base64 to ArrayBuffer. Returns null on invalid input
|
|
293
|
+
* instead of throwing a DOMException.
|
|
294
|
+
*/
|
|
295
|
+
function safeBase64ToArrayBuffer(base64) {
|
|
296
|
+
try {
|
|
297
|
+
return base64ToArrayBuffer(base64);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
console.error("[vinext] Invalid base64 in cache entry");
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=kv-cache-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-cache-handler.js","sourceRoot":"","sources":["../../src/cloudflare/kv-cache-handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cloudflare KV-backed CacheHandler for vinext.\n *\n * Provides persistent ISR caching on Cloudflare Workers using KV as the\n * storage backend. Supports time-based expiry (stale-while-revalidate)\n * and tag-based invalidation.\n *\n * Usage in worker/index.ts:\n *\n * import { KVCacheHandler } from \"vinext/cloudflare\";\n * import { setCacheHandler } from \"vinext/shims/cache\";\n *\n * export default {\n * async fetch(request: Request, env: Env) {\n * setCacheHandler(new KVCacheHandler(env.VINEXT_CACHE));\n * // ... rest of worker handler\n * }\n * };\n *\n * Wrangler config (wrangler.jsonc):\n *\n * {\n * \"kv_namespaces\": [\n * { \"binding\": \"VINEXT_CACHE\", \"id\": \"<your-kv-namespace-id>\" }\n * ]\n * }\n */\n\nimport type {\n CacheHandler,\n CacheHandlerValue,\n IncrementalCacheValue,\n} from \"../shims/cache.js\";\n\n// Cloudflare KV namespace interface (matches Workers types)\ninterface KVNamespace {\n get(key: string, options?: { type?: string }): Promise<string | null>;\n get(key: string, options: { type: \"arrayBuffer\" }): Promise<ArrayBuffer | null>;\n put(\n key: string,\n value: string | ArrayBuffer | ReadableStream,\n options?: { expirationTtl?: number; metadata?: Record<string, unknown> },\n ): Promise<void>;\n delete(key: string): Promise<void>;\n list(options?: {\n prefix?: string;\n limit?: number;\n cursor?: string;\n }): Promise<{\n keys: Array<{ name: string; metadata?: Record<string, unknown> }>;\n list_complete: boolean;\n cursor?: string;\n }>;\n}\n\n/** Shape stored in KV for each cache entry. */\ninterface KVCacheEntry {\n value: IncrementalCacheValue | null;\n tags: string[];\n lastModified: number;\n /** Absolute timestamp (ms) after which the entry is \"stale\" (but still served). */\n revalidateAt: number | null;\n}\n\n/** Key prefix for tag invalidation timestamps. */\nconst TAG_PREFIX = \"__tag:\";\n\n/** Key prefix for cache entries. */\nconst ENTRY_PREFIX = \"cache:\";\n\n/** Max tag length to prevent KV key abuse. */\nconst MAX_TAG_LENGTH = 256;\n\n/**\n * Validate a cache tag. Returns null if invalid.\n * Note: `:` is rejected because TAG_PREFIX and ENTRY_PREFIX use `:` as a\n * separator — allowing `:` in user tags could cause ambiguous key lookups.\n */\nfunction validateTag(tag: string): string | null {\n if (typeof tag !== \"string\" || tag.length === 0 || tag.length > MAX_TAG_LENGTH) return null;\n // Block control characters, path separators, and KV-special characters.\n // eslint-disable-next-line no-control-regex -- intentional: reject control chars in tags\n if (/[\\x00-\\x1f/\\\\:]/.test(tag)) return null;\n return tag;\n}\n\nexport class KVCacheHandler implements CacheHandler {\n private kv: KVNamespace;\n private prefix: string;\n\n constructor(kvNamespace: KVNamespace, options?: { appPrefix?: string }) {\n this.kv = kvNamespace;\n this.prefix = options?.appPrefix ? `${options.appPrefix}:` : \"\";\n }\n\n async get(\n key: string,\n _ctx?: Record<string, unknown>,\n ): Promise<CacheHandlerValue | null> {\n const kvKey = this.prefix + ENTRY_PREFIX + key;\n const raw = await this.kv.get(kvKey);\n if (!raw) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Corrupted JSON — delete and treat as miss\n await this.kv.delete(kvKey);\n return null;\n }\n\n // Validate deserialized shape before using\n const entry = validateCacheEntry(parsed);\n if (!entry) {\n console.error(\"[vinext] Invalid cache entry shape for key:\", key);\n await this.kv.delete(kvKey);\n return null;\n }\n\n // Restore ArrayBuffer fields that were base64-encoded for JSON storage\n if (entry.value) {\n const ok = restoreArrayBuffers(entry.value);\n if (!ok) {\n // base64 decode failed — corrupted entry, treat as miss\n await this.kv.delete(kvKey);\n return null;\n }\n }\n\n // Check tag-based invalidation (parallel for lower latency)\n if (entry.tags.length > 0) {\n const tagResults = await Promise.all(\n entry.tags.map((tag) => this.kv.get(this.prefix + TAG_PREFIX + tag)),\n );\n for (let i = 0; i < entry.tags.length; i++) {\n const tagTime = tagResults[i];\n if (tagTime) {\n const tagTimestamp = Number(tagTime);\n if (Number.isNaN(tagTimestamp) || tagTimestamp >= entry.lastModified) {\n // Tag was invalidated after this entry, or timestamp is corrupted\n // — treat as miss to force re-render\n await this.kv.delete(kvKey);\n return null;\n }\n }\n }\n }\n\n // Check time-based expiry — return stale with cacheState\n if (entry.revalidateAt !== null && Date.now() > entry.revalidateAt) {\n return {\n lastModified: entry.lastModified,\n value: entry.value,\n cacheState: \"stale\",\n };\n }\n\n return {\n lastModified: entry.lastModified,\n value: entry.value,\n };\n }\n\n async set(\n key: string,\n data: IncrementalCacheValue | null,\n ctx?: Record<string, unknown>,\n ): Promise<void> {\n // Collect, validate, and dedupe tags from data and context\n const tagSet = new Set<string>();\n if (data && \"tags\" in data && Array.isArray(data.tags)) {\n for (const t of data.tags) {\n const validated = validateTag(t);\n if (validated) tagSet.add(validated);\n }\n }\n if (ctx && \"tags\" in ctx && Array.isArray(ctx.tags)) {\n for (const t of ctx.tags as string[]) {\n const validated = validateTag(t);\n if (validated) tagSet.add(validated);\n }\n }\n const tags = [...tagSet];\n\n // Determine revalidation time\n let revalidateAt: number | null = null;\n if (ctx) {\n const revalidate =\n (ctx as any).cacheControl?.revalidate ?? (ctx as any).revalidate;\n if (typeof revalidate === \"number\" && revalidate > 0) {\n revalidateAt = Date.now() + revalidate * 1000;\n }\n }\n if (\n data &&\n \"revalidate\" in data &&\n typeof data.revalidate === \"number\" &&\n data.revalidate > 0\n ) {\n revalidateAt = Date.now() + data.revalidate * 1000;\n }\n\n // Prepare entry — convert ArrayBuffers to base64 for JSON storage\n const serializable = data ? serializeForJSON(data) : null;\n\n const entry: KVCacheEntry = {\n value: serializable,\n tags,\n lastModified: Date.now(),\n revalidateAt,\n };\n\n // Calculate KV TTL — keep entries well beyond their revalidate window\n // (10x revalidate period, clamped to 60s–30d) so stale-while-revalidate\n // can serve stale content while background regeneration happens.\n let expirationTtl: number | undefined;\n if (revalidateAt !== null) {\n const revalidateSeconds = Math.ceil((revalidateAt - Date.now()) / 1000);\n // Keep in KV for 10x the revalidation period, up to 30 days\n expirationTtl = Math.min(revalidateSeconds * 10, 30 * 24 * 3600);\n // KV minimum TTL is 60 seconds\n expirationTtl = Math.max(expirationTtl, 60);\n }\n\n await this.kv.put(this.prefix + ENTRY_PREFIX + key, JSON.stringify(entry), {\n expirationTtl,\n });\n }\n\n async revalidateTag(\n tags: string | string[],\n _durations?: { expire?: number },\n ): Promise<void> {\n const tagList = Array.isArray(tags) ? tags : [tags];\n const now = Date.now();\n const validTags = tagList.filter((t) => validateTag(t) !== null);\n // Store invalidation timestamp for each tag\n // Use a long TTL (30 days) so recent invalidations are always found\n await Promise.all(\n validTags.map((tag) =>\n this.kv.put(this.prefix + TAG_PREFIX + tag, String(now), {\n expirationTtl: 30 * 24 * 3600,\n }),\n ),\n );\n }\n\n resetRequestCache(): void {\n // No-op — KV is stateless per request\n }\n}\n\n// ---------------------------------------------------------------------------\n// Validation helpers\n// ---------------------------------------------------------------------------\n\nconst VALID_KINDS = new Set([\n \"FETCH\",\n \"APP_PAGE\",\n \"PAGES\",\n \"APP_ROUTE\",\n \"REDIRECT\",\n \"IMAGE\",\n]);\n\n/**\n * Validate that a parsed JSON value has the expected KVCacheEntry shape.\n * Returns the validated entry or null if the shape is invalid.\n */\nfunction validateCacheEntry(raw: unknown): KVCacheEntry | null {\n if (!raw || typeof raw !== \"object\") return null;\n\n const obj = raw as Record<string, unknown>;\n\n // Required fields\n if (typeof obj.lastModified !== \"number\") return null;\n if (!Array.isArray(obj.tags)) return null;\n if (\n obj.revalidateAt !== null &&\n typeof obj.revalidateAt !== \"number\"\n )\n return null;\n\n // value must be null or a valid cache value object with a known kind\n if (obj.value !== null) {\n if (!obj.value || typeof obj.value !== \"object\") return null;\n const value = obj.value as Record<string, unknown>;\n if (typeof value.kind !== \"string\" || !VALID_KINDS.has(value.kind))\n return null;\n }\n\n return raw as KVCacheEntry;\n}\n\n// ---------------------------------------------------------------------------\n// ArrayBuffer serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Deep-clone a cache value, converting ArrayBuffer fields to base64 strings\n * so the entire structure can be JSON.stringify'd for KV storage.\n */\nfunction serializeForJSON(value: IncrementalCacheValue): IncrementalCacheValue {\n if (value.kind === \"APP_PAGE\") {\n return {\n ...value,\n rscData: value.rscData\n ? (arrayBufferToBase64(value.rscData) as any)\n : undefined,\n };\n }\n if (value.kind === \"APP_ROUTE\") {\n return {\n ...value,\n body: arrayBufferToBase64(value.body) as any,\n };\n }\n if (value.kind === \"IMAGE\") {\n return {\n ...value,\n buffer: arrayBufferToBase64(value.buffer) as any,\n };\n }\n return value;\n}\n\n/**\n * Restore base64 strings back to ArrayBuffers after JSON.parse.\n * Returns false if any base64 decode fails (corrupted entry).\n */\nfunction restoreArrayBuffers(value: IncrementalCacheValue): boolean {\n if (value.kind === \"APP_PAGE\" && typeof value.rscData === \"string\") {\n const decoded = safeBase64ToArrayBuffer(value.rscData as any);\n if (!decoded) return false;\n (value as any).rscData = decoded;\n }\n if (value.kind === \"APP_ROUTE\" && typeof value.body === \"string\") {\n const decoded = safeBase64ToArrayBuffer(value.body as any);\n if (!decoded) return false;\n (value as any).body = decoded;\n }\n if (value.kind === \"IMAGE\" && typeof value.buffer === \"string\") {\n const decoded = safeBase64ToArrayBuffer(value.buffer as any);\n if (!decoded) return false;\n (value as any).buffer = decoded;\n }\n return true;\n}\n\nfunction arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Safely decode base64 to ArrayBuffer. Returns null on invalid input\n * instead of throwing a DOMException.\n */\nfunction safeBase64ToArrayBuffer(base64: string): ArrayBuffer | null {\n try {\n return base64ToArrayBuffer(base64);\n } catch {\n console.error(\"[vinext] Invalid base64 in cache entry\");\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TPR: Traffic-aware Pre-Rendering
|
|
3
|
+
*
|
|
4
|
+
* Uses Cloudflare zone analytics to determine which pages actually get
|
|
5
|
+
* traffic, and pre-renders only those during deploy. The pre-rendered
|
|
6
|
+
* HTML is uploaded to KV in the same format ISR uses at runtime — no
|
|
7
|
+
* runtime changes needed.
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Parse wrangler config to find custom domain and KV namespace
|
|
11
|
+
* 2. Resolve the Cloudflare zone for the custom domain
|
|
12
|
+
* 3. Query zone analytics (GraphQL) for top pages by request count
|
|
13
|
+
* 4. Walk ranked list until coverage threshold is met
|
|
14
|
+
* 5. Start the built production server locally
|
|
15
|
+
* 6. Fetch each hot route to produce HTML
|
|
16
|
+
* 7. Upload pre-rendered HTML to KV (same KVCacheEntry format ISR reads)
|
|
17
|
+
*
|
|
18
|
+
* TPR is an experimental feature enabled via --experimental-tpr. It
|
|
19
|
+
* gracefully skips when no custom domain, no API token, no traffic data,
|
|
20
|
+
* or no KV namespace is configured.
|
|
21
|
+
*/
|
|
22
|
+
export interface TPROptions {
|
|
23
|
+
/** Project root directory. */
|
|
24
|
+
root: string;
|
|
25
|
+
/** Traffic coverage percentage (0–100). Default: 90. */
|
|
26
|
+
coverage: number;
|
|
27
|
+
/** Hard cap on number of pages to pre-render. Default: 1000. */
|
|
28
|
+
limit: number;
|
|
29
|
+
/** Analytics lookback window in hours. Default: 24. */
|
|
30
|
+
window: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TPRResult {
|
|
33
|
+
/** Total unique page paths found in analytics. */
|
|
34
|
+
totalPaths: number;
|
|
35
|
+
/** Number of pages successfully pre-rendered and uploaded. */
|
|
36
|
+
prerenderedCount: number;
|
|
37
|
+
/** Actual traffic coverage achieved (percentage). */
|
|
38
|
+
coverageAchieved: number;
|
|
39
|
+
/** Wall-clock duration of the TPR step in milliseconds. */
|
|
40
|
+
durationMs: number;
|
|
41
|
+
/** If TPR was skipped, the reason. */
|
|
42
|
+
skipped?: string;
|
|
43
|
+
}
|
|
44
|
+
interface TrafficEntry {
|
|
45
|
+
path: string;
|
|
46
|
+
requests: number;
|
|
47
|
+
}
|
|
48
|
+
interface SelectedRoutes {
|
|
49
|
+
routes: TrafficEntry[];
|
|
50
|
+
totalRequests: number;
|
|
51
|
+
coveredRequests: number;
|
|
52
|
+
coveragePercent: number;
|
|
53
|
+
}
|
|
54
|
+
interface WranglerConfig {
|
|
55
|
+
accountId?: string;
|
|
56
|
+
kvNamespaceId?: string;
|
|
57
|
+
customDomain?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:
|
|
61
|
+
* account_id, VINEXT_CACHE KV namespace ID, and custom domain.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseWranglerConfig(root: string): WranglerConfig | null;
|
|
64
|
+
/**
|
|
65
|
+
* Walk the ranked traffic list, accumulating request counts until the
|
|
66
|
+
* coverage target is met or the hard cap is reached.
|
|
67
|
+
*/
|
|
68
|
+
export declare function selectRoutes(traffic: TrafficEntry[], coverageTarget: number, limit: number): SelectedRoutes;
|
|
69
|
+
/**
|
|
70
|
+
* Run the TPR pipeline: query traffic, select routes, pre-render, upload.
|
|
71
|
+
*
|
|
72
|
+
* Designed to be called between the build step and wrangler deploy in
|
|
73
|
+
* the `vinext deploy` pipeline. Gracefully skips (never errors) when
|
|
74
|
+
* the prerequisites aren't met.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runTPR(options: TPROptions): Promise<TPRResult>;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=tpr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tpr.d.ts","sourceRoot":"","sources":["../../src/cloudflare/tpr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,MAAM,WAAW,UAAU;IACzB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,gBAAgB,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAQD,UAAU,cAAc;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBvE;AAkXD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EAAE,EACvB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,GACZ,cAAc,CAuBhB;AAyPD;;;;;;GAMG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAmIpE"}
|