@rsc-kit/core 0.10.0 → 0.11.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 * 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"]}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,iDAAiD;AACjD,4FAA4F;AAC5F,mCAAmC;AACnC,MAAM;AACN,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,6EAA6E;AAC7E,8EAA8E;AAC9E,iBAAiB;AACjB,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,qDAAqD;AACrD,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAC/E,4EAA4E;AAC5E,yEAAyE;AACzE,0EAA0E;AAC1E,gDAAgD;AAChD,EAAE;AACF,yEAAyE;AACzE,0CAA0C;AAE1C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAoC9C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAA;AAEvD,MAAM,OAAO,GAAG,UAA8C,CAAA;AAE9D,IAAI,KAAK,GAAyB,IAAI,CAAA;AAOtC,SAAS,KAAK;IACZ,OAAQ,OAAO,CAAC,KAAK,CAAuB,IAAI,IAAI,CAAA;AACtD,CAAC;AAED,SAAS,IAAI;IACX,MAAM,KAAK,GAAG,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAA;IAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,wFAAwF;YACtF,oDAAoD,CACvD,CAAA;IACH,CAAC;IAED,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IAEjB,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;AAC3D,CAAC;AAgDD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;AAExD,IAAI,UAAU,GAAyB,IAAI,CAAA;AAO3C,SAAS,KAAK;IACZ,OAAQ,OAAO,CAAC,KAAK,CAA4B,IAAI,IAAI,CAAA;AAC3D,CAAC;AAED,iEAAiE;AACjE,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,IAAI,GAAG,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAA;IAEhC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,oFAAoF;YACzF,sCAAsC,CACzC,CAAA;IACH,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,iFAAiF;YACtF,sFAAsF;YACtF,iBAAiB,CACpB,CAAA;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH;;;;;;;;;GASG;AACH,SAAS,QAAQ,CAAC,KAAY;IAC5B,MAAM,KAAK,GAAG,CAAC,MAAmC,EAAE,EAAE,CACpD,CAAC,GAAG,IAAsB,EAAE,EAAE;QAC5B,QAAQ,CAAC,mBAAmB,CAAC,CAAA;QAE7B,OAAQ,KAAK,CAAC,OAAO,CAAC,MAAM,CAA8B,CAAC,GAAG,IAAI,CAAC,CAAA;IACrE,CAAC,CAAA;IAEH,MAAM,KAAK,GAAY,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE;QAC9C,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ;YAC5B,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACzE,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAA;YACxB,CAAC;YAED,oEAAoE;YACpE,kEAAkE;YAClE,sEAAsE;YACtE,sEAAsE;YACtE,WAAW;YACX,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,OAAO,CAAC,EAAyD,EAAE,OAAiB,EAAE,EAAE,CACtF,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;YACvE,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;YAErD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;QACjE,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAA;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAsE;IAEtE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,UAAU,KAAK,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YAC9C,OAAO,CAAC,KAAK,CAAC,KAAK,QAAiC,CAAA;QACtD,CAAC,CAAC,CAAA;QAEF,MAAM,UAAU,CAAA;IAClB,CAAC;IAED,MAAM,IAAI,GAAU,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAE7D,OAAO,MAAM,KAAK,EAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CACnC,GAAG,CAAC;QACF,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO;QACzB,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACpB,CAAC;KACF,CAAC,CACH,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG,gCAAgC,CAAA;AAEpD,2EAA2E;AAC3E,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAEnC,qEAAqE;AACrE,MAAM,UAAU,eAAe,CAAC,MAA+D;IAC7F,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,6BAA6B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI;YAC1D,6EAA6E;YAC7E,oFAAoF,CACvF,CAAA;IACH,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAA;QAE3D,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAClE,sFAAsF,CACzF,CAAA;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI;QAC/B,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;QAC7B,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;QACjC,2EAA2E;QAC3E,uDAAuD;QACvD,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC;KAC1C,EAAE,CAAC;QACX,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CACb,cAAc,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,yBAAyB;gBACvE,yEAAyE,CAC5E,CAAA;QACH,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACpE,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAA;IAExB,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACzE,IAAI,CAAC,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;IAC/D,4EAA4E;IAC5E,2EAA2E;IAC3E,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,CAAA;IACnC,IAAI,CAAC,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;IAC9C,IAAI,CAAC,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3F,IAAI,CAAC,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAClC,IAAI,CAAC,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACtC,IAAI,CAAC,CAAC,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAE5C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,6EAA6E;IAC7E,0EAA0E;IAC1E,qCAAqC;IACrC,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAEzF,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAElE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,OAAO,GAAkB,EAAE,EAAQ,EAAE;QAC/E,mEAAmE;QACnE,+DAA+D;QAC/D,QAAQ,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,MAAM,CACxC,YAAY,EACZ,eAAe,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAC1C,CAAA;IACH,CAAC,CAAA;IAED,OAAO;QACL,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;QAC3B,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,MAAM;QAC7B,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QAC7B,GAAG,EAAE,KAAK;QACV,wEAAwE;QACxE,+DAA+D;QAC/D,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;KAC3E,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,uEAAuE;IACvE,8BAA8B;IAC9B,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC,cAAc,CAAC,CAAA;AAClD,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,GAAG;IACvB,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,MAAc;IAClC,MAAM,GAAG,GAA2B,EAAE,CAAA;IAEtC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAE5B,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,SAAQ;QAEvB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAErC,IAAI,CAAC,IAAI;YAAE,SAAQ;QAEnB,uEAAuE;QACvE,2DAA2D;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAErC,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAA;QACjB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAI,IAAiB,EAAE,GAAqB;IAC3E,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,KAAK,KAAK,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACzC,OAAO,CAAC,KAAK,CAAC,KAAK,QAA4B,CAAA;QACjD,CAAC,CAAC,CAAA;QAEF,MAAM,KAAK,CAAA;IACb,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,YAAY,OAAO,CAAA;IACzC,MAAM,KAAK,GAAS;QAClB,OAAO,EAAE,IAAI,KAAK,IAAI;QACtB,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI;QACpD,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE;QACtF,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;QACjC,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,EAAE;KACX,CAAA;IAED,OAAO,MAAM,KAAK,EAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AACvC,CAAC;AAED;;;;;;GAMG;AACH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,MAAM,KAAK,GAAG,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAA;IAEjC,IAAI,CAAC,KAAK;QAAE,OAAM;IAElB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IAEjB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAErD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,IAAI,KAAK,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,IAAI,EAAE,CAAA;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,6EAA6E;IAC7E,iDAAiD;IACjD,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;IAEpB,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC/D,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,EAAE,CAAA;IAExB,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,eAAe,EAAE,CAAA;AAClE,CAAC","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 */\n/**\n * Record a read without suspending on it.\n *\n * For a reader that CAN answer during a build but whose answer would be wrong\n * to store — an api route's query string, which exists and is empty, and which\n * the build has no business freezing an answer to. `never()` is for a reader\n * with nothing to return; this is for one that has something and should still\n * be noticed.\n *\n * A no-op outside a render scope, so a handler called at request time pays a\n * property read and nothing else.\n */\nexport function noteRequestRead(by: string): void {\n const store = scope()?.getStore()\n\n if (!store) return\n\n if (!store.readBy.includes(by)) store.readBy.push(by)\n}\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"]}
package/dist/vite.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { WebManifestOptions } from './webManifest.js';
1
2
  import type { PluginOption } from 'vite';
2
3
  export interface RscKitOptions {
3
4
  /** Project root. Defaults to RSC_PROJECT_ROOT, then cwd. */
@@ -188,5 +189,37 @@ export interface RscKitOptions {
188
189
  * Nothing else is touched. An action is a POST and must never be answered from
189
190
  * a cache; anything cross-origin is somebody else's to cache.
190
191
  */
191
- export declare const SERVICE_WORKER: (version: string, precache: string[]) => string;
192
+ export declare const SERVICE_WORKER: (version: string, precache: string[], frozen?: string[]) => string;
193
+ /**
194
+ * Write the worker beside the assets it caches.
195
+ *
196
+ * The version is a hash of what is being precached rather than a build id from
197
+ * the environment. The names are content-hashed already, so a build that
198
+ * changed nothing produces the same list and leaves the visitor's cache alone,
199
+ * and a build that changed anything produces a different one — which is the
200
+ * whole of cache invalidation, without asking anyone to set a variable.
201
+ */
202
+ /**
203
+ * Write the web app manifest into the client output, beside the assets.
204
+ *
205
+ * Into the same directory the browser is served from, because that is where a
206
+ * relative icon path resolves — a manifest served from somewhere else resolves
207
+ * its icons somewhere else too, and the failure is a browser that quietly does
208
+ * not offer to install.
209
+ */
210
+ /**
211
+ * The manifest an app declared beside its routes.
212
+ *
213
+ * `src/app/manifest.ts`, default-exporting the object — the same shape every
214
+ * other thing about a route tree takes here, and the reason this is a file
215
+ * rather than a `vite.config.ts` key. A manifest is not build configuration
216
+ * any more than a page is; it is one more thing the app declares about itself,
217
+ * and it belongs where the app is.
218
+ *
219
+ * Evaluated with the TypeScript stripped rather than imported: this runs while
220
+ * the plugin is being constructed, long before there is a module graph to pull
221
+ * it through, and importing an app module here would drag its imports in with
222
+ * it. The file is a literal object by contract, which is all that has to parse.
223
+ */
224
+ export declare function declaredManifest(appDir: string): WebManifestOptions | null;
192
225
  export declare function rscKit(options?: RscKitOptions): PluginOption[];
package/dist/vite.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // carry the route composition and the worker's render contract, and supplies
13
13
  // the structural config (entries, output dirs, base). @vitejs/plugin-rsc is
14
14
  // included here so it always runs before any react() layer the app adds.
15
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
15
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
16
16
  import { gzipSync } from 'node:zlib';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { createRequire } from 'node:module';
@@ -20,6 +20,9 @@ import { dirname, join, relative, resolve } from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
  import rsc from '@vitejs/plugin-rsc';
22
22
  import { loadEnv } from 'vite';
23
+ import { REPORT_FILE, buildReport } from './buildReport.js';
24
+ import { MANIFEST_PATH, manifestWarning, webManifest } from './webManifest.js';
25
+ import { ASSET_BASE, appAssets, headTags } from './appAssets.js';
23
26
  import { httpHostCalls } from './hostCalls.js';
24
27
  // Resolved once per rscKit() call. One build runs in one process, so these are
25
28
  // module state rather than threaded through every helper.
@@ -58,6 +61,8 @@ let isWatch = false;
58
61
  let viewTransitions = false;
59
62
  /** Whether a service worker is generated and registered — see options. */
60
63
  let offline = false;
64
+ let webManifestOptions = null;
65
+ let foundAssets = { favicon: null, icons: [], appleIcon: null, openGraph: null, twitter: null };
61
66
  /** Host functions to generate stubs for — see RscKitOptions.hostActions. */
62
67
  let hostActions;
63
68
  /**
@@ -201,6 +206,20 @@ function resolvePaths(options) {
201
206
  prerenderAfterBuild = options.prerender ?? process.env.RSC_PRERENDER !== '0';
202
207
  viewTransitions = options.viewTransitions === true;
203
208
  offline = options.offline === true;
209
+ // One place, and it is the file. A plugin option as well would be the same
210
+ // thing sayable in two places, which is the problem the file was moved to
211
+ // solve rather than a convenience to keep beside it.
212
+ webManifestOptions = declaredManifest(join(sourceDir, 'app'));
213
+ foundAssets = appAssets(join(sourceDir, 'app'));
214
+ // An app that put icons where they could be found has already listed them.
215
+ // Writing them again in the manifest is the same set in two places, and the
216
+ // one that goes stale is the one nobody looks at.
217
+ if (webManifestOptions && !webManifestOptions.icons?.length && foundAssets.icons.length) {
218
+ webManifestOptions = {
219
+ ...webManifestOptions,
220
+ icons: foundAssets.icons.map((icon) => icon.href),
221
+ };
222
+ }
204
223
  hostActions = options.hostActions ?? fileHostActions(projectRoot);
205
224
  }
206
225
  function log(...args) {
@@ -547,11 +566,21 @@ function reportAllDynamic() {
547
566
  * Nothing else is touched. An action is a POST and must never be answered from
548
567
  * a cache; anything cross-origin is somebody else's to cache.
549
568
  */
550
- export const SERVICE_WORKER = (version, precache) => `// GENERATED by rscKit() — do not edit.
569
+ export const SERVICE_WORKER = (version, precache, frozen = []) => `// GENERATED by rscKit() — do not edit.
551
570
  const VERSION = ${JSON.stringify(version)}
552
571
  const CACHE = 'rsc-kit-' + VERSION
553
572
  const PRECACHE = ${JSON.stringify(precache, null, 2)}
554
573
 
574
+ // The urls the build stored whole. Their answer cannot change until the next
575
+ // deploy, and a deploy changes VERSION and sweeps this cache — so for these,
576
+ // and only these, the cache is the better source and the network is the
577
+ // fallback rather than the other way round.
578
+ //
579
+ // Everything else stays network-first. A page that reads the request has a
580
+ // right answer that depends on the request, and serving yesterday's from a
581
+ // cache would be wrong in a way the visitor cannot see.
582
+ const FROZEN = new Set(${JSON.stringify(frozen)})
583
+
555
584
  self.addEventListener('install', (event) => {
556
585
  // The new worker takes over rather than waiting for every tab to close.
557
586
  // Safe here because assets are content-hashed: a page already open keeps
@@ -622,6 +651,40 @@ self.addEventListener('fetch', (event) => {
622
651
  return
623
652
  }
624
653
 
654
+ // A stored page, asked for without a query string. Cache first, and refresh
655
+ // in the background so the next visit has the new one even if this worker
656
+ // never updates.
657
+ //
658
+ // The query matters for the same reason it matters to a stored api route:
659
+ // the build answered the bare url, and a page reading ?q= answers
660
+ // differently for every value of it.
661
+ if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
662
+ event.respondWith(
663
+ caches.match(keyFor(request)).then((hit) => {
664
+ const fresh = fetch(request)
665
+ .then((response) => {
666
+ if (mayStore(response)) {
667
+ const copy = response.clone()
668
+ caches.open(CACHE).then((cache) => cache.put(keyFor(request), copy))
669
+ }
670
+
671
+ return response
672
+ })
673
+ .catch((error) => {
674
+ if (hit) return hit
675
+
676
+ throw error
677
+ })
678
+
679
+ // Not awaited when there is a hit: the point is that the visitor does
680
+ // not wait for the network for a page that cannot have changed.
681
+ return hit ?? fresh
682
+ }),
683
+ )
684
+
685
+ return
686
+ }
687
+
625
688
  event.respondWith(
626
689
  fetch(request)
627
690
  .then((response) => {
@@ -698,7 +761,98 @@ self.addEventListener('fetch', (event) => {
698
761
  * and a build that changed anything produces a different one — which is the
699
762
  * whole of cache invalidation, without asking anyone to set a variable.
700
763
  */
701
- function writeServiceWorker(clientDir) {
764
+ /**
765
+ * Write the web app manifest into the client output, beside the assets.
766
+ *
767
+ * Into the same directory the browser is served from, because that is where a
768
+ * relative icon path resolves — a manifest served from somewhere else resolves
769
+ * its icons somewhere else too, and the failure is a browser that quietly does
770
+ * not offer to install.
771
+ */
772
+ /**
773
+ * The manifest an app declared beside its routes.
774
+ *
775
+ * `src/app/manifest.ts`, default-exporting the object — the same shape every
776
+ * other thing about a route tree takes here, and the reason this is a file
777
+ * rather than a `vite.config.ts` key. A manifest is not build configuration
778
+ * any more than a page is; it is one more thing the app declares about itself,
779
+ * and it belongs where the app is.
780
+ *
781
+ * Evaluated with the TypeScript stripped rather than imported: this runs while
782
+ * the plugin is being constructed, long before there is a module graph to pull
783
+ * it through, and importing an app module here would drag its imports in with
784
+ * it. The file is a literal object by contract, which is all that has to parse.
785
+ */
786
+ export function declaredManifest(appDir) {
787
+ for (const extension of ['ts', 'tsx', 'js', 'mjs']) {
788
+ const file = join(appDir, `manifest.${extension}`);
789
+ if (!existsSync(file))
790
+ continue;
791
+ const source = readFileSync(file, 'utf-8');
792
+ // The object literal after `export default`, with a `satisfies` or `as`
793
+ // annotation tolerated after it — which is how anyone who wants the type
794
+ // checked will actually write the file, and getting that wrong would refuse
795
+ // the recommended spelling.
796
+ //
797
+ // A manifest that is not a literal — computed, imported from elsewhere — is
798
+ // refused loudly rather than silently ignored, because the failure would
799
+ // otherwise be an app that is simply not installable with nothing said.
800
+ const match = /export\s+default\s+(\{[\s\S]*\})(?:\s+(?:satisfies|as)\s+[\w.<>\[\]| ]+)?\s*;?\s*$/.exec(source.trim());
801
+ if (!match) {
802
+ throw new Error(`[rsc-kit] app/manifest.${extension} must default-export an object literal.\n` +
803
+ 'It is read at build time, before there is a module graph to evaluate it in, so it ' +
804
+ 'cannot be computed or imported from elsewhere.');
805
+ }
806
+ try {
807
+ // Function rather than JSON.parse: the file is TypeScript source with
808
+ // unquoted keys, trailing commas and comments in it, none of which JSON
809
+ // accepts and all of which are ordinary in a file a person edits.
810
+ return new Function(`return (${match[1]})`)();
811
+ }
812
+ catch (error) {
813
+ throw new Error(`[rsc-kit] Could not read app/manifest.${extension}: ${error.message}`);
814
+ }
815
+ }
816
+ return null;
817
+ }
818
+ /**
819
+ * Copy the icons and share images into the client output.
820
+ *
821
+ * They live in `app/` so the build can read them - an icon's filename decides
822
+ * what goes in the manifest - but a browser has to be able to fetch them, and
823
+ * nothing serves `app/`. Copied rather than symlinked: the output is what gets
824
+ * deployed, and a link into a source tree that is not deployed points nowhere.
825
+ */
826
+ function copyAppAssets(clientDir) {
827
+ if (!existsSync(clientDir))
828
+ return;
829
+ const all = [
830
+ ...(foundAssets.favicon ? [foundAssets.favicon] : []),
831
+ ...foundAssets.icons,
832
+ ...(foundAssets.appleIcon ? [foundAssets.appleIcon] : []),
833
+ ...(foundAssets.openGraph ? [foundAssets.openGraph] : []),
834
+ ...(foundAssets.twitter ? [foundAssets.twitter] : []),
835
+ ];
836
+ if (all.length === 0)
837
+ return;
838
+ for (const asset of all) {
839
+ const to = join(clientDir, asset.href.slice(1));
840
+ mkdirSync(dirname(to), { recursive: true });
841
+ copyFileSync(join(sourceDir, 'app', asset.file), to);
842
+ }
843
+ log(`icons: ${all.length} copied from app/`);
844
+ }
845
+ function writeWebManifest(clientDir, options) {
846
+ if (!existsSync(clientDir))
847
+ return;
848
+ writeFileSync(join(clientDir, MANIFEST_PATH.slice(1)), webManifest(options));
849
+ const warning = manifestWarning(options);
850
+ if (warning)
851
+ log(warning);
852
+ else
853
+ log(`manifest: ${options.name} is installable`);
854
+ }
855
+ function writeServiceWorker(clientDir, frozen = []) {
702
856
  if (!existsSync(clientDir))
703
857
  return;
704
858
  const files = [];
@@ -718,7 +872,7 @@ function writeServiceWorker(clientDir) {
718
872
  walk(clientDir, '');
719
873
  const precache = ['/', ...files.map((file) => `/${file}`)].sort();
720
874
  const version = createHash('sha256').update(precache.join('\n')).digest('hex').slice(0, 12);
721
- writeFileSync(join(clientDir, 'sw.js'), SERVICE_WORKER(version, precache));
875
+ writeFileSync(join(clientDir, 'sw.js'), SERVICE_WORKER(version, precache, frozen));
722
876
  log(`offline: ${precache.length} files precached as rsc-kit-${version}`);
723
877
  }
724
878
  /**
@@ -768,6 +922,7 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir) {
768
922
  // Weighed from the page the prerenderer just wrote, so the column is what
769
923
  // that page actually loads rather than a total every route is charged for.
770
924
  const weigh = weighClientJs(assetsDir);
925
+ const sized = new Map();
771
926
  const pending = [];
772
927
  const results = await prerender({
773
928
  engine,
@@ -779,9 +934,12 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir) {
779
934
  const file = [`${key}.html`, `${key}.ppr.html`]
780
935
  .map((name) => join(staticDir, name))
781
936
  .find((path) => existsSync(path));
937
+ const bytes = file ? weigh(readFileSync(file, 'utf-8')) : null;
938
+ if (bytes !== null)
939
+ sized.set(r.url, bytes);
782
940
  pending.push({
783
941
  line: ` ${mark[r.type] ?? ' '} ${r.url}`,
784
- bytes: file ? weigh(readFileSync(file, 'utf-8')) : null,
942
+ bytes,
785
943
  extra: [
786
944
  ...(r.reason ? [` ${r.reason}`] : []),
787
945
  ...(r.warning ? [` ⚠ ${r.warning}`] : []),
@@ -793,7 +951,8 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir) {
793
951
  // either answered or could not, which is the same question the table above
794
952
  // is already answering — a second list under its own heading would be two
795
953
  // places to look for one fact.
796
- const apis = await prerenderApiRoutes(engine, engine.manifest(), writeTo(staticDir));
954
+ const manifest = engine.manifest();
955
+ const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
797
956
  for (const api of apis) {
798
957
  pending.push({
799
958
  line: ` ${api.type === 'frozen' ? '○' : 'ƒ'} ${api.url}`,
@@ -811,6 +970,16 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir) {
811
970
  console.log(line);
812
971
  }
813
972
  const count = (type) => results.filter((r) => r.type === type).length;
973
+ // Written from the rows that were just printed rather than recomputed: the
974
+ // report and the terminal must not be able to disagree about what happened.
975
+ writeFileSync(join(outDir, REPORT_FILE), buildReport(results.map((r) => ({
976
+ url: r.url,
977
+ component: r.component,
978
+ type: r.type,
979
+ reason: r.reason,
980
+ warning: r.warning ?? null,
981
+ clientJs: sized.get(r.url) ?? null,
982
+ })), apis.map((a) => ({ url: a.url, name: a.name, type: a.type, reason: a.reason }))));
814
983
  const note = notes(results);
815
984
  const counted = [...results, ...apis];
816
985
  console.log(`
@@ -824,6 +993,25 @@ ${legend(counted)}
824
993
  }
825
994
  if (output === 'export')
826
995
  await exportAfterPrerender(results, staticDir, assetsDir);
996
+ // The urls whose answer cannot change until the next build. The service
997
+ // worker serves these from its cache first rather than asking the network
998
+ // and falling back — see writeServiceWorker.
999
+ //
1000
+ // Two narrowings beyond "frozen", and neither is redundant even though the
1001
+ // worker would survive without them. A guarded route IS frozen — the guard
1002
+ // is a serving decision, not a build one — but its response is per visitor
1003
+ // and must never be read from a cache that has no notion of who asked. A
1004
+ // redirect is stored as a destination rather than a page, so there is no
1005
+ // document to serve from a cache at all.
1006
+ //
1007
+ // Both are already refused downstream: a guarded response carries no-store
1008
+ // and a 3xx is not `ok`. Saying it here as well means the list means what it
1009
+ // says, rather than being a wider list that happens to be filtered later.
1010
+ const guarded = new Set(manifest.routes.filter((r) => r.middleware?.length).map((r) => r.component));
1011
+ return results
1012
+ .filter((r) => r.type === 'frozen' && !guarded.has(r.component))
1013
+ .filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) + '.html')))
1014
+ .map((r) => r.url);
827
1015
  }
828
1016
  /**
829
1017
  * Weighs the javascript one stored page makes the browser download.
@@ -1357,6 +1545,7 @@ import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameP
1357
1545
  import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
1358
1546
  import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
1359
1547
  import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
1548
+ import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
1360
1549
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
1361
1550
  import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
1362
1551
  import { httpHostCalls } from ${JSON.stringify(join(packageDir, 'hostCalls'))}
@@ -1381,6 +1570,20 @@ ${mapEntries.join('\n')}
1381
1570
  * Empty for a page that exported none, which is the common case — the lookup
1382
1571
  * below then hands the url through untouched and costs a property read.
1383
1572
  */
1573
+ /**
1574
+ * The manifest to link from every page, or null when the app declared none.
1575
+ *
1576
+ * A literal rather than a define: defines are configured per environment and
1577
+ * this is read while rendering, in the rsc one. Generated in, so an app with
1578
+ * no manifest carries the word null and no branch worth taking.
1579
+ */
1580
+ /** Head tags for the icons and share images found in app/. */
1581
+ const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
1582
+
1583
+ const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
1584
+ ? { href: MANIFEST_PATH, ...(webManifestOptions.themeColor ? { themeColor: webManifestOptions.themeColor } : {}) }
1585
+ : null)}
1586
+
1384
1587
  const urlSchemas: Record<string, { params?: any; searchParams?: any }> = {
1385
1588
  ${schemaEntries.join('\n')}
1386
1589
  }
@@ -1401,6 +1604,33 @@ ${apiEntries.join('\n')}
1401
1604
  * without a body. Answering 405 instead breaks link checkers and anything that
1402
1605
  * probes before it fetches.
1403
1606
  */
1607
+ /**
1608
+ * A promise that does not start until something awaits it.
1609
+ *
1610
+ * A thenable rather than a promise for the reason pageSearchParams is one:
1611
+ * every handler is handed all three of these whether it reads them or not, and
1612
+ * a real promise would run the work - and record the read - for every route on
1613
+ * every request.
1614
+ */
1615
+ function lazily<T>(start: () => Promise<T>): Promise<T> {
1616
+ let pending: Promise<T> | null = null
1617
+
1618
+ const begin = (): Promise<T> => {
1619
+ if (!pending) {
1620
+ pending = start()
1621
+ pending.catch(() => {})
1622
+ }
1623
+
1624
+ return pending
1625
+ }
1626
+
1627
+ return {
1628
+ then: (ok, fail) => begin().then(ok, fail),
1629
+ catch: (fail) => begin().catch(fail),
1630
+ finally: (done) => begin().finally(done),
1631
+ } as Promise<T>
1632
+ }
1633
+
1404
1634
  /** Whether a method may carry a body worth reading. */
1405
1635
  function hasBody(method: string): boolean {
1406
1636
  return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE'
@@ -1454,26 +1684,39 @@ export async function handleApiRoute(
1454
1684
  return new Response('Method not allowed', { status: 405, headers: { Allow: allow } })
1455
1685
  }
1456
1686
 
1457
- // Schemas are optional, per route and per kind. A route that exported none
1458
- // reaches the handler with exactly what it always did — the raw params, the
1459
- // URLSearchParams, and a body nobody has read — so nothing written before
1460
- // this existed changes behaviour.
1461
- let input
1687
+ // Awaited by the handler, not before it - the same shape a page's props have,
1688
+ // and for more than symmetry. A route that never awaits its query string
1689
+ // provably does not vary by it, so the build can store one answer and the
1690
+ // host can serve it for any query at all. Resolved eagerly here, that fact
1691
+ // is unknowable and every ?utm_source= misses the stored answer.
1692
+ //
1693
+ // Schemas stay optional per route and per kind. A route that exported none
1694
+ // gets the raw params, the URLSearchParams, and a body nobody has read.
1695
+ const input = {
1696
+ params: lazily(() => parseParams(mod.params, params)),
1697
+ searchParams: lazily(() => {
1698
+ // Declaring a schema is itself a statement that the query matters, so a
1699
+ // route with one is treated as varying by it whether or not the handler
1700
+ // reaches for the value.
1701
+ noteRequestRead('searchParams')
1702
+
1703
+ return parseSearchParams(mod.searchParams, new URL(request.url).searchParams)
1704
+ }),
1705
+ body: hasBody(method) ? lazily(() => parseBody(mod.body, request)) : undefined,
1706
+ }
1707
+
1708
+ if (mod.searchParams) noteRequestRead('searchParams')
1709
+
1710
+ let answer
1462
1711
  try {
1463
- input = {
1464
- params: await parseParams(mod.params, params),
1465
- searchParams: await parseSearchParams(
1466
- mod.searchParams,
1467
- new URL(request.url).searchParams,
1468
- ),
1469
- body: hasBody(method) ? await parseBody(mod.body, request) : undefined,
1470
- }
1712
+ answer = await handler(request, input)
1471
1713
  } catch (error) {
1714
+ // A refusal raised by one of the thenables above surfaces here, because the
1715
+ // handler awaited it and did not catch it. Anything else is the route's own
1716
+ // failure and is left to the caller.
1472
1717
  return refusedInput(error)
1473
1718
  }
1474
1719
 
1475
- const answer = await handler(request, input)
1476
-
1477
1720
  // A HEAD answered by GET must not carry a body.
1478
1721
  if (method === 'HEAD' && answer instanceof Response) {
1479
1722
  return new Response(null, { status: answer.status, headers: answer.headers })
@@ -1868,6 +2111,27 @@ async function renderTree(
1868
2111
  const md = await resolveMetadata(component, props, layouts)
1869
2112
  const head: unknown[] = []
1870
2113
 
2114
+ // Rendered into the tree rather than written into the app's layout: React
2115
+ // hoists a link and a meta into <head> from anywhere, so this works for an
2116
+ // app that already has a layout and never asks anyone to edit one. The
2117
+ // engine knows the manifest exists; the app should not have to.
2118
+ // Icons and share images the build found in app/. Rendered here so React
2119
+ // hoists them into <head>, the same way the manifest link is — an app never
2120
+ // edits its layout to get a favicon.
2121
+ for (const [i, tag] of APP_HEAD.entries()) {
2122
+ head.push(createElement(tag.tag, { key: '__a' + i, ...tag.props }))
2123
+ }
2124
+
2125
+ if (WEB_MANIFEST) {
2126
+ head.push(createElement('link', { key: '__mf', rel: 'manifest', href: WEB_MANIFEST.href }))
2127
+
2128
+ if (WEB_MANIFEST.themeColor) {
2129
+ head.push(
2130
+ createElement('meta', { key: '__tc', name: 'theme-color', content: WEB_MANIFEST.themeColor }),
2131
+ )
2132
+ }
2133
+ }
2134
+
1871
2135
  if (md) {
1872
2136
  if (md.title != null) {
1873
2137
  // The element is what a server render puts in <head>, and what a route
@@ -3517,9 +3781,16 @@ export function rscKit(options = {}) {
3517
3781
  const staticDir = clientOut
3518
3782
  ? join(dirname(clientOut), 'server', NITRO_STATIC_DIR)
3519
3783
  : join(outDir, NITRO_STATIC_DIR);
3520
- await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir);
3784
+ const frozen = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir);
3785
+ // Manifest first. The service worker precaches whatever it finds in this
3786
+ // directory, so writing it afterwards leaves it out of the list — and an
3787
+ // installed app whose manifest is the one file that needs the network is
3788
+ // the wrong way round.
3789
+ copyAppAssets(clientOut ?? publicAssetsDir);
3790
+ if (webManifestOptions)
3791
+ writeWebManifest(clientOut ?? publicAssetsDir, webManifestOptions);
3521
3792
  if (offline)
3522
- writeServiceWorker(clientOut ?? publicAssetsDir);
3793
+ writeServiceWorker(clientOut ?? publicAssetsDir, frozen);
3523
3794
  },
3524
3795
  configResolved(config) {
3525
3796
  isWatch = config.build?.watch != null;