@rsc-kit/core 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/LoadingBoundary.d.ts +5 -0
- package/dist/js/LoadingBoundary.js +19 -0
- package/dist/js/LoadingBoundary.js.map +1 -0
- package/dist/js/fallbackReport.js +10 -8
- package/dist/js/fallbackReport.js.map +1 -1
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +80 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +142 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadataRoutes.d.ts +20 -0
- package/dist/metadataRoutes.js +35 -0
- package/dist/metadataRoutes.js.map +1 -1
- package/dist/prerender.js +2 -0
- package/dist/prerender.js.map +1 -1
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routeSchema.d.ts +49 -0
- package/dist/routeSchema.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +27 -3
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +53 -2
- package/dist/vite.js +503 -38
- package/dist/vite.js.map +1 -1
- package/package.json +15 -3
package/dist/revalidate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revalidate.js","sourceRoot":"","sources":["../src/revalidate.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,wEAAwE;AACxE,2EAA2E;AAC3E,8EAA8E;AAC9E,wEAAwE;AACxE,EAAE;AACF,iBAAiB;AACjB,4CAA4C;AAC5C,iCAAiC;AACjC,qEAAqE;AACrE,0BAA0B;AAC1B,MAAM;AACN,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,uCAAuC;
|
|
1
|
+
{"version":3,"file":"revalidate.js","sourceRoot":"","sources":["../src/revalidate.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,wEAAwE;AACxE,2EAA2E;AAC3E,8EAA8E;AAC9E,wEAAwE;AACxE,EAAE;AACF,iBAAiB;AACjB,4CAA4C;AAC5C,iCAAiC;AACjC,qEAAqE;AACrE,0BAA0B;AAC1B,MAAM;AACN,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,uCAAuC;AAUvC;;;;;;;;;;;GAWG;AACH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAE7D,MAAM,OAAO,GAAG,UAA8C,CAAC;AAE/D,IAAI,KAAK,GAAyB,IAAI,CAAC;AACvC,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;AAChC,4EAA4E;AAC5E,kCAAkC;AAClC,IAAI,GAA4B,OAAO;IAEvC,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAkD,CAAC;IAExE,IAAI,OAAO;QAAE,OAAO,IAAI,OAAO,EAAE,CAAC;IAElC,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAE/D,OAAO,IAAI,iBAAiB,EAAwB,CAAC;AACvD,CAAC;AAED,mEAAmE;AACnE,SAAS,KAAK;IACZ,OAAQ,OAAO,CAAC,KAAK,CAAuB,IAAI,IAAI,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,MAAwB;IACjD,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc;IAC5B,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,SAAS,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAA0C;IAE1C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,uEAAuE;QACvE,kEAAkE;QAClE,KAAK,KAAK,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACzC,OAAO,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,OAAO,MAAM,KAAK,EAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC","sourcesContent":["// Marking part of a page stale from inside a server action.\n//\n// The point is one round trip. An action that changes an order could tell the\n// browser to go and re-fetch something, but then the answer the user is\n// waiting on arrives before the screen is right, and the fix costs another\n// request. Marking instead lets the action's own answer carry the re-rendered\n// region back with it — the client applies it and never knows it asked.\n//\n// 'use server'\n// export async function addOrder(order) {\n// await orders.insert(order)\n// revalidate('orders') // the section or slot by that name\n// return { ok: true }\n// }\n//\n// Scoped to the action that is running, not global, because two requests can\n// be in flight at once and marking is per-request state. In Laravel this is a\n// `scoped()` binding on the container.\n\nimport type { RevalidateTarget } from \"./routes.js\";\n\n/** What this needs from a runtime: somewhere to keep per-action state. */\ninterface Scope {\n getStore(): Set<string> | undefined;\n run<T>(store: Set<string>, fn: () => T): T;\n}\n\n/**\n * One store, however many copies of this module exist.\n *\n * The app's actions are bundled into the server bundle; the host that runs\n * them is not. So this module is loaded twice, and two scopes mean the action\n * marks in one and the host reads the other — everything appears to work, the\n * action's answer simply never carries anything back, and no error is raised\n * on either side.\n *\n * The engine installs its host callable on globalThis for exactly this\n * reason. This is the same crossing.\n */\nconst SCOPE = Symbol.for(\"@rsc-kit/core.revalidation-scope\");\n\nconst globals = globalThis as Record<symbol | string, unknown>;\n\nlet ready: Promise<void> | null = null;\nlet warned = false;\n\n/**\n * AsyncLocalStorage, wherever this happens to be running.\n *\n * The global first, because that is where a Worker has it — and where the\n * engine puts it, since @vitejs/plugin-rsc assigns\n * `globalThis.AsyncLocalStorage` for React's edge build. Then the module,\n * which is where Node, Bun and Deno have it.\n *\n * There is no third branch on purpose. The engine bundle imports\n * `node:async_hooks` statically, so async context is a requirement of the\n * whole system rather than of this file: a runtime without it cannot load the\n * engine at all, and a fallback here would be code that can never run.\n */\nexport async function resolveScope(\n // Injectable so the branch a Worker takes can be tested from a runtime that\n // would otherwise never reach it.\n from: Record<string, unknown> = globals,\n): Promise<Scope> {\n const Ambient = from.AsyncLocalStorage as (new () => Scope) | undefined;\n\n if (Ambient) return new Ambient();\n\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n\n return new AsyncLocalStorage<Set<string>>() as Scope;\n}\n\n/** The scope, once resolved. Null before the first action runs. */\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null;\n}\n\n/**\n * Mark a region of the current page for re-rendering.\n *\n * `'page'` and `'all'` re-render the page or the whole document; any other\n * name is a section or a parallel slot. The names are typed to the ones the\n * build found, so a typo stops compiling; a name that reaches the renderer\n * unknown anyway is refused with the names it does know, rather than quietly\n * refreshing nothing.\n *\n * Outside an action this does nothing rather than throwing: it is reasonable\n * for shared code to mark, and unreasonable for that to fail when the same\n * function is called during an ordinary render.\n */\nexport function revalidate(target: RevalidateTarget): void {\n scope()?.getStore()?.add(target);\n}\n\n/** Whether anything is listening, for a host that wants to warn about the rest. */\nexport function isRevalidating(): boolean {\n return scope()?.getStore() !== undefined;\n}\n\n/**\n * Run an action with somewhere for its marks to go, and collect them.\n *\n * The marks are read *after* the action has run, because what it invalidated\n * is only known once its work is done.\n */\nexport async function withRevalidation<T>(\n run: (taken: () => string[]) => Promise<T>,\n): Promise<T> {\n if (!globals[SCOPE]) {\n // Resolved once and shared, for the same reason the scope itself is: a\n // second copy of this module must not end up with a second scope.\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved;\n });\n\n await ready;\n }\n\n const marked = new Set<string>();\n\n return await scope()!.run(marked, () => run(() => [...marked]));\n}\n"]}
|
package/dist/routeSchema.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApiPattern } from './routes.js';
|
|
1
2
|
/**
|
|
2
3
|
* Standard Schema, restated rather than depended on.
|
|
3
4
|
*
|
|
@@ -43,6 +44,54 @@ export interface PageProps<P = never, S = never> {
|
|
|
43
44
|
params: Promise<[P] extends [never] ? Record<string, string> : ParamsOf<P>>;
|
|
44
45
|
searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The names a pattern binds: `/api/greet/[name]` binds `name`, and
|
|
49
|
+
* `/docs/[...path]` binds `path` — one string, the slashes kept.
|
|
50
|
+
*/
|
|
51
|
+
type SegmentNames<P extends string> = P extends `${string}[...${infer N}]${infer Rest}` ? N | SegmentNames<Rest> : P extends `${string}[${infer N}]${infer Rest}` ? N | SegmentNames<Rest> : never;
|
|
52
|
+
/** What a route's params resolve to, from its pattern: `{ name: string }`. */
|
|
53
|
+
export type ApiParams<P extends string> = {
|
|
54
|
+
[K in SegmentNames<P>]: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The second argument of an api route's handler.
|
|
58
|
+
*
|
|
59
|
+
* Every field is a promise, because the engine hands them lazily - a route
|
|
60
|
+
* that never awaits its query string provably does not vary by it, so the
|
|
61
|
+
* build can store one answer - and a sync read of `params.name` has to be a
|
|
62
|
+
* compile error rather than a route that quietly answers 404 to everything.
|
|
63
|
+
*
|
|
64
|
+
* Name the route and the params are typed from its segments; the pattern is
|
|
65
|
+
* checked against the routes the build found, so a typo fails `tsc`:
|
|
66
|
+
*
|
|
67
|
+
* export async function GET(request: Request, { params }: RouteContext<'/api/greet/[name]'>) {
|
|
68
|
+
* const { name } = await params // string
|
|
69
|
+
* }
|
|
70
|
+
*
|
|
71
|
+
* Or hand it the route's own schemas, the way PageProps takes a page's:
|
|
72
|
+
*
|
|
73
|
+
* export const params = z.object({ id: z.coerce.number() })
|
|
74
|
+
* export async function GET(request: Request, { params }: RouteContext<typeof params>) {
|
|
75
|
+
* const { id } = await params // number
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* With neither, params is the raw segments and searchParams the raw
|
|
79
|
+
* URLSearchParams, still behind a promise.
|
|
80
|
+
*/
|
|
81
|
+
export interface RouteContext<P extends ApiPattern | StandardSchemaV1 = never, S extends StandardSchemaV1 = never, B extends StandardSchemaV1 = never> {
|
|
82
|
+
params: Promise<[
|
|
83
|
+
P
|
|
84
|
+
] extends [never] ? Record<string, string> : P extends ApiPattern ? ApiParams<P> : ParamsOf<P>>;
|
|
85
|
+
searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>;
|
|
86
|
+
/** Undefined for a method with no body; a promise of the parsed body otherwise. */
|
|
87
|
+
body: [B] extends [never] ? Promise<unknown> | undefined : Promise<ParamsOf<B>>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A whole handler, typed from the route it answers:
|
|
91
|
+
*
|
|
92
|
+
* export const GET: RouteHandler<'/api/greet/[name]'> = async (request, { params }) => …
|
|
93
|
+
*/
|
|
94
|
+
export type RouteHandler<P extends ApiPattern | StandardSchemaV1 = never, S extends StandardSchemaV1 = never, B extends StandardSchemaV1 = never> = (request: Request, context: RouteContext<P, S, B>) => Response | Promise<Response>;
|
|
46
95
|
/** A query string the page's own schema refused. */
|
|
47
96
|
export declare class SearchParamsError extends Error {
|
|
48
97
|
readonly errors: Record<string, string[]>;
|
package/dist/routeSchema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routeSchema.js","sourceRoot":"","sources":["../src/routeSchema.ts"],"names":[],"mappings":"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","sourcesContent":["// Typing and checking what a url carries.\n//\n// // src/app/posts/[slug]/page.tsx\n// export const params = z.object({ slug: z.string().min(1) })\n// export const searchParams = z.object({\n// page: z.coerce.number().int().min(1).default(1),\n// })\n//\n// Both are ordinary exports beside the page, read by the build the same way\n// `metadata` and `generateStaticParams` are. Any Standard Schema works — Zod,\n// Valibot, ArkType — because the schema is asked to validate itself and this\n// module never imports one.\n//\n// What it buys beyond types: `?page=3` arrives as the number 3 rather than the\n// string \"3\", a missing one arrives as the default rather than undefined, and\n// `?page=banana` is refused in one place instead of surviving as NaN into\n// whatever the page does with it.\n//\n// The two fail differently, and that is the point of having both:\n//\n// params a url that does not describe a page. /posts/ tells a\n// crawler and a cache 404, not 500, so this is notFound().\n// searchParams the page exists and the query was wrong, which is the\n// nearest error.tsx — the same place any other bad input goes.\n\nimport { NotFoundSignal } from './notFound.js'\n\n/**\n * Standard Schema, restated rather than depended on.\n *\n * It is an interface, and a package for it would be a dependency that ships\n * nothing. Kept to the shape this module reads.\n */\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>\n readonly types?: { readonly input: Input; readonly output: Output }\n }\n}\n\ninterface StandardSchemaResult<Output> {\n readonly value?: Output\n readonly issues?: ReadonlyArray<{\n readonly message: string\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>\n }>\n}\n\n/** What a page gets from its own `params` export, already parsed. */\nexport type ParamsOf<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never\n\n/**\n * The props a page with schemas receives.\n *\n * Written as a type rather than generated into a .d.ts because it needs no\n * build step to be correct: the schemas are exports of the same file, so a\n * page can refer to its own.\n *\n * export const params = z.object({ slug: z.string() })\n *\n * export default async function Page({ params: p }: PageProps<typeof params>) {\n * const { slug } = await p // string, not unknown\n * }\n */\nexport interface PageProps<P = never, S = never> {\n params: Promise<[P] extends [never] ? Record<string, string> : ParamsOf<P>>\n searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>\n}\n\n/**\n * The mark that says a refusal came from a route schema.\n *\n * Symbol.for for the reason every mark in this package is: the app's pages are\n * bundled apart from the engine, so each side gets its own copy of this class\n * and `instanceof` is false between them.\n */\nconst MARK = Symbol.for('@rsc-kit/core.route-input-error')\n\n/** A query string the page's own schema refused. */\nexport class SearchParamsError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('The query string is not valid for this page.')\n this.name = 'SearchParamsError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[MARK] = true\n }\n}\n\n/** Whether this is a refused query string, whichever copy of the class built it. */\nexport function isSearchParamsError(error: unknown): error is SearchParamsError {\n return (\n typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[MARK] === true\n )\n}\n\n/** The field an issue belongs to, joined so a nested one reads as it was named. */\nfunction fieldOf(path: NonNullable<StandardSchemaResult<unknown>['issues']>[number]['path']): string {\n if (!path || path.length === 0) return ''\n\n return path\n .map((segment) => (typeof segment === 'object' && segment !== null ? segment.key : segment))\n .join('.')\n}\n\nfunction issuesToErrors(\n issues: NonNullable<StandardSchemaResult<unknown>['issues']>,\n): Record<string, string[]> {\n const errors: Record<string, string[]> = {}\n\n for (const issue of issues) {\n ;(errors[fieldOf(issue.path)] ??= []).push(issue.message)\n }\n\n return errors\n}\n\n/**\n * A URLSearchParams as the object a schema expects.\n *\n * A key that appears more than once becomes an array, and one that appears\n * once stays a scalar — so `?tag=a&tag=b` reaches `z.array(z.string())` and\n * `?q=shoes` reaches `z.string()` without the schema having to know which\n * shape the url happened to take.\n */\nexport function searchParamsToObject(search: URLSearchParams): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n\n for (const key of new Set(search.keys())) {\n const all = search.getAll(key)\n\n out[key] = all.length > 1 ? all : all[0]\n }\n\n return out\n}\n\n/**\n * Run a page's `params` schema, or hand back what the url gave.\n *\n * A refusal is notFound() rather than an error: the route matched a pattern,\n * but the values in it do not describe a page, and 404 is the answer a crawler\n * and a cache both need.\n */\nexport async function parseParams(\n schema: StandardSchemaV1 | undefined,\n value: Record<string, unknown>,\n): Promise<unknown> {\n if (!schema) return value\n\n const result = await schema['~standard'].validate(value)\n\n if (result.issues && result.issues.length > 0) {\n // The message is for a log, never a response: it repeats back a piece of\n // the url, and the not-found page is what the visitor sees either way.\n throw new NotFoundSignal(\n 'No page for these params: ' +\n Object.entries(issuesToErrors(result.issues))\n .map(([field, messages]) => `${field || 'params'} ${messages[0]}`)\n .join(', '),\n )\n }\n\n return result.value\n}\n\n/**\n * Run a page's `searchParams` schema, or hand back the URLSearchParams.\n *\n * A refusal throws into the nearest error.tsx, which can read `errors` for the\n * fields. The page exists — only the query was wrong — so this is deliberately\n * not a 404.\n */\nexport async function parseSearchParams(\n schema: StandardSchemaV1 | undefined,\n search: URLSearchParams,\n): Promise<unknown> {\n if (!schema) return search\n\n const result = await schema['~standard'].validate(searchParamsToObject(search))\n\n if (result.issues && result.issues.length > 0) {\n throw new SearchParamsError(issuesToErrors(result.issues))\n }\n\n return result.value\n}\n\n/**\n * What an api route's schemas turn a request into.\n *\n * Every field is optional and every one is opt-in: a route that exports no\n * schema gets the raw values it always got, and a handler written before any\n * of this existed keeps working untouched.\n */\nexport interface ApiInput {\n /** Parsed by the route's `params` export, or the raw url segments. */\n params: unknown\n /** Parsed by the route's `searchParams` export, or the URLSearchParams. */\n searchParams: unknown\n /** Parsed by the route's `body` export. Undefined when it declared none. */\n body?: unknown\n}\n\n/**\n * Its own mark, not the query string's.\n *\n * Sharing one was a real bug for as long as it took a test to run: both\n * refusals answered the check for a bad query string, whichever was asked\n * first, so every refused body came back 400 instead of 422.\n */\nconst BODY_MARK = Symbol.for('@rsc-kit/core.body-error')\n\n/** A request body the route's own schema refused. */\nexport class BodyError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('The request body is not valid.')\n this.name = 'BodyError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[BODY_MARK] = true\n }\n}\n\n/**\n * Whether this is a refused body, whichever copy of the class built it.\n *\n * The mark rather than instanceof, for the reason every check in this package\n * uses one: an api route is bundled apart from the engine that calls it, so\n * each side has its own copy of this class and instanceof is false between\n * them — the refusal would surface as an unhandled 500.\n */\nexport function isBodyError(error: unknown): error is BodyError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[BODY_MARK] === true\n )\n}\n\n/**\n * Run a route's `body` schema against whatever the request carries.\n *\n * JSON and form encodings both, because an api route answers a url and a url\n * is posted to by both. Anything else is handed to the schema as the raw text,\n * which is the only honest thing to do with a body this does not understand.\n */\nexport async function parseBody(\n schema: StandardSchemaV1 | undefined,\n request: Request,\n): Promise<unknown> {\n if (!schema) return undefined\n\n const type = request.headers.get('Content-Type') ?? ''\n let raw: unknown\n\n try {\n if (type.includes('json')) {\n raw = await request.json()\n } else if (type.includes('form')) {\n raw = searchParamsToObject(new URLSearchParams(await request.text()))\n } else {\n raw = await request.text()\n }\n } catch {\n // Malformed json is a refusal about the body as a whole rather than any\n // field, which is the empty-string key the form errors already use.\n throw new BodyError({ '': ['The request body could not be read.'] })\n }\n\n const result = await schema['~standard'].validate(raw)\n\n if (result.issues && result.issues.length > 0) {\n throw new BodyError(issuesToErrors(result.issues))\n }\n\n return result.value\n}\n"]}
|
|
1
|
+
{"version":3,"file":"routeSchema.js","sourceRoot":"","sources":["../src/routeSchema.ts"],"names":[],"mappings":"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","sourcesContent":["// Typing and checking what a url carries.\n//\n// // src/app/posts/[slug]/page.tsx\n// export const params = z.object({ slug: z.string().min(1) })\n// export const searchParams = z.object({\n// page: z.coerce.number().int().min(1).default(1),\n// })\n//\n// Both are ordinary exports beside the page, read by the build the same way\n// `metadata` and `generateStaticParams` are. Any Standard Schema works — Zod,\n// Valibot, ArkType — because the schema is asked to validate itself and this\n// module never imports one.\n//\n// What it buys beyond types: `?page=3` arrives as the number 3 rather than the\n// string \"3\", a missing one arrives as the default rather than undefined, and\n// `?page=banana` is refused in one place instead of surviving as NaN into\n// whatever the page does with it.\n//\n// The two fail differently, and that is the point of having both:\n//\n// params a url that does not describe a page. /posts/ tells a\n// crawler and a cache 404, not 500, so this is notFound().\n// searchParams the page exists and the query was wrong, which is the\n// nearest error.tsx — the same place any other bad input goes.\n\nimport { NotFoundSignal } from './notFound.js'\nimport type { ApiPattern } from './routes.js'\n\n/**\n * Standard Schema, restated rather than depended on.\n *\n * It is an interface, and a package for it would be a dependency that ships\n * nothing. Kept to the shape this module reads.\n */\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>\n readonly types?: { readonly input: Input; readonly output: Output }\n }\n}\n\ninterface StandardSchemaResult<Output> {\n readonly value?: Output\n readonly issues?: ReadonlyArray<{\n readonly message: string\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>\n }>\n}\n\n/** What a page gets from its own `params` export, already parsed. */\nexport type ParamsOf<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never\n\n/**\n * The props a page with schemas receives.\n *\n * Written as a type rather than generated into a .d.ts because it needs no\n * build step to be correct: the schemas are exports of the same file, so a\n * page can refer to its own.\n *\n * export const params = z.object({ slug: z.string() })\n *\n * export default async function Page({ params: p }: PageProps<typeof params>) {\n * const { slug } = await p // string, not unknown\n * }\n */\nexport interface PageProps<P = never, S = never> {\n params: Promise<[P] extends [never] ? Record<string, string> : ParamsOf<P>>\n searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>\n}\n\n/**\n * The names a pattern binds: `/api/greet/[name]` binds `name`, and\n * `/docs/[...path]` binds `path` — one string, the slashes kept.\n */\ntype SegmentNames<P extends string> = P extends `${string}[...${infer N}]${infer Rest}`\n ? N | SegmentNames<Rest>\n : P extends `${string}[${infer N}]${infer Rest}`\n ? N | SegmentNames<Rest>\n : never\n\n/** What a route's params resolve to, from its pattern: `{ name: string }`. */\nexport type ApiParams<P extends string> = { [K in SegmentNames<P>]: string }\n\n/**\n * The second argument of an api route's handler.\n *\n * Every field is a promise, because the engine hands them lazily - a route\n * that never awaits its query string provably does not vary by it, so the\n * build can store one answer - and a sync read of `params.name` has to be a\n * compile error rather than a route that quietly answers 404 to everything.\n *\n * Name the route and the params are typed from its segments; the pattern is\n * checked against the routes the build found, so a typo fails `tsc`:\n *\n * export async function GET(request: Request, { params }: RouteContext<'/api/greet/[name]'>) {\n * const { name } = await params // string\n * }\n *\n * Or hand it the route's own schemas, the way PageProps takes a page's:\n *\n * export const params = z.object({ id: z.coerce.number() })\n * export async function GET(request: Request, { params }: RouteContext<typeof params>) {\n * const { id } = await params // number\n * }\n *\n * With neither, params is the raw segments and searchParams the raw\n * URLSearchParams, still behind a promise.\n */\nexport interface RouteContext<\n P extends ApiPattern | StandardSchemaV1 = never,\n S extends StandardSchemaV1 = never,\n B extends StandardSchemaV1 = never,\n> {\n params: Promise<\n [P] extends [never]\n ? Record<string, string>\n : P extends ApiPattern\n ? ApiParams<P>\n : ParamsOf<P>\n >\n searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>\n /** Undefined for a method with no body; a promise of the parsed body otherwise. */\n body: [B] extends [never] ? Promise<unknown> | undefined : Promise<ParamsOf<B>>\n}\n\n/**\n * A whole handler, typed from the route it answers:\n *\n * export const GET: RouteHandler<'/api/greet/[name]'> = async (request, { params }) => …\n */\nexport type RouteHandler<\n P extends ApiPattern | StandardSchemaV1 = never,\n S extends StandardSchemaV1 = never,\n B extends StandardSchemaV1 = never,\n> = (\n request: Request,\n context: RouteContext<P, S, B>,\n) => Response | Promise<Response>\n\n/**\n * The mark that says a refusal came from a route schema.\n *\n * Symbol.for for the reason every mark in this package is: the app's pages are\n * bundled apart from the engine, so each side gets its own copy of this class\n * and `instanceof` is false between them.\n */\nconst MARK = Symbol.for('@rsc-kit/core.route-input-error')\n\n/** A query string the page's own schema refused. */\nexport class SearchParamsError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('The query string is not valid for this page.')\n this.name = 'SearchParamsError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[MARK] = true\n }\n}\n\n/** Whether this is a refused query string, whichever copy of the class built it. */\nexport function isSearchParamsError(error: unknown): error is SearchParamsError {\n return (\n typeof error === 'object' && error !== null && (error as Record<symbol, unknown>)[MARK] === true\n )\n}\n\n/** The field an issue belongs to, joined so a nested one reads as it was named. */\nfunction fieldOf(path: NonNullable<StandardSchemaResult<unknown>['issues']>[number]['path']): string {\n if (!path || path.length === 0) return ''\n\n return path\n .map((segment) => (typeof segment === 'object' && segment !== null ? segment.key : segment))\n .join('.')\n}\n\nfunction issuesToErrors(\n issues: NonNullable<StandardSchemaResult<unknown>['issues']>,\n): Record<string, string[]> {\n const errors: Record<string, string[]> = {}\n\n for (const issue of issues) {\n ;(errors[fieldOf(issue.path)] ??= []).push(issue.message)\n }\n\n return errors\n}\n\n/**\n * A URLSearchParams as the object a schema expects.\n *\n * A key that appears more than once becomes an array, and one that appears\n * once stays a scalar — so `?tag=a&tag=b` reaches `z.array(z.string())` and\n * `?q=shoes` reaches `z.string()` without the schema having to know which\n * shape the url happened to take.\n */\nexport function searchParamsToObject(search: URLSearchParams): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n\n for (const key of new Set(search.keys())) {\n const all = search.getAll(key)\n\n out[key] = all.length > 1 ? all : all[0]\n }\n\n return out\n}\n\n/**\n * Run a page's `params` schema, or hand back what the url gave.\n *\n * A refusal is notFound() rather than an error: the route matched a pattern,\n * but the values in it do not describe a page, and 404 is the answer a crawler\n * and a cache both need.\n */\nexport async function parseParams(\n schema: StandardSchemaV1 | undefined,\n value: Record<string, unknown>,\n): Promise<unknown> {\n if (!schema) return value\n\n const result = await schema['~standard'].validate(value)\n\n if (result.issues && result.issues.length > 0) {\n // The message is for a log, never a response: it repeats back a piece of\n // the url, and the not-found page is what the visitor sees either way.\n throw new NotFoundSignal(\n 'No page for these params: ' +\n Object.entries(issuesToErrors(result.issues))\n .map(([field, messages]) => `${field || 'params'} ${messages[0]}`)\n .join(', '),\n )\n }\n\n return result.value\n}\n\n/**\n * Run a page's `searchParams` schema, or hand back the URLSearchParams.\n *\n * A refusal throws into the nearest error.tsx, which can read `errors` for the\n * fields. The page exists — only the query was wrong — so this is deliberately\n * not a 404.\n */\nexport async function parseSearchParams(\n schema: StandardSchemaV1 | undefined,\n search: URLSearchParams,\n): Promise<unknown> {\n if (!schema) return search\n\n const result = await schema['~standard'].validate(searchParamsToObject(search))\n\n if (result.issues && result.issues.length > 0) {\n throw new SearchParamsError(issuesToErrors(result.issues))\n }\n\n return result.value\n}\n\n/**\n * What an api route's schemas turn a request into.\n *\n * Every field is optional and every one is opt-in: a route that exports no\n * schema gets the raw values it always got, and a handler written before any\n * of this existed keeps working untouched.\n */\nexport interface ApiInput {\n /** Parsed by the route's `params` export, or the raw url segments. */\n params: unknown\n /** Parsed by the route's `searchParams` export, or the URLSearchParams. */\n searchParams: unknown\n /** Parsed by the route's `body` export. Undefined when it declared none. */\n body?: unknown\n}\n\n/**\n * Its own mark, not the query string's.\n *\n * Sharing one was a real bug for as long as it took a test to run: both\n * refusals answered the check for a bad query string, whichever was asked\n * first, so every refused body came back 400 instead of 422.\n */\nconst BODY_MARK = Symbol.for('@rsc-kit/core.body-error')\n\n/** A request body the route's own schema refused. */\nexport class BodyError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('The request body is not valid.')\n this.name = 'BodyError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[BODY_MARK] = true\n }\n}\n\n/**\n * Whether this is a refused body, whichever copy of the class built it.\n *\n * The mark rather than instanceof, for the reason every check in this package\n * uses one: an api route is bundled apart from the engine that calls it, so\n * each side has its own copy of this class and instanceof is false between\n * them — the refusal would surface as an unhandled 500.\n */\nexport function isBodyError(error: unknown): error is BodyError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[BODY_MARK] === true\n )\n}\n\n/**\n * Run a route's `body` schema against whatever the request carries.\n *\n * JSON and form encodings both, because an api route answers a url and a url\n * is posted to by both. Anything else is handed to the schema as the raw text,\n * which is the only honest thing to do with a body this does not understand.\n */\nexport async function parseBody(\n schema: StandardSchemaV1 | undefined,\n request: Request,\n): Promise<unknown> {\n if (!schema) return undefined\n\n const type = request.headers.get('Content-Type') ?? ''\n let raw: unknown\n\n try {\n if (type.includes('json')) {\n raw = await request.json()\n } else if (type.includes('form')) {\n raw = searchParamsToObject(new URLSearchParams(await request.text()))\n } else {\n raw = await request.text()\n }\n } catch {\n // Malformed json is a refusal about the body as a whole rather than any\n // field, which is the empty-string key the form errors already use.\n throw new BodyError({ '': ['The request body could not be read.'] })\n }\n\n const result = await schema['~standard'].validate(raw)\n\n if (result.issues && result.issues.length > 0) {\n throw new BodyError(issuesToErrors(result.issues))\n }\n\n return result.value\n}\n"]}
|
package/dist/routes.d.ts
CHANGED
|
@@ -36,6 +36,19 @@ export type Href = Unregistered extends true ? string : Filled<RoutePattern> | `
|
|
|
36
36
|
/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
|
|
37
37
|
export interface RegisterApi {
|
|
38
38
|
}
|
|
39
|
+
/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
|
|
40
|
+
export interface RegisterRegions {
|
|
41
|
+
}
|
|
42
|
+
/** The region names the build found: section names and slot names. */
|
|
43
|
+
export type RegionName = RegisterRegions extends {
|
|
44
|
+
regions: infer R extends string;
|
|
45
|
+
} ? R : string;
|
|
46
|
+
type NoRegions = string extends RegionName ? true : false;
|
|
47
|
+
/**
|
|
48
|
+
* What `revalidate()` takes: the page, the whole document, or a region by
|
|
49
|
+
* name. `string` until the build has written the types.
|
|
50
|
+
*/
|
|
51
|
+
export type RevalidateTarget = NoRegions extends true ? string : "page" | "all" | RegionName;
|
|
39
52
|
/** The api route patterns this app declared: `'/api/orders/[id]'`. */
|
|
40
53
|
export type ApiPattern = RegisterApi extends {
|
|
41
54
|
apis: infer R extends string;
|
|
@@ -71,17 +84,17 @@ type SearchMap = Register extends {
|
|
|
71
84
|
type StripQuery<H extends string> = H extends `${infer P}?${string}` ? P : H extends `${infer P}#${string}` ? P : H;
|
|
72
85
|
/** The pattern a written href belongs to: `/posts/hello` is `/posts/[slug]`. */
|
|
73
86
|
type PatternOf<H extends string> = RoutePattern extends infer P ? P extends string ? StripQuery<H> extends Filled<P> ? P : never : never : never;
|
|
74
|
-
type IsUnion<T, U = T> = T extends unknown ?
|
|
87
|
+
type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : never;
|
|
75
88
|
type SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined;
|
|
76
89
|
type InputOf<S> = S extends {
|
|
77
|
-
|
|
90
|
+
"~standard": {
|
|
78
91
|
types?: {
|
|
79
92
|
input: infer I;
|
|
80
93
|
};
|
|
81
94
|
};
|
|
82
95
|
} ? I : never;
|
|
83
96
|
type OutputOf<S> = S extends {
|
|
84
|
-
|
|
97
|
+
"~standard": {
|
|
85
98
|
types?: {
|
|
86
99
|
output: infer O;
|
|
87
100
|
};
|
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 ?
|
|
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 ?
|
|
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
|
/**
|
package/dist/routes.js.map
CHANGED
|
@@ -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
|
|
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 +
|
|
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 ===
|
|
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(
|
|
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(
|
|
76
|
+
const methods = route.methods.includes("HEAD") || !route.methods.includes("GET")
|
|
72
77
|
? route.methods
|
|
73
|
-
: [...route.methods,
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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(
|
|
177
|
+
const mounted = held.split(",").filter(Boolean);
|
|
160
178
|
let shared = 0;
|
|
161
|
-
while (shared < mounted.length &&
|
|
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
|
|
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 =
|
|
197
|
-
return (pathname.replace(/\/+$/,
|
|
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,
|
|
223
|
+
return cut === -1 ? [url, ""] : [url.slice(0, cut), url.slice(cut)];
|
|
202
224
|
}
|
|
203
225
|
//# sourceMappingURL=routing.js.map
|
package/dist/routing.js.map
CHANGED
|
@@ -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/testing.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface TestAppOptions {
|
|
|
19
19
|
/** The origin requests are made against. Nothing reads it; it is a url. */
|
|
20
20
|
origin?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* How this project builds: its own `build` script, run by the package manager
|
|
24
|
+
* this test is running under. Without a script, Vite directly - on Bun's
|
|
25
|
+
* runtime when the tests are, since a bin's node shebang would otherwise
|
|
26
|
+
* start it under Node.
|
|
27
|
+
*/
|
|
28
|
+
/** @internal Exported for its test. */
|
|
29
|
+
export declare function buildCommand(root: string): [string, string[]];
|
|
22
30
|
/**
|
|
23
31
|
* Build the app if it needs it, load it, and hand back something to fetch from.
|
|
24
32
|
*
|