@rsc-kit/core 0.17.0 → 0.18.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.
Files changed (52) hide show
  1. package/dist/barrelImports.d.ts +6 -0
  2. package/dist/barrelImports.js +93 -0
  3. package/dist/barrelImports.js.map +1 -0
  4. package/dist/events.d.ts +57 -0
  5. package/dist/events.js +113 -0
  6. package/dist/events.js.map +1 -0
  7. package/dist/host.d.ts +16 -6
  8. package/dist/host.js +219 -124
  9. package/dist/host.js.map +1 -1
  10. package/dist/hostCalls.d.ts +27 -0
  11. package/dist/hostCalls.js +127 -12
  12. package/dist/hostCalls.js.map +1 -1
  13. package/dist/hostRouting.d.ts +53 -0
  14. package/dist/hostRouting.js +100 -0
  15. package/dist/hostRouting.js.map +1 -0
  16. package/dist/js/DefaultRouteError.d.ts +3 -0
  17. package/dist/js/DefaultRouteError.js +61 -0
  18. package/dist/js/DefaultRouteError.js.map +1 -0
  19. package/dist/js/queryClient.d.ts +2 -1
  20. package/dist/js/queryClient.js +0 -12
  21. package/dist/js/queryClient.js.map +1 -1
  22. package/dist/js/router.d.ts +2 -2
  23. package/dist/js/router.js.map +1 -1
  24. package/dist/js/useEvents.d.ts +25 -0
  25. package/dist/js/useEvents.js +78 -0
  26. package/dist/js/useEvents.js.map +1 -0
  27. package/dist/js/usePolling.d.ts +29 -0
  28. package/dist/js/usePolling.js +141 -0
  29. package/dist/js/usePolling.js.map +1 -0
  30. package/dist/manifest.d.ts +18 -1
  31. package/dist/manifest.js.map +1 -1
  32. package/dist/metadataRoutes.d.ts +20 -0
  33. package/dist/metadataRoutes.js +35 -0
  34. package/dist/metadataRoutes.js.map +1 -1
  35. package/dist/prerender.js +1 -0
  36. package/dist/prerender.js.map +1 -1
  37. package/dist/redirect.d.ts +16 -2
  38. package/dist/redirect.js +10 -19
  39. package/dist/redirect.js.map +1 -1
  40. package/dist/revalidate.d.ts +6 -4
  41. package/dist/revalidate.js +6 -5
  42. package/dist/revalidate.js.map +1 -1
  43. package/dist/routes.d.ts +16 -3
  44. package/dist/routes.js +5 -5
  45. package/dist/routes.js.map +1 -1
  46. package/dist/routing.d.ts +2 -2
  47. package/dist/routing.js +47 -25
  48. package/dist/routing.js.map +1 -1
  49. package/dist/vite.d.ts +42 -2
  50. package/dist/vite.js +333 -5
  51. package/dist/vite.js.map +1 -1
  52. package/package.json +13 -1
package/dist/routes.js CHANGED
@@ -59,14 +59,14 @@ export function searchString(search) {
59
59
  export function withSearch(path, search) {
60
60
  if (!search)
61
61
  return path;
62
- const hashAt = path.indexOf('#');
63
- const hash = hashAt === -1 ? '' : path.slice(hashAt);
62
+ const hashAt = path.indexOf("#");
63
+ const hash = hashAt === -1 ? "" : path.slice(hashAt);
64
64
  const before = hashAt === -1 ? path : path.slice(0, hashAt);
65
- const queryAt = before.indexOf('?');
65
+ const queryAt = before.indexOf("?");
66
66
  const base = queryAt === -1 ? before : before.slice(0, queryAt);
67
- const existing = queryAt === -1 ? '' : before.slice(queryAt + 1);
67
+ const existing = queryAt === -1 ? "" : before.slice(queryAt + 1);
68
68
  const added = searchString(search);
69
- const query = [existing, added].filter(Boolean).join('&');
69
+ const query = [existing, added].filter(Boolean).join("&");
70
70
  return query ? `${base}?${query}${hash}` : `${base}${hash}`;
71
71
  }
72
72
  /**
@@ -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 } ? R : 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 = `${string}://${string}` | `mailto:${string}` | `tel:${string}` | `#${string}` | `?${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 */\nexport type Href = Unregistered extends true\n ? string\n : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | OffRoute\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/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string } ? R : 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/**\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 } ? S : undefined\n\ntype SearchMap = Register extends { search: infer M } ? M : {}\n\ntype StripQuery<H extends string> = H extends `${infer P}?${string}` ? P : H extends `${infer P}#${string}` ? P : 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 ? ([U] extends [T] ? false : true) : never\n\ntype SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined\n\ntype InputOf<S> = S extends { '~standard': { types?: { input: infer I } } } ? I : never\ntype OutputOf<S> = S extends { '~standard': { types?: { output: infer O } } } ? O : never\n\ntype OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[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> = {} 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> ? [search?: SearchFor<H>] : [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":"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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 */\nexport type Href = Unregistered extends true\n ? string\n : | Filled<RoutePattern>\n | `${Filled<RoutePattern>}?${string}`\n | `${Filled<RoutePattern>}#${string}`\n | OffRoute;\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/**\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"]}
package/dist/routing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ManifestApiRoute, ManifestRoute, RouteManifest } from './manifest.js';
1
+ import type { ManifestApiRoute, ManifestRoute, RouteManifest } from "./manifest.js";
2
2
  /** A matched route and the params its url segments bound. */
3
3
  export interface MatchedRoute {
4
4
  route: ManifestRoute;
@@ -27,7 +27,7 @@ export declare function matchApiRoute(manifest: RouteManifest, pathname: string)
27
27
  * HEAD is included whenever GET is, because it is answered by GET.
28
28
  */
29
29
  export declare function allowFor(route: ManifestApiRoute): string;
30
- export declare function matchRoute(manifest: RouteManifest, pathname: string): MatchedRoute | null;
30
+ export declare function matchRoute(manifest: RouteManifest, pathname: string, host?: string | null): MatchedRoute | null;
31
31
  /**
32
32
  * The interceptor registered for a slot, if this url has one.
33
33
  *
package/dist/routing.js CHANGED
@@ -15,12 +15,17 @@ function indexRoutes(routes) {
15
15
  const scored = [];
16
16
  const exact = new Map();
17
17
  for (const route of routes) {
18
- const score = route.segments.reduce((n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0), 0);
18
+ const score = route.segments.reduce((n, s) => n +
19
+ (s.type === "static"
20
+ ? 2
21
+ : s.type === "param" || s.type === "host"
22
+ ? 1
23
+ : 0), 0);
19
24
  scored.push({ route, score });
20
25
  // An all-static route matches exactly one path, with the highest score any
21
26
  // route can have for that path. First one in wins, as in the scan.
22
- if (route.segments.every((s) => s.type === 'static')) {
23
- const key = route.segments.map((s) => s.value).join('/');
27
+ if (route.segments.every((s) => s.type === "static")) {
28
+ const key = route.segments.map((s) => s.value).join("/");
24
29
  if (!exact.has(key))
25
30
  exact.set(key, route);
26
31
  }
@@ -44,9 +49,9 @@ function indexedApis(manifest) {
44
49
  return found;
45
50
  }
46
51
  export function matchApiRoute(manifest, pathname) {
47
- const parts = pathname.split('/').filter(Boolean);
52
+ const parts = pathname.split("/").filter(Boolean);
48
53
  const { scored, exact } = indexedApis(manifest);
49
- const direct = exact.get(parts.join('/'));
54
+ const direct = exact.get(parts.join("/"));
50
55
  if (direct)
51
56
  return { route: direct, params: {} };
52
57
  let best = null;
@@ -68,15 +73,15 @@ export function matchApiRoute(manifest, pathname) {
68
73
  * HEAD is included whenever GET is, because it is answered by GET.
69
74
  */
70
75
  export function allowFor(route) {
71
- const methods = route.methods.includes('HEAD') || !route.methods.includes('GET')
76
+ const methods = route.methods.includes("HEAD") || !route.methods.includes("GET")
72
77
  ? route.methods
73
- : [...route.methods, 'HEAD'];
74
- return methods.join(', ');
78
+ : [...route.methods, "HEAD"];
79
+ return methods.join(", ");
75
80
  }
76
- export function matchRoute(manifest, pathname) {
77
- const parts = pathname.split('/').filter(Boolean);
81
+ export function matchRoute(manifest, pathname, host = null) {
82
+ const parts = pathname.split("/").filter(Boolean);
78
83
  const { scored, exact } = indexed(manifest);
79
- const direct = exact.get(parts.join('/'));
84
+ const direct = exact.get(parts.join("/"));
80
85
  if (direct)
81
86
  return { route: direct, params: {} };
82
87
  let best = null;
@@ -86,7 +91,7 @@ export function matchRoute(manifest, pathname) {
86
91
  for (const { route, score } of scored) {
87
92
  if (score <= bestScore)
88
93
  continue;
89
- const bound = bindSegments(route.segments, parts);
94
+ const bound = bindSegments(route.segments, parts, host);
90
95
  if (!bound)
91
96
  continue;
92
97
  best = { route, params: bound };
@@ -102,7 +107,7 @@ export function matchRoute(manifest, pathname) {
102
107
  * modal opens over.
103
108
  */
104
109
  export function matchIntercept(manifest, pathname, slot) {
105
- const parts = pathname.split('/').filter(Boolean);
110
+ const parts = pathname.split("/").filter(Boolean);
106
111
  let best = null;
107
112
  let bestScore = -1;
108
113
  for (const intercept of manifest.intercepts) {
@@ -117,7 +122,7 @@ export function matchIntercept(manifest, pathname, slot) {
117
122
  // the guard is chosen by one while the content comes from the other, so a
118
123
  // url could be checked against the unguarded route and rendered from the
119
124
  // guarded one.
120
- const score = intercept.segments.reduce((n, segment) => n + (segment.type === 'static' ? 2 : segment.type === 'param' ? 1 : 0), 0);
125
+ const score = intercept.segments.reduce((n, segment) => n + (segment.type === "static" ? 2 : segment.type === "param" ? 1 : 0), 0);
121
126
  if (score > bestScore) {
122
127
  best = { component: intercept.component, params };
123
128
  bestScore = score;
@@ -125,22 +130,35 @@ export function matchIntercept(manifest, pathname, slot) {
125
130
  }
126
131
  return best;
127
132
  }
128
- function bindSegments(segments, parts) {
133
+ function bindSegments(segments, parts, host = null) {
129
134
  const params = {};
130
135
  for (let i = 0; i < segments.length; i++) {
131
136
  const segment = segments[i];
132
- if (segment.type === 'catchAll') {
137
+ if (segment.type === "catchAll") {
133
138
  // Swallows the rest, including none of it.
134
- params[segment.value] = parts.slice(i).join('/');
139
+ params[segment.value] = parts.slice(i).join("/");
135
140
  return params;
136
141
  }
137
142
  if (i >= parts.length)
138
143
  return null;
139
- if (segment.type === 'static') {
144
+ // The leading part is the host's when one was given: a static directory
145
+ // of that name takes it (admin.example.com -> app/admin), and so does a
146
+ // host segment. A path part never binds a host segment, so
147
+ // example.com/nope is not a tenant called "nope".
148
+ const fromHost = i === 0 && host !== null;
149
+ if (segment.type === "static") {
140
150
  if (parts[i] !== segment.value)
141
151
  return null;
142
152
  continue;
143
153
  }
154
+ if (segment.type === "host") {
155
+ if (!fromHost)
156
+ return null;
157
+ params[segment.value] = parts[i];
158
+ continue;
159
+ }
160
+ if (fromHost)
161
+ return null;
144
162
  params[segment.value] = decodeURIComponent(parts[i]);
145
163
  }
146
164
  return segments.length === parts.length ? params : null;
@@ -156,9 +174,11 @@ function bindSegments(segments, parts) {
156
174
  export function sharedDepth(held, chain) {
157
175
  if (!held)
158
176
  return 0;
159
- const mounted = held.split(',').filter(Boolean);
177
+ const mounted = held.split(",").filter(Boolean);
160
178
  let shared = 0;
161
- while (shared < mounted.length && shared < chain.length && mounted[shared] === chain[shared]) {
179
+ while (shared < mounted.length &&
180
+ shared < chain.length &&
181
+ mounted[shared] === chain[shared]) {
162
182
  shared++;
163
183
  }
164
184
  return shared;
@@ -177,7 +197,9 @@ export function sharedDepth(held, chain) {
177
197
  */
178
198
  export function retentionKey(path, interceptSlot) {
179
199
  const normalised = normalisePath(path);
180
- return interceptSlot ? `__intercept:${interceptSlot}:${normalised}` : normalised;
200
+ return interceptSlot
201
+ ? `__intercept:${interceptSlot}:${normalised}`
202
+ : normalised;
181
203
  }
182
204
  /**
183
205
  * The same page written two ways is one key.
@@ -192,12 +214,12 @@ export function retentionKey(path, interceptSlot) {
192
214
  function normalisePath(path) {
193
215
  // Absolute urls reach this from the initial page, which is identified by
194
216
  // href rather than by the path a link would use.
195
- const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, '');
196
- const [pathname, rest = ''] = splitQuery(withoutOrigin);
197
- return (pathname.replace(/\/+$/, '') || '/') + rest;
217
+ const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, "");
218
+ const [pathname, rest = ""] = splitQuery(withoutOrigin);
219
+ return (pathname.replace(/\/+$/, "") || "/") + rest;
198
220
  }
199
221
  function splitQuery(url) {
200
222
  const cut = url.search(/[?#]/);
201
- return cut === -1 ? [url, ''] : [url.slice(0, cut), url.slice(cut)];
223
+ return cut === -1 ? [url, ""] : [url.slice(0, cut), url.slice(cut)];
202
224
  }
203
225
  //# sourceMappingURL=routing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routing.js","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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","sourcesContent":["// Matching a url to a route, and deciding how much of the page to send.\n//\n// Pure: no Request, no Response, no filesystem. That is what lets the same\n// implementation serve three callers — the request handler, the prerenderer\n// enumerating what to build, and the generated server bundle, which embeds its\n// own route table and answers these without a manifest being passed around at\n// all.\n//\n// Every host would otherwise write this, and would get the same two things\n// wrong: answering /docs/new with [slug], and treating the layout chain as a\n// set rather than a sequence.\n\nimport type { ManifestApiRoute, ManifestRoute, RouteManifest, RouteSegment } from './manifest.js'\n\n/** A matched route and the params its url segments bound. */\nexport interface MatchedRoute {\n route: ManifestRoute\n params: Record<string, string>\n}\n\n/**\n * Match a pathname against the manifest's segments.\n *\n * The manifest stores segments rather than a pattern string, because the\n * pattern is the host's dialect — Laravel writes `{slug}`, Hono writes\n * `:slug`, and neither is the build's business. Matching them directly means\n * no dialect at all.\n *\n * Static segments beat dynamic ones at the same position: /docs/new is the\n * page called new, not the page called [slug] with slug=new.\n */\n/** A `route.ts` the url matched, and the params it bound. */\nexport interface MatchedApiRoute {\n route: ManifestApiRoute\n params: Record<string, string>\n}\n\n/**\n * The api route a url answers, if one does.\n *\n * Exact segment binding only, scored the same way pages are: more static\n * segments wins, a catch-all is weakest. Reusing that is the point — an api\n * route takes `[id]` and `[...rest]` because the matcher was already there.\n */\n/**\n * A manifest's routes, indexed once.\n *\n * Every request used to score every route with a reduce over its segments,\n * which at fifty routes is nothing and at five hundred is the request. The\n * score is a property of the route, so it is computed once; and a path that\n * is entirely static - most of them - is answered from a map without\n * touching the list at all. Keyed by the manifest object, so a host that\n * swaps manifests gets a fresh index and a dropped one is collected.\n */\ninterface Indexed<R extends { segments: RouteSegment[] }> {\n scored: { route: R; score: number }[]\n exact: Map<string, R>\n}\n\nconst indexes = new WeakMap<object, Indexed<ManifestRoute>>()\nconst apiIndexes = new WeakMap<object, Indexed<ManifestApiRoute>>()\n\nfunction indexRoutes<R extends { segments: RouteSegment[] }>(routes: R[]): Indexed<R> {\n const scored: { route: R; score: number }[] = []\n const exact = new Map<string, R>()\n\n for (const route of routes) {\n const score = route.segments.reduce(\n (n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0),\n 0,\n )\n\n scored.push({ route, score })\n\n // An all-static route matches exactly one path, with the highest score any\n // route can have for that path. First one in wins, as in the scan.\n if (route.segments.every((s) => s.type === 'static')) {\n const key = route.segments.map((s) => s.value).join('/')\n\n if (!exact.has(key)) exact.set(key, route)\n }\n }\n\n return { scored, exact }\n}\n\nfunction indexed(manifest: RouteManifest): Indexed<ManifestRoute> {\n let found = indexes.get(manifest)\n\n if (!found) {\n found = indexRoutes(manifest.routes)\n indexes.set(manifest, found)\n }\n\n return found\n}\n\nfunction indexedApis(manifest: RouteManifest): Indexed<ManifestApiRoute> {\n let found = apiIndexes.get(manifest)\n\n if (!found) {\n found = indexRoutes(manifest.apis ?? [])\n apiIndexes.set(manifest, found)\n }\n\n return found\n}\n\nexport function matchApiRoute(\n manifest: RouteManifest,\n pathname: string,\n): MatchedApiRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n const { scored, exact } = indexedApis(manifest)\n const direct = exact.get(parts.join('/'))\n\n if (direct) return { route: direct, params: {} }\n\n let best: MatchedApiRoute | null = null\n let bestScore = -1\n\n for (const { route, score } of scored) {\n if (score <= bestScore) continue\n\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n best = { route, params: bound }\n bestScore = score\n }\n\n return best\n}\n\n/**\n * What a 405 should say this route does answer.\n *\n * HEAD is included whenever GET is, because it is answered by GET.\n */\nexport function allowFor(route: ManifestApiRoute): string {\n const methods = route.methods.includes('HEAD') || !route.methods.includes('GET')\n ? route.methods\n : [...route.methods, 'HEAD']\n\n return methods.join(', ')\n}\n\nexport function matchRoute(manifest: RouteManifest, pathname: string): MatchedRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n const { scored, exact } = indexed(manifest)\n const direct = exact.get(parts.join('/'))\n\n if (direct) return { route: direct, params: {} }\n\n let best: MatchedRoute | null = null\n let bestScore = -1\n\n // More static segments wins; a catch-all is the weakest possible match. A\n // route that could not beat the best so far is not worth binding.\n for (const { route, score } of scored) {\n if (score <= bestScore) continue\n\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n best = { route, params: bound }\n bestScore = score\n }\n\n return best\n}\n\n/**\n * The interceptor registered for a slot, if this url has one.\n *\n * Matched against the url being navigated *to*: `(.)posts/[slug]` intercepts\n * /posts/anything, and the params it binds are the target's, not the page the\n * modal opens over.\n */\nexport function matchIntercept(\n manifest: RouteManifest,\n pathname: string,\n slot: string,\n): { component: string; params: Record<string, string> } | null {\n const parts = pathname.split('/').filter(Boolean)\n let best: { component: string; params: Record<string, string> } | null = null\n let bestScore = -1\n\n for (const intercept of manifest.intercepts) {\n if (intercept.slot !== slot) continue\n\n const params = bindSegments(intercept.segments, parts)\n\n if (!params) continue\n\n // Scored exactly as matchRoute scores, and for the same reason: a static\n // segment beats a dynamic one at the same position. Taking the first match\n // instead made the two disagree about which route a url belongs to — and\n // the guard is chosen by one while the content comes from the other, so a\n // url could be checked against the unguarded route and rendered from the\n // guarded one.\n const score = intercept.segments.reduce(\n (n, segment) => n + (segment.type === 'static' ? 2 : segment.type === 'param' ? 1 : 0),\n 0,\n )\n\n if (score > bestScore) {\n best = { component: intercept.component, params }\n bestScore = score\n }\n }\n\n return best\n}\n\nfunction bindSegments(segments: RouteSegment[], parts: string[]): Record<string, string> | null {\n const params: Record<string, string> = {}\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]\n\n if (segment.type === 'catchAll') {\n // Swallows the rest, including none of it.\n params[segment.value] = parts.slice(i).join('/')\n\n return params\n }\n\n if (i >= parts.length) return null\n\n if (segment.type === 'static') {\n if (parts[i] !== segment.value) return null\n continue\n }\n\n params[segment.value] = decodeURIComponent(parts[i])\n }\n\n return segments.length === parts.length ? params : null\n}\n\n/**\n * How much of the layout chain the client already holds.\n *\n * It sends the chain outermost-first; the shared prefix is what it can keep.\n * Answering with a whole document instead is not merely wasteful — it replaces\n * the root, and replacing the root unmounts every page retained behind the\n * current one, so a half-typed form does not survive going back.\n */\nexport function sharedDepth(held: string | null, chain: string[]): number {\n if (!held) return 0\n\n const mounted = held.split(',').filter(Boolean)\n let shared = 0\n\n while (shared < mounted.length && shared < chain.length && mounted[shared] === chain[shared]) {\n shared++\n }\n\n return shared\n}\n\n/**\n * What a page is remembered as, for retention and for the prefetch cache.\n *\n * The same url intercepted and not intercepted are two different things to go\n * back to — a modal over the feed, and the post on its own page — so they\n * cannot share a key or restoring one returns the other.\n *\n * Shared because both halves compute it: the host stores under this key and\n * the client looks under it. Written out at each of the seven places that\n * needed it, they only had to disagree once, and the symptom is a navigation\n * that silently rebuilds a page it was holding.\n */\nexport function retentionKey(path: string, interceptSlot?: string | null): string {\n const normalised = normalisePath(path)\n\n return interceptSlot ? `__intercept:${interceptSlot}:${normalised}` : normalised\n}\n\n/**\n * The same page written two ways is one key.\n *\n * A static host serves /orders as a directory, so the browser's url ends in a\n * slash while the build wrote the page down as /orders. Left unequal, the\n * entry retained for a page is never the one looked up on the way back: it\n * stays in the document, hidden, while a second copy is fetched and rendered\n * beside it — so the form you were filling in is there, and not the one you\n * are looking at.\n */\nfunction normalisePath(path: string): string {\n // Absolute urls reach this from the initial page, which is identified by\n // href rather than by the path a link would use.\n const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^/]+/i, '')\n const [pathname, rest = ''] = splitQuery(withoutOrigin)\n\n return (pathname.replace(/\\/+$/, '') || '/') + rest\n}\n\nfunction splitQuery(url: string): [string, string] {\n const cut = url.search(/[?#]/)\n\n return cut === -1 ? [url, ''] : [url.slice(0, cut), url.slice(cut)]\n}\n"]}
1
+ {"version":3,"file":"routing.js","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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","sourcesContent":["// Matching a url to a route, and deciding how much of the page to send.\n//\n// Pure: no Request, no Response, no filesystem. That is what lets the same\n// implementation serve three callers — the request handler, the prerenderer\n// enumerating what to build, and the generated server bundle, which embeds its\n// own route table and answers these without a manifest being passed around at\n// all.\n//\n// Every host would otherwise write this, and would get the same two things\n// wrong: answering /docs/new with [slug], and treating the layout chain as a\n// set rather than a sequence.\n\nimport type {\n ManifestApiRoute,\n ManifestRoute,\n RouteManifest,\n RouteSegment,\n} from \"./manifest.js\";\n\n/** A matched route and the params its url segments bound. */\nexport interface MatchedRoute {\n route: ManifestRoute;\n params: Record<string, string>;\n}\n\n/**\n * Match a pathname against the manifest's segments.\n *\n * The manifest stores segments rather than a pattern string, because the\n * pattern is the host's dialect — Laravel writes `{slug}`, Hono writes\n * `:slug`, and neither is the build's business. Matching them directly means\n * no dialect at all.\n *\n * Static segments beat dynamic ones at the same position: /docs/new is the\n * page called new, not the page called [slug] with slug=new.\n */\n/** A `route.ts` the url matched, and the params it bound. */\nexport interface MatchedApiRoute {\n route: ManifestApiRoute;\n params: Record<string, string>;\n}\n\n/**\n * The api route a url answers, if one does.\n *\n * Exact segment binding only, scored the same way pages are: more static\n * segments wins, a catch-all is weakest. Reusing that is the point — an api\n * route takes `[id]` and `[...rest]` because the matcher was already there.\n */\n/**\n * A manifest's routes, indexed once.\n *\n * Every request used to score every route with a reduce over its segments,\n * which at fifty routes is nothing and at five hundred is the request. The\n * score is a property of the route, so it is computed once; and a path that\n * is entirely static - most of them - is answered from a map without\n * touching the list at all. Keyed by the manifest object, so a host that\n * swaps manifests gets a fresh index and a dropped one is collected.\n */\ninterface Indexed<R extends { segments: RouteSegment[] }> {\n scored: { route: R; score: number }[];\n exact: Map<string, R>;\n}\n\nconst indexes = new WeakMap<object, Indexed<ManifestRoute>>();\nconst apiIndexes = new WeakMap<object, Indexed<ManifestApiRoute>>();\n\nfunction indexRoutes<R extends { segments: RouteSegment[] }>(\n routes: R[],\n): Indexed<R> {\n const scored: { route: R; score: number }[] = [];\n const exact = new Map<string, R>();\n\n for (const route of routes) {\n const score = route.segments.reduce(\n (n, s) =>\n n +\n (s.type === \"static\"\n ? 2\n : s.type === \"param\" || s.type === \"host\"\n ? 1\n : 0),\n 0,\n );\n\n scored.push({ route, score });\n\n // An all-static route matches exactly one path, with the highest score any\n // route can have for that path. First one in wins, as in the scan.\n if (route.segments.every((s) => s.type === \"static\")) {\n const key = route.segments.map((s) => s.value).join(\"/\");\n\n if (!exact.has(key)) exact.set(key, route);\n }\n }\n\n return { scored, exact };\n}\n\nfunction indexed(manifest: RouteManifest): Indexed<ManifestRoute> {\n let found = indexes.get(manifest);\n\n if (!found) {\n found = indexRoutes(manifest.routes);\n indexes.set(manifest, found);\n }\n\n return found;\n}\n\nfunction indexedApis(manifest: RouteManifest): Indexed<ManifestApiRoute> {\n let found = apiIndexes.get(manifest);\n\n if (!found) {\n found = indexRoutes(manifest.apis ?? []);\n apiIndexes.set(manifest, found);\n }\n\n return found;\n}\n\nexport function matchApiRoute(\n manifest: RouteManifest,\n pathname: string,\n): MatchedApiRoute | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n const { scored, exact } = indexedApis(manifest);\n const direct = exact.get(parts.join(\"/\"));\n\n if (direct) return { route: direct, params: {} };\n\n let best: MatchedApiRoute | null = null;\n let bestScore = -1;\n\n for (const { route, score } of scored) {\n if (score <= bestScore) continue;\n\n const bound = bindSegments(route.segments, parts);\n\n if (!bound) continue;\n\n best = { route, params: bound };\n bestScore = score;\n }\n\n return best;\n}\n\n/**\n * What a 405 should say this route does answer.\n *\n * HEAD is included whenever GET is, because it is answered by GET.\n */\nexport function allowFor(route: ManifestApiRoute): string {\n const methods =\n route.methods.includes(\"HEAD\") || !route.methods.includes(\"GET\")\n ? route.methods\n : [...route.methods, \"HEAD\"];\n\n return methods.join(\", \");\n}\n\nexport function matchRoute(\n manifest: RouteManifest,\n pathname: string,\n host: string | null = null,\n): MatchedRoute | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n const { scored, exact } = indexed(manifest);\n const direct = exact.get(parts.join(\"/\"));\n\n if (direct) return { route: direct, params: {} };\n\n let best: MatchedRoute | null = null;\n let bestScore = -1;\n\n // More static segments wins; a catch-all is the weakest possible match. A\n // route that could not beat the best so far is not worth binding.\n for (const { route, score } of scored) {\n if (score <= bestScore) continue;\n\n const bound = bindSegments(route.segments, parts, host);\n\n if (!bound) continue;\n\n best = { route, params: bound };\n bestScore = score;\n }\n\n return best;\n}\n\n/**\n * The interceptor registered for a slot, if this url has one.\n *\n * Matched against the url being navigated *to*: `(.)posts/[slug]` intercepts\n * /posts/anything, and the params it binds are the target's, not the page the\n * modal opens over.\n */\nexport function matchIntercept(\n manifest: RouteManifest,\n pathname: string,\n slot: string,\n): { component: string; params: Record<string, string> } | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n let best: { component: string; params: Record<string, string> } | null = null;\n let bestScore = -1;\n\n for (const intercept of manifest.intercepts) {\n if (intercept.slot !== slot) continue;\n\n const params = bindSegments(intercept.segments, parts);\n\n if (!params) continue;\n\n // Scored exactly as matchRoute scores, and for the same reason: a static\n // segment beats a dynamic one at the same position. Taking the first match\n // instead made the two disagree about which route a url belongs to — and\n // the guard is chosen by one while the content comes from the other, so a\n // url could be checked against the unguarded route and rendered from the\n // guarded one.\n const score = intercept.segments.reduce(\n (n, segment) =>\n n + (segment.type === \"static\" ? 2 : segment.type === \"param\" ? 1 : 0),\n 0,\n );\n\n if (score > bestScore) {\n best = { component: intercept.component, params };\n bestScore = score;\n }\n }\n\n return best;\n}\n\nfunction bindSegments(\n segments: RouteSegment[],\n parts: string[],\n host: string | null = null,\n): Record<string, string> | null {\n const params: Record<string, string> = {};\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n if (segment.type === \"catchAll\") {\n // Swallows the rest, including none of it.\n params[segment.value] = parts.slice(i).join(\"/\");\n\n return params;\n }\n\n if (i >= parts.length) return null;\n\n // The leading part is the host's when one was given: a static directory\n // of that name takes it (admin.example.com -> app/admin), and so does a\n // host segment. A path part never binds a host segment, so\n // example.com/nope is not a tenant called \"nope\".\n const fromHost = i === 0 && host !== null;\n\n if (segment.type === \"static\") {\n if (parts[i] !== segment.value) return null;\n continue;\n }\n\n if (segment.type === \"host\") {\n if (!fromHost) return null;\n\n params[segment.value] = parts[i];\n continue;\n }\n\n if (fromHost) return null;\n\n params[segment.value] = decodeURIComponent(parts[i]);\n }\n\n return segments.length === parts.length ? params : null;\n}\n\n/**\n * How much of the layout chain the client already holds.\n *\n * It sends the chain outermost-first; the shared prefix is what it can keep.\n * Answering with a whole document instead is not merely wasteful — it replaces\n * the root, and replacing the root unmounts every page retained behind the\n * current one, so a half-typed form does not survive going back.\n */\nexport function sharedDepth(held: string | null, chain: string[]): number {\n if (!held) return 0;\n\n const mounted = held.split(\",\").filter(Boolean);\n let shared = 0;\n\n while (\n shared < mounted.length &&\n shared < chain.length &&\n mounted[shared] === chain[shared]\n ) {\n shared++;\n }\n\n return shared;\n}\n\n/**\n * What a page is remembered as, for retention and for the prefetch cache.\n *\n * The same url intercepted and not intercepted are two different things to go\n * back to — a modal over the feed, and the post on its own page — so they\n * cannot share a key or restoring one returns the other.\n *\n * Shared because both halves compute it: the host stores under this key and\n * the client looks under it. Written out at each of the seven places that\n * needed it, they only had to disagree once, and the symptom is a navigation\n * that silently rebuilds a page it was holding.\n */\nexport function retentionKey(\n path: string,\n interceptSlot?: string | null,\n): string {\n const normalised = normalisePath(path);\n\n return interceptSlot\n ? `__intercept:${interceptSlot}:${normalised}`\n : normalised;\n}\n\n/**\n * The same page written two ways is one key.\n *\n * A static host serves /orders as a directory, so the browser's url ends in a\n * slash while the build wrote the page down as /orders. Left unequal, the\n * entry retained for a page is never the one looked up on the way back: it\n * stays in the document, hidden, while a second copy is fetched and rendered\n * beside it — so the form you were filling in is there, and not the one you\n * are looking at.\n */\nfunction normalisePath(path: string): string {\n // Absolute urls reach this from the initial page, which is identified by\n // href rather than by the path a link would use.\n const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^/]+/i, \"\");\n const [pathname, rest = \"\"] = splitQuery(withoutOrigin);\n\n return (pathname.replace(/\\/+$/, \"\") || \"/\") + rest;\n}\n\nfunction splitQuery(url: string): [string, string] {\n const cut = url.search(/[?#]/);\n\n return cut === -1 ? [url, \"\"] : [url.slice(0, cut), url.slice(cut)];\n}\n"]}
package/dist/vite.d.ts CHANGED
@@ -5,6 +5,42 @@ export interface RscKitOptions {
5
5
  projectRoot?: string;
6
6
  /** Directory holding the app/ route tree. Defaults to `src`. */
7
7
  sourceDir?: string;
8
+ /**
9
+ * Say what built the site: `<meta name="generator" content="rsc-kit">` in
10
+ * every document and `X-Powered-By: rsc-kit` on every response. The name
11
+ * only, never the version - a version in every response is what a
12
+ * vulnerability scanner filters on. On by default; `false` for a team
13
+ * whose policy strips every framework identifier.
14
+ *
15
+ * `X-RSC-Kit: stored | rendered | shell` - how a response was answered, the
16
+ * header a developer reads in the Network tab - is not this and is always
17
+ * sent: it names no product, and a CDN rule or a check can key on it.
18
+ */
19
+ identify?: boolean;
20
+ /**
21
+ * Unroll named imports from a barrel package on the server, in development.
22
+ *
23
+ * `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports
24
+ * fifteen hundred icons, one module each; the browser gets the package
25
+ * pre-bundled, the server environments do not, so Vite loaded every icon
26
+ * module on the first request. The import is rewritten to the module that
27
+ * owns the name, read from the barrel itself. On by default; `false` to
28
+ * leave imports as written.
29
+ */
30
+ barrelImports?: boolean;
31
+ /**
32
+ * The site's own hosts, beyond the one in the root layout's metadataBase.
33
+ *
34
+ * A request from any other host is routed with the host in front of the
35
+ * path: `admin.example.com/users` reaches `app/admin/users/page.tsx`, the
36
+ * file `example.com/admin/users` reaches by path; `acme.example.com/`
37
+ * reaches `app/[domain]/page.tsx` with `domain: "acme"`, and a custom
38
+ * domain `acme.com/` the same file with `domain: "acme.com"`. The site's
39
+ * own hosts keep path routing, so an app adds tenants without moving a
40
+ * file. `www.` of each is its own too. With no metadataBase and nothing
41
+ * here, every host is the site's own and nothing is prepended.
42
+ */
43
+ hosts?: string[];
8
44
  /**
9
45
  * Serve the app from a service worker, so it survives a reload with no
10
46
  * network at all.
@@ -94,8 +130,12 @@ export interface RscKitOptions {
94
130
  * because a second setting that could disagree with the first is a bug
95
131
  * waiting to be filed. `false` restores the plain 404.
96
132
  *
97
- * Development only. A built deployment's server.ts decides for itself what
98
- * to do with a url it does not own.
133
+ * The built server forwards too — the entry that carries this is the one
134
+ * Nitro bundles — with the origin read at build time from the project's
135
+ * .env, while host calls read it from the process at runtime. A build on a
136
+ * machine whose .env names a different backend than production's sets
137
+ * RSC_BACKEND for the build. The name predates that: it was the dev server's
138
+ * alone before Nitro built the server from the same entry.
99
139
  */
100
140
  devFallback?: string | false;
101
141
  /**