@rsc-kit/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action.d.ts +21 -0
- package/dist/action.js +67 -36
- package/dist/action.js.map +1 -1
- package/dist/apiPrerender.d.ts +25 -0
- package/dist/apiPrerender.js +195 -0
- package/dist/apiPrerender.js.map +1 -0
- package/dist/host.d.ts +12 -0
- package/dist/host.js +140 -2
- package/dist/host.js.map +1 -1
- package/dist/js/RouteErrorBoundary.d.ts +36 -0
- package/dist/js/RouteErrorBoundary.js +43 -0
- package/dist/js/RouteErrorBoundary.js.map +1 -0
- package/dist/js/queryClient.js +21 -1
- package/dist/js/queryClient.js.map +1 -1
- package/dist/manifest.d.ts +33 -0
- package/dist/manifest.js.map +1 -1
- package/dist/notFound.d.ts +24 -0
- package/dist/notFound.js +107 -0
- package/dist/notFound.js.map +1 -0
- package/dist/prerender.d.ts +48 -4
- package/dist/prerender.js +79 -9
- package/dist/prerender.js.map +1 -1
- package/dist/query.d.ts +23 -0
- package/dist/query.js +45 -3
- package/dist/query.js.map +1 -1
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +11 -1
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +7 -0
- package/dist/request.js +31 -6
- package/dist/request.js.map +1 -1
- package/dist/routeSchema.d.ts +115 -0
- package/dist/routeSchema.js +182 -0
- package/dist/routeSchema.js.map +1 -0
- package/dist/routing.d.ts +20 -1
- package/dist/routing.js +30 -7
- package/dist/routing.js.map +1 -1
- package/dist/vite.js +560 -27
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryClient.js","sourceRoot":"","sources":["../../src/js/queryClient.ts"],"names":[],"mappings":"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","sourcesContent":["// The browser half of `query()`: send a read as a GET.\n//\n// That is the whole of it. There is no cache here, no batching and no\n// deduplication, because TanStack Query and SWR already do those and do them\n// better — this owns the transport and they own everything above it.\n//\n// The awkward fact it is built around: a server function's id is not readable\n// from the client. React keeps it in a module-private WeakMap and exposes no\n// getter, so there is no `ref.$$id` to build a url from. What the client *can*\n// do is call the reference and be handed the id by React itself — the stub's\n// whole body is `callServer(id, args)`, and this package already owns\n// `callServer`.\n//\n// So a read opens a one-shot slot and calls the reference; the transport claims\n// the slot and sends a GET instead of posting. The slot is also what says this\n// is a read at all: calling `getListings(kind)` directly is still a POST, and\n// `fetchQuery(getListings, [kind])` is the same call as a GET. Nothing has to\n// know a list of query ids, on either side of the build.\n\n/** Where reads are answered. Must match HEADER.queryPath on the server. */\nconst QUERY_PATH = '/_rsc/query'\n\n/**\n * Sent on every read, and required by the endpoint.\n *\n * Not decoration. A GET with no unusual header is a *simple* request, so any\n * page anywhere can trigger one with `<img src=\"…/_rsc/query?…\">` and it goes\n * out with the visitor's cookies — CORS stops the attacker reading the answer,\n * but the read still runs. This header is not CORS-safelisted, so the browser\n * preflights it, and nothing here answers a preflight.\n *\n * That restores exactly the protection a POST had: a `POST` carrying\n * `X-RSC-Action` is non-simple for the same reason.\n */\nconst QUERY_HEADER = 'X-RSC-Query'\n\n/**\n * How long a url may get before the read goes as a POST instead.\n *\n * Conservative. Proxies and CDNs start refusing somewhere between 8k and 16k,\n * and the failure is a 414 from a machine that is not ours.\n */\nconst MAX_URL = 6_000\n\ntype Reader = (...args: unknown[]) => Promise<unknown>\n\n/**\n * The slot a read opens before calling the reference.\n *\n * One-shot and synchronous: React's stub calls `callServer` in its own body\n * with nothing awaited in between, so exactly one transport call can claim it.\n */\nlet slot: { claimed: boolean; promise: Promise<unknown> | null } | null = null\n\n/**\n * Claim the open slot, if a read opened one.\n *\n * Called from the app's `callServer` before it posts. Returns null when this is\n * an ordinary action, which is every call that did not come through\n * `fetchQuery`.\n */\nexport function claimRead(id: string, args: unknown[]): Promise<unknown> | null {\n if (!slot || slot.claimed) return null\n\n slot.claimed = true\n slot.promise = send(id, args)\n\n return slot.promise\n}\n\n/**\n * Read a query, over GET.\n *\n * Hand this to a cache library and let it decide everything else:\n *\n * queryFn: () => fetchQuery(getListings, [kind])\n * useSWR(['listings', kind], () => fetchQuery(getListings, [kind]))\n *\n * Every call goes to the server, which is what a fetcher needs — staleness,\n * revalidation, retries, polling and deduplication all belong to the library\n * holding the answer, not to the thing that fetches it.\n */\nexport function fetchQuery<Data>(\n reference: (...args: never[]) => Promise<Data>,\n args: unknown[] = [],\n): Promise<Data> {\n if (typeof window === 'undefined') {\n // React's SSR runtime refuses a server-function call during the initial\n // render, and reaching a query's id means calling its reference — so this\n // cannot work here. Raised with the fix in it rather than left to surface\n // as React's more general message about fetch waterfalls.\n //\n // A cache library runs its fetcher in an effect, so the server render never\n // reaches this; a server component that wants the data during render should\n // await the query directly, which needs none of this.\n throw new Error(\n 'A query was read during server rendering. Read it in a server component with await, or through a cache library, whose fetcher runs after hydration.',\n )\n }\n\n const opened = { claimed: false, promise: null as Promise<unknown> | null }\n\n slot = opened\n\n try {\n // React's stub reaches callServer synchronously, so the slot is claimed by\n // the time this returns. The stub's own promise is discarded: the one the\n // transport made is the one that settles with the answer.\n ;(reference as unknown as Reader)(...(args as unknown[]))\n } finally {\n // Cleared here and nowhere else, so a reference that throws on the way in\n // does not leave the slot open for the next read to claim by accident.\n // Nothing is returned from this block: a `return` in `finally` discards\n // whatever the `try` was throwing, which would turn a reference that threw\n // synchronously into the misleading bound-reference error below.\n slot = null\n }\n\n if (!opened.claimed || !opened.promise) {\n // The only way here is a reference whose call path is asynchronous, which\n // today means one that was `.bind()`-ed. Loud, because the quiet version is\n // a read that silently went out as a POST.\n throw new Error(\n 'A query was called through a bound reference. Pass the exported query itself, unbound.',\n )\n }\n\n return opened.promise as Promise<Data>\n}\n\nasync function send(id: string, args: unknown[]): Promise<unknown> {\n const encoded = await encode(args)\n\n // encodeReply answers with FormData the moment an argument holds a File, and\n // a url cannot carry one. Rather than refuse a call that would have worked as\n // an action, the read falls back to a POST: it stops being cacheable, which\n // is the only thing it loses.\n if (typeof encoded !== 'string') return await post(id, args, 'an argument contained a File')\n\n const url = `${QUERY_PATH}?id=${encodeURIComponent(id)}&args=${encodeURIComponent(encoded)}`\n\n if (url.length > MAX_URL) return await post(id, args, 'the arguments are too large for a url')\n\n const res = await fetch(url, {\n headers: {\n [QUERY_HEADER]: '1',\n Accept: 'text/x-component',\n // Where the read came from. Same reason an action sends it: a host that\n // guards by route needs to know which page is asking.\n 'X-RSC-Referer': window.location.pathname + window.location.search,\n },\n })\n\n if (!res.ok || !res.body) {\n throw new Error((await res.text().catch(() => '')) || `Query failed: ${res.status}`)\n }\n\n return await deserialize(res.body)\n}\n\n/**\n * The same read, as an action.\n *\n * Reached only when the arguments cannot ride in a url. Warned about in\n * development rather than silently tolerated, because the read still works and\n * the thing that changed — it is no longer a GET, so nothing can cache it — is\n * otherwise invisible.\n */\nasync function post(id: string, args: unknown[], why: string): Promise<unknown> {\n if (import.meta.env?.DEV) {\n console.warn(`[rsc-kit] A query was sent as a POST because ${why}. It will not be cached.`)\n }\n\n return await asAction(id, args)\n}\n\n/**\n * The Flight codec and the action transport, installed by the app bootstrap.\n *\n * Injected rather than imported, and not for testing: this module is reached\n * from client components, so importing the browser runtime here would pull a\n * second copy of it into that graph — two client-reference registries, where\n * components resolve to undefined with nothing logged. The bootstrap already\n * holds the one true copy.\n */\nlet deserialize: (stream: ReadableStream) => Promise<unknown> = () => {\n throw new Error('No Flight decoder installed. createViteRscApp() sets one up.')\n}\n\nlet encode: (args: unknown[]) => Promise<string | FormData> = () => {\n throw new Error('No Flight encoder installed. createViteRscApp() sets one up.')\n}\n\nlet asAction: (id: string, args: unknown[]) => Promise<unknown> = () => {\n throw new Error('No action transport installed. createViteRscApp() sets one up.')\n}\n\nexport function setQueryCodec(codec: {\n deserialize: (stream: ReadableStream) => Promise<unknown>\n encode: (args: unknown[]) => Promise<string | FormData>\n asAction: (id: string, args: unknown[]) => Promise<unknown>\n}): void {\n deserialize = codec.deserialize\n encode = codec.encode\n asAction = codec.asAction\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryClient.js","sourceRoot":"","sources":["../../src/js/queryClient.ts"],"names":[],"mappings":"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","sourcesContent":["// The browser half of `query()`: send a read as a GET.\n//\n// That is the whole of it. There is no cache here, no batching and no\n// deduplication, because TanStack Query and SWR already do those and do them\n// better — this owns the transport and they own everything above it.\n//\n// The awkward fact it is built around: a server function's id is not readable\n// from the client. React keeps it in a module-private WeakMap and exposes no\n// getter, so there is no `ref.$$id` to build a url from. What the client *can*\n// do is call the reference and be handed the id by React itself — the stub's\n// whole body is `callServer(id, args)`, and this package already owns\n// `callServer`.\n//\n// So a read opens a one-shot slot and calls the reference; the transport claims\n// the slot and sends a GET instead of posting. The slot is also what says this\n// is a read at all: calling `getListings(kind)` directly is still a POST, and\n// `fetchQuery(getListings, [kind])` is the same call as a GET. Nothing has to\n// know a list of query ids, on either side of the build.\n\n/** Where reads are answered. Must match HEADER.queryPath on the server. */\nconst QUERY_PATH = '/_rsc/query'\n\n/**\n * Sent on every read, and required by the endpoint.\n *\n * Not decoration. A GET with no unusual header is a *simple* request, so any\n * page anywhere can trigger one with `<img src=\"…/_rsc/query?…\">` and it goes\n * out with the visitor's cookies — CORS stops the attacker reading the answer,\n * but the read still runs. This header is not CORS-safelisted, so the browser\n * preflights it, and nothing here answers a preflight.\n *\n * That restores exactly the protection a POST had: a `POST` carrying\n * `X-RSC-Action` is non-simple for the same reason.\n */\nconst QUERY_HEADER = 'X-RSC-Query'\n\n/**\n * How long a url may get before the read goes as a POST instead.\n *\n * Conservative. Proxies and CDNs start refusing somewhere between 8k and 16k,\n * and the failure is a 414 from a machine that is not ours.\n */\nconst MAX_URL = 6_000\n\ntype Reader = (...args: unknown[]) => Promise<unknown>\n\n/**\n * The slot a read opens before calling the reference.\n *\n * One-shot and synchronous: React's stub calls `callServer` in its own body\n * with nothing awaited in between, so exactly one transport call can claim it.\n */\nlet slot: { claimed: boolean; promise: Promise<unknown> | null } | null = null\n\n/**\n * Claim the open slot, if a read opened one.\n *\n * Called from the app's `callServer` before it posts. Returns null when this is\n * an ordinary action, which is every call that did not come through\n * `fetchQuery`.\n */\nexport function claimRead(id: string, args: unknown[]): Promise<unknown> | null {\n if (!slot || slot.claimed) return null\n\n slot.claimed = true\n slot.promise = send(id, args)\n\n return slot.promise\n}\n\n/**\n * Read a query, over GET.\n *\n * Hand this to a cache library and let it decide everything else:\n *\n * queryFn: () => fetchQuery(getListings, [kind])\n * useSWR(['listings', kind], () => fetchQuery(getListings, [kind]))\n *\n * Every call goes to the server, which is what a fetcher needs — staleness,\n * revalidation, retries, polling and deduplication all belong to the library\n * holding the answer, not to the thing that fetches it.\n */\nexport function fetchQuery<Data>(\n reference: (...args: never[]) => Promise<Data>,\n args: unknown[] = [],\n): Promise<Data> {\n if (typeof window === 'undefined') {\n // React's SSR runtime refuses a server-function call during the initial\n // render, and reaching a query's id means calling its reference — so this\n // cannot work here. Raised with the fix in it rather than left to surface\n // as React's more general message about fetch waterfalls.\n //\n // A cache library runs its fetcher in an effect, so the server render never\n // reaches this; a server component that wants the data during render should\n // await the query directly, which needs none of this.\n throw new Error(\n 'A query was read during server rendering. Read it in a server component with await, or through a cache library, whose fetcher runs after hydration.',\n )\n }\n\n const opened = { claimed: false, promise: null as Promise<unknown> | null }\n\n slot = opened\n\n try {\n // React's stub reaches callServer synchronously, so the slot is claimed by\n // the time this returns. The stub's own promise is discarded: the one the\n // transport made is the one that settles with the answer.\n ;(reference as unknown as Reader)(...(args as unknown[]))\n } finally {\n // Cleared here and nowhere else, so a reference that throws on the way in\n // does not leave the slot open for the next read to claim by accident.\n // Nothing is returned from this block: a `return` in `finally` discards\n // whatever the `try` was throwing, which would turn a reference that threw\n // synchronously into the misleading bound-reference error below.\n slot = null\n }\n\n if (!opened.claimed || !opened.promise) {\n // The only way here is a reference whose call path is asynchronous, which\n // today means one that was `.bind()`-ed. Loud, because the quiet version is\n // a read that silently went out as a POST.\n throw new Error(\n 'A query was called through a bound reference. Pass the exported query itself, unbound.',\n )\n }\n\n return opened.promise as Promise<Data>\n}\n\nasync function send(id: string, args: unknown[]): Promise<unknown> {\n const encoded = await encode(args)\n\n // encodeReply answers with FormData the moment an argument holds a File, and\n // a url cannot carry one. Rather than refuse a call that would have worked as\n // an action, the read falls back to a POST: it stops being cacheable, which\n // is the only thing it loses.\n if (typeof encoded !== 'string') return await post(id, args, 'an argument contained a File')\n\n const url = `${QUERY_PATH}?id=${encodeURIComponent(id)}&args=${encodeURIComponent(encoded)}`\n\n if (url.length > MAX_URL) return await post(id, args, 'the arguments are too large for a url')\n\n const res = await fetch(url, {\n headers: {\n [QUERY_HEADER]: '1',\n Accept: 'text/x-component',\n // Where the read came from. Same reason an action sends it: a host that\n // guards by route needs to know which page is asking.\n 'X-RSC-Referer': window.location.pathname + window.location.search,\n },\n })\n\n if (!res.ok || !res.body) {\n throw await failureFrom(res)\n }\n\n return await deserialize(res.body)\n}\n\n/**\n * What a refused read rejects with.\n *\n * A cache library reports failure by rejection, so this has to be an Error —\n * and one carrying the message the server meant, rather than a status code. A\n * validation refusal keeps its fields on `errors`, where a form can read them.\n */\nasync function failureFrom(res: Response): Promise<Error> {\n const body = await res.text().catch(() => '')\n\n try {\n const parsed = JSON.parse(body) as { message?: string; errors?: Record<string, string[]> }\n const error = new Error(parsed.message || `Query failed: ${res.status}`)\n\n if (parsed.errors) (error as Error & { errors?: unknown }).errors = parsed.errors\n\n return error\n } catch {\n return new Error(body || `Query failed: ${res.status}`)\n }\n}\n\n/**\n * The same read, as an action.\n *\n * Reached only when the arguments cannot ride in a url. Warned about in\n * development rather than silently tolerated, because the read still works and\n * the thing that changed — it is no longer a GET, so nothing can cache it — is\n * otherwise invisible.\n */\nasync function post(id: string, args: unknown[], why: string): Promise<unknown> {\n if (import.meta.env?.DEV) {\n console.warn(`[rsc-kit] A query was sent as a POST because ${why}. It will not be cached.`)\n }\n\n return await asAction(id, args)\n}\n\n/**\n * The Flight codec and the action transport, installed by the app bootstrap.\n *\n * Injected rather than imported, and not for testing: this module is reached\n * from client components, so importing the browser runtime here would pull a\n * second copy of it into that graph — two client-reference registries, where\n * components resolve to undefined with nothing logged. The bootstrap already\n * holds the one true copy.\n */\nlet deserialize: (stream: ReadableStream) => Promise<unknown> = () => {\n throw new Error('No Flight decoder installed. createViteRscApp() sets one up.')\n}\n\nlet encode: (args: unknown[]) => Promise<string | FormData> = () => {\n throw new Error('No Flight encoder installed. createViteRscApp() sets one up.')\n}\n\nlet asAction: (id: string, args: unknown[]) => Promise<unknown> = () => {\n throw new Error('No action transport installed. createViteRscApp() sets one up.')\n}\n\nexport function setQueryCodec(codec: {\n deserialize: (stream: ReadableStream) => Promise<unknown>\n encode: (args: unknown[]) => Promise<string | FormData>\n asAction: (id: string, args: unknown[]) => Promise<unknown>\n}): void {\n deserialize = codec.deserialize\n encode = codec.encode\n asAction = codec.asAction\n}\n"]}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface ManifestRoute {
|
|
|
7
7
|
segments: RouteSegment[];
|
|
8
8
|
layouts: string[];
|
|
9
9
|
loadings: string[];
|
|
10
|
+
/**
|
|
11
|
+
* `error.tsx` files above this route, outermost first.
|
|
12
|
+
*
|
|
13
|
+
* The nearest one to a failure catches it, the same way the nearest
|
|
14
|
+
* `loading.tsx` is the fallback. Optional: a manifest from a build before
|
|
15
|
+
* error boundaries existed has none.
|
|
16
|
+
*/
|
|
17
|
+
errors?: string[];
|
|
10
18
|
/**
|
|
11
19
|
* `middleware.ts` files above this route, outermost first.
|
|
12
20
|
*
|
|
@@ -68,6 +76,29 @@ export interface ManifestIntercept {
|
|
|
68
76
|
/** (.) same level, (..) one up, (...) from the root. */
|
|
69
77
|
marker: string;
|
|
70
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* A `route.ts` — an api endpoint rather than a page.
|
|
81
|
+
*
|
|
82
|
+
* Separate from `routes` because it is matched before them and answered
|
|
83
|
+
* without rendering anything: no layouts, no payload, no client. A url cannot
|
|
84
|
+
* be both, and the build refuses one that is.
|
|
85
|
+
*/
|
|
86
|
+
export interface ManifestApiRoute {
|
|
87
|
+
/** The module name, as the engine's registry keys it. */
|
|
88
|
+
name: string;
|
|
89
|
+
segments: RouteSegment[];
|
|
90
|
+
/** Which methods the file exports, so a 405 can name the rest. */
|
|
91
|
+
methods: string[];
|
|
92
|
+
/**
|
|
93
|
+
* `middleware.ts` files above this route, outermost first.
|
|
94
|
+
*
|
|
95
|
+
* The same chain a page in that directory runs. A route.ts sits among the
|
|
96
|
+
* pages it belongs with, so a guard on the directory covers it too —
|
|
97
|
+
* anything else would mean adding an endpoint under a guarded path silently
|
|
98
|
+
* opened a hole in it.
|
|
99
|
+
*/
|
|
100
|
+
middleware: string[];
|
|
101
|
+
}
|
|
71
102
|
export interface RouteManifest {
|
|
72
103
|
version: number;
|
|
73
104
|
build: {
|
|
@@ -77,4 +108,6 @@ export interface RouteManifest {
|
|
|
77
108
|
};
|
|
78
109
|
routes: ManifestRoute[];
|
|
79
110
|
intercepts: ManifestIntercept[];
|
|
111
|
+
/** Optional: a manifest from a build before api routes existed has none. */
|
|
112
|
+
apis?: ManifestApiRoute[];
|
|
80
113
|
}
|
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 type: 'static' | 'param' | 'catchAll'\n value: string\n}\n\nexport interface ManifestRoute {\n component: string\n segments: RouteSegment[]\n layouts: string[]\n loadings: 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 * Whether this route ships the client runtime.\n *\n * False renders to HTML and stops: no bootstrap, so no React, no Flight\n * client, no router. A client component on such a route is inert markup — a\n * button that does nothing — so the build refuses the combination rather\n * than shipping it.\n */\n clientJs: 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\nexport interface RouteManifest {\n version: number\n build: { output: string; exportPath: string; payloadName: string }\n routes: ManifestRoute[]\n intercepts: ManifestIntercept[]\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 type: 'static' | 'param' | 'catchAll'\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 * `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 * Whether this route ships the client runtime.\n *\n * False renders to HTML and stops: no bootstrap, so no React, no Flight\n * client, no router. A client component on such a route is inert markup — a\n * button that does nothing — so the build refuses the combination rather\n * than shipping it.\n */\n clientJs: 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 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}\n\nexport interface RouteManifest {\n version: number\n build: { output: string; exportPath: string; payloadName: string }\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,24 @@
|
|
|
1
|
+
/** Thrown to stop rendering a page whose subject does not exist. */
|
|
2
|
+
export declare class NotFoundSignal extends Error {
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
/** Whether this is a missing page, whichever copy of the class threw it. */
|
|
6
|
+
export declare function isNotFoundSignal(error: unknown): error is NotFoundSignal;
|
|
7
|
+
/** The digest for a missing page, or null for any other error. */
|
|
8
|
+
export declare function notFoundDigest(error: unknown): string | null;
|
|
9
|
+
/** Whether a digest describes a missing page. */
|
|
10
|
+
export declare function isNotFoundDigest(digest: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Answer this url with the not-found page.
|
|
13
|
+
*
|
|
14
|
+
* Never returns: it throws, which stops the component that called it. Do not
|
|
15
|
+
* wrap a call in `try`/`catch` without rethrowing what you do not recognise —
|
|
16
|
+
* swallowing this turns a missing page into a blank region.
|
|
17
|
+
*
|
|
18
|
+
* Called above every Suspense boundary, the response is a real 404 carrying
|
|
19
|
+
* not-found.tsx, which is what a crawler and a cache need to see. Called
|
|
20
|
+
* inside one, the shell is already on the wire, so the boundary shows its
|
|
21
|
+
* fallback instead and the status stays 200 — put the lookup above the
|
|
22
|
+
* boundary when the status matters.
|
|
23
|
+
*/
|
|
24
|
+
export declare function notFound(): never;
|
package/dist/notFound.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Saying a url does not name a page, from inside a render.
|
|
2
|
+
//
|
|
3
|
+
// import { notFound } from '@rsc-kit/core/not-found'
|
|
4
|
+
//
|
|
5
|
+
// export default async function PostPage({ params }) {
|
|
6
|
+
// const post = await findPost((await params).slug)
|
|
7
|
+
// if (!post) notFound()
|
|
8
|
+
// ...
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// Until this existed, "no such page" could only be decided by the router,
|
|
12
|
+
// before anything rendered. But most of the time it is not knowable then: the
|
|
13
|
+
// route matched, the url is well-formed, and whether the thing exists is a
|
|
14
|
+
// question only the page can ask. The alternative was an error boundary and a
|
|
15
|
+
// 500, which tells a crawler the page is broken rather than absent.
|
|
16
|
+
//
|
|
17
|
+
// Delivery follows the same two windows a redirect has, and for the same
|
|
18
|
+
// reason — headers flush when the shell resolves:
|
|
19
|
+
//
|
|
20
|
+
// Before the shell — nothing is written yet, so the host answers 404 and
|
|
21
|
+
// renders not-found.tsx. This is where a params check lands, and where a
|
|
22
|
+
// lookup at the top of a page lands.
|
|
23
|
+
//
|
|
24
|
+
// After the shell — the status line is gone. React carries an error digest
|
|
25
|
+
// for every server error, so the mark travels there and the boundary around
|
|
26
|
+
// it shows its fallback rather than a stack.
|
|
27
|
+
/**
|
|
28
|
+
* The mark that says an error is a missing page.
|
|
29
|
+
*
|
|
30
|
+
* A property rather than `instanceof`, for the reason this project keeps
|
|
31
|
+
* meeting: the app's components are bundled apart from the engine that renders
|
|
32
|
+
* them, so each side evaluates its own copy of this module and its own copy of
|
|
33
|
+
* the class. `instanceof` is simply false across that seam — the page would
|
|
34
|
+
* render as an unhandled error, and the only clue would be a 500 where a 404
|
|
35
|
+
* was meant.
|
|
36
|
+
*/
|
|
37
|
+
const MARK = Symbol.for('@rsc-kit/core.not-found-signal');
|
|
38
|
+
/**
|
|
39
|
+
* Where a render records that it wants the not-found page.
|
|
40
|
+
*
|
|
41
|
+
* The same scope a redirect records in, reached through the well-known symbol
|
|
42
|
+
* rather than by importing it: this module is evaluated in the app bundle, the
|
|
43
|
+
* engine and the browser, and only one of those should be pulling in the
|
|
44
|
+
* redirect machinery.
|
|
45
|
+
*
|
|
46
|
+
* Recording is necessary because the throw alone does not survive. React
|
|
47
|
+
* catches what a component threw and re-raises its own error, whose message is
|
|
48
|
+
* stripped in production — so the host testing the caught value would work in
|
|
49
|
+
* development and answer 500 in production, which is the worst of the two
|
|
50
|
+
* possible bugs.
|
|
51
|
+
*/
|
|
52
|
+
const SCOPE = Symbol.for('@rsc-kit/core.redirect-scope');
|
|
53
|
+
function record() {
|
|
54
|
+
const scope = globalThis[SCOPE];
|
|
55
|
+
const store = scope?.getStore();
|
|
56
|
+
if (store)
|
|
57
|
+
store.notFound = true;
|
|
58
|
+
}
|
|
59
|
+
/** Thrown to stop rendering a page whose subject does not exist. */
|
|
60
|
+
export class NotFoundSignal extends Error {
|
|
61
|
+
constructor(message = 'Not found') {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'NotFoundSignal';
|
|
64
|
+
this[MARK] = true;
|
|
65
|
+
// In the constructor rather than in notFound(), so every way of raising
|
|
66
|
+
// one is recorded — a params schema refusing is not routed through the
|
|
67
|
+
// public function and must answer the same way.
|
|
68
|
+
record();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Whether this is a missing page, whichever copy of the class threw it. */
|
|
72
|
+
export function isNotFoundSignal(error) {
|
|
73
|
+
return (typeof error === 'object' && error !== null && error[MARK] === true);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The prefix that carries a missing page inside React's error digest.
|
|
77
|
+
*
|
|
78
|
+
* React replaces a server error's message with an opaque digest in production,
|
|
79
|
+
* and the digest is the one part it is guaranteed to transmit. Kept distinct
|
|
80
|
+
* from the redirect prefix so neither is ever read as the other.
|
|
81
|
+
*/
|
|
82
|
+
const PREFIX = 'RSC_NOT_FOUND';
|
|
83
|
+
/** The digest for a missing page, or null for any other error. */
|
|
84
|
+
export function notFoundDigest(error) {
|
|
85
|
+
return isNotFoundSignal(error) ? PREFIX : null;
|
|
86
|
+
}
|
|
87
|
+
/** Whether a digest describes a missing page. */
|
|
88
|
+
export function isNotFoundDigest(digest) {
|
|
89
|
+
return digest === PREFIX;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Answer this url with the not-found page.
|
|
93
|
+
*
|
|
94
|
+
* Never returns: it throws, which stops the component that called it. Do not
|
|
95
|
+
* wrap a call in `try`/`catch` without rethrowing what you do not recognise —
|
|
96
|
+
* swallowing this turns a missing page into a blank region.
|
|
97
|
+
*
|
|
98
|
+
* Called above every Suspense boundary, the response is a real 404 carrying
|
|
99
|
+
* not-found.tsx, which is what a crawler and a cache need to see. Called
|
|
100
|
+
* inside one, the shell is already on the wire, so the boundary shows its
|
|
101
|
+
* fallback instead and the status stays 200 — put the lookup above the
|
|
102
|
+
* boundary when the status matters.
|
|
103
|
+
*/
|
|
104
|
+
export function notFound() {
|
|
105
|
+
throw new NotFoundSignal();
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=notFound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notFound.js","sourceRoot":"","sources":["../src/notFound.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,uDAAuD;AACvD,EAAE;AACF,yDAAyD;AACzD,uDAAuD;AACvD,4BAA4B;AAC5B,UAAU;AACV,MAAM;AACN,EAAE;AACF,0EAA0E;AAC1E,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,oEAAoE;AACpE,EAAE;AACF,yEAAyE;AACzE,kDAAkD;AAClD,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,+CAA+C;AAE/C;;;;;;;;;GASG;AACH,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;AAEzD;;;;;;;;;;;;;GAaG;AACH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;AAExD,SAAS,MAAM;IACb,MAAM,KAAK,GAAI,UAAsC,CAAC,KAAK,CAE9C,CAAA;IAEb,MAAM,KAAK,GAAG,KAAK,EAAE,QAAQ,EAAE,CAAA;IAE/B,IAAI,KAAK;QAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAA;AAClC,CAAC;AAED,oEAAoE;AACpE,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAC3B;QAAC,IAA2C,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;QAE1D,wEAAwE;QACxE,uEAAuE;QACvE,gDAAgD;QAChD,MAAM,EAAE,CAAA;IACV,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAAiC,CAAC,IAAI,CAAC,KAAK,IAAI,CACjG,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,GAAG,eAAe,CAAA;AAE9B,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,OAAO,MAAM,KAAK,MAAM,CAAA;AAC1B,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,IAAI,cAAc,EAAE,CAAA;AAC5B,CAAC","sourcesContent":["// Saying a url does not name a page, from inside a render.\n//\n// import { notFound } from '@rsc-kit/core/not-found'\n//\n// export default async function PostPage({ params }) {\n// const post = await findPost((await params).slug)\n// if (!post) notFound()\n// ...\n// }\n//\n// Until this existed, \"no such page\" could only be decided by the router,\n// before anything rendered. But most of the time it is not knowable then: the\n// route matched, the url is well-formed, and whether the thing exists is a\n// question only the page can ask. The alternative was an error boundary and a\n// 500, which tells a crawler the page is broken rather than absent.\n//\n// Delivery follows the same two windows a redirect has, and for the same\n// reason — headers flush when the shell resolves:\n//\n// Before the shell — nothing is written yet, so the host answers 404 and\n// renders not-found.tsx. This is where a params check lands, and where a\n// lookup at the top of a page lands.\n//\n// After the shell — the status line is gone. React carries an error digest\n// for every server error, so the mark travels there and the boundary around\n// it shows its fallback rather than a stack.\n\n/**\n * The mark that says an error is a missing page.\n *\n * A property rather than `instanceof`, for the reason this project keeps\n * meeting: the app's components are bundled apart from the engine that renders\n * them, so each side evaluates its own copy of this module and its own copy of\n * the class. `instanceof` is simply false across that seam — the page would\n * render as an unhandled error, and the only clue would be a 500 where a 404\n * was meant.\n */\nconst MARK = Symbol.for('@rsc-kit/core.not-found-signal')\n\n/**\n * Where a render records that it wants the not-found page.\n *\n * The same scope a redirect records in, reached through the well-known symbol\n * rather than by importing it: this module is evaluated in the app bundle, the\n * engine and the browser, and only one of those should be pulling in the\n * redirect machinery.\n *\n * Recording is necessary because the throw alone does not survive. React\n * catches what a component threw and re-raises its own error, whose message is\n * stripped in production — so the host testing the caught value would work in\n * development and answer 500 in production, which is the worst of the two\n * possible bugs.\n */\nconst SCOPE = Symbol.for('@rsc-kit/core.redirect-scope')\n\nfunction record(): void {\n const scope = (globalThis as Record<symbol, unknown>)[SCOPE] as\n | { getStore(): { notFound?: boolean } | undefined }\n | undefined\n\n const store = scope?.getStore()\n\n if (store) store.notFound = true\n}\n\n/** Thrown to stop rendering a page whose subject does not exist. */\nexport class NotFoundSignal extends Error {\n constructor(message = 'Not found') {\n super(message)\n this.name = 'NotFoundSignal'\n ;(this as unknown as Record<symbol, boolean>)[MARK] = true\n\n // In the constructor rather than in notFound(), so every way of raising\n // one is recorded — a params schema refusing is not routed through the\n // public function and must answer the same way.\n record()\n }\n}\n\n/** Whether this is a missing page, whichever copy of the class threw it. */\nexport function isNotFoundSignal(error: unknown): error is NotFoundSignal {\n return (\n typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[MARK] === true\n )\n}\n\n/**\n * The prefix that carries a missing page inside React's error digest.\n *\n * React replaces a server error's message with an opaque digest in production,\n * and the digest is the one part it is guaranteed to transmit. Kept distinct\n * from the redirect prefix so neither is ever read as the other.\n */\nconst PREFIX = 'RSC_NOT_FOUND'\n\n/** The digest for a missing page, or null for any other error. */\nexport function notFoundDigest(error: unknown): string | null {\n return isNotFoundSignal(error) ? PREFIX : null\n}\n\n/** Whether a digest describes a missing page. */\nexport function isNotFoundDigest(digest: unknown): boolean {\n return digest === PREFIX\n}\n\n/**\n * Answer this url with the not-found page.\n *\n * Never returns: it throws, which stops the component that called it. Do not\n * wrap a call in `try`/`catch` without rethrowing what you do not recognise —\n * swallowing this turns a missing page into a blank region.\n *\n * Called above every Suspense boundary, the response is a real 404 carrying\n * not-found.tsx, which is what a crawler and a cache need to see. Called\n * inside one, the shell is already on the wire, so the boundary shows its\n * fallback instead and the status stays 200 — put the lookup above the\n * boundary when the status matters.\n */\nexport function notFound(): never {\n throw new NotFoundSignal()\n}\n"]}
|
package/dist/prerender.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ManifestRoute, RouteManifest } from './manifest.js';
|
|
2
2
|
export interface PrerenderEngine {
|
|
3
3
|
manifest?(): RouteManifest;
|
|
4
|
+
/**
|
|
5
|
+
* A route.ts, answered. Optional: a bundle built before api routes existed
|
|
6
|
+
* has none, and the build then simply stores no route.
|
|
7
|
+
*/
|
|
8
|
+
handleApiRoute?(name: string, request: Request, params: Record<string, string>, allow: string): Promise<Response>;
|
|
4
9
|
getStaticParams?(component: string): Promise<Record<string, string>[] | null>;
|
|
5
10
|
handleRscPprShell(component: string, props?: Record<string, unknown>, layouts?: {
|
|
6
11
|
component: string;
|
|
@@ -11,6 +16,14 @@ export interface PrerenderEngine {
|
|
|
11
16
|
shellHtml: string;
|
|
12
17
|
timedOut: boolean;
|
|
13
18
|
usedDynamicApis: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The host calls this render made, by name.
|
|
21
|
+
*
|
|
22
|
+
* So the build can say which one kept the page from being frozen rather
|
|
23
|
+
* than only that something did. Optional: an engine built before this
|
|
24
|
+
* existed reports nothing, and the line is printed without a reason.
|
|
25
|
+
*/
|
|
26
|
+
dynamicBecause?: string[];
|
|
14
27
|
error?: string;
|
|
15
28
|
/**
|
|
16
29
|
* Where the render stopped, when it stopped — React's own resumable state.
|
|
@@ -109,11 +122,15 @@ export interface PrerenderResult {
|
|
|
109
122
|
* blocked — nothing could be stored. Fails the build.
|
|
110
123
|
* error — the render itself failed, or the build refused what it produced.
|
|
111
124
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
125
|
+
* dynamic — answered per request, and correctly so. Api routes only.
|
|
126
|
+
*
|
|
127
|
+
* There is no such outcome for a PAGE. A page that cannot be stored has its
|
|
128
|
+
* boundary in the wrong place, and the fix is to move it — not to declare
|
|
129
|
+
* the problem away, which is how the slow ones get forgotten. An api route
|
|
130
|
+
* has no boundary to move: it either depends on the request or it does not,
|
|
131
|
+
* and depending on it is the ordinary case rather than a mistake.
|
|
115
132
|
*/
|
|
116
|
-
type: 'frozen' | 'shell' | 'blocked' | 'error';
|
|
133
|
+
type: 'frozen' | 'shell' | 'blocked' | 'error' | 'dynamic';
|
|
117
134
|
reason: string | null;
|
|
118
135
|
/**
|
|
119
136
|
* Something worth knowing that is not a failure.
|
|
@@ -158,6 +175,33 @@ export declare function closeDocument(html: string): string;
|
|
|
158
175
|
export declare function legend(results: {
|
|
159
176
|
type: string;
|
|
160
177
|
}[]): string;
|
|
178
|
+
/**
|
|
179
|
+
* How much JavaScript a route makes the browser download.
|
|
180
|
+
*
|
|
181
|
+
* Gzipped, because that is what crosses the wire — uncompressed bytes are a
|
|
182
|
+
* number nobody is served.
|
|
183
|
+
*
|
|
184
|
+
* A real per-route figure only because client components are chunked per
|
|
185
|
+
* module. Grouped the way plugin-rsc groups them by default, every route in an
|
|
186
|
+
* app loads every client component and this column is one number repeated down
|
|
187
|
+
* the page.
|
|
188
|
+
*/
|
|
189
|
+
export declare function clientJsSize(bytes: number): string;
|
|
190
|
+
/**
|
|
191
|
+
* The paragraph a mark cannot hold.
|
|
192
|
+
*
|
|
193
|
+
* "dynamic — called rpc(\"getUser\")" says what happened and not why the build
|
|
194
|
+
* could not simply make the call, which is the first thing someone asks when a
|
|
195
|
+
* page they expected to be static is not. The backend is running; the build is
|
|
196
|
+
* not talking to it.
|
|
197
|
+
*
|
|
198
|
+
* Printed once under the summary rather than beside each route: an app where
|
|
199
|
+
* every page reads from the backend would otherwise repeat the same paragraph
|
|
200
|
+
* forty times.
|
|
201
|
+
*/
|
|
202
|
+
export declare function notes(results: {
|
|
203
|
+
reason?: string | null;
|
|
204
|
+
}[]): string;
|
|
161
205
|
/**
|
|
162
206
|
* The one-line tally under the legend, in the legend's own words.
|
|
163
207
|
*/
|
package/dist/prerender.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// shapes and anything that reads them works for either.
|
|
17
17
|
import { withRedirect } from './redirect.js';
|
|
18
18
|
import { withCache } from './cache.js';
|
|
19
|
-
import { requestWasRead, withRequest } from './request.js';
|
|
19
|
+
import { requestReadBy, requestWasRead, withRequest } from './request.js';
|
|
20
20
|
import { watchNondeterminism, whileRendering } from './nondeterminism.js';
|
|
21
21
|
/** What a prerenderer needs from the built bundle, beyond serving a request. */
|
|
22
22
|
/**
|
|
@@ -129,7 +129,7 @@ export function legend(results) {
|
|
|
129
129
|
if (has('shell')) {
|
|
130
130
|
lines.push(' \u25D0 (Partial Prerender) prerendered as static HTML with dynamic server-streamed content');
|
|
131
131
|
}
|
|
132
|
-
if (has('blocked')) {
|
|
132
|
+
if (has('blocked') || has('dynamic')) {
|
|
133
133
|
lines.push(' \u0192 (Dynamic) server-rendered on demand');
|
|
134
134
|
}
|
|
135
135
|
if (has('error')) {
|
|
@@ -137,6 +137,47 @@ export function legend(results) {
|
|
|
137
137
|
}
|
|
138
138
|
return lines.join('\n');
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* How much JavaScript a route makes the browser download.
|
|
142
|
+
*
|
|
143
|
+
* Gzipped, because that is what crosses the wire — uncompressed bytes are a
|
|
144
|
+
* number nobody is served.
|
|
145
|
+
*
|
|
146
|
+
* A real per-route figure only because client components are chunked per
|
|
147
|
+
* module. Grouped the way plugin-rsc groups them by default, every route in an
|
|
148
|
+
* app loads every client component and this column is one number repeated down
|
|
149
|
+
* the page.
|
|
150
|
+
*/
|
|
151
|
+
export function clientJsSize(bytes) {
|
|
152
|
+
if (bytes === 0)
|
|
153
|
+
return 'no js';
|
|
154
|
+
const kb = bytes / 1000;
|
|
155
|
+
return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} kB`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The paragraph a mark cannot hold.
|
|
159
|
+
*
|
|
160
|
+
* "dynamic — called rpc(\"getUser\")" says what happened and not why the build
|
|
161
|
+
* could not simply make the call, which is the first thing someone asks when a
|
|
162
|
+
* page they expected to be static is not. The backend is running; the build is
|
|
163
|
+
* not talking to it.
|
|
164
|
+
*
|
|
165
|
+
* Printed once under the summary rather than beside each route: an app where
|
|
166
|
+
* every page reads from the backend would otherwise repeat the same paragraph
|
|
167
|
+
* forty times.
|
|
168
|
+
*/
|
|
169
|
+
export function notes(results) {
|
|
170
|
+
const said = (text) => results.some((r) => r.reason?.includes(text));
|
|
171
|
+
if (!said('rpc('))
|
|
172
|
+
return '';
|
|
173
|
+
return (' A build has no backend to call. rpc() suspends instead of answering, so a page\n' +
|
|
174
|
+
' that reads through one ships a shell and finishes for whoever asks — which is\n' +
|
|
175
|
+
' almost always what you want, since that data is rarely the same for everyone.\n' +
|
|
176
|
+
'\n' +
|
|
177
|
+
' If a page really should be frozen, move the read out of rpc(). If it really\n' +
|
|
178
|
+
' should be per visitor, say so with await connection() and the intent is on the\n' +
|
|
179
|
+
' page rather than inferred from a call that happened to suspend.');
|
|
180
|
+
}
|
|
140
181
|
/**
|
|
141
182
|
* The one-line tally under the legend, in the legend's own words.
|
|
142
183
|
*/
|
|
@@ -147,8 +188,9 @@ export function summary(results) {
|
|
|
147
188
|
parts.push(`${count('frozen')} static`);
|
|
148
189
|
if (count('shell'))
|
|
149
190
|
parts.push(`${count('shell')} partial prerender`);
|
|
150
|
-
if (count('blocked'))
|
|
151
|
-
parts.push(`${count('blocked')} dynamic`);
|
|
191
|
+
if (count('blocked') + count('dynamic')) {
|
|
192
|
+
parts.push(`${count('blocked') + count('dynamic')} dynamic`);
|
|
193
|
+
}
|
|
152
194
|
if (count('error'))
|
|
153
195
|
parts.push(`${count('error')} failed`);
|
|
154
196
|
return parts.join(', ') || 'nothing to store';
|
|
@@ -273,6 +315,26 @@ export async function prerender(options) {
|
|
|
273
315
|
const props = options.props ? options.props(route, params) : params;
|
|
274
316
|
const layouts = route.layouts.map((component) => ({ component, props: {} }));
|
|
275
317
|
const unlistedNow = route.segments.some((seg) => seg.type !== 'static') && !route.staticParams;
|
|
318
|
+
/**
|
|
319
|
+
* Why this route ships a shell rather than a whole page.
|
|
320
|
+
*
|
|
321
|
+
* In the order that answers the question soonest. A named call is the most
|
|
322
|
+
* actionable — you can go and look at it — so it wins over the two
|
|
323
|
+
* structural reasons, either of which may also be true.
|
|
324
|
+
*/
|
|
325
|
+
const shellReason = (calls) => {
|
|
326
|
+
if (calls.length)
|
|
327
|
+
return 'dynamic — called ' + calls.join(', ');
|
|
328
|
+
// One shell serving every url the route matches. generateStaticParams is
|
|
329
|
+
// what turns it into a page per url.
|
|
330
|
+
if (unlistedNow)
|
|
331
|
+
return 'one shell for every url — add generateStaticParams to store each';
|
|
332
|
+
// Nothing reached for the request; the data simply took too long. The
|
|
333
|
+
// page is fine, it just finishes per visitor.
|
|
334
|
+
if (shell?.timedOut)
|
|
335
|
+
return 'data took longer than the build budget';
|
|
336
|
+
return null;
|
|
337
|
+
};
|
|
276
338
|
const said = (type, reason) => ({
|
|
277
339
|
// A route standing in for many urls reports the pattern. Reporting the
|
|
278
340
|
// placeholder url instead prints `/posts/_`, which looks like a page.
|
|
@@ -295,7 +357,7 @@ export async function prerender(options) {
|
|
|
295
357
|
// must not share an answer just because they were built in the same run.
|
|
296
358
|
// No request, deliberately: a page that reads one is caught below rather
|
|
297
359
|
// than frozen holding whatever the build machine happened to send.
|
|
298
|
-
const [{ shell, redirected, readRequest }, nondeterministic] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
360
|
+
const [{ shell, redirected, readRequest, readBy }, nondeterministic] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
299
361
|
try {
|
|
300
362
|
return {
|
|
301
363
|
shell: await engine.handleRscPprShell(route.component, props, layouts, route.loadings, route.slots,
|
|
@@ -305,6 +367,9 @@ export async function prerender(options) {
|
|
|
305
367
|
unlistedNow ? '' : url),
|
|
306
368
|
redirected: taken(),
|
|
307
369
|
readRequest: requestWasRead(),
|
|
370
|
+
// What reached for it, so the build can name the call rather than
|
|
371
|
+
// only report that the page is dynamic.
|
|
372
|
+
readBy: requestReadBy(),
|
|
308
373
|
};
|
|
309
374
|
}
|
|
310
375
|
catch (error) {
|
|
@@ -315,7 +380,7 @@ export async function prerender(options) {
|
|
|
315
380
|
const refused = taken();
|
|
316
381
|
if (!refused)
|
|
317
382
|
throw error;
|
|
318
|
-
return { shell: null, redirected: refused, readRequest: requestWasRead() };
|
|
383
|
+
return { shell: null, redirected: refused, readRequest: requestWasRead(), readBy: requestReadBy() };
|
|
319
384
|
}
|
|
320
385
|
}))));
|
|
321
386
|
// A page that leaves rather than renders is not a build failure, and it is
|
|
@@ -407,7 +472,11 @@ export async function prerender(options) {
|
|
|
407
472
|
'<Suspense> above the waiting, and it has a skeleton to store.');
|
|
408
473
|
}
|
|
409
474
|
await writeShell(route, url, body, shell.postponed);
|
|
410
|
-
|
|
475
|
+
// Why it is a shell rather than a whole page. "◐ /orders" on its own
|
|
476
|
+
// leaves someone reading the build output to guess what did it, which is
|
|
477
|
+
// the question this line exists to answer.
|
|
478
|
+
const calls = [...(shell.dynamicBecause ?? []), ...(readBy ?? [])];
|
|
479
|
+
return await withRootFallbackChecked(said('shell', shellReason(calls)));
|
|
411
480
|
}
|
|
412
481
|
// Warned, not refused.
|
|
413
482
|
//
|
|
@@ -429,8 +498,9 @@ export async function prerender(options) {
|
|
|
429
498
|
return result;
|
|
430
499
|
result.warning =
|
|
431
500
|
`froze ${nondeterministic.join(' and ')} — a stored page keeps whatever that ` +
|
|
432
|
-
'returned at build time.
|
|
433
|
-
'
|
|
501
|
+
'returned at build time. If it should differ per visitor, await connection() so ' +
|
|
502
|
+
'the page renders per request; if only the browser needs it, use(browser()) keeps ' +
|
|
503
|
+
'it out of the build entirely.';
|
|
434
504
|
return result;
|
|
435
505
|
};
|
|
436
506
|
// Rendered whole, with params that were invented because the route listed
|