@rsc-kit/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading the request from inside a render.\n//\n// import { headers, cookies } from '@rsc-kit/core/request'\n//\n// export default async function middleware() {\n// const locale = cookies().get('locale') ?? negotiate(headers().get('accept-language'))\n// if (!locale) redirect('/en')\n// }\n//\n// Server components have no arguments beyond their props, and middleware has\n// none at all, so without this an app on a JavaScript host cannot see a header\n// — no locale negotiation, no subdomain, no cookie. The Laravel host escaped\n// that through rpc(), because PHP has the request; this is the same thing for\n// everyone else.\n//\n// Read-only on purpose. Setting a cookie means writing a response header, and\n// by the time a component renders the shell may already have been flushed — an\n// API that appears to set one and silently does not is worse than not having\n// it. Set them in the host, which owns the response.\n//\n// Asynchronous on purpose too, and not for ergonomics. A build has no request,\n// so a read there suspends and never settles — which is the same thing the\n// prerender probe does to a host call. Anything still waiting when the probe's\n// budget expires becomes a fallback in the frozen shell, so a read inside a\n// Suspense boundary leaves the shell frozen and makes only that boundary\n// dynamic. Reading synchronously could only mark the whole route dynamic,\n// because there would be nothing to suspend on.\n//\n// Scoped to the request, on the same store cache() uses. Two requests in\n// flight cannot see each other's headers.\n\nimport { resolveScope } from './revalidate.js'\n\n/** What a host can supply: a real Request, or the parts of one. */\nexport type RequestLike = Request | { url: string; headers: Record<string, string> } | null\n\ninterface Slot {\n /** False during a build: there is nothing to answer with. */\n request: boolean\n url: string | null\n /**\n * Held as Headers rather than as a Request.\n *\n * `new Request(url, { headers })` drops Cookie: a spec-compliant Headers\n * built with the \"request\" guard refuses the forbidden header names, and\n * Cookie is one of them. `new Headers()` has no guard and keeps it. Bun\n * happens to allow both, which is why rebuilding a Request looked fine until\n * it ran anywhere stricter — and what it loses is every cookie, silently.\n */\n headers: Headers\n original: Request | null\n /**\n * Whether anything asked. Only for the build's report — the classification\n * comes from the suspension, not from this.\n */\n read: boolean\n}\n\nconst SCOPE = Symbol.for('@rsc-kit/core.request-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\nfunction slot(): Slot {\n const store = scope()?.getStore()\n\n if (!store) {\n throw new Error(\n 'No request in scope. headers() and cookies() are for a render — middleware, a layout, ' +\n 'a page — and there is nothing to read outside one.',\n )\n }\n\n store.read = true\n\n return store\n}\n\n/**\n * The request's headers.\n *\n * Empty during a build, where there is no request: the page is marked as\n * needing one instead, and rendered on demand rather than frozen.\n */\nexport async function headers(): Promise<Headers> {\n const store = slot()\n\n return store.request ? store.headers : never()\n}\n\n/** How a cookie should be written. The names browsers use. */\nexport interface CookieOptions {\n path?: string\n domain?: string\n maxAge?: number\n expires?: Date\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n partitioned?: boolean\n}\n\nexport interface Cookies {\n get(name: string): string | undefined\n has(name: string): boolean\n getAll(): Record<string, string>\n /**\n * Write one on the response.\n *\n * Only from a server action, which has a response of its own that has not\n * been sent. A render has already flushed its headers by the time a\n * component runs — that is what makes the first paint fast — so this throws\n * there rather than appearing to work.\n */\n set(name: string, value: string, options?: CookieOptions): void\n /** Write one that expires immediately. Same rule about where. */\n delete(name: string, options?: CookieOptions): void\n}\n\n/** Kept for the name it had when it could only read. */\nexport type ReadonlyCookies = Cookies\n\n/**\n * The response being built, while it can still be changed.\n *\n * Open from the moment a request is picked up until the host constructs its\n * Response, and sealed the instant it does. Middleware runs inside that window\n * — before any rendering starts — so a header set there is on the answer. A\n * component runs after it, during streaming, where the status line and headers\n * are already on the wire; setting one there is refused rather than dropped.\n */\ninterface Draft {\n headers: Headers\n sealed: boolean\n}\n\nconst DRAFT = Symbol.for('@rsc-kit/core.response-draft')\n\nlet draftReady: Promise<void> | null = null\n\ninterface DraftScope {\n getStore(): Draft | undefined\n run<T>(store: Draft, fn: () => T): T\n}\n\nfunction draft(): DraftScope | null {\n return (globals[DRAFT] as DraftScope | undefined) ?? null\n}\n\n/** The open draft, or an explanation of why there is not one. */\nfunction writable(what: string): Draft {\n const open = draft()?.getStore()\n\n if (!open) {\n throw new Error(\n `${what} needs a response that has not been sent. Middleware and server actions have one; ` +\n 'a script outside a request does not.',\n )\n }\n\n if (open.sealed) {\n throw new Error(\n `${what} was called after the response had been sent. Headers go out before the render ` +\n 'starts — that is what makes the first paint fast — so set them in middleware, which ' +\n 'runs before it.',\n )\n }\n\n return open\n}\n\n/**\n * Headers to put on the answer.\n *\n * Mutate it as you would any Headers. `append` is the one to reach for when a\n * header may legitimately appear twice; `set` replaces.\n */\n/**\n * The draft's headers, refusing writes once the response has gone.\n *\n * A proxy rather than the Headers itself: returning the live object means code\n * that captured it before the response was built can go on mutating a thing\n * nobody will read. Nothing in this package does that — the host copies the\n * headers onto the response the instant it seals — but \"safe because of the\n * order two other functions happen to run in\" is not a property, and an app\n * holding the object across an await would find writes silently doing nothing.\n */\nfunction sealable(draft: Draft): Headers {\n const guard = (method: 'set' | 'append' | 'delete') =>\n (...args: [string, string]) => {\n writable('responseHeaders()')\n\n return (draft.headers[method] as (...a: string[]) => void)(...args)\n }\n\n const proxy: Headers = new Proxy(draft.headers, {\n get(target, property, receiver) {\n if (property === 'set' || property === 'append' || property === 'delete') {\n return guard(property)\n }\n\n // A maplike forEach passes the object it was called on as the third\n // callback argument, and binding to the target makes that the raw\n // Headers — a live, unguarded reference handed out by the very method\n // meant to be read-only. Substituted for the proxy so there is no way\n // through.\n if (property === 'forEach') {\n return (fn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown) =>\n target.forEach((value, key) => fn.call(thisArg, value, key, proxy))\n }\n\n const value = Reflect.get(target, property, receiver)\n\n return typeof value === 'function' ? value.bind(target) : value\n },\n })\n\n return proxy\n}\n\nexport function responseHeaders(): Headers {\n return sealable(writable('responseHeaders()'))\n}\n\n/**\n * Run a request with a response that can still be changed, and take what was\n * put on it.\n *\n * `seal()` is called by the host the moment it builds the Response, so\n * anything set afterwards is refused instead of silently going nowhere.\n */\nexport async function withResponseDraft<T>(\n run: (draft: { taken: () => Headers; seal: () => void }) => Promise<T>,\n): Promise<T> {\n if (!globals[DRAFT]) {\n draftReady ??= resolveScope().then((resolved) => {\n globals[DRAFT] ??= resolved as unknown as DraftScope\n })\n\n await draftReady\n }\n\n const open: Draft = { headers: new Headers(), sealed: false }\n\n return await draft()!.run(open, () =>\n run({\n taken: () => open.headers,\n seal: () => {\n open.sealed = true\n },\n }),\n )\n}\n\n/**\n * A cookie name, as RFC 6265 defines one: a token, so no separators at all.\n *\n * Checked rather than escaped because there is no escaping — a name is not a\n * quoted string. An app that derives one from user input (`pref_${key}`) would\n * otherwise let that input close the pair and open another: a name of\n * `session=attacker; Path=/; HttpOnly; x` serializes to a header whose *first*\n * pair is a session cookie the caller chose, and the browser reads the first.\n */\nconst COOKIE_NAME = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n/** No separator may appear in an attribute either, for the same reason. */\nconst COOKIE_ATTRIBUTE = /[;,\\r\\n]/\n\n/** `name=value; Path=/; HttpOnly` — the header a browser expects. */\nexport function serializeCookie(cookie: { name: string; value: string; options: CookieOptions }): string {\n if (!COOKIE_NAME.test(cookie.name)) {\n throw new Error(\n `Not a usable cookie name: ${JSON.stringify(cookie.name)}. ` +\n 'A name is a token — letters, digits and !#$%&\\'*+-.^_`|~ — with no spaces, ' +\n 'semicolons or equals signs. Nothing escapes them, so one cannot be encoded around.',\n )\n }\n\n if (cookie.options.sameSite !== undefined) {\n const value = String(cookie.options.sameSite).toLowerCase()\n\n if (value !== 'strict' && value !== 'lax' && value !== 'none') {\n throw new Error(\n `Not a SameSite value: ${JSON.stringify(cookie.options.sameSite)}. ` +\n 'It is written into the header as given, so anything else becomes further attributes.',\n )\n }\n }\n\n for (const [attribute, value] of [\n ['path', cookie.options.path],\n ['domain', cookie.options.domain],\n // Trusted because it is typed as a Date — but a cast reaches this, and the\n // result lands in the header verbatim like the others.\n ['expires', cookie.options.expires?.toUTCString()],\n ] as const) {\n if (typeof value === 'string' && COOKIE_ATTRIBUTE.test(value)) {\n throw new Error(\n `The cookie ${attribute} ${JSON.stringify(value)} contains a separator. ` +\n 'It would be read as further attributes rather than as part of this one.',\n )\n }\n }\n\n const parts = [`${cookie.name}=${encodeURIComponent(cookie.value)}`]\n const o = cookie.options\n\n if (o.maxAge !== undefined) parts.push(`Max-Age=${Math.floor(o.maxAge)}`)\n if (o.expires) parts.push(`Expires=${o.expires.toUTCString()}`)\n // Defaulted, because a cookie without one is scoped to the path that set it\n // — a session written by POST /_rsc/action would not be sent for any page.\n parts.push(`Path=${o.path ?? '/'}`)\n if (o.domain) parts.push(`Domain=${o.domain}`)\n if (o.sameSite) parts.push(`SameSite=${o.sameSite[0].toUpperCase()}${o.sameSite.slice(1)}`)\n if (o.secure) parts.push('Secure')\n if (o.httpOnly) parts.push('HttpOnly')\n if (o.partitioned) parts.push('Partitioned')\n\n return parts.join('; ')\n}\n\nexport async function cookies(): Promise<Cookies> {\n const parsed = parseCookies((await headers()).get('cookie') ?? '')\n\n const write = (name: string, value: string, options: CookieOptions = {}): void => {\n // Appended, never set: several cookies on one response are several\n // Set-Cookie headers, and replacing would leave only the last.\n writable('cookies().set()').headers.append(\n 'Set-Cookie',\n serializeCookie({ name, value, options }),\n )\n }\n\n return {\n get: (name) => parsed[name],\n has: (name) => name in parsed,\n getAll: () => ({ ...parsed }),\n set: write,\n // Expired rather than removed: a browser drops a cookie when it is told\n // one has already passed, and there is no other way to say it.\n delete: (name, options = {}) => write(name, '', { ...options, maxAge: 0 }),\n }\n}\n\n/**\n * The whole request, for a host-specific need the accessors do not cover.\n *\n * Null on a host that forwards only the parts — the worker behind Laravel has\n * a socket, not a request. Use headers() and cookies(), which work everywhere.\n */\nexport async function request(): Promise<Request | null> {\n const store = slot()\n\n return store.request ? store.original : never()\n}\n\n/**\n * Mark everything below as belonging to the request, not to the build.\n *\n * export default async function Orders() {\n * await connection()\n *\n * const rows = await db.query('select * from orders')\n *\n * return <ul>{rows.map(...)}</ul>\n * }\n *\n * At build time this never resolves, so nothing after it runs: the query is not\n * made, the boundary above becomes a hole, and the rest of the page still\n * freezes. At request time it resolves immediately and the component runs\n * normally.\n *\n * **Once is enough.** It is a barrier, not a wrapper — everything after it in\n * this component belongs to the request, however many calls that is. Repeating\n * it before each query does nothing.\n *\n * It needs a boundary above it. With a <Suspense> or a loading.tsx there is a\n * fallback to store and the page becomes a shell; with neither, nothing can\n * paint and the build refuses the route rather than storing a blank.\n *\n * Reach for it when the build should not run something — a database the build\n * machine cannot see, or a value that must differ per visitor. A query the\n * build CAN run, whose answer is the same for everyone, wants none of this: it\n * should be frozen.\n *\n * The same name and behaviour as Next's `connection()`.\n */\nexport async function connection(): Promise<void> {\n const store = slot()\n\n // No request means a build. Suspend rather than continue, the same way\n // headers() and cookies() do.\n if (!store.request) return never()\n}\n\n/** The url this request was made to, whichever way the host supplied it. */\nexport async function url(): Promise<string | null> {\n const store = slot()\n\n return store.request ? store.url : never()\n}\n\nfunction parseCookies(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n\n for (const part of header.split(';')) {\n const eq = part.indexOf('=')\n\n if (eq === -1) continue\n\n const name = part.slice(0, eq).trim()\n\n if (!name) continue\n\n // Decoded because that is how they were written. A malformed escape is\n // left as it arrived rather than throwing the render away.\n const raw = part.slice(eq + 1).trim()\n\n try {\n out[name] = decodeURIComponent(raw)\n } catch {\n out[name] = raw\n }\n }\n\n return out\n}\n\n/**\n * Run one request with its headers readable.\n *\n * `null` is the build: a scope with nothing in it, so a page that reads is\n * caught rather than silently frozen holding whatever the machine that built\n * it happened to send.\n */\nexport async function withRequest<T>(from: RequestLike, run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const isRequest = from instanceof Request\n const store: Slot = {\n request: from !== null,\n url: from ? (isRequest ? from.url : from.url) : null,\n headers: from ? (isRequest ? from.headers : new Headers(from.headers)) : new Headers(),\n original: isRequest ? from : null,\n read: false,\n }\n\n return await scope()!.run(store, run)\n}\n\n/**\n * A promise that never settles, so the caller suspends.\n *\n * What a read does during a build. React treats it exactly as it treats a host\n * call that never answers: the component suspends, its Suspense fallback goes\n * into the shell, and the probe's budget decides the rest.\n */\nfunction never(): Promise<never> {\n slot().read = true\n\n return new Promise(() => {})\n}\n\n/**\n * Whether anything read the request during the scope that is open.\n *\n * For the prerenderer, which opens one with no request and asks afterwards.\n */\nexport function requestWasRead(): boolean {\n return scope()?.getStore()?.read ?? false\n}\n\n/**\n * The query string, as a page receives it.\n *\n * Derived from the request rather than passed down the render, so no host has\n * to forward it separately — and so it suspends during a build for the same\n * reason every other read does. A frozen page cannot know the query it will be\n * asked for.\n */\nexport async function searchParams(): Promise<URLSearchParams> {\n const from = await url()\n\n return from ? new URL(from).searchParams : new URLSearchParams()\n}\n"]}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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","sourcesContent":["// Reading the request from inside a render.\n//\n// import { headers, cookies } from '@rsc-kit/core/request'\n//\n// export default async function middleware() {\n// const locale = cookies().get('locale') ?? negotiate(headers().get('accept-language'))\n// if (!locale) redirect('/en')\n// }\n//\n// Server components have no arguments beyond their props, and middleware has\n// none at all, so without this an app on a JavaScript host cannot see a header\n// — no locale negotiation, no subdomain, no cookie. The Laravel host escaped\n// that through rpc(), because PHP has the request; this is the same thing for\n// everyone else.\n//\n// Read-only on purpose. Setting a cookie means writing a response header, and\n// by the time a component renders the shell may already have been flushed — an\n// API that appears to set one and silently does not is worse than not having\n// it. Set them in the host, which owns the response.\n//\n// Asynchronous on purpose too, and not for ergonomics. A build has no request,\n// so a read there suspends and never settles — which is the same thing the\n// prerender probe does to a host call. Anything still waiting when the probe's\n// budget expires becomes a fallback in the frozen shell, so a read inside a\n// Suspense boundary leaves the shell frozen and makes only that boundary\n// dynamic. Reading synchronously could only mark the whole route dynamic,\n// because there would be nothing to suspend on.\n//\n// Scoped to the request, on the same store cache() uses. Two requests in\n// flight cannot see each other's headers.\n\nimport { resolveScope } from './revalidate.js'\n\n/** What a host can supply: a real Request, or the parts of one. */\nexport type RequestLike = Request | { url: string; headers: Record<string, string> } | null\n\ninterface Slot {\n /** False during a build: there is nothing to answer with. */\n request: boolean\n url: string | null\n /**\n * Held as Headers rather than as a Request.\n *\n * `new Request(url, { headers })` drops Cookie: a spec-compliant Headers\n * built with the \"request\" guard refuses the forbidden header names, and\n * Cookie is one of them. `new Headers()` has no guard and keeps it. Bun\n * happens to allow both, which is why rebuilding a Request looked fine until\n * it ran anywhere stricter — and what it loses is every cookie, silently.\n */\n headers: Headers\n original: Request | null\n /**\n * Whether anything asked. Only for the build's report — the classification\n * comes from the suspension, not from this.\n */\n read: boolean\n /**\n * What read it, in the order they did.\n *\n * Only the name of the accessor — `cookies()`, `headers()` — which is enough\n * for the build to say why a page could not be frozen. Without it the answer\n * is \"something in this page reached for the request\", and finding which is\n * the part that takes an afternoon.\n */\n readBy: string[]\n}\n\nconst SCOPE = Symbol.for('@rsc-kit/core.request-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\ninterface Scope {\n getStore(): Slot | undefined\n run<T>(store: Slot, fn: () => T): T\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\nfunction slot(): Slot {\n const store = scope()?.getStore()\n\n if (!store) {\n throw new Error(\n 'No request in scope. headers() and cookies() are for a render — middleware, a layout, ' +\n 'a page — and there is nothing to read outside one.',\n )\n }\n\n store.read = true\n\n return store\n}\n\n/**\n * The request's headers.\n *\n * Empty during a build, where there is no request: the page is marked as\n * needing one instead, and rendered on demand rather than frozen.\n */\nexport async function headers(): Promise<Headers> {\n const store = slot()\n\n return store.request ? store.headers : never('headers()')\n}\n\n/** How a cookie should be written. The names browsers use. */\nexport interface CookieOptions {\n path?: string\n domain?: string\n maxAge?: number\n expires?: Date\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n partitioned?: boolean\n}\n\nexport interface Cookies {\n get(name: string): string | undefined\n has(name: string): boolean\n getAll(): Record<string, string>\n /**\n * Write one on the response.\n *\n * Only from a server action, which has a response of its own that has not\n * been sent. A render has already flushed its headers by the time a\n * component runs — that is what makes the first paint fast — so this throws\n * there rather than appearing to work.\n */\n set(name: string, value: string, options?: CookieOptions): void\n /** Write one that expires immediately. Same rule about where. */\n delete(name: string, options?: CookieOptions): void\n}\n\n/** Kept for the name it had when it could only read. */\nexport type ReadonlyCookies = Cookies\n\n/**\n * The response being built, while it can still be changed.\n *\n * Open from the moment a request is picked up until the host constructs its\n * Response, and sealed the instant it does. Middleware runs inside that window\n * — before any rendering starts — so a header set there is on the answer. A\n * component runs after it, during streaming, where the status line and headers\n * are already on the wire; setting one there is refused rather than dropped.\n */\ninterface Draft {\n headers: Headers\n sealed: boolean\n}\n\nconst DRAFT = Symbol.for('@rsc-kit/core.response-draft')\n\nlet draftReady: Promise<void> | null = null\n\ninterface DraftScope {\n getStore(): Draft | undefined\n run<T>(store: Draft, fn: () => T): T\n}\n\nfunction draft(): DraftScope | null {\n return (globals[DRAFT] as DraftScope | undefined) ?? null\n}\n\n/** The open draft, or an explanation of why there is not one. */\nfunction writable(what: string): Draft {\n const open = draft()?.getStore()\n\n if (!open) {\n throw new Error(\n `${what} needs a response that has not been sent. Middleware and server actions have one; ` +\n 'a script outside a request does not.',\n )\n }\n\n if (open.sealed) {\n throw new Error(\n `${what} was called after the response had been sent. Headers go out before the render ` +\n 'starts — that is what makes the first paint fast — so set them in middleware, which ' +\n 'runs before it.',\n )\n }\n\n return open\n}\n\n/**\n * Headers to put on the answer.\n *\n * Mutate it as you would any Headers. `append` is the one to reach for when a\n * header may legitimately appear twice; `set` replaces.\n */\n/**\n * The draft's headers, refusing writes once the response has gone.\n *\n * A proxy rather than the Headers itself: returning the live object means code\n * that captured it before the response was built can go on mutating a thing\n * nobody will read. Nothing in this package does that — the host copies the\n * headers onto the response the instant it seals — but \"safe because of the\n * order two other functions happen to run in\" is not a property, and an app\n * holding the object across an await would find writes silently doing nothing.\n */\nfunction sealable(draft: Draft): Headers {\n const guard = (method: 'set' | 'append' | 'delete') =>\n (...args: [string, string]) => {\n writable('responseHeaders()')\n\n return (draft.headers[method] as (...a: string[]) => void)(...args)\n }\n\n const proxy: Headers = new Proxy(draft.headers, {\n get(target, property, receiver) {\n if (property === 'set' || property === 'append' || property === 'delete') {\n return guard(property)\n }\n\n // A maplike forEach passes the object it was called on as the third\n // callback argument, and binding to the target makes that the raw\n // Headers — a live, unguarded reference handed out by the very method\n // meant to be read-only. Substituted for the proxy so there is no way\n // through.\n if (property === 'forEach') {\n return (fn: (value: string, key: string, parent: Headers) => void, thisArg?: unknown) =>\n target.forEach((value, key) => fn.call(thisArg, value, key, proxy))\n }\n\n const value = Reflect.get(target, property, receiver)\n\n return typeof value === 'function' ? value.bind(target) : value\n },\n })\n\n return proxy\n}\n\nexport function responseHeaders(): Headers {\n return sealable(writable('responseHeaders()'))\n}\n\n/**\n * Run a request with a response that can still be changed, and take what was\n * put on it.\n *\n * `seal()` is called by the host the moment it builds the Response, so\n * anything set afterwards is refused instead of silently going nowhere.\n */\nexport async function withResponseDraft<T>(\n run: (draft: { taken: () => Headers; seal: () => void }) => Promise<T>,\n): Promise<T> {\n if (!globals[DRAFT]) {\n draftReady ??= resolveScope().then((resolved) => {\n globals[DRAFT] ??= resolved as unknown as DraftScope\n })\n\n await draftReady\n }\n\n const open: Draft = { headers: new Headers(), sealed: false }\n\n return await draft()!.run(open, () =>\n run({\n taken: () => open.headers,\n seal: () => {\n open.sealed = true\n },\n }),\n )\n}\n\n/**\n * A cookie name, as RFC 6265 defines one: a token, so no separators at all.\n *\n * Checked rather than escaped because there is no escaping — a name is not a\n * quoted string. An app that derives one from user input (`pref_${key}`) would\n * otherwise let that input close the pair and open another: a name of\n * `session=attacker; Path=/; HttpOnly; x` serializes to a header whose *first*\n * pair is a session cookie the caller chose, and the browser reads the first.\n */\nconst COOKIE_NAME = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/\n\n/** No separator may appear in an attribute either, for the same reason. */\nconst COOKIE_ATTRIBUTE = /[;,\\r\\n]/\n\n/** `name=value; Path=/; HttpOnly` — the header a browser expects. */\nexport function serializeCookie(cookie: { name: string; value: string; options: CookieOptions }): string {\n if (!COOKIE_NAME.test(cookie.name)) {\n throw new Error(\n `Not a usable cookie name: ${JSON.stringify(cookie.name)}. ` +\n 'A name is a token — letters, digits and !#$%&\\'*+-.^_`|~ — with no spaces, ' +\n 'semicolons or equals signs. Nothing escapes them, so one cannot be encoded around.',\n )\n }\n\n if (cookie.options.sameSite !== undefined) {\n const value = String(cookie.options.sameSite).toLowerCase()\n\n if (value !== 'strict' && value !== 'lax' && value !== 'none') {\n throw new Error(\n `Not a SameSite value: ${JSON.stringify(cookie.options.sameSite)}. ` +\n 'It is written into the header as given, so anything else becomes further attributes.',\n )\n }\n }\n\n for (const [attribute, value] of [\n ['path', cookie.options.path],\n ['domain', cookie.options.domain],\n // Trusted because it is typed as a Date — but a cast reaches this, and the\n // result lands in the header verbatim like the others.\n ['expires', cookie.options.expires?.toUTCString()],\n ] as const) {\n if (typeof value === 'string' && COOKIE_ATTRIBUTE.test(value)) {\n throw new Error(\n `The cookie ${attribute} ${JSON.stringify(value)} contains a separator. ` +\n 'It would be read as further attributes rather than as part of this one.',\n )\n }\n }\n\n const parts = [`${cookie.name}=${encodeURIComponent(cookie.value)}`]\n const o = cookie.options\n\n if (o.maxAge !== undefined) parts.push(`Max-Age=${Math.floor(o.maxAge)}`)\n if (o.expires) parts.push(`Expires=${o.expires.toUTCString()}`)\n // Defaulted, because a cookie without one is scoped to the path that set it\n // — a session written by POST /_rsc/action would not be sent for any page.\n parts.push(`Path=${o.path ?? '/'}`)\n if (o.domain) parts.push(`Domain=${o.domain}`)\n if (o.sameSite) parts.push(`SameSite=${o.sameSite[0].toUpperCase()}${o.sameSite.slice(1)}`)\n if (o.secure) parts.push('Secure')\n if (o.httpOnly) parts.push('HttpOnly')\n if (o.partitioned) parts.push('Partitioned')\n\n return parts.join('; ')\n}\n\nexport async function cookies(): Promise<Cookies> {\n // Named before the await, because `headers()` never settles during a build —\n // nothing after this line runs there, and the reason would be recorded as\n // headers() for a call nobody wrote.\n const store = slot()\n\n if (!store.request && !store.readBy.includes('cookies()')) store.readBy.push('cookies()')\n\n const parsed = parseCookies((await headers()).get('cookie') ?? '')\n\n const write = (name: string, value: string, options: CookieOptions = {}): void => {\n // Appended, never set: several cookies on one response are several\n // Set-Cookie headers, and replacing would leave only the last.\n writable('cookies().set()').headers.append(\n 'Set-Cookie',\n serializeCookie({ name, value, options }),\n )\n }\n\n return {\n get: (name) => parsed[name],\n has: (name) => name in parsed,\n getAll: () => ({ ...parsed }),\n set: write,\n // Expired rather than removed: a browser drops a cookie when it is told\n // one has already passed, and there is no other way to say it.\n delete: (name, options = {}) => write(name, '', { ...options, maxAge: 0 }),\n }\n}\n\n/**\n * The whole request, for a host-specific need the accessors do not cover.\n *\n * Null on a host that forwards only the parts — the worker behind Laravel has\n * a socket, not a request. Use headers() and cookies(), which work everywhere.\n */\nexport async function request(): Promise<Request | null> {\n const store = slot()\n\n return store.request ? store.original : never('request()')\n}\n\n/**\n * Mark everything below as belonging to the request, not to the build.\n *\n * export default async function Orders() {\n * await connection()\n *\n * const rows = await db.query('select * from orders')\n *\n * return <ul>{rows.map(...)}</ul>\n * }\n *\n * At build time this never resolves, so nothing after it runs: the query is not\n * made, the boundary above becomes a hole, and the rest of the page still\n * freezes. At request time it resolves immediately and the component runs\n * normally.\n *\n * **Once is enough.** It is a barrier, not a wrapper — everything after it in\n * this component belongs to the request, however many calls that is. Repeating\n * it before each query does nothing.\n *\n * It needs a boundary above it. With a <Suspense> or a loading.tsx there is a\n * fallback to store and the page becomes a shell; with neither, nothing can\n * paint and the build refuses the route rather than storing a blank.\n *\n * Reach for it when the build should not run something — a database the build\n * machine cannot see, or a value that must differ per visitor. A query the\n * build CAN run, whose answer is the same for everyone, wants none of this: it\n * should be frozen.\n *\n * The same name and behaviour as Next's `connection()`.\n */\nexport async function connection(): Promise<void> {\n const store = slot()\n\n // No request means a build. Suspend rather than continue, the same way\n // headers() and cookies() do.\n if (!store.request) return never('connection()')\n}\n\n/** The url this request was made to, whichever way the host supplied it. */\nexport async function url(): Promise<string | null> {\n const store = slot()\n\n return store.request ? store.url : never('url()')\n}\n\nfunction parseCookies(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n\n for (const part of header.split(';')) {\n const eq = part.indexOf('=')\n\n if (eq === -1) continue\n\n const name = part.slice(0, eq).trim()\n\n if (!name) continue\n\n // Decoded because that is how they were written. A malformed escape is\n // left as it arrived rather than throwing the render away.\n const raw = part.slice(eq + 1).trim()\n\n try {\n out[name] = decodeURIComponent(raw)\n } catch {\n out[name] = raw\n }\n }\n\n return out\n}\n\n/**\n * Run one request with its headers readable.\n *\n * `null` is the build: a scope with nothing in it, so a page that reads is\n * caught rather than silently frozen holding whatever the machine that built\n * it happened to send.\n */\nexport async function withRequest<T>(from: RequestLike, run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const isRequest = from instanceof Request\n const store: Slot = {\n request: from !== null,\n url: from ? (isRequest ? from.url : from.url) : null,\n headers: from ? (isRequest ? from.headers : new Headers(from.headers)) : new Headers(),\n original: isRequest ? from : null,\n read: false,\n readBy: [],\n }\n\n return await scope()!.run(store, run)\n}\n\n/**\n * A promise that never settles, so the caller suspends.\n *\n * What a read does during a build. React treats it exactly as it treats a host\n * call that never answers: the component suspends, its Suspense fallback goes\n * into the shell, and the probe's budget decides the rest.\n */\nfunction never(by: string): Promise<never> {\n const store = slot()\n\n store.read = true\n\n if (!store.readBy.includes(by)) store.readBy.push(by)\n\n return new Promise(() => {})\n}\n\n/**\n * Whether anything read the request during the scope that is open.\n *\n * For the prerenderer, which opens one with no request and asks afterwards.\n */\nexport function requestWasRead(): boolean {\n return scope()?.getStore()?.read ?? false\n}\n\n/**\n * What reached for the request during the scope that is open.\n *\n * For the build, which asks afterwards so it can say why a page is rendered per\n * visitor rather than only that it is.\n */\nexport function requestReadBy(): string[] {\n return scope()?.getStore()?.readBy ?? []\n}\n\n/**\n * The query string, as a page receives it.\n *\n * Derived from the request rather than passed down the render, so no host has\n * to forward it separately — and so it suspends during a build for the same\n * reason every other read does. A frozen page cannot know the query it will be\n * asked for.\n */\nexport async function searchParams(): Promise<URLSearchParams> {\n // Named before the await, so the build reports the call someone wrote rather\n // than url(), which this happens to be built on.\n const store = slot()\n\n if (!store.request && !store.readBy.includes('searchParams()')) {\n store.readBy.push('searchParams()')\n }\n\n const from = await url()\n\n return from ? new URL(from).searchParams : new URLSearchParams()\n}\n"]}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Standard Schema, restated rather than depended on.
3
+ *
4
+ * It is an interface, and a package for it would be a dependency that ships
5
+ * nothing. Kept to the shape this module reads.
6
+ */
7
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
8
+ readonly '~standard': {
9
+ readonly version: 1;
10
+ readonly vendor: string;
11
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
12
+ readonly types?: {
13
+ readonly input: Input;
14
+ readonly output: Output;
15
+ };
16
+ };
17
+ }
18
+ interface StandardSchemaResult<Output> {
19
+ readonly value?: Output;
20
+ readonly issues?: ReadonlyArray<{
21
+ readonly message: string;
22
+ readonly path?: ReadonlyArray<PropertyKey | {
23
+ readonly key: PropertyKey;
24
+ }>;
25
+ }>;
26
+ }
27
+ /** What a page gets from its own `params` export, already parsed. */
28
+ export type ParamsOf<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
29
+ /**
30
+ * The props a page with schemas receives.
31
+ *
32
+ * Written as a type rather than generated into a .d.ts because it needs no
33
+ * build step to be correct: the schemas are exports of the same file, so a
34
+ * page can refer to its own.
35
+ *
36
+ * export const params = z.object({ slug: z.string() })
37
+ *
38
+ * export default async function Page({ params: p }: PageProps<typeof params>) {
39
+ * const { slug } = await p // string, not unknown
40
+ * }
41
+ */
42
+ export interface PageProps<P = never, S = never> {
43
+ params: Promise<[P] extends [never] ? Record<string, string> : ParamsOf<P>>;
44
+ searchParams: Promise<[S] extends [never] ? URLSearchParams : ParamsOf<S>>;
45
+ }
46
+ /** A query string the page's own schema refused. */
47
+ export declare class SearchParamsError extends Error {
48
+ readonly errors: Record<string, string[]>;
49
+ constructor(errors: Record<string, string[]>);
50
+ }
51
+ /** Whether this is a refused query string, whichever copy of the class built it. */
52
+ export declare function isSearchParamsError(error: unknown): error is SearchParamsError;
53
+ /**
54
+ * A URLSearchParams as the object a schema expects.
55
+ *
56
+ * A key that appears more than once becomes an array, and one that appears
57
+ * once stays a scalar — so `?tag=a&tag=b` reaches `z.array(z.string())` and
58
+ * `?q=shoes` reaches `z.string()` without the schema having to know which
59
+ * shape the url happened to take.
60
+ */
61
+ export declare function searchParamsToObject(search: URLSearchParams): Record<string, unknown>;
62
+ /**
63
+ * Run a page's `params` schema, or hand back what the url gave.
64
+ *
65
+ * A refusal is notFound() rather than an error: the route matched a pattern,
66
+ * but the values in it do not describe a page, and 404 is the answer a crawler
67
+ * and a cache both need.
68
+ */
69
+ export declare function parseParams(schema: StandardSchemaV1 | undefined, value: Record<string, unknown>): Promise<unknown>;
70
+ /**
71
+ * Run a page's `searchParams` schema, or hand back the URLSearchParams.
72
+ *
73
+ * A refusal throws into the nearest error.tsx, which can read `errors` for the
74
+ * fields. The page exists — only the query was wrong — so this is deliberately
75
+ * not a 404.
76
+ */
77
+ export declare function parseSearchParams(schema: StandardSchemaV1 | undefined, search: URLSearchParams): Promise<unknown>;
78
+ /**
79
+ * What an api route's schemas turn a request into.
80
+ *
81
+ * Every field is optional and every one is opt-in: a route that exports no
82
+ * schema gets the raw values it always got, and a handler written before any
83
+ * of this existed keeps working untouched.
84
+ */
85
+ export interface ApiInput {
86
+ /** Parsed by the route's `params` export, or the raw url segments. */
87
+ params: unknown;
88
+ /** Parsed by the route's `searchParams` export, or the URLSearchParams. */
89
+ searchParams: unknown;
90
+ /** Parsed by the route's `body` export. Undefined when it declared none. */
91
+ body?: unknown;
92
+ }
93
+ /** A request body the route's own schema refused. */
94
+ export declare class BodyError extends Error {
95
+ readonly errors: Record<string, string[]>;
96
+ constructor(errors: Record<string, string[]>);
97
+ }
98
+ /**
99
+ * Whether this is a refused body, whichever copy of the class built it.
100
+ *
101
+ * The mark rather than instanceof, for the reason every check in this package
102
+ * uses one: an api route is bundled apart from the engine that calls it, so
103
+ * each side has its own copy of this class and instanceof is false between
104
+ * them — the refusal would surface as an unhandled 500.
105
+ */
106
+ export declare function isBodyError(error: unknown): error is BodyError;
107
+ /**
108
+ * Run a route's `body` schema against whatever the request carries.
109
+ *
110
+ * JSON and form encodings both, because an api route answers a url and a url
111
+ * is posted to by both. Anything else is handed to the schema as the raw text,
112
+ * which is the only honest thing to do with a body this does not understand.
113
+ */
114
+ export declare function parseBody(schema: StandardSchemaV1 | undefined, request: Request): Promise<unknown>;
115
+ export {};
@@ -0,0 +1,182 @@
1
+ // Typing and checking what a url carries.
2
+ //
3
+ // // src/app/posts/[slug]/page.tsx
4
+ // export const params = z.object({ slug: z.string().min(1) })
5
+ // export const searchParams = z.object({
6
+ // page: z.coerce.number().int().min(1).default(1),
7
+ // })
8
+ //
9
+ // Both are ordinary exports beside the page, read by the build the same way
10
+ // `metadata` and `generateStaticParams` are. Any Standard Schema works — Zod,
11
+ // Valibot, ArkType — because the schema is asked to validate itself and this
12
+ // module never imports one.
13
+ //
14
+ // What it buys beyond types: `?page=3` arrives as the number 3 rather than the
15
+ // string "3", a missing one arrives as the default rather than undefined, and
16
+ // `?page=banana` is refused in one place instead of surviving as NaN into
17
+ // whatever the page does with it.
18
+ //
19
+ // The two fail differently, and that is the point of having both:
20
+ //
21
+ // params a url that does not describe a page. /posts/ tells a
22
+ // crawler and a cache 404, not 500, so this is notFound().
23
+ // searchParams the page exists and the query was wrong, which is the
24
+ // nearest error.tsx — the same place any other bad input goes.
25
+ import { NotFoundSignal } from './notFound.js';
26
+ /**
27
+ * The mark that says a refusal came from a route schema.
28
+ *
29
+ * Symbol.for for the reason every mark in this package is: the app's pages are
30
+ * bundled apart from the engine, so each side gets its own copy of this class
31
+ * and `instanceof` is false between them.
32
+ */
33
+ const MARK = Symbol.for('@rsc-kit/core.route-input-error');
34
+ /** A query string the page's own schema refused. */
35
+ export class SearchParamsError extends Error {
36
+ errors;
37
+ constructor(errors) {
38
+ super('The query string is not valid for this page.');
39
+ this.name = 'SearchParamsError';
40
+ this.errors = errors;
41
+ this[MARK] = true;
42
+ }
43
+ }
44
+ /** Whether this is a refused query string, whichever copy of the class built it. */
45
+ export function isSearchParamsError(error) {
46
+ return (typeof error === 'object' && error !== null && error[MARK] === true);
47
+ }
48
+ /** The field an issue belongs to, joined so a nested one reads as it was named. */
49
+ function fieldOf(path) {
50
+ if (!path || path.length === 0)
51
+ return '';
52
+ return path
53
+ .map((segment) => (typeof segment === 'object' && segment !== null ? segment.key : segment))
54
+ .join('.');
55
+ }
56
+ function issuesToErrors(issues) {
57
+ const errors = {};
58
+ for (const issue of issues) {
59
+ ;
60
+ (errors[fieldOf(issue.path)] ??= []).push(issue.message);
61
+ }
62
+ return errors;
63
+ }
64
+ /**
65
+ * A URLSearchParams as the object a schema expects.
66
+ *
67
+ * A key that appears more than once becomes an array, and one that appears
68
+ * once stays a scalar — so `?tag=a&tag=b` reaches `z.array(z.string())` and
69
+ * `?q=shoes` reaches `z.string()` without the schema having to know which
70
+ * shape the url happened to take.
71
+ */
72
+ export function searchParamsToObject(search) {
73
+ const out = {};
74
+ for (const key of new Set(search.keys())) {
75
+ const all = search.getAll(key);
76
+ out[key] = all.length > 1 ? all : all[0];
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * Run a page's `params` schema, or hand back what the url gave.
82
+ *
83
+ * A refusal is notFound() rather than an error: the route matched a pattern,
84
+ * but the values in it do not describe a page, and 404 is the answer a crawler
85
+ * and a cache both need.
86
+ */
87
+ export async function parseParams(schema, value) {
88
+ if (!schema)
89
+ return value;
90
+ const result = await schema['~standard'].validate(value);
91
+ if (result.issues && result.issues.length > 0) {
92
+ // The message is for a log, never a response: it repeats back a piece of
93
+ // the url, and the not-found page is what the visitor sees either way.
94
+ throw new NotFoundSignal('No page for these params: ' +
95
+ Object.entries(issuesToErrors(result.issues))
96
+ .map(([field, messages]) => `${field || 'params'} ${messages[0]}`)
97
+ .join(', '));
98
+ }
99
+ return result.value;
100
+ }
101
+ /**
102
+ * Run a page's `searchParams` schema, or hand back the URLSearchParams.
103
+ *
104
+ * A refusal throws into the nearest error.tsx, which can read `errors` for the
105
+ * fields. The page exists — only the query was wrong — so this is deliberately
106
+ * not a 404.
107
+ */
108
+ export async function parseSearchParams(schema, search) {
109
+ if (!schema)
110
+ return search;
111
+ const result = await schema['~standard'].validate(searchParamsToObject(search));
112
+ if (result.issues && result.issues.length > 0) {
113
+ throw new SearchParamsError(issuesToErrors(result.issues));
114
+ }
115
+ return result.value;
116
+ }
117
+ /**
118
+ * Its own mark, not the query string's.
119
+ *
120
+ * Sharing one was a real bug for as long as it took a test to run: both
121
+ * refusals answered the check for a bad query string, whichever was asked
122
+ * first, so every refused body came back 400 instead of 422.
123
+ */
124
+ const BODY_MARK = Symbol.for('@rsc-kit/core.body-error');
125
+ /** A request body the route's own schema refused. */
126
+ export class BodyError extends Error {
127
+ errors;
128
+ constructor(errors) {
129
+ super('The request body is not valid.');
130
+ this.name = 'BodyError';
131
+ this.errors = errors;
132
+ this[BODY_MARK] = true;
133
+ }
134
+ }
135
+ /**
136
+ * Whether this is a refused body, whichever copy of the class built it.
137
+ *
138
+ * The mark rather than instanceof, for the reason every check in this package
139
+ * uses one: an api route is bundled apart from the engine that calls it, so
140
+ * each side has its own copy of this class and instanceof is false between
141
+ * them — the refusal would surface as an unhandled 500.
142
+ */
143
+ export function isBodyError(error) {
144
+ return (typeof error === 'object' &&
145
+ error !== null &&
146
+ error[BODY_MARK] === true);
147
+ }
148
+ /**
149
+ * Run a route's `body` schema against whatever the request carries.
150
+ *
151
+ * JSON and form encodings both, because an api route answers a url and a url
152
+ * is posted to by both. Anything else is handed to the schema as the raw text,
153
+ * which is the only honest thing to do with a body this does not understand.
154
+ */
155
+ export async function parseBody(schema, request) {
156
+ if (!schema)
157
+ return undefined;
158
+ const type = request.headers.get('Content-Type') ?? '';
159
+ let raw;
160
+ try {
161
+ if (type.includes('json')) {
162
+ raw = await request.json();
163
+ }
164
+ else if (type.includes('form')) {
165
+ raw = searchParamsToObject(new URLSearchParams(await request.text()));
166
+ }
167
+ else {
168
+ raw = await request.text();
169
+ }
170
+ }
171
+ catch {
172
+ // Malformed json is a refusal about the body as a whole rather than any
173
+ // field, which is the empty-string key the form errors already use.
174
+ throw new BodyError({ '': ['The request body could not be read.'] });
175
+ }
176
+ const result = await schema['~standard'].validate(raw);
177
+ if (result.issues && result.issues.length > 0) {
178
+ throw new BodyError(issuesToErrors(result.issues));
179
+ }
180
+ return result.value;
181
+ }
182
+ //# sourceMappingURL=routeSchema.js.map
@@ -0,0 +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"]}
package/dist/routing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { 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;
@@ -15,6 +15,25 @@ export interface MatchedRoute {
15
15
  * Static segments beat dynamic ones at the same position: /docs/new is the
16
16
  * page called new, not the page called [slug] with slug=new.
17
17
  */
18
+ /** A `route.ts` the url matched, and the params it bound. */
19
+ export interface MatchedApiRoute {
20
+ route: ManifestApiRoute;
21
+ params: Record<string, string>;
22
+ }
23
+ /**
24
+ * The api route a url answers, if one does.
25
+ *
26
+ * Exact segment binding only, scored the same way pages are: more static
27
+ * segments wins, a catch-all is weakest. Reusing that is the point — an api
28
+ * route takes `[id]` and `[...rest]` because the matcher was already there.
29
+ */
30
+ export declare function matchApiRoute(manifest: RouteManifest, pathname: string): MatchedApiRoute | null;
31
+ /**
32
+ * What a 405 should say this route does answer.
33
+ *
34
+ * HEAD is included whenever GET is, because it is answered by GET.
35
+ */
36
+ export declare function allowFor(route: ManifestApiRoute): string;
18
37
  export declare function matchRoute(manifest: RouteManifest, pathname: string): MatchedRoute | null;
19
38
  /**
20
39
  * The interceptor registered for a slot, if this url has one.
package/dist/routing.js CHANGED
@@ -10,16 +10,39 @@
10
10
  // wrong: answering /docs/new with [slug], and treating the layout chain as a
11
11
  // set rather than a sequence.
12
12
  /**
13
- * Match a pathname against the manifest's segments.
13
+ * The api route a url answers, if one does.
14
14
  *
15
- * The manifest stores segments rather than a pattern string, because the
16
- * pattern is the host's dialect — Laravel writes `{slug}`, Hono writes
17
- * `:slug`, and neither is the build's business. Matching them directly means
18
- * no dialect at all.
15
+ * Exact segment binding only, scored the same way pages are: more static
16
+ * segments wins, a catch-all is weakest. Reusing that is the point — an api
17
+ * route takes `[id]` and `[...rest]` because the matcher was already there.
18
+ */
19
+ export function matchApiRoute(manifest, pathname) {
20
+ const parts = pathname.split('/').filter(Boolean);
21
+ let best = null;
22
+ let bestScore = -1;
23
+ for (const route of manifest.apis ?? []) {
24
+ const bound = bindSegments(route.segments, parts);
25
+ if (!bound)
26
+ continue;
27
+ const score = route.segments.reduce((n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0), 0);
28
+ if (score > bestScore) {
29
+ best = { route, params: bound };
30
+ bestScore = score;
31
+ }
32
+ }
33
+ return best;
34
+ }
35
+ /**
36
+ * What a 405 should say this route does answer.
19
37
  *
20
- * Static segments beat dynamic ones at the same position: /docs/new is the
21
- * page called new, not the page called [slug] with slug=new.
38
+ * HEAD is included whenever GET is, because it is answered by GET.
22
39
  */
40
+ export function allowFor(route) {
41
+ const methods = route.methods.includes('HEAD') || !route.methods.includes('GET')
42
+ ? route.methods
43
+ : [...route.methods, 'HEAD'];
44
+ return methods.join(', ');
45
+ }
23
46
  export function matchRoute(manifest, pathname) {
24
47
  const parts = pathname.split('/').filter(Boolean);
25
48
  let best = null;
@@ -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 { 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 */\nexport function matchRoute(manifest: RouteManifest, pathname: string): MatchedRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n let best: MatchedRoute | null = null\n let bestScore = -1\n\n for (const route of manifest.routes) {\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n // More static segments wins; a catch-all is the weakest possible match.\n const score = route.segments.reduce(\n (n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0),\n 0,\n )\n\n if (score > bestScore) {\n best = { route, params: bound }\n bestScore = score\n }\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 { 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 */\nexport function matchApiRoute(\n manifest: RouteManifest,\n pathname: string,\n): MatchedApiRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n let best: MatchedApiRoute | null = null\n let bestScore = -1\n\n for (const route of manifest.apis ?? []) {\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n const score = route.segments.reduce(\n (n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0),\n 0,\n )\n\n if (score > bestScore) {\n best = { route, params: bound }\n bestScore = score\n }\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 let best: MatchedRoute | null = null\n let bestScore = -1\n\n for (const route of manifest.routes) {\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n // More static segments wins; a catch-all is the weakest possible match.\n const score = route.segments.reduce(\n (n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0),\n 0,\n )\n\n if (score > bestScore) {\n best = { route, params: bound }\n bestScore = score\n }\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"]}