@rsc-kit/core 0.30.6 → 0.30.8

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.
@@ -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 * `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 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\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 routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
@@ -17,8 +17,8 @@ export declare function isNotFoundDigest(digest: unknown): boolean;
17
17
  *
18
18
  * Called above every Suspense boundary, the response is a real 404 carrying
19
19
  * not-found.tsx. Called inside one, the shell is already on the wire, so the
20
- * boundary shows its fallback, the status stays 200 and the page ends with a
21
- * noindex tag. A search engine is answered with the finished page, so it
20
+ * status stays 200 and the page ends with a noindex tag; the browser then
21
+ * shows not-found.tsx where the page was. A search engine is answered with the finished page, so it
22
22
  * gets a real 404 wherever this was called.
23
23
  */
24
24
  export declare function notFound(): never;
package/dist/notFound.js CHANGED
@@ -22,8 +22,9 @@
22
22
  // lookup at the top of a page lands.
23
23
  //
24
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.
25
+ // for every server error, so the mark travels there. The browser's boundary
26
+ // takes it for the page's answer, not a failure, and asks the server for
27
+ // not-found.tsx to put where the page was - layouts kept, the url as it is.
27
28
  // The page ends with a noindex tag, and a crawler - which is answered with
28
29
  // the finished document, see crawler.ts - gets a real 404 regardless.
29
30
  /**
@@ -99,8 +100,8 @@ export function isNotFoundDigest(digest) {
99
100
  *
100
101
  * Called above every Suspense boundary, the response is a real 404 carrying
101
102
  * not-found.tsx. Called inside one, the shell is already on the wire, so the
102
- * boundary shows its fallback, the status stays 200 and the page ends with a
103
- * noindex tag. A search engine is answered with the finished page, so it
103
+ * status stays 200 and the page ends with a noindex tag; the browser then
104
+ * shows not-found.tsx where the page was. A search engine is answered with the finished page, so it
104
105
  * gets a real 404 wherever this was called.
105
106
  */
106
107
  export function notFound() {
@@ -1 +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;AAC/C,6EAA6E;AAC7E,wEAAwE;AAExE;;;;;;;;;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// The page ends with a noindex tag, and a crawler - which is answered with\n// the finished document, see crawler.ts - gets a real 404 regardless.\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. Called inside one, the shell is already on the wire, so the\n * boundary shows its fallback, the status stays 200 and the page ends with a\n * noindex tag. A search engine is answered with the finished page, so it\n * gets a real 404 wherever this was called.\n */\nexport function notFound(): never {\n throw new NotFoundSignal()\n}\n"]}
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,2EAA2E;AAC3E,8EAA8E;AAC9E,6EAA6E;AAC7E,wEAAwE;AAExE;;;;;;;;;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. The browser's boundary\n// takes it for the page's answer, not a failure, and asks the server for\n// not-found.tsx to put where the page was - layouts kept, the url as it is.\n// The page ends with a noindex tag, and a crawler - which is answered with\n// the finished document, see crawler.ts - gets a real 404 regardless.\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. Called inside one, the shell is already on the wire, so the\n * status stays 200 and the page ends with a noindex tag; the browser then\n * shows not-found.tsx where the page was. A search engine is answered with the finished page, so it\n * gets a real 404 wherever this was called.\n */\nexport function notFound(): never {\n throw new NotFoundSignal()\n}\n"]}
package/dist/vite.js CHANGED
@@ -703,6 +703,7 @@ function routeManifest() {
703
703
  layouts: ancestors(name, "layout").map((n) => n),
704
704
  loadings: ancestors(name, "loading").map((n) => n),
705
705
  errors: ancestors(name, "error"),
706
+ notFounds: ancestors(name, "not-found"),
706
707
  middleware: ancestors(name, "middleware").map((n) => n),
707
708
  slots,
708
709
  sections: names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dirOf(name)),
@@ -2953,16 +2954,22 @@ export function instrumentationImport(instrumentation) {
2953
2954
  }
2954
2955
  function generateEntryRsc(fallbackOrigin = "") {
2955
2956
  const instrumentation = instrumentationFile();
2956
- // The 404 page, if the app has one, and the layouts it renders inside.
2957
- // Computed here rather than looked up at runtime: not-found is not a route,
2958
- // so the manifest has no entry to read its chain from.
2959
- const notFoundComponent = [...components.keys()].find((name) => name.endsWith("/not-found"));
2960
- const notFoundLayouts = notFoundComponent
2961
- ? [...components.keys()]
2957
+ // Every 404 page the app has, and the layouts each renders inside. Computed
2958
+ // here rather than looked up at runtime: not-found is not a route, so the
2959
+ // manifest has no entry to read its chain from. Which one answers is decided
2960
+ // per request: the nearest above the route that said notFound(), and for a
2961
+ // url no route owns, the one at the root.
2962
+ const notFoundFiles = [...components.keys()].filter((name) => name.endsWith("/not-found") && !name.includes("/@"));
2963
+ const notFoundLayouts = {};
2964
+ for (const file of notFoundFiles) {
2965
+ notFoundLayouts[file] = [...components.keys()]
2962
2966
  .filter((name) => name.endsWith("/layout") &&
2963
- notFoundComponent.startsWith(name.slice(0, -"layout".length)))
2964
- .sort((a, b) => a.length - b.length)
2965
- : [];
2967
+ file.startsWith(name.slice(0, -"layout".length)))
2968
+ .sort((a, b) => a.length - b.length);
2969
+ }
2970
+ // The root is `app/not-found`: two segments. A nested one alone does not
2971
+ // answer a url nothing owns - it belongs to the routes beneath it.
2972
+ const rootNotFound = notFoundFiles.find((name) => name.split("/").length === 2) ?? null;
2966
2973
  const imports = [];
2967
2974
  const mapEntries = [];
2968
2975
  const metaEntries = [];
@@ -3035,14 +3042,14 @@ import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDi
3035
3042
  import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
3036
3043
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
3037
3044
  import { cancelledByConsumer } from ${JSON.stringify(join(packageDir, "js/fallbackReport"))}
3038
- import { createRscHandler, pageSaidNotFound, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
3045
+ import { createRscHandler, notFoundRouteOf, pageSaidNotFound, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
3039
3046
  import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
3040
3047
  import { backendVersions, installBackendVersionSource } from ${JSON.stringify(join(packageDir, "changed"))}
3041
3048
  import { RefreshOn } from ${JSON.stringify(join(packageDir, "js/refreshOn"))}
3042
3049
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
3043
3050
  import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
3044
3051
  import { isQuery, queryCacheControl, isQueryValidationError, queryRefusal } from ${JSON.stringify(join(packageDir, "query"))}
3045
- import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
3052
+ import { isActionValidationError, isActionRefusal, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
3046
3053
  import { forgetCached } from ${JSON.stringify(join(packageDir, "cache"))}
3047
3054
  import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
3048
3055
  import { isOutdatedOptimizedDep, outdatedDepResponse } from ${JSON.stringify(join(packageDir, "devReload"))}
@@ -3699,6 +3706,21 @@ function checkedSearchParams(
3699
3706
 
3700
3707
  let errorChains: Record<string, string[]> | null = null
3701
3708
 
3709
+ let notFoundChains: Record<string, string[]> | null = null
3710
+
3711
+ /** The not-found.tsx files above a component, outermost first. */
3712
+ function notFoundChain(component: string): string[] {
3713
+ if (!notFoundChains) {
3714
+ notFoundChains = {}
3715
+
3716
+ for (const route of manifest().routes as { component: string; notFounds?: string[] }[]) {
3717
+ if (route.notFounds?.length) notFoundChains[route.component] = route.notFounds
3718
+ }
3719
+ }
3720
+
3721
+ return notFoundChains[component] ?? []
3722
+ }
3723
+
3702
3724
  /** The error.tsx files above a component, outermost first. */
3703
3725
  function errorChain(component: string): string[] {
3704
3726
  if (!errorChains) {
@@ -4984,14 +5006,33 @@ export async function handleAction(
4984
5006
  // returned { validationErrors } that <Form> reads. Left thrown, React
4985
5007
  // serialises the rejection opaquely — production strips the message — and the
4986
5008
  // fields it named never reach the browser.
5009
+ //
5010
+ // A refusal is the same: the stub the build writes for a backend action is a
5011
+ // plain function, and when the backend turns the call down (Refuse(503, ...),
5012
+ // abort(429, ...), Rsc::refuse) it throws a refusal whose message was written
5013
+ // for the person asking. Its message is the result's serverError, which is
5014
+ // where a form's formError reads. Its data is not sent: there is no
5015
+ // .refusal(schema) on a plain function to check it against.
4987
5016
  let result: unknown
4988
5017
 
4989
5018
  try {
4990
5019
  result = await (action as (...a: unknown[]) => unknown)(...args)
4991
5020
  } catch (error) {
4992
- if (!isActionValidationError(error)) throw error
5021
+ if (isActionRefusal(error)) {
5022
+ if (error.data !== undefined) {
5023
+ console.error(
5024
+ '[rsc-kit] ' + actionId + ' refused with data, and is a plain function with no .refusal(schema) to check it - the message was sent, the data was not: ' + error.message,
5025
+ )
5026
+ }
4993
5027
 
4994
- result = { validationErrors: error.errors }
5028
+ // Marked, so the browser rejects with it when a stub is awaited directly:
5029
+ // a refusal resolved as a result is taken for the success it is typed as.
5030
+ result = { serverError: error.message, __rscRefused: error.refusalStatus }
5031
+ } else if (isActionValidationError(error)) {
5032
+ result = { validationErrors: error.errors }
5033
+ } else {
5034
+ throw error
5035
+ }
4995
5036
  }
4996
5037
 
4997
5038
  // Read after the action has run: what it invalidated is only known once its
@@ -5702,25 +5743,40 @@ function wantsPage(request: Request): boolean {
5702
5743
  return (request.headers.get('Accept') ?? '').includes('text/html')
5703
5744
  }
5704
5745
 
5746
+ /**
5747
+ * The not-found.tsx that answers: the nearest above the route that said
5748
+ * notFound(), else - for a url no route owns - the one at the root, else none.
5749
+ */
5750
+ function nearestNotFound(request: Request): string | null {
5751
+ const route = notFoundRouteOf(request)
5752
+ const chain = route === undefined ? [] : notFoundChain(route)
5753
+
5754
+ return chain[chain.length - 1] ?? ${JSON.stringify(rootNotFound)}
5755
+ }
5756
+
5757
+ const NOT_FOUND_LAYOUTS: Record<string, string[]> = ${JSON.stringify(notFoundLayouts)}
5758
+
5705
5759
  async function renderNotFound(request: Request): Promise<Response> {
5706
- ${notFoundComponent
5760
+ const target = nearestNotFound(request)
5761
+ ${notFoundFiles.length
5707
5762
  ? `
5708
5763
  // A navigation, not a document: answer with the not-found tree as a
5709
5764
  // payload, at the depth the client already holds, so the router renders
5710
5765
  // it in place - the layout stays, the url changes, nothing reloads. The
5711
5766
  // status is still 404; a payload is a payload whatever it says.
5712
- if (request.headers.get('X-RSC')) {
5767
+ if (target && request.headers.get('X-RSC')) {
5713
5768
  try {
5714
- const chain = ${JSON.stringify(notFoundLayouts)}
5769
+ const layouts = NOT_FOUND_LAYOUTS[target] ?? []
5770
+ const chain = layouts
5715
5771
  const from = sharedDepth(request.headers.get('X-RSC-Segments'), chain)
5716
5772
  const { rscPayload } = await handleRscPayload(
5717
- ${JSON.stringify(notFoundComponent)},
5773
+ target,
5718
5774
  {},
5719
- ${JSON.stringify(notFoundLayouts.map((component) => ({ component, props: {} })))},
5775
+ layouts.map((component) => ({ component, props: {} })),
5720
5776
  [],
5721
5777
  {},
5722
5778
  from,
5723
- '/404',
5779
+ new URL(request.url).pathname,
5724
5780
  )
5725
5781
 
5726
5782
  return new Response(rscPayload, {
@@ -5739,15 +5795,17 @@ async function renderNotFound(request: Request): Promise<Response> {
5739
5795
  }
5740
5796
 
5741
5797
  try {
5798
+ if (!target) throw new Error('no not-found.tsx')
5799
+
5742
5800
  const { htmlStream } = await handleRscHtmlStream(
5743
- ${JSON.stringify(notFoundComponent)},
5801
+ target,
5744
5802
  {},
5745
- ${JSON.stringify(notFoundLayouts.map((component) => ({ component, props: {} })))},
5803
+ (NOT_FOUND_LAYOUTS[target] ?? []).map((component) => ({ component, props: {} })),
5746
5804
  [],
5747
5805
  {},
5748
5806
  {},
5749
5807
  undefined,
5750
- '/404',
5808
+ new URL(request.url).pathname,
5751
5809
  )
5752
5810
 
5753
5811
  return new Response(htmlStream, {