@rsc-kit/core 0.19.0 → 0.20.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.
@@ -82,6 +82,12 @@ export interface ManifestIntercept {
82
82
  export interface ManifestApiRoute {
83
83
  /** The module name, as the engine's registry keys it. */
84
84
  name: string;
85
+ /**
86
+ * The file that answers, relative to the source directory - `app/api/x/route.ts`,
87
+ * or `app/sitemap.ts` for a route the build synthesised from a metadata file
88
+ * - so a line about the route can say where to look.
89
+ */
90
+ source?: string;
85
91
  segments: RouteSegment[];
86
92
  /** Which methods the file exports, so a 405 can name the rest. */
87
93
  methods: string[];
@@ -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 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: {\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 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 * `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}\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 routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { Href, Route, SearchFor } from './routes.js';
1
+ import type { Route, SearchFor } from './routes.js';
2
2
  import type { Redirection } from './redirectDigest.js';
3
3
  export { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js';
4
4
  export type { Redirection } from './redirectDigest.js';
@@ -10,7 +10,7 @@ export type { Redirection } from './redirectDigest.js';
10
10
  * swallowing this turns a redirect into a blank region.
11
11
  *
12
12
  * `location` is typed to the routes the build found, so a redirect to a page
13
- * that no longer exists stops compiling. Cast with `as Href` when the
13
+ * that no longer exists stops compiling. Cast with `as Route` when the
14
14
  * destination is computed — remembering where someone was going and sending
15
15
  * them back to it is the usual case.
16
16
  *
@@ -26,7 +26,7 @@ export type { Redirection } from './redirectDigest.js';
26
26
  * same check `Link` puts on its `search` prop - so a key the page never
27
27
  * reads, or a number written as text, does not compile.
28
28
  */
29
- export type RedirectOptions<H extends Href> = ({} extends SearchFor<H> ? {
29
+ export type RedirectOptions<H extends Route> = ({} extends SearchFor<H> ? {
30
30
  search?: SearchFor<H>;
31
31
  } : {
32
32
  search: SearchFor<H>;
@@ -1 +1 @@
1
- {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,sDAAsD;AACtD,EAAE;AACF,0DAA0D;AAC1D,8CAA8C;AAC9C,0CAA0C;AAC1C,UAAU;AACV,MAAM;AACN,EAAE;AACF,yEAAyE;AACzE,4EAA4E;AAC5E,sEAAsE;AACtE,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+DAA+D;AAC/D,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,4EAA4E;AAC5E,qCAAqC;AACrC,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,sEAAsE;AACtE,+CAA+C;AAC/C,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,iDAAiD;AAEjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGpD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAuB3G;;;;;;;GAOG;AACH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;AAExD,MAAM,OAAO,GAAG,UAA8C,CAAA;AAE9D,IAAI,KAAK,GAAyB,IAAI,CAAA;AAEtC,SAAS,KAAK;IACZ,OAAQ,OAAO,CAAC,KAAK,CAAuB,IAAI,IAAI,CAAA;AACtD,CAAC;AAiCD,MAAM,UAAU,QAAQ,CACtB,QAAW,EACX,GAAG,IAAuG;IAE1G,0EAA0E;IAC1E,sCAAsC;IACtC,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACnF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAA;IACpC,MAAM,MAAM,GAAI,OAA+B,CAAC,MAAM,CAAA;IAEtD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAE,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AACvF,CAAC;AAED,SAAS,UAAU,CAAC,QAAc,EAAE,MAAc;IAChD,wEAAwE;IACxE,4EAA4E;IAC5E,uDAAuD;IACvD,kBAAkB,CAAC,QAAQ,CAAC,CAAA;IAE5B,2EAA2E;IAC3E,6EAA6E;IAC7E,gCAAgC;IAChC,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,yBAAyB,GAAG,MAAM,GAAG,gDAAgD,CACtF,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAA;IAEjC,4EAA4E;IAC5E,6DAA6D;IAC7D,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC7B,KAAK,CAAC,QAAQ,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;IACvC,CAAC;IAED,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAC5C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,IAAI,IAAI,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,KAAK,IAAI,CAAA;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAoD;IAEpD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,KAAK,KAAK,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACzC,OAAO,CAAC,KAAK,CAAC,KAAK,QAA4B,CAAA;QACjD,CAAC,CAAC,CAAA;QAEF,MAAM,KAAK,CAAA;IACb,CAAC;IAED,MAAM,IAAI,GAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IAEtD,OAAO,MAAM,KAAK,EAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AACjE,CAAC","sourcesContent":["// Redirecting from inside a render.\n//\n// import { redirect } from '@rsc-kit/core/redirect'\n//\n// export default async function ProductPage({ slug }) {\n// const product = await findProduct(slug)\n// if (!product) redirect('/products')\n// ...\n// }\n//\n// The hard part is not throwing — it is that a redirect decided during a\n// render may be decided after the response has already begun. Headers flush\n// early on purpose (see the stream-start invariant), so by the time a\n// component deep in a Suspense boundary changes its mind, the 200 is gone.\n//\n// So there are two windows, and which one a redirect lands in is decided by\n// where it was thrown rather than by anything the caller does:\n//\n// Before the shell resolves — nothing has been written. The host answers\n// with a real 3xx, or for a navigation with X-RSC-Redirect, and the browser\n// never sees the page. This is the window every guard lands in, because a\n// guard runs above the boundaries.\n//\n// After the shell resolves — the shell is already on the wire. It holds\n// layouts and Suspense fallbacks, so no data from the redirecting subtree\n// has been shown. The location travels to the client instead, which\n// performs it as an ordinary SPA navigation.\n//\n// Neither window costs a buffered byte. The first exists because the host\n// already awaits the shell before writing anything; the second because React\n// already carries an error digest to the client.\n\nimport { assertSafeRedirect } from './safeUrl.js'\nimport { withSearch } from './routes.js'\nimport type { Href, Route, SearchFor } from './routes.js'\nimport { resolveScope } from './revalidate.js'\nimport { RedirectSignal } from './redirectDigest.js'\nimport type { Redirection } from './redirectDigest.js'\n\nexport { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js'\nexport type { Redirection } from './redirectDigest.js'\n\n/** Per-render state, for the same reason revalidation has it: two can be in flight. */\ninterface Slot {\n redirect: Redirection | null\n /**\n * Whether the render asked for the not-found page.\n *\n * Shares this scope rather than opening a second one, because it is the same\n * question asked once per render — \"did this page decide to answer as\n * something other than itself\" — and every render path is already wrapped in\n * this one. notFound.ts sets it through the scope symbol without importing\n * this module; see the note there.\n */\n notFound?: boolean\n}\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\n/**\n * One scope, however many copies of this module exist.\n *\n * The app's components are bundled into the server bundle and the host is\n * not, so this module is loaded twice. Two scopes would mean the component\n * records in one and the host reads the other: the redirect is simply never\n * honoured, and nothing on either side reports a problem.\n */\nconst SCOPE = Symbol.for('@rsc-kit/core.redirect-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\n/**\n * Leave this page for another one.\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 redirect into a blank region.\n *\n * `location` is typed to the routes the build found, so a redirect to a page\n * that no longer exists stops compiling. Cast with `as Href` when the\n * destination is computed — remembering where someone was going and sending\n * them back to it is the usual case.\n *\n * Where it is called decides how it is delivered, and the difference matters\n * for anything that must not be seen. A call above every Suspense boundary is\n * answered with a status code before a byte is written. A call inside one is\n * answered after the shell — which holds no data from inside the boundary,\n * but does hold whatever the layouts above it rendered.\n */\n/**\n * What a redirect may carry beside its destination. `search` is typed to the\n * destination page's own `searchParams` schema when it exports one - the\n * same check `Link` puts on its `search` prop - so a key the page never\n * reads, or a number written as text, does not compile.\n */\nexport type RedirectOptions<H extends Href> = ({} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> }) & {\n /** 307 unless said otherwise; 308 for a permanent one. */\n status?: number\n}\n\nexport function redirect<H extends Route>(\n location: H,\n ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]\n): never {\n // The second argument was a status alone once; it still is, and an object\n // carries the query string beside it.\n const options = typeof rest[0] === 'number' ? { status: rest[0] } : (rest[0] ?? {})\n const status = options.status ?? 307\n const search = (options as { search?: object }).search\n\n return redirectTo(search ? (withSearch(location, search) as Href) : location, status)\n}\n\nfunction redirectTo(location: Href, status: number): never {\n // Refused here, at the one place every delivery path leads back to. The\n // destination reaches location.href on the client and an inline script in a\n // document, and a javascript: url runs in all of them.\n assertSafeRedirect(location)\n\n // A redirect is a 3xx. Anything else produces a response carrying Location\n // that no browser acts on — the page renders, the redirect silently does not\n // happen, and nothing says why.\n if (status < 300 || status > 399) {\n throw new Error(\n 'Not a redirect status: ' + status + '. A redirect is 3xx; 307 preserves the method.',\n )\n }\n\n const store = scope()?.getStore()\n\n // First wins. A layout that redirects and a page that also redirects should\n // land where the outer one said, not wherever finished last.\n if (store && !store.redirect) {\n store.redirect = { location, status }\n }\n\n throw new RedirectSignal(location, status)\n}\n\n/** The redirect asked for during the current render, if any. */\nexport function currentRedirect(): Redirection | null {\n return scope()?.getStore()?.redirect ?? null\n}\n\n/**\n * Whether the current render called notFound().\n *\n * Read rather than caught, for the reason the redirect above is: React\n * re-raises a component's error as its own, so the thrown signal does not\n * arrive intact at the host in production.\n */\nexport function currentNotFound(): boolean {\n return scope()?.getStore()?.notFound === true\n}\n\n/**\n * Run a render with somewhere for a redirect to be recorded, and report one.\n *\n * `taken()` is read twice by a streaming host: once when the shell resolves,\n * to answer with a status code, and again when the stream ends, for a\n * redirect that arrived too late for one.\n */\nexport async function withRedirect<T>(\n run: (taken: () => Redirection | null) => Promise<T>,\n): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const slot: Slot = { redirect: null, notFound: false }\n\n return await scope()!.run(slot, () => run(() => slot.redirect))\n}\n"]}
1
+ {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":"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","sourcesContent":["// Redirecting from inside a render.\n//\n// import { redirect } from '@rsc-kit/core/redirect'\n//\n// export default async function ProductPage({ slug }) {\n// const product = await findProduct(slug)\n// if (!product) redirect('/products')\n// ...\n// }\n//\n// The hard part is not throwing — it is that a redirect decided during a\n// render may be decided after the response has already begun. Headers flush\n// early on purpose (see the stream-start invariant), so by the time a\n// component deep in a Suspense boundary changes its mind, the 200 is gone.\n//\n// So there are two windows, and which one a redirect lands in is decided by\n// where it was thrown rather than by anything the caller does:\n//\n// Before the shell resolves — nothing has been written. The host answers\n// with a real 3xx, or for a navigation with X-RSC-Redirect, and the browser\n// never sees the page. This is the window every guard lands in, because a\n// guard runs above the boundaries.\n//\n// After the shell resolves — the shell is already on the wire. It holds\n// layouts and Suspense fallbacks, so no data from the redirecting subtree\n// has been shown. The location travels to the client instead, which\n// performs it as an ordinary SPA navigation.\n//\n// Neither window costs a buffered byte. The first exists because the host\n// already awaits the shell before writing anything; the second because React\n// already carries an error digest to the client.\n\nimport { assertSafeRedirect } from './safeUrl.js'\nimport { withSearch } from './routes.js'\nimport type { Route, SearchFor } from './routes.js'\nimport { resolveScope } from './revalidate.js'\nimport { RedirectSignal } from './redirectDigest.js'\nimport type { Redirection } from './redirectDigest.js'\n\nexport { RedirectSignal, isRedirectSignal, redirectDigest, parseRedirectDigest } from './redirectDigest.js'\nexport type { Redirection } from './redirectDigest.js'\n\n/** Per-render state, for the same reason revalidation has it: two can be in flight. */\ninterface Slot {\n redirect: Redirection | null\n /**\n * Whether the render asked for the not-found page.\n *\n * Shares this scope rather than opening a second one, because it is the same\n * question asked once per render — \"did this page decide to answer as\n * something other than itself\" — and every render path is already wrapped in\n * this one. notFound.ts sets it through the scope symbol without importing\n * this module; see the note there.\n */\n notFound?: boolean\n}\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\n/**\n * One scope, however many copies of this module exist.\n *\n * The app's components are bundled into the server bundle and the host is\n * not, so this module is loaded twice. Two scopes would mean the component\n * records in one and the host reads the other: the redirect is simply never\n * honoured, and nothing on either side reports a problem.\n */\nconst SCOPE = Symbol.for('@rsc-kit/core.redirect-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\n/**\n * Leave this page for another one.\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 redirect into a blank region.\n *\n * `location` is typed to the routes the build found, so a redirect to a page\n * that no longer exists stops compiling. Cast with `as Route` when the\n * destination is computed — remembering where someone was going and sending\n * them back to it is the usual case.\n *\n * Where it is called decides how it is delivered, and the difference matters\n * for anything that must not be seen. A call above every Suspense boundary is\n * answered with a status code before a byte is written. A call inside one is\n * answered after the shell — which holds no data from inside the boundary,\n * but does hold whatever the layouts above it rendered.\n */\n/**\n * What a redirect may carry beside its destination. `search` is typed to the\n * destination page's own `searchParams` schema when it exports one - the\n * same check `Link` puts on its `search` prop - so a key the page never\n * reads, or a number written as text, does not compile.\n */\nexport type RedirectOptions<H extends Route> = ({} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> }) & {\n /** 307 unless said otherwise; 308 for a permanent one. */\n status?: number\n}\n\nexport function redirect<H extends Route>(\n location: H,\n ...rest: {} extends SearchFor<H> ? [options?: RedirectOptions<H> | number] : [options: RedirectOptions<H>]\n): never {\n // The second argument was a status alone once; it still is, and an object\n // carries the query string beside it.\n const options = typeof rest[0] === 'number' ? { status: rest[0] } : (rest[0] ?? {})\n const status = options.status ?? 307\n const search = (options as { search?: object }).search\n\n return redirectTo(search ? (withSearch(location, search) as Route) : location, status)\n}\n\nfunction redirectTo(location: Route, status: number): never {\n // Refused here, at the one place every delivery path leads back to. The\n // destination reaches location.href on the client and an inline script in a\n // document, and a javascript: url runs in all of them.\n assertSafeRedirect(location)\n\n // A redirect is a 3xx. Anything else produces a response carrying Location\n // that no browser acts on — the page renders, the redirect silently does not\n // happen, and nothing says why.\n if (status < 300 || status > 399) {\n throw new Error(\n 'Not a redirect status: ' + status + '. A redirect is 3xx; 307 preserves the method.',\n )\n }\n\n const store = scope()?.getStore()\n\n // First wins. A layout that redirects and a page that also redirects should\n // land where the outer one said, not wherever finished last.\n if (store && !store.redirect) {\n store.redirect = { location, status }\n }\n\n throw new RedirectSignal(location, status)\n}\n\n/** The redirect asked for during the current render, if any. */\nexport function currentRedirect(): Redirection | null {\n return scope()?.getStore()?.redirect ?? null\n}\n\n/**\n * Whether the current render called notFound().\n *\n * Read rather than caught, for the reason the redirect above is: React\n * re-raises a component's error as its own, so the thrown signal does not\n * arrive intact at the host in production.\n */\nexport function currentNotFound(): boolean {\n return scope()?.getStore()?.notFound === true\n}\n\n/**\n * Run a render with somewhere for a redirect to be recorded, and report one.\n *\n * `taken()` is read twice by a streaming host: once when the shell resolves,\n * to answer with a status code, and again when the stream ends, for a\n * redirect that arrived too late for one.\n */\nexport async function withRedirect<T>(\n run: (taken: () => Redirection | null) => Promise<T>,\n): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const slot: Slot = { redirect: null, notFound: false }\n\n return await scope()!.run(slot, () => run(() => slot.redirect))\n}\n"]}
package/dist/routes.d.ts CHANGED
@@ -30,7 +30,7 @@ type OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` |
30
30
  * A url this app can answer, or one that deliberately leaves it.
31
31
  *
32
32
  * Cast when the destination is computed rather than written:
33
- * `href={path as Href}`.
33
+ * `href={path as Route}`.
34
34
  */
35
35
  /**
36
36
  * A url this app answers - a page or a route.ts - or one that deliberately
@@ -43,8 +43,7 @@ type OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` |
43
43
  * `Route` is the same type under the name Next uses, so a port keeps the
44
44
  * word it already has.
45
45
  */
46
- export type Href = Unregistered extends true ? string : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}` | OffRoute;
47
- export type Route = Href;
46
+ export type Route = Unregistered extends true ? string : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}` | OffRoute;
48
47
  /** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
49
48
  export interface RegisterApi {
50
49
  }
@@ -72,9 +71,7 @@ type NoApis = string extends ApiPattern ? true : false;
72
71
  * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed
73
72
  * because that is how a GET is parameterised.
74
73
  */
75
- export type ApiHref = NoApis extends true ? string : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;
76
- /** A url a route.ts answers, alone: what `apiUrl()` narrows a fetch to. */
77
- export type ApiRoute = ApiHref;
74
+ export type ApiRoute = NoApis extends true ? string : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;
78
75
  /**
79
76
  * An api url, checked against the routes the build found.
80
77
  *
@@ -87,7 +84,7 @@ export type ApiRoute = ApiHref;
87
84
  * Wrong path, and it stops compiling. Renamed the directory, and every call
88
85
  * site says so rather than one of them 404ing in production.
89
86
  */
90
- export declare function apiUrl(href: ApiHref): string;
87
+ export declare function apiUrl(href: ApiRoute): string;
91
88
  /** What a page module contributes: its `searchParams` export, or nothing. For the generated file. */
92
89
  export type SearchExportOf<M> = M extends {
93
90
  searchParams: infer S;
@@ -140,7 +137,7 @@ export type LooseSearch = Record<string, Scalar | readonly (string | number)[]>;
140
137
  * The search params a link to `H` may carry.
141
138
  *
142
139
  * The page's schema input when `H` is one declared route with a schema;
143
- * otherwise anything. "One route" matters: `path as Href` is every route at
140
+ * otherwise anything. "One route" matters: `path as Route` is every route at
144
141
  * once, and a link that could go anywhere cannot be held to one page's schema.
145
142
  */
146
143
  export type SearchFor<H extends string> = Unregistered extends true ? LooseSearch : IsUnion<H> extends true ? LooseSearch : [PatternOf<H>] extends [never] ? LooseSearch : SchemaFor<PatternOf<H>> extends undefined ? LooseSearch : LinkInputOf<SchemaFor<PatternOf<H>>>;
@@ -166,5 +163,5 @@ export declare function withSearch(path: string, search: object | undefined): st
166
163
  * `Link` has the same check on its own `search` prop. This is for `visit`,
167
164
  * `prefetch`, `redirect` and anything else that wants the finished string.
168
165
  */
169
- export declare function href<H extends Href>(path: H, ...rest: {} extends SearchFor<H> ? [search?: SearchFor<H>] : [search: SearchFor<H>]): Href;
166
+ export declare function href<H extends Route>(path: H, ...rest: {} extends SearchFor<H> ? [search?: SearchFor<H>] : [search: SearchFor<H>]): Route;
170
167
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string }\n ? R\n : string;\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false;\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P;\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute =\n | `${string}://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Href}`.\n */\n/**\n * A url this app answers - a page or a route.ts - or one that deliberately\n * leaves it. One type, as Next's `Route` is one type: what `<Link href>`,\n * `visit()` and `redirect()` take. A route.ts is a full navigation rather\n * than a payload fetch, and never prefetched - the client knows which urls\n * are routes and treats a link to one as the anchor it is - so the type\n * does not have to keep them apart to keep a hover from running one.\n *\n * `Route` is the same type under the name Next uses, so a port keeps the\n * word it already has.\n */\nexport type Href = Unregistered extends true\n ? string\n : | Filled<RoutePattern>\n | `${Filled<RoutePattern>}?${string}`\n | `${Filled<RoutePattern>}#${string}`\n | Filled<ApiPattern>\n | `${Filled<ApiPattern>}?${string}`\n | OffRoute;\n\nexport type Route = Href;\n\n// ── Api routes ───────────────────────────────────────────────────────────────\n//\n// Their own union rather than part of Href, because they are not pages and a\n// link to one is almost always a mistake — an <a href=\"/api/orders\"> navigates\n// the browser away to a json document. Keeping them apart means `Link` refuses\n// an api url and `apiUrl()` refuses a page, which is the pair of mistakes worth\n// catching.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterApi {}\n\n// ── Regions a page can re-render on its own ──────────────────────────────────\n//\n// Every `section('name', …)` the build found, and every `@slot` directory.\n// `revalidate('orders')` is checked against them, so a name that matches no\n// section - a typo, or a section renamed since - stops compiling instead of\n// being refused by the renderer at runtime.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterRegions {}\n\n/** The region names the build found: section names and slot names. */\nexport type RegionName = RegisterRegions extends {\n regions: infer R extends string;\n}\n ? R\n : string;\n\ntype NoRegions = string extends RegionName ? true : false;\n\n/**\n * What `revalidate()` takes: the page, the whole document, or a region by\n * name. `string` until the build has written the types.\n */\nexport type RevalidateTarget = NoRegions extends true\n ? string\n : \"page\" | \"all\" | RegionName;\n\n/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string }\n ? R\n : string;\n\ntype NoApis = string extends ApiPattern ? true : false;\n\n/**\n * A url an api route in this app answers.\n *\n * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed\n * because that is how a GET is parameterised.\n */\nexport type ApiHref = NoApis extends true\n ? string\n : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;\n\n/** A url a route.ts answers, alone: what `apiUrl()` narrows a fetch to. */\nexport type ApiRoute = ApiHref;\n\n/**\n * An api url, checked against the routes the build found.\n *\n * await fetch(apiUrl(`/api/orders/${id}`))\n *\n * A function rather than a bare type so it can be used inline at a call site\n * that is typed `string` — `fetch` takes any string, so nothing would check the\n * argument without somewhere to put the type. It returns what it was given.\n *\n * Wrong path, and it stops compiling. Renamed the directory, and every call\n * site says so rather than one of them 404ing in production.\n */\nexport function apiUrl(href: ApiHref): string {\n return href;\n}\n\n// ── Search params, typed per route ───────────────────────────────────────────\n//\n// A page that exports a `searchParams` schema has said what its query string\n// means. The generated file records that schema per pattern:\n//\n// interface Register {\n// search: { '/search': SearchExportOf<typeof import('../src/app/search/page')> }\n// }\n//\n// and from there a link to `/search` is checked against the same schema the\n// page parses with — a `page` that must be a number is a number on the link,\n// a `q` the page requires is required to write the link, and a key the page\n// never reads does not compile. One schema, both ends. A route that exports\n// none takes anything; an href that is not a single route (computed, cast, or\n// off-site) takes anything too, because there is nothing to check it against.\n\n/** What a page module contributes: its `searchParams` export, or nothing. For the generated file. */\nexport type SearchExportOf<M> = M extends { searchParams: infer S }\n ? S\n : undefined;\n\ntype SearchMap = Register extends { search: infer M } ? M : {};\n\ntype StripQuery<H extends string> = H extends `${infer P}?${string}`\n ? P\n : H extends `${infer P}#${string}`\n ? P\n : H;\n\n/** The pattern a written href belongs to: `/posts/hello` is `/posts/[slug]`. */\ntype PatternOf<H extends string> = RoutePattern extends infer P\n ? P extends string\n ? StripQuery<H> extends Filled<P>\n ? P\n : never\n : never\n : never;\n\ntype IsUnion<T, U = T> = T extends unknown\n ? [U] extends [T]\n ? false\n : true\n : never;\n\ntype SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined;\n\ntype InputOf<S> = S extends { \"~standard\": { types?: { input: infer I } } }\n ? I\n : never;\ntype OutputOf<S> = S extends { \"~standard\": { types?: { output: infer O } } }\n ? O\n : never;\n\ntype OptionalKeys<T> = {\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * What a link may write for a schema: the keys the schema's input requires\n * are required, the rest optional, and every value is the schema's OUTPUT\n * type. Output rather than input because `z.coerce.number()` takes `unknown`\n * in - that is what coercion means - and a link typed by it would accept\n * `page: 'two'`. The output is the number the page will actually see.\n */\ntype LinkInputOf<S> = Simplify<\n { [K in RequiredKeys<InputOf<S>> & keyof OutputOf<S>]: OutputOf<S>[K] } & {\n [K in OptionalKeys<InputOf<S>> & keyof OutputOf<S>]?: OutputOf<S>[K];\n }\n>;\n\ntype Scalar = string | number | boolean | null | undefined;\n\n/** What a link may carry when nothing declares otherwise. */\nexport type LooseSearch = Record<string, Scalar | readonly (string | number)[]>;\n\n/**\n * The search params a link to `H` may carry.\n *\n * The page's schema input when `H` is one declared route with a schema;\n * otherwise anything. \"One route\" matters: `path as Href` is every route at\n * once, and a link that could go anywhere cannot be held to one page's schema.\n */\nexport type SearchFor<H extends string> = Unregistered extends true\n ? LooseSearch\n : IsUnion<H> extends true\n ? LooseSearch\n : [PatternOf<H>] extends [never]\n ? LooseSearch\n : SchemaFor<PatternOf<H>> extends undefined\n ? LooseSearch\n : LinkInputOf<SchemaFor<PatternOf<H>>>;\n\n/**\n * The `search` prop, required exactly when the page's schema has a required\n * key. A page that needs `q` is not reachable without one, so the link that\n * omits it is the bug — caught here rather than on the page's error boundary.\n */\nexport type SearchProp<H extends string> =\n {} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> };\n\n/** A query string from an object: scalars stringified, arrays repeated, null and undefined dropped. */\nexport function searchString(search: object): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n if (value === null || value === undefined) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item));\n } else {\n params.set(key, String(value));\n }\n }\n\n return params.toString();\n}\n\n/** `path` with `search` appended, keeping any query and hash already on it. */\nexport function withSearch(path: string, search: object | undefined): string {\n if (!search) return path;\n\n const hashAt = path.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : path.slice(hashAt);\n const before = hashAt === -1 ? path : path.slice(0, hashAt);\n const queryAt = before.indexOf(\"?\");\n const base = queryAt === -1 ? before : before.slice(0, queryAt);\n const existing = queryAt === -1 ? \"\" : before.slice(queryAt + 1);\n const added = searchString(search);\n const query = [existing, added].filter(Boolean).join(\"&\");\n\n return query ? `${base}?${query}${hash}` : `${base}${hash}`;\n}\n\n/**\n * A typed url with its search params, for the places that take a string.\n *\n * visit(href('/search', { q: 'shoes', page: 2 }))\n *\n * `Link` has the same check on its own `search` prop. This is for `visit`,\n * `prefetch`, `redirect` and anything else that wants the finished string.\n */\nexport function href<H extends Href>(\n path: H,\n ...rest: {} extends SearchFor<H>\n ? [search?: SearchFor<H>]\n : [search: SearchFor<H>]\n): Href {\n return withSearch(path, rest[0] as object | undefined) as Href;\n}\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,kEAAkE;AAClE,6EAA6E;AAC7E,8EAA8E;AAC9E,8CAA8C;AAC9C,EAAE;AACF,8EAA8E;AAC9E,sCAAsC;AACtC,EAAE;AACF,4CAA4C;AAC5C,2DAA2D;AAC3D,MAAM;AACN,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,kBAAkB;AA4HlB;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,MAAM,CAAC,IAAc;IACnC,OAAO,IAAI,CAAC;AACd,CAAC;AA0GD,uGAAuG;AACvG,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAEpD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAA0B;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE1D,OAAO,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,IAAI,CAClB,IAAO,EACP,GAAG,IAEuB;IAE1B,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAuB,CAAU,CAAC;AAClE,CAAC","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string }\n ? R\n : string;\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false;\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P;\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute =\n | `${string}://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Route}`.\n */\n/**\n * A url this app answers - a page or a route.ts - or one that deliberately\n * leaves it. One type, as Next's `Route` is one type: what `<Link href>`,\n * `visit()` and `redirect()` take. A route.ts is a full navigation rather\n * than a payload fetch, and never prefetched - the client knows which urls\n * are routes and treats a link to one as the anchor it is - so the type\n * does not have to keep them apart to keep a hover from running one.\n *\n * `Route` is the same type under the name Next uses, so a port keeps the\n * word it already has.\n */\nexport type Route = Unregistered extends true\n ? string\n : | Filled<RoutePattern>\n | `${Filled<RoutePattern>}?${string}`\n | `${Filled<RoutePattern>}#${string}`\n | Filled<ApiPattern>\n | `${Filled<ApiPattern>}?${string}`\n | OffRoute;\n\n// ── Api routes ───────────────────────────────────────────────────────────────\n//\n// Their own union rather than part of Route, because they are not pages and a\n// link to one is almost always a mistake — an <a href=\"/api/orders\"> navigates\n// the browser away to a json document. Keeping them apart means `Link` refuses\n// an api url and `apiUrl()` refuses a page, which is the pair of mistakes worth\n// catching.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterApi {}\n\n// ── Regions a page can re-render on its own ──────────────────────────────────\n//\n// Every `section('name', …)` the build found, and every `@slot` directory.\n// `revalidate('orders')` is checked against them, so a name that matches no\n// section - a typo, or a section renamed since - stops compiling instead of\n// being refused by the renderer at runtime.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterRegions {}\n\n/** The region names the build found: section names and slot names. */\nexport type RegionName = RegisterRegions extends {\n regions: infer R extends string;\n}\n ? R\n : string;\n\ntype NoRegions = string extends RegionName ? true : false;\n\n/**\n * What `revalidate()` takes: the page, the whole document, or a region by\n * name. `string` until the build has written the types.\n */\nexport type RevalidateTarget = NoRegions extends true\n ? string\n : \"page\" | \"all\" | RegionName;\n\n/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string }\n ? R\n : string;\n\ntype NoApis = string extends ApiPattern ? true : false;\n\n/**\n * A url an api route in this app answers.\n *\n * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed\n * because that is how a GET is parameterised.\n */\nexport type ApiRoute = NoApis extends true\n ? string\n : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;\n\n/**\n * An api url, checked against the routes the build found.\n *\n * await fetch(apiUrl(`/api/orders/${id}`))\n *\n * A function rather than a bare type so it can be used inline at a call site\n * that is typed `string` — `fetch` takes any string, so nothing would check the\n * argument without somewhere to put the type. It returns what it was given.\n *\n * Wrong path, and it stops compiling. Renamed the directory, and every call\n * site says so rather than one of them 404ing in production.\n */\nexport function apiUrl(href: ApiRoute): string {\n return href;\n}\n\n// ── Search params, typed per route ───────────────────────────────────────────\n//\n// A page that exports a `searchParams` schema has said what its query string\n// means. The generated file records that schema per pattern:\n//\n// interface Register {\n// search: { '/search': SearchExportOf<typeof import('../src/app/search/page')> }\n// }\n//\n// and from there a link to `/search` is checked against the same schema the\n// page parses with — a `page` that must be a number is a number on the link,\n// a `q` the page requires is required to write the link, and a key the page\n// never reads does not compile. One schema, both ends. A route that exports\n// none takes anything; an href that is not a single route (computed, cast, or\n// off-site) takes anything too, because there is nothing to check it against.\n\n/** What a page module contributes: its `searchParams` export, or nothing. For the generated file. */\nexport type SearchExportOf<M> = M extends { searchParams: infer S }\n ? S\n : undefined;\n\ntype SearchMap = Register extends { search: infer M } ? M : {};\n\ntype StripQuery<H extends string> = H extends `${infer P}?${string}`\n ? P\n : H extends `${infer P}#${string}`\n ? P\n : H;\n\n/** The pattern a written href belongs to: `/posts/hello` is `/posts/[slug]`. */\ntype PatternOf<H extends string> = RoutePattern extends infer P\n ? P extends string\n ? StripQuery<H> extends Filled<P>\n ? P\n : never\n : never\n : never;\n\ntype IsUnion<T, U = T> = T extends unknown\n ? [U] extends [T]\n ? false\n : true\n : never;\n\ntype SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined;\n\ntype InputOf<S> = S extends { \"~standard\": { types?: { input: infer I } } }\n ? I\n : never;\ntype OutputOf<S> = S extends { \"~standard\": { types?: { output: infer O } } }\n ? O\n : never;\n\ntype OptionalKeys<T> = {\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * What a link may write for a schema: the keys the schema's input requires\n * are required, the rest optional, and every value is the schema's OUTPUT\n * type. Output rather than input because `z.coerce.number()` takes `unknown`\n * in - that is what coercion means - and a link typed by it would accept\n * `page: 'two'`. The output is the number the page will actually see.\n */\ntype LinkInputOf<S> = Simplify<\n { [K in RequiredKeys<InputOf<S>> & keyof OutputOf<S>]: OutputOf<S>[K] } & {\n [K in OptionalKeys<InputOf<S>> & keyof OutputOf<S>]?: OutputOf<S>[K];\n }\n>;\n\ntype Scalar = string | number | boolean | null | undefined;\n\n/** What a link may carry when nothing declares otherwise. */\nexport type LooseSearch = Record<string, Scalar | readonly (string | number)[]>;\n\n/**\n * The search params a link to `H` may carry.\n *\n * The page's schema input when `H` is one declared route with a schema;\n * otherwise anything. \"One route\" matters: `path as Route` is every route at\n * once, and a link that could go anywhere cannot be held to one page's schema.\n */\nexport type SearchFor<H extends string> = Unregistered extends true\n ? LooseSearch\n : IsUnion<H> extends true\n ? LooseSearch\n : [PatternOf<H>] extends [never]\n ? LooseSearch\n : SchemaFor<PatternOf<H>> extends undefined\n ? LooseSearch\n : LinkInputOf<SchemaFor<PatternOf<H>>>;\n\n/**\n * The `search` prop, required exactly when the page's schema has a required\n * key. A page that needs `q` is not reachable without one, so the link that\n * omits it is the bug — caught here rather than on the page's error boundary.\n */\nexport type SearchProp<H extends string> =\n {} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> };\n\n/** A query string from an object: scalars stringified, arrays repeated, null and undefined dropped. */\nexport function searchString(search: object): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n if (value === null || value === undefined) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item));\n } else {\n params.set(key, String(value));\n }\n }\n\n return params.toString();\n}\n\n/** `path` with `search` appended, keeping any query and hash already on it. */\nexport function withSearch(path: string, search: object | undefined): string {\n if (!search) return path;\n\n const hashAt = path.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : path.slice(hashAt);\n const before = hashAt === -1 ? path : path.slice(0, hashAt);\n const queryAt = before.indexOf(\"?\");\n const base = queryAt === -1 ? before : before.slice(0, queryAt);\n const existing = queryAt === -1 ? \"\" : before.slice(queryAt + 1);\n const added = searchString(search);\n const query = [existing, added].filter(Boolean).join(\"&\");\n\n return query ? `${base}?${query}${hash}` : `${base}${hash}`;\n}\n\n/**\n * A typed url with its search params, for the places that take a string.\n *\n * visit(href('/search', { q: 'shoes', page: 2 }))\n *\n * `Link` has the same check on its own `search` prop. This is for `visit`,\n * `prefetch`, `redirect` and anything else that wants the finished string.\n */\nexport function href<H extends Route>(\n path: H,\n ...rest: {} extends SearchFor<H>\n ? [search?: SearchFor<H>]\n : [search: SearchFor<H>]\n): Route {\n return withSearch(path, rest[0] as object | undefined) as Route;\n}\n"]}
package/dist/useSsr.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * The module with its directive taken out, for the environment it runs in.
3
+ *
4
+ * Where server components render the directive turns the module into
5
+ * proxies; in the ssr environment the module is the real thing and the
6
+ * directive is a string with no meaning left - which the bundler says so
7
+ * about, once per build, as MODULE_LEVEL_DIRECTIVE "may not be preserved".
8
+ * Nothing needed preserving. Removed, and the warning with it. Null when the
9
+ * code has no directive.
10
+ */
11
+ export declare function withoutSsrDirective(code: string): string | null;
1
12
  export declare const SSR_GUIDE = "https://docs.rsc-kit.dev/guides/emails";
2
13
  export declare function hasUseSsr(code: string): boolean;
3
14
  export declare class UseSsrError extends Error {
package/dist/useSsr.js CHANGED
@@ -21,6 +21,21 @@ import { basename, relative } from "node:path";
21
21
  * export named, at build and in dev, rather than proxied into nonsense.
22
22
  */
23
23
  const DIRECTIVE = /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*["']use ssr["']/;
24
+ /**
25
+ * The module with its directive taken out, for the environment it runs in.
26
+ *
27
+ * Where server components render the directive turns the module into
28
+ * proxies; in the ssr environment the module is the real thing and the
29
+ * directive is a string with no meaning left - which the bundler says so
30
+ * about, once per build, as MODULE_LEVEL_DIRECTIVE "may not be preserved".
31
+ * Nothing needed preserving. Removed, and the warning with it. Null when the
32
+ * code has no directive.
33
+ */
34
+ export function withoutSsrDirective(code) {
35
+ if (!DIRECTIVE.test(code))
36
+ return null;
37
+ return code.replace(/(^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)["']use ssr["'];?[ \t]*\n?/, "$1");
38
+ }
24
39
  export const SSR_GUIDE = "https://docs.rsc-kit.dev/guides/emails";
25
40
  export function hasUseSsr(code) {
26
41
  return DIRECTIVE.test(code.slice(0, 2048));
@@ -1 +1 @@
1
- {"version":3,"file":"useSsr.js","sourceRoot":"","sources":["../src/useSsr.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename, relative } from \"node:path\";\n\n/**\n * \"use ssr\": a module that runs in the ssr environment and is called from\n * where server components render.\n *\n * react-dom/server cannot run where server components render. React's\n * server build refuses to load it, and the refusal is right: the renderer\n * needs the client build's internals, and the components it would render\n * import that same server `react` - no useState, no useContext. A template\n * and the call that renders it have to load together somewhere React DOM's\n * server renderer can: the ssr environment, which every app here already\n * has. plugin-rsc exposes that as a call to write by hand,\n * `import.meta.viteRsc.import('./render', { environment: 'ssr' })`, and\n * nobody should have to. So a directive names the module instead, the way\n * \"use client\" names one, and this rewrites it for the server-components\n * environment into proxies of its exports that call across. Everything\n * else imports it normally.\n *\n * The exports are functions and the calls are async, because the answer\n * crosses environments as a promise. Anything else is refused with the\n * export named, at build and in dev, rather than proxied into nonsense.\n */\nconst DIRECTIVE = /^\\s*(?:\\/\\/[^\\n]*\\n|\\/\\*[\\s\\S]*?\\*\\/\\s*)*[\"']use ssr[\"']/;\n\nexport const SSR_GUIDE = \"https://docs.rsc-kit.dev/guides/emails\";\n\nexport function hasUseSsr(code: string): boolean {\n return DIRECTIVE.test(code.slice(0, 2048));\n}\n\nexport class UseSsrError extends Error {}\n\n/** Comments blanked, so an `export` inside one is not read as one. */\nfunction withoutComments(code: string): string {\n return code\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, (m) => m.replace(/[^\\n]/g, \" \"))\n .replace(\n /(^|[^:\\\\])\\/\\/[^\\n]*/g,\n (m, lead: string) => lead + \" \".repeat(m.length - lead.length),\n );\n}\n\nconst NAME = \"[A-Za-z_$][\\\\w$]*\";\nconst NON_FUNCTION = /^\\s*(?:new\\b|[\"'`\\d[{]|true\\b|false\\b|null\\b)/;\n\n/** The runtime exports a \"use ssr\" module can offer: named functions, and a default. */\nexport function ssrExports(\n code: string,\n file: string,\n): { named: string[]; hasDefault: boolean } {\n const src = withoutComments(code);\n const named = new Set<string>();\n let hasDefault = false;\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(async\\\\s+)?function\\\\s*\\\\*?\\\\s*(${NAME})`,\n \"gm\",\n ),\n )) {\n if (!m[1]) {\n throw new UseSsrError(\n `\"use ssr\": \\`${m[2]}\\` in ${file} is called from another environment, and the answer crosses as a promise. Make it an async function.`,\n );\n }\n\n named.add(m[2]);\n }\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(?:const|let|var)\\\\s+(${NAME})\\\\s*(?::[^=\\\\n]*)?=([^\\\\n]*)`,\n \"gm\",\n ),\n )) {\n if (NON_FUNCTION.test(m[2])) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; \\`${m[1]}\\` in ${file} is a value. Put it in a module without the directive.`,\n );\n }\n\n named.add(m[1]);\n }\n\n if (/^[ \\t]*export\\s+(?:const|let|var)\\s*[[{]/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} destructures an export. Export each function by its own name; that is what gets called across.`,\n );\n }\n\n if (/^[ \\t]*export\\s+(?:abstract\\s+)?(?:class|enum)\\b/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; ${file} exports a class or enum.`,\n );\n }\n\n if (/^[ \\t]*export\\s*(?:\\{|\\*)/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} re-exports. Export the functions where they are declared; \\`export { … }\\` and \\`export *\\` cannot be called across.`,\n );\n }\n\n const dflt = /^[ \\t]*export\\s+default\\s+(async\\s+)?(function\\b)?/m.exec(src);\n\n if (dflt) {\n if (dflt[2] && !dflt[1]) {\n throw new UseSsrError(\n `\"use ssr\": the default export of ${file} is called from another environment. Make it an async function.`,\n );\n }\n\n hasDefault = true;\n }\n\n return { named: [...named], hasDefault };\n}\n\n/**\n * The module the server-components environment gets in place of a \"use ssr\"\n * one: its exports, as async proxies that call the real module where it\n * runs. Null when the module carries no directive.\n */\nexport function ssrProxyModule(\n code: string,\n id: string,\n root: string = process.cwd(),\n): string | null {\n if (!hasUseSsr(code)) return null;\n\n const path = id.split(\"?\")[0];\n const file = relative(root, path);\n const { named, hasDefault } = ssrExports(code, file);\n\n return (\n [\n `// \"use ssr\": ${file} runs in the ssr environment, where React DOM's server renderer can. These call across.`,\n `const __rsc_kit_ssr = import.meta.viteRsc.import(${JSON.stringify(\"./\" + basename(path))}, { environment: 'ssr' });`,\n ...named.map(\n (name) =>\n `export const ${name} = async (...args) => (await __rsc_kit_ssr).${name}(...args);`,\n ),\n ...(hasDefault\n ? [\n \"export default async (...args) => (await __rsc_kit_ssr).default(...args);\",\n ]\n : []),\n ].join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Any of react-dom's server renderer entries. */\nexport const SERVER_RENDERER = /^react-dom\\/server(?:\\.[a-z]+)?$/;\n\n/** What a module gets in place of react-dom/server where server components render. */\nexport function serverRendererMessage(importer: string | null): string {\n return (\n \"react-dom/server cannot run where server components render: React's server build has no client internals for it, and the components it would render import that same React.\" +\n (importer ? ` Imported by ${importer}.` : \"\") +\n ' Put the rendering - the template and the call - in a module that starts with \"use ssr\": it runs in the ssr environment, and its exports are called from here as async functions. ' +\n SSR_GUIDE\n );\n}\n"]}
1
+ {"version":3,"file":"useSsr.js","sourceRoot":"","sources":["../src/useSsr.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename, relative } from \"node:path\";\n\n/**\n * \"use ssr\": a module that runs in the ssr environment and is called from\n * where server components render.\n *\n * react-dom/server cannot run where server components render. React's\n * server build refuses to load it, and the refusal is right: the renderer\n * needs the client build's internals, and the components it would render\n * import that same server `react` - no useState, no useContext. A template\n * and the call that renders it have to load together somewhere React DOM's\n * server renderer can: the ssr environment, which every app here already\n * has. plugin-rsc exposes that as a call to write by hand,\n * `import.meta.viteRsc.import('./render', { environment: 'ssr' })`, and\n * nobody should have to. So a directive names the module instead, the way\n * \"use client\" names one, and this rewrites it for the server-components\n * environment into proxies of its exports that call across. Everything\n * else imports it normally.\n *\n * The exports are functions and the calls are async, because the answer\n * crosses environments as a promise. Anything else is refused with the\n * export named, at build and in dev, rather than proxied into nonsense.\n */\nconst DIRECTIVE = /^\\s*(?:\\/\\/[^\\n]*\\n|\\/\\*[\\s\\S]*?\\*\\/\\s*)*[\"']use ssr[\"']/;\n\n/**\n * The module with its directive taken out, for the environment it runs in.\n *\n * Where server components render the directive turns the module into\n * proxies; in the ssr environment the module is the real thing and the\n * directive is a string with no meaning left - which the bundler says so\n * about, once per build, as MODULE_LEVEL_DIRECTIVE \"may not be preserved\".\n * Nothing needed preserving. Removed, and the warning with it. Null when the\n * code has no directive.\n */\nexport function withoutSsrDirective(code: string): string | null {\n if (!DIRECTIVE.test(code)) return null;\n\n return code.replace(/(^\\s*(?:\\/\\/[^\\n]*\\n|\\/\\*[\\s\\S]*?\\*\\/\\s*)*)[\"']use ssr[\"'];?[ \\t]*\\n?/, \"$1\");\n}\n\nexport const SSR_GUIDE = \"https://docs.rsc-kit.dev/guides/emails\";\n\nexport function hasUseSsr(code: string): boolean {\n return DIRECTIVE.test(code.slice(0, 2048));\n}\n\nexport class UseSsrError extends Error {}\n\n/** Comments blanked, so an `export` inside one is not read as one. */\nfunction withoutComments(code: string): string {\n return code\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, (m) => m.replace(/[^\\n]/g, \" \"))\n .replace(\n /(^|[^:\\\\])\\/\\/[^\\n]*/g,\n (m, lead: string) => lead + \" \".repeat(m.length - lead.length),\n );\n}\n\nconst NAME = \"[A-Za-z_$][\\\\w$]*\";\nconst NON_FUNCTION = /^\\s*(?:new\\b|[\"'`\\d[{]|true\\b|false\\b|null\\b)/;\n\n/** The runtime exports a \"use ssr\" module can offer: named functions, and a default. */\nexport function ssrExports(\n code: string,\n file: string,\n): { named: string[]; hasDefault: boolean } {\n const src = withoutComments(code);\n const named = new Set<string>();\n let hasDefault = false;\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(async\\\\s+)?function\\\\s*\\\\*?\\\\s*(${NAME})`,\n \"gm\",\n ),\n )) {\n if (!m[1]) {\n throw new UseSsrError(\n `\"use ssr\": \\`${m[2]}\\` in ${file} is called from another environment, and the answer crosses as a promise. Make it an async function.`,\n );\n }\n\n named.add(m[2]);\n }\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(?:const|let|var)\\\\s+(${NAME})\\\\s*(?::[^=\\\\n]*)?=([^\\\\n]*)`,\n \"gm\",\n ),\n )) {\n if (NON_FUNCTION.test(m[2])) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; \\`${m[1]}\\` in ${file} is a value. Put it in a module without the directive.`,\n );\n }\n\n named.add(m[1]);\n }\n\n if (/^[ \\t]*export\\s+(?:const|let|var)\\s*[[{]/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} destructures an export. Export each function by its own name; that is what gets called across.`,\n );\n }\n\n if (/^[ \\t]*export\\s+(?:abstract\\s+)?(?:class|enum)\\b/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; ${file} exports a class or enum.`,\n );\n }\n\n if (/^[ \\t]*export\\s*(?:\\{|\\*)/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} re-exports. Export the functions where they are declared; \\`export { … }\\` and \\`export *\\` cannot be called across.`,\n );\n }\n\n const dflt = /^[ \\t]*export\\s+default\\s+(async\\s+)?(function\\b)?/m.exec(src);\n\n if (dflt) {\n if (dflt[2] && !dflt[1]) {\n throw new UseSsrError(\n `\"use ssr\": the default export of ${file} is called from another environment. Make it an async function.`,\n );\n }\n\n hasDefault = true;\n }\n\n return { named: [...named], hasDefault };\n}\n\n/**\n * The module the server-components environment gets in place of a \"use ssr\"\n * one: its exports, as async proxies that call the real module where it\n * runs. Null when the module carries no directive.\n */\nexport function ssrProxyModule(\n code: string,\n id: string,\n root: string = process.cwd(),\n): string | null {\n if (!hasUseSsr(code)) return null;\n\n const path = id.split(\"?\")[0];\n const file = relative(root, path);\n const { named, hasDefault } = ssrExports(code, file);\n\n return (\n [\n `// \"use ssr\": ${file} runs in the ssr environment, where React DOM's server renderer can. These call across.`,\n `const __rsc_kit_ssr = import.meta.viteRsc.import(${JSON.stringify(\"./\" + basename(path))}, { environment: 'ssr' });`,\n ...named.map(\n (name) =>\n `export const ${name} = async (...args) => (await __rsc_kit_ssr).${name}(...args);`,\n ),\n ...(hasDefault\n ? [\n \"export default async (...args) => (await __rsc_kit_ssr).default(...args);\",\n ]\n : []),\n ].join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Any of react-dom's server renderer entries. */\nexport const SERVER_RENDERER = /^react-dom\\/server(?:\\.[a-z]+)?$/;\n\n/** What a module gets in place of react-dom/server where server components render. */\nexport function serverRendererMessage(importer: string | null): string {\n return (\n \"react-dom/server cannot run where server components render: React's server build has no client internals for it, and the components it would render import that same React.\" +\n (importer ? ` Imported by ${importer}.` : \"\") +\n ' Put the rendering - the template and the call - in a module that starts with \"use ssr\": it runs in the ssr environment, and its exports are called from here as async functions. ' +\n SSR_GUIDE\n );\n}\n"]}
package/dist/vite.js CHANGED
@@ -30,7 +30,7 @@ import { serverImportsOfClientPackages } from "./clientImports.js";
30
30
  import { automaticSitemap, METADATA_ROUTES, ROOT_FILES, rootFileType, } from "./metadataRoutes.js";
31
31
  import { ownHosts } from "./hostRouting.js";
32
32
  import { unrollBarrelImports } from "./barrelImports.js";
33
- import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
33
+ import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, withoutSsrDirective } from "./useSsr.js";
34
34
  import { httpHostCalls } from "./hostCalls.js";
35
35
  import { clientPackages, importsServerRenderer, packageDir as installedPackageDir, packageEntryInGraph } from "./clientPackages.js";
36
36
  // Resolved once per rscKit() call. One build runs in one process, so these are
@@ -585,10 +585,16 @@ function routeManifest() {
585
585
  },
586
586
  routes,
587
587
  intercepts,
588
- apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
588
+ apis: [...apiRoutes.values()].map(({ name, methods, generated, absPath }) => ({
589
589
  name,
590
590
  segments: urlSegments(name),
591
591
  methods,
592
+ // The OpenAPI document is synthesised from nothing on disk.
593
+ source: generated
594
+ ? generated.file
595
+ ? relative(sourceDir, generated.file).replace(/\\/g, "/")
596
+ : undefined
597
+ : relative(sourceDir, absPath).replace(/\\/g, "/"),
592
598
  // A synthesised robots.txt or sitemap.xml runs no guard: it exists to be
593
599
  // read by anyone, and a guard on the root would 401 the crawler.
594
600
  middleware: generated ? [] : ancestors(name, "middleware"),
@@ -1735,7 +1741,7 @@ function renderRouteTypes(manifest) {
1735
1741
  "",
1736
1742
  "// `export {}` is load-bearing: in a file with no import or export,",
1737
1743
  "// `declare module` *replaces* the real module rather than augmenting it,",
1738
- "// and Href and route() vanish from it with no error to explain why.",
1744
+ "// and Route and route() vanish from it with no error to explain why.",
1739
1745
  "export {}",
1740
1746
  "",
1741
1747
  "declare module '@rsc-kit/core/routes' {",
@@ -3403,8 +3409,18 @@ async function runMiddleware(component: string, props: Record<string, unknown> =
3403
3409
  }
3404
3410
 
3405
3411
  // Sequential and awaited, outermost first: an outer guard refusing means
3406
- // the inner one should never have been asked.
3407
- await guard(props)
3412
+ // the inner one should never have been asked. A directory may declare
3413
+ // several as a list - reused checks imported from one place - and they
3414
+ // run in the order written, stopping at the first refusal.
3415
+ for (const check of Array.isArray(guard) ? guard : [guard]) {
3416
+ if (typeof check !== 'function') {
3417
+ throw new Error(
3418
+ 'Route middleware ' + name + ' exports something that is not a function or a list of them.',
3419
+ )
3420
+ }
3421
+
3422
+ await check(props)
3423
+ }
3408
3424
  }
3409
3425
  }
3410
3426
 
@@ -4926,10 +4942,16 @@ function useSsrModules() {
4926
4942
  return {
4927
4943
  name: "rsc-kit:use-ssr",
4928
4944
  enforce: "pre",
4929
- applyToEnvironment: (environment) => environment.name === "rsc",
4945
+ applyToEnvironment: (environment) => environment.name === "rsc" || environment.name === "ssr",
4930
4946
  transform(code, id) {
4931
4947
  if (!code.includes("use ssr"))
4932
4948
  return;
4949
+ // In the environment the module runs in, the directive has done its
4950
+ // work and is a string the bundler warns about. Out it goes.
4951
+ if (this.environment.name === "ssr") {
4952
+ const stripped = withoutSsrDirective(code);
4953
+ return stripped === null ? undefined : { code: stripped, map: null };
4954
+ }
4933
4955
  try {
4934
4956
  const proxy = ssrProxyModule(code, id);
4935
4957
  return proxy === null ? undefined : { code: proxy, map: null };
@@ -5281,10 +5303,12 @@ export function rscKit(options = {}) {
5281
5303
  // reflect-metadata runs it after the chunk that checks for it.
5282
5304
  // Traced, each is copied into .output/server/node_modules and
5283
5305
  // imported by the built server the way its author expected.
5306
+ // Only the ones the project has. Nitro's tracer says so, once per
5307
+ // name, for every entry it cannot find - and a list of every native
5308
+ // package anyone might install is mostly ones this app does not.
5284
5309
  nitro.options.traceDeps = [
5285
5310
  ...(nitro.options.traceDeps ?? []),
5286
- ...DEFAULT_SERVER_EXTERNALS,
5287
- ...(options.serverExternalPackages ?? []),
5311
+ ...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].filter((name) => installedPackageDir(name, projectRoot) !== null || packageEntryInGraph(projectRoot, name) !== null),
5288
5312
  ];
5289
5313
  // The assets, precompressed at build and served with their encoding
5290
5314
  // by Nitro's own static handler; the host gzips the rest as it