@rsc-kit/core 0.30.39 → 0.30.42
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/dist/cache.d.ts +79 -2
- package/dist/cache.js +151 -5
- package/dist/cache.js.map +1 -1
- package/dist/changed.js +2 -1
- package/dist/changed.js.map +1 -1
- package/dist/conformance.d.ts +1 -0
- package/dist/conformance.js +27 -0
- package/dist/conformance.js.map +1 -1
- package/dist/host.d.ts +28 -17
- package/dist/host.js +109 -31
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +27 -11
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/createViteRscApp.js +3 -4
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +3 -1
- package/dist/js/errors.js +5 -2
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.d.ts +16 -1
- package/dist/js/navigate.js +42 -11
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/payloadKey.d.ts +32 -0
- package/dist/js/payloadKey.js +61 -0
- package/dist/js/payloadKey.js.map +1 -0
- package/dist/js/updateStore.js +6 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js.map +1 -1
- package/dist/pageCache.d.ts +47 -0
- package/dist/pageCache.js +177 -0
- package/dist/pageCache.js.map +1 -0
- package/dist/prerender.js +4 -0
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +36 -0
- package/dist/request.js +80 -0
- package/dist/request.js.map +1 -1
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +119 -14
- package/dist/vite.js.map +1 -1
- package/package.json +3 -3
package/dist/js/updateStore.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* across a deploy is running javascript whose remaining chunks are gone. It
|
|
12
12
|
* works until it navigates somewhere that needs one.
|
|
13
13
|
*/
|
|
14
|
+
import { keyedPayloadUrl } from './payloadKey.js';
|
|
14
15
|
let updated = false;
|
|
15
16
|
const listeners = new Set();
|
|
16
17
|
/** A boolean, so React compares snapshots by value and does not loop. */
|
|
@@ -46,9 +47,12 @@ async function ask() {
|
|
|
46
47
|
if (!build)
|
|
47
48
|
return announce();
|
|
48
49
|
try {
|
|
49
|
-
const
|
|
50
|
+
const headers = { 'X-RSC': '1', 'X-RSC-Version': build };
|
|
51
|
+
// Its own url, as every payload request has: a cache that ignores Vary
|
|
52
|
+
// would answer the document's 200 and hide the 409 this asks for.
|
|
53
|
+
const response = await fetch(keyedPayloadUrl(window.location.href, headers), {
|
|
50
54
|
method: 'HEAD',
|
|
51
|
-
headers
|
|
55
|
+
headers,
|
|
52
56
|
cache: 'no-store',
|
|
53
57
|
});
|
|
54
58
|
if (response.status === 409)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updateStore.js","sourceRoot":"","sources":["../../src/js/updateStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,IAAI,OAAO,GAAG,KAAK,CAAA;AACnB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,yEAAyE;AACzE,SAAS,QAAQ;IACf,IAAI,OAAO;QAAE,OAAM;IAEnB,OAAO,GAAG,IAAI,CAAA;IACd,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,IAAI,UAAU,GAAyB,IAAI,CAAA;AAE3C,KAAK,UAAU,OAAO;IACpB,wEAAwE;IACxE,gEAAgE;IAChE,IAAI,OAAO;QAAE,OAAM;IACnB,IAAI,UAAU;QAAE,OAAO,UAAU,CAAA;IAEjC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;QAC9B,UAAU,GAAG,IAAI,CAAA;IACnB,CAAC,CAAC,CAAA;IAEF,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,GAAG;IAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,4BAA4B,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAA;IAE3F,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,EAAE,CAAA;IAE7B,IAAI,CAAC;QACH,MAAM,
|
|
1
|
+
{"version":3,"file":"updateStore.js","sourceRoot":"","sources":["../../src/js/updateStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,IAAI,OAAO,GAAG,KAAK,CAAA;AACnB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,yEAAyE;AACzE,SAAS,QAAQ;IACf,IAAI,OAAO;QAAE,OAAM;IAEnB,OAAO,GAAG,IAAI,CAAA;IACd,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,IAAI,UAAU,GAAyB,IAAI,CAAA;AAE3C,KAAK,UAAU,OAAO;IACpB,wEAAwE;IACxE,gEAAgE;IAChE,IAAI,OAAO;QAAE,OAAM;IACnB,IAAI,UAAU;QAAE,OAAO,UAAU,CAAA;IAEjC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;QAC9B,UAAU,GAAG,IAAI,CAAA;IACnB,CAAC,CAAC,CAAA;IAEF,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,GAAG;IAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,4BAA4B,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAA;IAE3F,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,EAAE,CAAA;IAE7B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,CAAA;QACxD,uEAAuE;QACvE,kEAAkE;QAClE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE;YAC3E,MAAM,EAAE,MAAM;YACd,OAAO;YACP,KAAK,EAAE,UAAU;SAClB,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;YAAE,QAAQ,EAAE,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,EAAE,CAAA;IACZ,CAAC;AACH,CAAC;AAED,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,eAAe,IAAI,SAAS,EAAE,CAAC;IACrE,SAAS,CAAC,aAAa,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,EAAE;QAC1E,IAAK,KAAK,CAAC,IAAiC,EAAE,IAAI,KAAK,iBAAiB;YAAE,KAAK,OAAO,EAAE,CAAA;IAC1F,CAAC,CAAC,CAAA;IAEF,wEAAwE;IACxE,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,gBAAgB,GAAG,SAAS,CAAC,aAAa,CAAC,UAAU,KAAK,IAAI,CAAA;IAEpE,SAAS,CAAC,aAAa,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChE,IAAI,gBAAgB;YAAE,KAAK,OAAO,EAAE,CAAA;IACtC,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACvB,QAAQ,EAAE,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAoB;IACrD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAEvB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACzC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/**\n * Whether a newer build is live while this page is still running the old one.\n *\n * Free of React for the same reason onlineStore is: the value arrives from a\n * service worker message, which has nothing to do with rendering, and the hook\n * that reads it lives next door.\n *\n * The situation it reports is real rather than theoretical. This package's\n * worker activates immediately rather than waiting for every tab to close, and\n * activating sweeps the previous build's cache — so a page that has been open\n * across a deploy is running javascript whose remaining chunks are gone. It\n * works until it navigates somewhere that needs one.\n */\n\nimport { keyedPayloadUrl } from './payloadKey.js'\n\nlet updated = false\nconst listeners = new Set<() => void>()\n\n/** A boolean, so React compares snapshots by value and does not loop. */\nfunction announce(): void {\n if (updated) return\n\n updated = true\n listeners.forEach((fn) => fn())\n}\n\n/**\n * The worker's news is about the worker: a newer one took over. Whether\n * this PAGE is stale is a different question, and the server answers it -\n * a page loaded from the network while the new worker was still installing\n * is already the new build, and told to reload it reloaded into itself.\n * One request, with the build the document says it is: 409 is stale, 200\n * is current, and no answer at all is treated as stale, the safe reading.\n */\nlet confirming: Promise<void> | null = null\n\nasync function confirm(): Promise<void> {\n // One deploy is two signals - the worker's message and controllerchange\n // - and was two requests. One in flight at a time answers both.\n if (updated) return\n if (confirming) return confirming\n\n confirming = ask().finally(() => {\n confirming = null\n })\n\n return confirming\n}\n\nasync function ask(): Promise<void> {\n const build = document.querySelector('meta[name=\"rsc-kit:build\"]')?.getAttribute('content')\n\n if (!build) return announce()\n\n try {\n const headers = { 'X-RSC': '1', 'X-RSC-Version': build }\n // Its own url, as every payload request has: a cache that ignores Vary\n // would answer the document's 200 and hide the 409 this asks for.\n const response = await fetch(keyedPayloadUrl(window.location.href, headers), {\n method: 'HEAD',\n headers,\n cache: 'no-store',\n })\n\n if (response.status === 409) announce()\n } catch {\n announce()\n }\n}\n\nif (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {\n navigator.serviceWorker.addEventListener('message', (event: MessageEvent) => {\n if ((event.data as { type?: string } | null)?.type === 'rsc-kit:updated') void confirm()\n })\n\n // A worker that took control after this page loaded is the same news by\n // another route — it fires when the page was open across a deploy and the\n // message was posted before this listener existed. Only when a worker was\n // already in control when the page loaded: the first worker a visitor ever\n // gets claims the page too, and that is an install, not an update.\n const controlledAtLoad = navigator.serviceWorker.controller !== null\n\n navigator.serviceWorker.addEventListener('controllerchange', () => {\n if (controlledAtLoad) void confirm()\n })\n}\n\nexport function isUpdated(): boolean {\n return updated\n}\n\n/**\n * The server said so itself - a 409 to this page's build on a request the\n * visitor did not make. No confirmation needed: the answer is the\n * confirmation. The next navigation is a document load.\n */\nexport function markStale(): void {\n announce()\n}\n\nexport function subscribeToUpdates(callback: () => void): () => void {\n listeners.add(callback)\n\n return () => listeners.delete(callback)\n}\n\n/** The server render's answer. A fresh document is by definition not stale. */\nexport function updatedOnServer(): boolean {\n return false\n}\n"]}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface ManifestRoute {
|
|
|
74
74
|
* only the bundle can run it.
|
|
75
75
|
*/
|
|
76
76
|
staticParams: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Seconds a shared cache may keep this page: the smallest
|
|
79
|
+
* `export const revalidate` across its layouts and the page itself, as Next
|
|
80
|
+
* takes it. Absent when none says, or one says 0 - rendered per request, as
|
|
81
|
+
* before. A page with it is not frozen at build; see pageCache.ts.
|
|
82
|
+
*/
|
|
83
|
+
revalidate?: number;
|
|
77
84
|
}
|
|
78
85
|
export interface ManifestIntercept {
|
|
79
86
|
component: string;
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `not-found.tsx` files above this route, outermost first.\n *\n * A `notFound()` from this route's page or guard is answered by the nearest\n * one, rendered inside the layouts above it; the way `error.tsx` is the\n * nearest above a failure. A url no route owns has no route to ask, and is\n * answered by the one at the root. Optional: a manifest from a build before\n * this has none, and the root one answers everything.\n */\n notFounds?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n /**\n * The file that answers, relative to the source directory - `app/api/x/route.ts`,\n * or `app/sitemap.ts` for a route the build synthesised from a metadata file\n * - so a line about the route can say where to look.\n */\n source?: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n /** Host middleware above this route (`export const middleware = [...]`), outermost first. */\n hostMiddleware?: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n /**\n * Whether any page or section declares `refreshOn`, so the server signs\n * names - and must refuse to serve, in production, without a key every\n * instance shares.\n */\n refreshOn?: boolean;\n /**\n * rscKit({ headers }): set on every response, and handed to an edge with\n * every page it serves from its cache. Fixed for the build.\n */\n headers?: Record<string, string>;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `not-found.tsx` files above this route, outermost first.\n *\n * A `notFound()` from this route's page or guard is answered by the nearest\n * one, rendered inside the layouts above it; the way `error.tsx` is the\n * nearest above a failure. A url no route owns has no route to ask, and is\n * answered by the one at the root. Optional: a manifest from a build before\n * this has none, and the root one answers everything.\n */\n notFounds?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n /**\n * Seconds a shared cache may keep this page: the smallest\n * `export const revalidate` across its layouts and the page itself, as Next\n * takes it. Absent when none says, or one says 0 - rendered per request, as\n * before. A page with it is not frozen at build; see pageCache.ts.\n */\n revalidate?: number;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n /**\n * The file that answers, relative to the source directory - `app/api/x/route.ts`,\n * or `app/sitemap.ts` for a route the build synthesised from a metadata file\n * - so a line about the route can say where to look.\n */\n source?: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n /** Host middleware above this route (`export const middleware = [...]`), outermost first. */\n hostMiddleware?: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n /**\n * Whether any page or section declares `refreshOn`, so the server signs\n * names - and must refuse to serve, in production, without a key every\n * instance shares.\n */\n refreshOn?: boolean;\n /**\n * rscKit({ headers }): set on every response, and handed to an edge with\n * every page it serves from its cache. Fixed for the build.\n */\n headers?: Record<string, string>;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** What a request for a `revalidate` page carries from the moment it arrives. */
|
|
2
|
+
export interface PageCaching {
|
|
3
|
+
/** Seconds a shared cache may keep the page. */
|
|
4
|
+
revalidate: number;
|
|
5
|
+
/** The url path, for the log line. */
|
|
6
|
+
path: string;
|
|
7
|
+
/** A check that failed before anything rendered, or after the page said notFound(). */
|
|
8
|
+
refused: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Decide, for a request as it arrived, whether its answer may be judged at all.
|
|
12
|
+
*
|
|
13
|
+
* Null for anything but a GET or HEAD of a page that exports revalidate - the
|
|
14
|
+
* answer is then what it always was. Keyed by the request the app sees, so the
|
|
15
|
+
* entry in front, which renders a 404, finds it again.
|
|
16
|
+
*/
|
|
17
|
+
export declare function cachingFor(arrived: Request, revalidate: number | undefined): PageCaching | null;
|
|
18
|
+
/** What cachingFor() decided for this request, if anything. */
|
|
19
|
+
export declare function cachingOf(request: Request): PageCaching | null;
|
|
20
|
+
/**
|
|
21
|
+
* Note what the render has read so far, for an answer that will be finished
|
|
22
|
+
* somewhere else: the page said notFound(), and the entry in front renders
|
|
23
|
+
* not-found.tsx in a scope of its own, where these reads are gone.
|
|
24
|
+
*/
|
|
25
|
+
export declare function noteReadsSoFar(entry: PageCaching): void;
|
|
26
|
+
/**
|
|
27
|
+
* What a public answer says: kept by shared caches for N seconds, refreshed
|
|
28
|
+
* in the background for a day after that, never by the browser. A 404 for at
|
|
29
|
+
* most a minute, so a scanner walking random urls cannot hold pages out.
|
|
30
|
+
*/
|
|
31
|
+
export declare function publicCacheControl(revalidate: number, status: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Finish the answer to a `revalidate` page: rendered whole, then judged.
|
|
34
|
+
*
|
|
35
|
+
* Called inside the request's scope, after every header the request will get
|
|
36
|
+
* is on the answer - cookies included - so what it judges is what is sent.
|
|
37
|
+
* Reading the body to its end is what waits for the render: a Suspense'd
|
|
38
|
+
* child that reads cookies() is still running when the headers exist.
|
|
39
|
+
*/
|
|
40
|
+
export declare function finishCacheable(request: Request, response: Response, entry: PageCaching,
|
|
41
|
+
/**
|
|
42
|
+
* Whether the answer turned out to be not-found.tsx, asked once the body is
|
|
43
|
+
* read: a page can say notFound() in the middle of its stream, after a 200
|
|
44
|
+
* went out. A payload is a 200 either way - the router reads the page from
|
|
45
|
+
* it - but a not-found one is kept no longer than a 404.
|
|
46
|
+
*/
|
|
47
|
+
saidNotFound?: () => boolean): Promise<Response>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// A page a shared cache may keep: `export const revalidate = N`.
|
|
2
|
+
//
|
|
3
|
+
// The page is rendered whole, judged, then sent. Public only when every check
|
|
4
|
+
// passed for that render; otherwise `private, no-store`, exactly as before,
|
|
5
|
+
// with `Cache-Status: rsc-kit; fwd=bypass; detail=<check>` saying which one
|
|
6
|
+
// failed and a line in the log saying where. Cache-Control is the contract a
|
|
7
|
+
// host caches by; Cache-Status is for counting and for the developer asking
|
|
8
|
+
// why a page was not kept.
|
|
9
|
+
//
|
|
10
|
+
// The checks, in the order they can be made:
|
|
11
|
+
//
|
|
12
|
+
// 1. payload-url - a GET or HEAD, and a payload asked for at its own `?_rsc=<key>` url, and a
|
|
13
|
+
// document never at one: a cache that ignores Vary keys on
|
|
14
|
+
// the url alone, so either the other way round hands one
|
|
15
|
+
// kind of answer to a request for the other
|
|
16
|
+
// 2. the render read nothing that belongs to one visitor: cookies(),
|
|
17
|
+
// headers(), request(), connection() - in the page, a layout, a
|
|
18
|
+
// Suspense'd child, metadata or middleware.ts
|
|
19
|
+
// 3. host-middleware - Laravel route middleware runs against the session
|
|
20
|
+
// 4. set-cookie - nothing set a cookie
|
|
21
|
+
// 5. nonce - no CSP nonce, which a shared copy would give everyone
|
|
22
|
+
// 6. status - 200 or 404; a redirect, a 5xx or a render React reported
|
|
23
|
+
// as failed inside the payload (failed-render) never is
|
|
24
|
+
//
|
|
25
|
+
// Backends render as nobody - see renderAsNobody() - so a session cookie set
|
|
26
|
+
// by the backend on every call does not make every page private.
|
|
27
|
+
import { HEADER, PER_CLIENT } from "./headers.js";
|
|
28
|
+
import { PAYLOAD_PARAM, payloadKey, withoutPayloadKey } from "./js/payloadKey.js";
|
|
29
|
+
import { renderFailed, visitorReads } from "./request.js";
|
|
30
|
+
/** A 404 is kept no longer than this, however long its page may be. */
|
|
31
|
+
const NOT_FOUND_LIMIT = 60;
|
|
32
|
+
/** How long a cache may serve a stale page while one request refreshes it. */
|
|
33
|
+
const STALE_WINDOW = 86_400;
|
|
34
|
+
const caching = new WeakMap();
|
|
35
|
+
/**
|
|
36
|
+
* Decide, for a request as it arrived, whether its answer may be judged at all.
|
|
37
|
+
*
|
|
38
|
+
* Null for anything but a GET or HEAD of a page that exports revalidate - the
|
|
39
|
+
* answer is then what it always was. Keyed by the request the app sees, so the
|
|
40
|
+
* entry in front, which renders a 404, finds it again.
|
|
41
|
+
*/
|
|
42
|
+
export function cachingFor(arrived, revalidate) {
|
|
43
|
+
// A post is never kept, and its answer already says so.
|
|
44
|
+
if (!revalidate || revalidate <= 0 || (arrived.method !== "GET" && arrived.method !== "HEAD"))
|
|
45
|
+
return null;
|
|
46
|
+
const url = new URL(arrived.url);
|
|
47
|
+
const entry = { revalidate, path: url.pathname, refused: urlRefusal(arrived, url) };
|
|
48
|
+
caching.set(withoutPayloadKey(arrived), entry);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
/** What cachingFor() decided for this request, if anything. */
|
|
52
|
+
export function cachingOf(request) {
|
|
53
|
+
return caching.get(withoutPayloadKey(request)) ?? null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check 1. A payload answers at `?_rsc=<key>`, with the key its headers give;
|
|
57
|
+
* a document never answers there. Either way round, a cache keying on the url
|
|
58
|
+
* alone would hand a request one kind of answer for the other.
|
|
59
|
+
*/
|
|
60
|
+
function urlRefusal(arrived, url) {
|
|
61
|
+
const key = url.searchParams.get(PAYLOAD_PARAM);
|
|
62
|
+
const payload = arrived.headers.get(HEADER.rsc) !== null;
|
|
63
|
+
if (payload ? key !== payloadKey(arrived.headers) : key !== null)
|
|
64
|
+
return "payload-url";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Note what the render has read so far, for an answer that will be finished
|
|
69
|
+
* somewhere else: the page said notFound(), and the entry in front renders
|
|
70
|
+
* not-found.tsx in a scope of its own, where these reads are gone.
|
|
71
|
+
*/
|
|
72
|
+
export function noteReadsSoFar(entry) {
|
|
73
|
+
entry.refused ??= readRefusal();
|
|
74
|
+
}
|
|
75
|
+
function readRefusal() {
|
|
76
|
+
if (renderFailed())
|
|
77
|
+
return "failed-render";
|
|
78
|
+
const read = visitorReads()[0];
|
|
79
|
+
return read ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** The detail token a read is reported under: `cookies() in Header` is `cookies`. */
|
|
82
|
+
function detailOf(refused) {
|
|
83
|
+
return refused.replace(/\(.*$/, "").replace(/[^a-z-]/gi, "-");
|
|
84
|
+
}
|
|
85
|
+
const logged = new Set();
|
|
86
|
+
const REASONS = {
|
|
87
|
+
"payload-url": "a payload was asked for without its ?_rsc= key, or a document with one. The router always adds it; this was another client.",
|
|
88
|
+
"host-middleware": "the route has host middleware, which runs against the visitor's session.",
|
|
89
|
+
"set-cookie": "the answer set a cookie.",
|
|
90
|
+
nonce: "the answer carries a CSP nonce, which a shared copy would give every visitor.",
|
|
91
|
+
status: "only a 200 or a 404 is kept.",
|
|
92
|
+
"failed-render": "React reported a failure inside the payload.",
|
|
93
|
+
};
|
|
94
|
+
/** Once per page and reason: a page refused on every request says so once. */
|
|
95
|
+
function logRefusal(entry, refused) {
|
|
96
|
+
const key = `${entry.path}\n${refused}`;
|
|
97
|
+
if (logged.has(key) || logged.size > 500)
|
|
98
|
+
return;
|
|
99
|
+
logged.add(key);
|
|
100
|
+
// A read names what read, and where: `cookies() in Header`.
|
|
101
|
+
const why = REASONS[refused] ?? `${refused} reads this visitor.`;
|
|
102
|
+
console.warn(`[rsc-kit] ${entry.path} exports revalidate, and was sent private: ${why}`);
|
|
103
|
+
}
|
|
104
|
+
function bypass(response, body, entry, refused) {
|
|
105
|
+
const headers = new Headers(response.headers);
|
|
106
|
+
headers.set("Cache-Control", PER_CLIENT);
|
|
107
|
+
headers.set("Cache-Status", `rsc-kit; fwd=bypass; detail=${detailOf(refused)}`);
|
|
108
|
+
headers.delete("ETag");
|
|
109
|
+
logRefusal(entry, refused);
|
|
110
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What a public answer says: kept by shared caches for N seconds, refreshed
|
|
114
|
+
* in the background for a day after that, never by the browser. A 404 for at
|
|
115
|
+
* most a minute, so a scanner walking random urls cannot hold pages out.
|
|
116
|
+
*/
|
|
117
|
+
export function publicCacheControl(revalidate, status) {
|
|
118
|
+
return status === 404
|
|
119
|
+
? `public, max-age=0, s-maxage=${Math.min(revalidate, NOT_FOUND_LIMIT)}, stale-while-revalidate=${NOT_FOUND_LIMIT}`
|
|
120
|
+
: `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}`;
|
|
121
|
+
}
|
|
122
|
+
async function etagOf(body) {
|
|
123
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", body));
|
|
124
|
+
let hex = "";
|
|
125
|
+
for (let i = 0; i < 12; i++)
|
|
126
|
+
hex += digest[i].toString(16).padStart(2, "0");
|
|
127
|
+
return `"${hex}"`;
|
|
128
|
+
}
|
|
129
|
+
function matches(ifNoneMatch, etag) {
|
|
130
|
+
if (!ifNoneMatch)
|
|
131
|
+
return false;
|
|
132
|
+
return ifNoneMatch
|
|
133
|
+
.split(",")
|
|
134
|
+
.map((tag) => tag.trim().replace(/^W\//, ""))
|
|
135
|
+
.some((tag) => tag === etag || tag === "*");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Finish the answer to a `revalidate` page: rendered whole, then judged.
|
|
139
|
+
*
|
|
140
|
+
* Called inside the request's scope, after every header the request will get
|
|
141
|
+
* is on the answer - cookies included - so what it judges is what is sent.
|
|
142
|
+
* Reading the body to its end is what waits for the render: a Suspense'd
|
|
143
|
+
* child that reads cookies() is still running when the headers exist.
|
|
144
|
+
*/
|
|
145
|
+
export async function finishCacheable(request, response, entry,
|
|
146
|
+
/**
|
|
147
|
+
* Whether the answer turned out to be not-found.tsx, asked once the body is
|
|
148
|
+
* read: a page can say notFound() in the middle of its stream, after a 200
|
|
149
|
+
* went out. A payload is a 200 either way - the router reads the page from
|
|
150
|
+
* it - but a not-found one is kept no longer than a 404.
|
|
151
|
+
*/
|
|
152
|
+
saidNotFound = () => false) {
|
|
153
|
+
if (entry.refused)
|
|
154
|
+
return bypass(response, response.body, entry, entry.refused);
|
|
155
|
+
if (response.status !== 200 && response.status !== 404)
|
|
156
|
+
return bypass(response, response.body, entry, "status");
|
|
157
|
+
const type = response.headers.get("Content-Type") ?? "";
|
|
158
|
+
if (!type.startsWith("text/html") && !type.startsWith("text/x-component")) {
|
|
159
|
+
return bypass(response, response.body, entry, "status");
|
|
160
|
+
}
|
|
161
|
+
const body = await response.arrayBuffer();
|
|
162
|
+
const refused = readRefusal() ??
|
|
163
|
+
(response.headers.getSetCookie().length > 0 ? "set-cookie" : null) ??
|
|
164
|
+
(/'nonce-/.test(response.headers.get("Content-Security-Policy") ?? "") ? "nonce" : null);
|
|
165
|
+
if (refused)
|
|
166
|
+
return bypass(response, body, entry, refused);
|
|
167
|
+
const headers = new Headers(response.headers);
|
|
168
|
+
const etag = await etagOf(body);
|
|
169
|
+
headers.set("Cache-Control", publicCacheControl(entry.revalidate, saidNotFound() ? 404 : response.status));
|
|
170
|
+
headers.set("ETag", etag);
|
|
171
|
+
if (response.status === 200 && matches(request.headers.get("If-None-Match"), etag)) {
|
|
172
|
+
headers.delete("Content-Length");
|
|
173
|
+
return new Response(null, { status: 304, headers });
|
|
174
|
+
}
|
|
175
|
+
return new Response(request.method === "HEAD" ? null : body, { status: response.status, statusText: response.statusText, headers });
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=pageCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageCache.js","sourceRoot":"","sources":["../src/pageCache.ts"],"names":[],"mappings":"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","sourcesContent":["// A page a shared cache may keep: `export const revalidate = N`.\n//\n// The page is rendered whole, judged, then sent. Public only when every check\n// passed for that render; otherwise `private, no-store`, exactly as before,\n// with `Cache-Status: rsc-kit; fwd=bypass; detail=<check>` saying which one\n// failed and a line in the log saying where. Cache-Control is the contract a\n// host caches by; Cache-Status is for counting and for the developer asking\n// why a page was not kept.\n//\n// The checks, in the order they can be made:\n//\n// 1. payload-url - a GET or HEAD, and a payload asked for at its own `?_rsc=<key>` url, and a\n// document never at one: a cache that ignores Vary keys on\n// the url alone, so either the other way round hands one\n// kind of answer to a request for the other\n// 2. the render read nothing that belongs to one visitor: cookies(),\n// headers(), request(), connection() - in the page, a layout, a\n// Suspense'd child, metadata or middleware.ts\n// 3. host-middleware - Laravel route middleware runs against the session\n// 4. set-cookie - nothing set a cookie\n// 5. nonce - no CSP nonce, which a shared copy would give everyone\n// 6. status - 200 or 404; a redirect, a 5xx or a render React reported\n// as failed inside the payload (failed-render) never is\n//\n// Backends render as nobody - see renderAsNobody() - so a session cookie set\n// by the backend on every call does not make every page private.\n\nimport { HEADER, PER_CLIENT } from \"./headers.js\";\nimport { PAYLOAD_PARAM, payloadKey, withoutPayloadKey } from \"./js/payloadKey.js\";\nimport { renderFailed, visitorReads } from \"./request.js\";\n\n/** What a request for a `revalidate` page carries from the moment it arrives. */\nexport interface PageCaching {\n /** Seconds a shared cache may keep the page. */\n revalidate: number;\n /** The url path, for the log line. */\n path: string;\n /** A check that failed before anything rendered, or after the page said notFound(). */\n refused: string | null;\n}\n\n/** A 404 is kept no longer than this, however long its page may be. */\nconst NOT_FOUND_LIMIT = 60;\n\n/** How long a cache may serve a stale page while one request refreshes it. */\nconst STALE_WINDOW = 86_400;\n\nconst caching = new WeakMap<Request, PageCaching>();\n\n/**\n * Decide, for a request as it arrived, whether its answer may be judged at all.\n *\n * Null for anything but a GET or HEAD of a page that exports revalidate - the\n * answer is then what it always was. Keyed by the request the app sees, so the\n * entry in front, which renders a 404, finds it again.\n */\nexport function cachingFor(arrived: Request, revalidate: number | undefined): PageCaching | null {\n // A post is never kept, and its answer already says so.\n if (!revalidate || revalidate <= 0 || (arrived.method !== \"GET\" && arrived.method !== \"HEAD\")) return null;\n\n const url = new URL(arrived.url);\n const entry: PageCaching = { revalidate, path: url.pathname, refused: urlRefusal(arrived, url) };\n\n caching.set(withoutPayloadKey(arrived), entry);\n\n return entry;\n}\n\n/** What cachingFor() decided for this request, if anything. */\nexport function cachingOf(request: Request): PageCaching | null {\n return caching.get(withoutPayloadKey(request)) ?? null;\n}\n\n/**\n * Check 1. A payload answers at `?_rsc=<key>`, with the key its headers give;\n * a document never answers there. Either way round, a cache keying on the url\n * alone would hand a request one kind of answer for the other.\n */\nfunction urlRefusal(arrived: Request, url: URL): string | null {\n const key = url.searchParams.get(PAYLOAD_PARAM);\n const payload = arrived.headers.get(HEADER.rsc) !== null;\n\n if (payload ? key !== payloadKey(arrived.headers) : key !== null) return \"payload-url\";\n\n return null;\n}\n\n/**\n * Note what the render has read so far, for an answer that will be finished\n * somewhere else: the page said notFound(), and the entry in front renders\n * not-found.tsx in a scope of its own, where these reads are gone.\n */\nexport function noteReadsSoFar(entry: PageCaching): void {\n entry.refused ??= readRefusal();\n}\n\nfunction readRefusal(): string | null {\n if (renderFailed()) return \"failed-render\";\n\n const read = visitorReads()[0];\n\n return read ?? null;\n}\n\n/** The detail token a read is reported under: `cookies() in Header` is `cookies`. */\nfunction detailOf(refused: string): string {\n return refused.replace(/\\(.*$/, \"\").replace(/[^a-z-]/gi, \"-\");\n}\n\nconst logged = new Set<string>();\n\nconst REASONS: Record<string, string> = {\n \"payload-url\":\n \"a payload was asked for without its ?_rsc= key, or a document with one. The router always adds it; this was another client.\",\n \"host-middleware\": \"the route has host middleware, which runs against the visitor's session.\",\n \"set-cookie\": \"the answer set a cookie.\",\n nonce: \"the answer carries a CSP nonce, which a shared copy would give every visitor.\",\n status: \"only a 200 or a 404 is kept.\",\n \"failed-render\": \"React reported a failure inside the payload.\",\n};\n\n/** Once per page and reason: a page refused on every request says so once. */\nfunction logRefusal(entry: PageCaching, refused: string): void {\n const key = `${entry.path}\\n${refused}`;\n\n if (logged.has(key) || logged.size > 500) return;\n\n logged.add(key);\n\n // A read names what read, and where: `cookies() in Header`.\n const why = REASONS[refused] ?? `${refused} reads this visitor.`;\n\n console.warn(`[rsc-kit] ${entry.path} exports revalidate, and was sent private: ${why}`);\n}\n\nfunction bypass(response: Response, body: BodyInit | null, entry: PageCaching, refused: string): Response {\n const headers = new Headers(response.headers);\n\n headers.set(\"Cache-Control\", PER_CLIENT);\n headers.set(\"Cache-Status\", `rsc-kit; fwd=bypass; detail=${detailOf(refused)}`);\n headers.delete(\"ETag\");\n logRefusal(entry, refused);\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers });\n}\n\n/**\n * What a public answer says: kept by shared caches for N seconds, refreshed\n * in the background for a day after that, never by the browser. A 404 for at\n * most a minute, so a scanner walking random urls cannot hold pages out.\n */\nexport function publicCacheControl(revalidate: number, status: number): string {\n return status === 404\n ? `public, max-age=0, s-maxage=${Math.min(revalidate, NOT_FOUND_LIMIT)}, stale-while-revalidate=${NOT_FOUND_LIMIT}`\n : `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${STALE_WINDOW}`;\n}\n\nasync function etagOf(body: ArrayBuffer): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", body));\n let hex = \"\";\n\n for (let i = 0; i < 12; i++) hex += digest[i].toString(16).padStart(2, \"0\");\n\n return `\"${hex}\"`;\n}\n\nfunction matches(ifNoneMatch: string | null, etag: string): boolean {\n if (!ifNoneMatch) return false;\n\n return ifNoneMatch\n .split(\",\")\n .map((tag) => tag.trim().replace(/^W\\//, \"\"))\n .some((tag) => tag === etag || tag === \"*\");\n}\n\n/**\n * Finish the answer to a `revalidate` page: rendered whole, then judged.\n *\n * Called inside the request's scope, after every header the request will get\n * is on the answer - cookies included - so what it judges is what is sent.\n * Reading the body to its end is what waits for the render: a Suspense'd\n * child that reads cookies() is still running when the headers exist.\n */\nexport async function finishCacheable(\n request: Request,\n response: Response,\n entry: PageCaching,\n /**\n * Whether the answer turned out to be not-found.tsx, asked once the body is\n * read: a page can say notFound() in the middle of its stream, after a 200\n * went out. A payload is a 200 either way - the router reads the page from\n * it - but a not-found one is kept no longer than a 404.\n */\n saidNotFound: () => boolean = () => false,\n): Promise<Response> {\n if (entry.refused) return bypass(response, response.body, entry, entry.refused);\n\n if (response.status !== 200 && response.status !== 404) return bypass(response, response.body, entry, \"status\");\n\n const type = response.headers.get(\"Content-Type\") ?? \"\";\n\n if (!type.startsWith(\"text/html\") && !type.startsWith(\"text/x-component\")) {\n return bypass(response, response.body, entry, \"status\");\n }\n\n const body = await response.arrayBuffer();\n const refused =\n readRefusal() ??\n (response.headers.getSetCookie().length > 0 ? \"set-cookie\" : null) ??\n (/'nonce-/.test(response.headers.get(\"Content-Security-Policy\") ?? \"\") ? \"nonce\" : null);\n\n if (refused) return bypass(response, body, entry, refused);\n\n const headers = new Headers(response.headers);\n const etag = await etagOf(body);\n\n headers.set(\"Cache-Control\", publicCacheControl(entry.revalidate, saidNotFound() ? 404 : response.status));\n headers.set(\"ETag\", etag);\n\n if (response.status === 200 && matches(request.headers.get(\"If-None-Match\"), etag)) {\n headers.delete(\"Content-Length\");\n\n return new Response(null, { status: 304, headers });\n }\n\n return new Response(request.method === \"HEAD\" ? null : body, { status: response.status, statusText: response.statusText, headers });\n}\n"]}
|
package/dist/prerender.js
CHANGED
|
@@ -351,6 +351,10 @@ function placeholders(route) {
|
|
|
351
351
|
export async function urlsToBuild(manifest, engine) {
|
|
352
352
|
const entries = [];
|
|
353
353
|
for (const route of manifest.routes) {
|
|
354
|
+
// Kept by a shared cache and refreshed every N seconds instead: frozen
|
|
355
|
+
// here, it would be the same until the next deploy, whatever it says.
|
|
356
|
+
if ((route.revalidate ?? 0) > 0)
|
|
357
|
+
continue;
|
|
354
358
|
const parameterised = route.segments.some((s) => s.type !== "static");
|
|
355
359
|
if (!parameterised) {
|
|
356
360
|
entries.push({ route, params: {}, url: urlFor(route, {}) });
|