@rsc-kit/core 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadataRoutes.d.ts +20 -0
- package/dist/metadataRoutes.js +35 -0
- package/dist/metadataRoutes.js.map +1 -1
- package/dist/prerender.js +1 -0
- package/dist/prerender.js.map +1 -1
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/vite.d.ts +42 -2
- package/dist/vite.js +333 -5
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryClient.js","sourceRoot":"","sources":["../../src/js/queryClient.ts"],"names":[],"mappings":"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","sourcesContent":["// The browser half of `query()`: send a read as a GET.\n//\n// That is the whole of it. There is no cache here, no batching and no\n// deduplication, because TanStack Query and SWR already do those and do them\n// better — this owns the transport and they own everything above it.\n//\n// The awkward fact it is built around: a server function's id is not readable\n// from the client. React keeps it in a module-private WeakMap and exposes no\n// getter, so there is no `ref.$$id` to build a url from. What the client *can*\n// do is call the reference and be handed the id by React itself — the stub's\n// whole body is `callServer(id, args)`, and this package already owns\n// `callServer`.\n//\n// So a read opens a one-shot slot and calls the reference; the transport claims\n// the slot and sends a GET instead of posting. The slot is also what says this\n// is a read at all: calling `getListings(kind)` directly is still a POST, and\n// `fetchQuery(getListings, [kind])` is the same call as a GET. Nothing has to\n// know a list of query ids, on either side of the build.\n\n/** Where reads are answered. Must match HEADER.queryPath on the server. */\nconst QUERY_PATH = '/_rsc/query'\n\n/**\n * Sent on every read, and required by the endpoint.\n *\n * Not decoration. A GET with no unusual header is a *simple* request, so any\n * page anywhere can trigger one with `<img src=\"…/_rsc/query?…\">` and it goes\n * out with the visitor's cookies — CORS stops the attacker reading the answer,\n * but the read still runs. This header is not CORS-safelisted, so the browser\n * preflights it, and nothing here answers a preflight.\n *\n * That restores exactly the protection a POST had: a `POST` carrying\n * `X-RSC-Action` is non-simple for the same reason.\n */\nconst QUERY_HEADER = 'X-RSC-Query'\n\n/**\n * How long a url may get before the read goes as a POST instead.\n *\n * Conservative. Proxies and CDNs start refusing somewhere between 8k and 16k,\n * and the failure is a 414 from a machine that is not ours.\n */\nconst MAX_URL = 6_000\n\ntype Reader = (...args: unknown[]) => Promise<unknown>\n\n/**\n * The slot a read opens before calling the reference.\n *\n * One-shot and synchronous: React's stub calls `callServer` in its own body\n * with nothing awaited in between, so exactly one transport call can claim it.\n */\nlet slot: { claimed: boolean; promise: Promise<unknown> | null } | null = null\n\n/**\n * Claim the open slot, if a read opened one.\n *\n * Called from the app's `callServer` before it posts. Returns null when this is\n * an ordinary action, which is every call that did not come through\n * `fetchQuery`.\n */\nexport function claimRead(id: string, args: unknown[]): Promise<unknown> | null {\n if (!slot || slot.claimed) return null\n\n slot.claimed = true\n slot.promise = send(id, args)\n\n return slot.promise\n}\n\n/**\n * Read a query, over GET.\n *\n * Hand this to a cache library and let it decide everything else:\n *\n * queryFn: () => fetchQuery(getListings, [kind])\n * useSWR(['listings', kind], () => fetchQuery(getListings, [kind]))\n *\n * Every call goes to the server, which is what a fetcher needs — staleness,\n * revalidation, retries, polling and deduplication all belong to the library\n * holding the answer, not to the thing that fetches it.\n */\nexport function fetchQuery<Data>(\n reference: (...args: never[]) => Promise<Data>,\n args: unknown[] = [],\n): Promise<Data> {\n if (typeof window === 'undefined') {\n // React's SSR runtime refuses a server-function call during the initial\n // render, and reaching a query's id means calling its reference — so this\n // cannot work here. Raised with the fix in it rather than left to surface\n // as React's more general message about fetch waterfalls.\n //\n // A cache library runs its fetcher in an effect, so the server render never\n // reaches this; a server component that wants the data during render should\n // await the query directly, which needs none of this.\n throw new Error(\n 'A query was read during server rendering. Read it in a server component with await, or through a cache library, whose fetcher runs after hydration.',\n )\n }\n\n const opened = { claimed: false, promise: null as Promise<unknown> | null }\n\n slot = opened\n\n try {\n // React's stub reaches callServer synchronously, so the slot is claimed by\n // the time this returns. The stub's own promise is discarded: the one the\n // transport made is the one that settles with the answer.\n ;(reference as unknown as Reader)(...(args as unknown[]))\n } finally {\n // Cleared here and nowhere else, so a reference that throws on the way in\n // does not leave the slot open for the next read to claim by accident.\n // Nothing is returned from this block: a `return` in `finally` discards\n // whatever the `try` was throwing, which would turn a reference that threw\n // synchronously into the misleading bound-reference error below.\n slot = null\n }\n\n if (!opened.claimed || !opened.promise) {\n // The only way here is a reference whose call path is asynchronous, which\n // today means one that was `.bind()`-ed. Loud, because the quiet version is\n // a read that silently went out as a POST.\n throw new Error(\n 'A query was called through a bound reference. Pass the exported query itself, unbound.',\n )\n }\n\n return opened.promise as Promise<Data>\n}\n\nasync function send(id: string, args: unknown[]): Promise<unknown> {\n const encoded = await encode(args)\n\n // encodeReply answers with FormData the moment an argument holds a File, and\n // a url cannot carry one. Rather than refuse a call that would have worked as\n // an action, the read falls back to a POST: it stops being cacheable, which\n // is the only thing it loses.\n if (typeof encoded !== 'string') return await post(id, args, 'an argument contained a File')\n\n const url = `${QUERY_PATH}?id=${encodeURIComponent(id)}&args=${encodeURIComponent(encoded)}`\n\n if (url.length > MAX_URL) return await post(id, args, 'the arguments are too large for a url')\n\n const res = await fetch(url, {\n headers: {\n [QUERY_HEADER]: '1',\n Accept: 'text/x-component',\n // Where the read came from. Same reason an action sends it: a host that\n // guards by route needs to know which page is asking.\n 'X-RSC-Referer': window.location.pathname + window.location.search,\n },\n })\n\n if (!res.ok || !res.body) {\n throw await failureFrom(res)\n }\n\n return await deserialize(res.body)\n}\n\n/**\n * What a refused read rejects with.\n *\n * A cache library reports failure by rejection, so this has to be an Error —\n * and one carrying the message the server meant, rather than a status code. A\n * validation refusal keeps its fields on `errors`, where a form can read them.\n */\nasync function failureFrom(res: Response): Promise<Error> {\n const body = await res.text().catch(() => '')\n\n try {\n const parsed = JSON.parse(body) as { message?: string; errors?: Record<string, string[]> }\n const error = new Error(parsed.message || `Query failed: ${res.status}`)\n\n if (parsed.errors) (error as Error & { errors?: unknown }).errors = parsed.errors\n\n return error\n } catch {\n return new Error(body || `Query failed: ${res.status}`)\n }\n}\n\n/**\n * The same read, as an action.\n *\n * Reached only when the arguments cannot ride in a url. Warned about in\n * development rather than silently tolerated, because the read still works and\n * the thing that changed — it is no longer a GET, so nothing can cache it — is\n * otherwise invisible.\n */\nasync function post(id: string, args: unknown[], why: string): Promise<unknown> {\n if (import.meta.env?.DEV) {\n console.warn(`[rsc-kit] A query was sent as a POST because ${why}. It will not be cached.`)\n }\n\n return await asAction(id, args)\n}\n\n/**\n * The Flight codec and the action transport, installed by the app bootstrap.\n *\n * Injected rather than imported, and not for testing: this module is reached\n * from client components, so importing the browser runtime here would pull a\n * second copy of it into that graph — two client-reference registries, where\n * components resolve to undefined with nothing logged. The bootstrap already\n * holds the one true copy.\n */\nlet deserialize: (stream: ReadableStream) => Promise<unknown> = () => {\n throw new Error('No Flight decoder installed. createViteRscApp() sets one up.')\n}\n\nlet encode: (args: unknown[]) => Promise<string | FormData> = () => {\n throw new Error('No Flight encoder installed. createViteRscApp() sets one up.')\n}\n\nlet asAction: (id: string, args: unknown[]) => Promise<unknown> = () => {\n throw new Error('No action transport installed. createViteRscApp() sets one up.')\n}\n\nexport function setQueryCodec(codec: {\n deserialize: (stream: ReadableStream) => Promise<unknown>\n encode: (args: unknown[]) => Promise<string | FormData>\n asAction: (id: string, args: unknown[]) => Promise<unknown>\n}): void {\n deserialize = codec.deserialize\n encode = codec.encode\n asAction = codec.asAction\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryClient.js","sourceRoot":"","sources":["../../src/js/queryClient.ts"],"names":[],"mappings":"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","sourcesContent":["// The browser half of `query()`: send a read as a GET.\n//\n// That is the whole of it. There is no cache here, no batching and no\n// deduplication, because TanStack Query and SWR already do those and do them\n// better — this owns the transport and they own everything above it.\n//\n// The awkward fact it is built around: a server function's id is not readable\n// from the client. React keeps it in a module-private WeakMap and exposes no\n// getter, so there is no `ref.$$id` to build a url from. What the client *can*\n// do is call the reference and be handed the id by React itself — the stub's\n// whole body is `callServer(id, args)`, and this package already owns\n// `callServer`.\n//\n// So a read opens a one-shot slot and calls the reference; the transport claims\n// the slot and sends a GET instead of posting. The slot is also what says this\n// is a read at all: calling `getListings(kind)` directly is still a POST, and\n// `fetchQuery(getListings, [kind])` is the same call as a GET. Nothing has to\n// know a list of query ids, on either side of the build.\n\n/** Where reads are answered. Must match HEADER.queryPath on the server. */\nconst QUERY_PATH = '/_rsc/query'\n\n/**\n * Sent on every read, and required by the endpoint.\n *\n * Not decoration. A GET with no unusual header is a *simple* request, so any\n * page anywhere can trigger one with `<img src=\"…/_rsc/query?…\">` and it goes\n * out with the visitor's cookies — CORS stops the attacker reading the answer,\n * but the read still runs. This header is not CORS-safelisted, so the browser\n * preflights it, and nothing here answers a preflight.\n *\n * That restores exactly the protection a POST had: a `POST` carrying\n * `X-RSC-Action` is non-simple for the same reason.\n */\nconst QUERY_HEADER = 'X-RSC-Query'\n\n/**\n * How long a url may get before the read goes as a POST instead.\n *\n * Conservative. Proxies and CDNs start refusing somewhere between 8k and 16k,\n * and the failure is a 414 from a machine that is not ours.\n */\nconst MAX_URL = 6_000\n\ntype Reader = (...args: unknown[]) => Promise<unknown>\n\n/**\n * The slot a read opens before calling the reference.\n *\n * One-shot and synchronous: React's stub calls `callServer` in its own body\n * with nothing awaited in between, so exactly one transport call can claim it.\n */\nlet slot: { claimed: boolean; promise: Promise<unknown> | null } | null = null\n\n/**\n * Claim the open slot, if a read opened one.\n *\n * Called from the app's `callServer` before it posts. Returns null when this is\n * an ordinary action, which is every call that did not come through\n * `fetchQuery`.\n */\nexport function claimRead(id: string, args: unknown[]): Promise<unknown> | null {\n if (!slot || slot.claimed) return null\n\n slot.claimed = true\n slot.promise = send(id, args)\n\n return slot.promise\n}\n\n/**\n * Read a query, over GET.\n *\n * Hand this to a cache library and let it decide everything else:\n *\n * queryFn: () => fetchQuery(getListings, [kind])\n * useSWR(['listings', kind], () => fetchQuery(getListings, [kind]))\n *\n * Every call goes to the server, which is what a fetcher needs — staleness,\n * revalidation, retries, polling and deduplication all belong to the library\n * holding the answer, not to the thing that fetches it.\n */\nexport function fetchQuery<Data>(reference: () => Promise<Data>, args?: []): Promise<Data>\n// The query's own parameters, so a wrong key fails the typecheck rather than\n// the read. NoInfer keeps the tuple from being inferred from the call site,\n// where a wrong key would simply widen it.\nexport function fetchQuery<Args extends unknown[], Data>(\n reference: (...args: Args) => Promise<Data>,\n args: NoInfer<Args>,\n): Promise<Data>\nexport function fetchQuery<Data>(\n reference: (...args: never[]) => Promise<Data>,\n args: unknown[] = [],\n): Promise<Data> {\n if (typeof window === 'undefined') {\n // React's SSR runtime refuses a server-function call during the initial\n // render, and reaching a query's id means calling its reference — so this\n // cannot work here. Raised with the fix in it rather than left to surface\n // as React's more general message about fetch waterfalls.\n //\n // A cache library runs its fetcher in an effect, so the server render never\n // reaches this; a server component that wants the data during render should\n // await the query directly, which needs none of this.\n throw new Error(\n 'A query was read during server rendering. Read it in a server component with await, or through a cache library, whose fetcher runs after hydration.',\n )\n }\n\n const opened = { claimed: false, promise: null as Promise<unknown> | null }\n\n slot = opened\n\n try {\n // React's stub reaches callServer synchronously, so the slot is claimed by\n // the time this returns. The stub's own promise is discarded: the one the\n // transport made is the one that settles with the answer.\n ;(reference as unknown as Reader)(...(args as unknown[]))\n } finally {\n // Cleared here and nowhere else, so a reference that throws on the way in\n // does not leave the slot open for the next read to claim by accident.\n // Nothing is returned from this block: a `return` in `finally` discards\n // whatever the `try` was throwing, which would turn a reference that threw\n // synchronously into the misleading bound-reference error below.\n slot = null\n }\n\n if (!opened.claimed || !opened.promise) {\n // The only way here is a reference whose call path is asynchronous, which\n // today means one that was `.bind()`-ed. Loud, because the quiet version is\n // a read that silently went out as a POST.\n throw new Error(\n 'A query was called through a bound reference. Pass the exported query itself, unbound.',\n )\n }\n\n return opened.promise as Promise<Data>\n}\n\nasync function send(id: string, args: unknown[]): Promise<unknown> {\n const encoded = await encode(args)\n\n // encodeReply answers with FormData the moment an argument holds a File, and\n // a url cannot carry one. Rather than refuse a call that would have worked as\n // an action, the read falls back to a POST: it stops being cacheable, which\n // is the only thing it loses.\n if (typeof encoded !== 'string') return await post(id, args, 'an argument contained a File')\n\n const url = `${QUERY_PATH}?id=${encodeURIComponent(id)}&args=${encodeURIComponent(encoded)}`\n\n if (url.length > MAX_URL) return await post(id, args, 'the arguments are too large for a url')\n\n const res = await fetch(url, {\n headers: {\n [QUERY_HEADER]: '1',\n Accept: 'text/x-component',\n // Where the read came from. Same reason an action sends it: a host that\n // guards by route needs to know which page is asking.\n 'X-RSC-Referer': window.location.pathname + window.location.search,\n },\n })\n\n if (!res.ok || !res.body) {\n throw await failureFrom(res)\n }\n\n return await deserialize(res.body)\n}\n\n/**\n * What a refused read rejects with.\n *\n * A cache library reports failure by rejection, so this has to be an Error —\n * and one carrying the message the server meant, rather than a status code. A\n * validation refusal keeps its fields on `errors`, where a form can read them.\n */\nasync function failureFrom(res: Response): Promise<Error> {\n const body = await res.text().catch(() => '')\n\n try {\n const parsed = JSON.parse(body) as { message?: string; errors?: Record<string, string[]> }\n const error = new Error(parsed.message || `Query failed: ${res.status}`)\n\n if (parsed.errors) (error as Error & { errors?: unknown }).errors = parsed.errors\n\n return error\n } catch {\n return new Error(body || `Query failed: ${res.status}`)\n }\n}\n\n/**\n * The same read, as an action.\n *\n * Reached only when the arguments cannot ride in a url. Warned about in\n * development rather than silently tolerated, because the read still works and\n * the thing that changed — it is no longer a GET, so nothing can cache it — is\n * otherwise invisible.\n */\nasync function post(id: string, args: unknown[], why: string): Promise<unknown> {\n if (import.meta.env?.DEV) {\n console.warn(`[rsc-kit] A query was sent as a POST because ${why}. It will not be cached.`)\n }\n\n return await asAction(id, args)\n}\n\n/**\n * The Flight codec and the action transport, installed by the app bootstrap.\n *\n * Injected rather than imported, and not for testing: this module is reached\n * from client components, so importing the browser runtime here would pull a\n * second copy of it into that graph — two client-reference registries, where\n * components resolve to undefined with nothing logged. The bootstrap already\n * holds the one true copy.\n */\nlet deserialize: (stream: ReadableStream) => Promise<unknown> = () => {\n throw new Error('No Flight decoder installed. createViteRscApp() sets one up.')\n}\n\nlet encode: (args: unknown[]) => Promise<string | FormData> = () => {\n throw new Error('No Flight encoder installed. createViteRscApp() sets one up.')\n}\n\nlet asAction: (id: string, args: unknown[]) => Promise<unknown> = () => {\n throw new Error('No action transport installed. createViteRscApp() sets one up.')\n}\n\nexport function setQueryCodec(codec: {\n deserialize: (stream: ReadableStream) => Promise<unknown>\n encode: (args: unknown[]) => Promise<string | FormData>\n asAction: (id: string, args: unknown[]) => Promise<unknown>\n}): void {\n deserialize = codec.deserialize\n encode = codec.encode\n asAction = codec.asAction\n}\n"]}
|
package/dist/js/router.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reads from window globals set by createViteRscApp, since client components
|
|
5
5
|
* are built in a separate build graph and cannot directly import navigate.ts.
|
|
6
6
|
*/
|
|
7
|
-
import type { Href } from "../routes.js";
|
|
7
|
+
import type { Href, RevalidateTarget } from "../routes.js";
|
|
8
8
|
export declare function visit(url: Href, opts?: {
|
|
9
9
|
replace?: boolean;
|
|
10
10
|
}): Promise<void>;
|
|
@@ -19,7 +19,7 @@ export declare function prefetch(url: Href, cacheForMs?: number): void;
|
|
|
19
19
|
* The page form leaves the layouts alone, so anything living in one — a count
|
|
20
20
|
* in a sidebar, say — will not move until you ask for 'all'.
|
|
21
21
|
*/
|
|
22
|
-
export declare function refresh(target?:
|
|
22
|
+
export declare function refresh(target?: RevalidateTarget): Promise<void>;
|
|
23
23
|
declare const router: {
|
|
24
24
|
visit: typeof visit;
|
|
25
25
|
prefetch: typeof prefetch;
|
package/dist/js/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/js/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,UAAU,KAAK,CACnB,GAAS,EACT,IAA4B;IAE5B,MAAM,GAAG,GAAI,MAAc,CAAC,cAAc,CAAC;IAE3C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAS,EAAE,UAAmB;IACrD,MAAM,EAAE,GAAI,MAAc,CAAC,cAAc,CAAC;IAE1C,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/js/router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,UAAU,KAAK,CACnB,GAAS,EACT,IAA4B;IAE5B,MAAM,GAAG,GAAI,MAAc,CAAC,cAAc,CAAC;IAE3C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAS,EAAE,UAAmB;IACrD,MAAM,EAAE,GAAI,MAAc,CAAC,cAAc,CAAC;IAE1C,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,OAAO,CAAC,MAAyB;IAC/C,MAAM,EAAE,GAAI,MAAc,CAAC,aAAa,CAAC;IAEzC,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC5C,eAAe,MAAM,CAAC","sourcesContent":["/**\n * Programmatic navigation API for use from client components.\n *\n * Reads from window globals set by createViteRscApp, since client components\n * are built in a separate build graph and cannot directly import navigate.ts.\n */\n\nimport type { Href, RevalidateTarget } from \"../routes.js\";\n\nexport function visit(\n url: Href,\n opts?: { replace?: boolean }\n): Promise<void> {\n const nav = (window as any).__rsc_navigate;\n\n if (!nav) {\n throw new Error(\"RSC navigation not initialized. Ensure createViteRscApp has been called.\");\n }\n\n return nav(url, opts);\n}\n\nexport function prefetch(url: Href, cacheForMs?: number): void {\n const fn = (window as any).__rsc_prefetch;\n\n if (!fn) {\n throw new Error(\"RSC navigation not initialized. Ensure createViteRscApp has been called.\");\n }\n\n fn(url, cacheForMs);\n}\n\n/**\n * Ask the server for part of the current page again.\n *\n * refresh() the page, leaving the layouts mounted\n * refresh('all') the whole document\n * refresh('orders') one parallel slot\n *\n * The page form leaves the layouts alone, so anything living in one — a count\n * in a sidebar, say — will not move until you ask for 'all'.\n */\nexport function refresh(target?: RevalidateTarget): Promise<void> {\n const fn = (window as any).__rsc_refresh;\n\n if (!fn) {\n throw new Error(\"RSC navigation not initialized. Ensure createViteRscApp has been called.\");\n }\n\n return fn(target);\n}\n\nconst router = { visit, prefetch, refresh };\nexport default router;\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type EventsStatus = "connecting" | "open" | "closed";
|
|
2
|
+
export interface EventsOptions<T> {
|
|
3
|
+
/** Off, and nothing connects. For a stream that waits on an id. */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Listen to one named event rather than the unnamed stream. */
|
|
6
|
+
event?: string;
|
|
7
|
+
/** Every message, as it arrives. */
|
|
8
|
+
onMessage?: (message: T) => void;
|
|
9
|
+
/**
|
|
10
|
+
* The connection failed or dropped. EventSource reconnects on its own, so
|
|
11
|
+
* this is for a notice, not a retry; `status` says whether it gave up.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (error: Event) => void;
|
|
14
|
+
/** How many messages `all` keeps. Default 100; 0 keeps none. */
|
|
15
|
+
keep?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface EventsState<T> {
|
|
18
|
+
latest: T | null;
|
|
19
|
+
all: T[];
|
|
20
|
+
status: EventsStatus;
|
|
21
|
+
error: Event | null;
|
|
22
|
+
/** Close the stream for good; the hook will not reconnect. */
|
|
23
|
+
close: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function useEvents<T = unknown>(url: string, options?: EventsOptions<T>): EventsState<T>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* A stream of server-sent events, as state.
|
|
4
|
+
*
|
|
5
|
+
* const { latest, status } = useEvents(`/api/orders/${id}/events`)
|
|
6
|
+
*
|
|
7
|
+
* Subscribes in an effect, so it is safe in a component that also renders on
|
|
8
|
+
* the server. The browser's EventSource reconnects on its own and resumes
|
|
9
|
+
* with Last-Event-ID when the route yielded ids; `status` says where it is.
|
|
10
|
+
* `onMessage` is for a store that already holds the value - TanStack's
|
|
11
|
+
* setQueryData, SWR's mutate, a setState - and `latest` for when the hook is
|
|
12
|
+
* the store.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useRef, useState } from "react";
|
|
15
|
+
export function useEvents(url, options = {}) {
|
|
16
|
+
const { enabled = true, event, keep = 100 } = options;
|
|
17
|
+
const onMessage = useRef(options.onMessage);
|
|
18
|
+
const onError = useRef(options.onError);
|
|
19
|
+
onMessage.current = options.onMessage;
|
|
20
|
+
onError.current = options.onError;
|
|
21
|
+
const [latest, setLatest] = useState(null);
|
|
22
|
+
const [all, setAll] = useState([]);
|
|
23
|
+
const [status, setStatus] = useState(enabled ? "connecting" : "closed");
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const source = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!enabled || typeof EventSource === "undefined") {
|
|
28
|
+
setStatus("closed");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const es = new EventSource(url);
|
|
32
|
+
source.current = es;
|
|
33
|
+
setStatus("connecting");
|
|
34
|
+
setError(null);
|
|
35
|
+
const receive = (e) => {
|
|
36
|
+
let message;
|
|
37
|
+
try {
|
|
38
|
+
message = JSON.parse(e.data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
message = e.data;
|
|
42
|
+
}
|
|
43
|
+
setLatest(message);
|
|
44
|
+
if (keep > 0)
|
|
45
|
+
setAll((prev) => prev.length >= keep
|
|
46
|
+
? [...prev.slice(1), message]
|
|
47
|
+
: [...prev, message]);
|
|
48
|
+
onMessage.current?.(message);
|
|
49
|
+
};
|
|
50
|
+
es.onopen = () => setStatus("open");
|
|
51
|
+
es.onerror = (e) => {
|
|
52
|
+
setError(e);
|
|
53
|
+
onError.current?.(e);
|
|
54
|
+
// EventSource reconnects on its own; CLOSED means it gave up.
|
|
55
|
+
setStatus(es.readyState === EventSource.CLOSED ? "closed" : "connecting");
|
|
56
|
+
};
|
|
57
|
+
if (event)
|
|
58
|
+
es.addEventListener(event, receive);
|
|
59
|
+
else
|
|
60
|
+
es.onmessage = receive;
|
|
61
|
+
return () => {
|
|
62
|
+
es.close();
|
|
63
|
+
source.current = null;
|
|
64
|
+
};
|
|
65
|
+
}, [url, enabled, event, keep]);
|
|
66
|
+
return {
|
|
67
|
+
latest,
|
|
68
|
+
all,
|
|
69
|
+
status,
|
|
70
|
+
error,
|
|
71
|
+
close: () => {
|
|
72
|
+
source.current?.close();
|
|
73
|
+
source.current = null;
|
|
74
|
+
setStatus("closed");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=useEvents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEvents.js","sourceRoot":"","sources":["../../src/js/useEvents.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * A stream of server-sent events, as state.\n *\n * const { latest, status } = useEvents(`/api/orders/${id}/events`)\n *\n * Subscribes in an effect, so it is safe in a component that also renders on\n * the server. The browser's EventSource reconnects on its own and resumes\n * with Last-Event-ID when the route yielded ids; `status` says where it is.\n * `onMessage` is for a store that already holds the value - TanStack's\n * setQueryData, SWR's mutate, a setState - and `latest` for when the hook is\n * the store.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type EventsStatus = \"connecting\" | \"open\" | \"closed\";\n\nexport interface EventsOptions<T> {\n /** Off, and nothing connects. For a stream that waits on an id. */\n enabled?: boolean;\n /** Listen to one named event rather than the unnamed stream. */\n event?: string;\n /** Every message, as it arrives. */\n onMessage?: (message: T) => void;\n /**\n * The connection failed or dropped. EventSource reconnects on its own, so\n * this is for a notice, not a retry; `status` says whether it gave up.\n */\n onError?: (error: Event) => void;\n /** How many messages `all` keeps. Default 100; 0 keeps none. */\n keep?: number;\n}\n\nexport interface EventsState<T> {\n latest: T | null;\n all: T[];\n status: EventsStatus;\n error: Event | null;\n /** Close the stream for good; the hook will not reconnect. */\n close: () => void;\n}\n\nexport function useEvents<T = unknown>(\n url: string,\n options: EventsOptions<T> = {},\n): EventsState<T> {\n const { enabled = true, event, keep = 100 } = options;\n const onMessage = useRef(options.onMessage);\n const onError = useRef(options.onError);\n\n onMessage.current = options.onMessage;\n onError.current = options.onError;\n\n const [latest, setLatest] = useState<T | null>(null);\n const [all, setAll] = useState<T[]>([]);\n const [status, setStatus] = useState<EventsStatus>(\n enabled ? \"connecting\" : \"closed\",\n );\n const [error, setError] = useState<Event | null>(null);\n const source = useRef<EventSource | null>(null);\n\n useEffect(() => {\n if (!enabled || typeof EventSource === \"undefined\") {\n setStatus(\"closed\");\n\n return;\n }\n\n const es = new EventSource(url);\n\n source.current = es;\n setStatus(\"connecting\");\n setError(null);\n\n const receive = (e: MessageEvent) => {\n let message: T;\n\n try {\n message = JSON.parse(e.data) as T;\n } catch {\n message = e.data as T;\n }\n\n setLatest(message);\n if (keep > 0)\n setAll((prev) =>\n prev.length >= keep\n ? [...prev.slice(1), message]\n : [...prev, message],\n );\n onMessage.current?.(message);\n };\n\n es.onopen = () => setStatus(\"open\");\n es.onerror = (e) => {\n setError(e);\n onError.current?.(e);\n // EventSource reconnects on its own; CLOSED means it gave up.\n setStatus(es.readyState === EventSource.CLOSED ? \"closed\" : \"connecting\");\n };\n\n if (event) es.addEventListener(event, receive as EventListener);\n else es.onmessage = receive;\n\n return () => {\n es.close();\n source.current = null;\n };\n }, [url, enabled, event, keep]);\n\n return {\n latest,\n all,\n status,\n error,\n close: () => {\n source.current?.close();\n source.current = null;\n setStatus(\"closed\");\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PollingOptions<T> {
|
|
2
|
+
/** Milliseconds between reads. */
|
|
3
|
+
every: number;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Every answer, as it arrives. */
|
|
6
|
+
onData?: (data: T) => void;
|
|
7
|
+
/** When a read is the last one: polling stops, and the answer is settled. */
|
|
8
|
+
until?: (data: T) => boolean;
|
|
9
|
+
/** Once, with the answer `until` accepted. */
|
|
10
|
+
onSettled?: (data: T) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Every read that failed. The next interval still reads; `error` is the
|
|
13
|
+
* state. `failures` counts the failed reads in a row, so a third one can be
|
|
14
|
+
* a toast where the first was a blip — a success resets it.
|
|
15
|
+
*/
|
|
16
|
+
onError?: (error: unknown, info: {
|
|
17
|
+
failures: number;
|
|
18
|
+
}) => void;
|
|
19
|
+
/** Keep reading while the tab is hidden. Off by default. */
|
|
20
|
+
whenHidden?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface PollingState<T> {
|
|
23
|
+
data: T | null;
|
|
24
|
+
error: unknown;
|
|
25
|
+
status: "idle" | "reading" | "paused" | "settled";
|
|
26
|
+
/** Read now, outside the interval - and start again after settling. */
|
|
27
|
+
refresh: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function usePolling<T>(read: () => Promise<T>, options: PollingOptions<T>): PollingState<T>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* A value read again on an interval, as state - until it settles, if asked.
|
|
4
|
+
*
|
|
5
|
+
* const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })
|
|
6
|
+
*
|
|
7
|
+
* // Until a job is done, then re-render the page that showed it.
|
|
8
|
+
* usePolling(() => fetchQuery(jobStatus, [id]), {
|
|
9
|
+
* every: 2_000,
|
|
10
|
+
* until: (job) => job.state === 'done' || job.state === 'failed',
|
|
11
|
+
* onSettled: () => refresh('page'),
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* The result is the data: whatever `read` returns is what `data` holds, and
|
|
15
|
+
* a query's Cache-Control lets a CDN absorb a thousand tabs polling the same
|
|
16
|
+
* thing into one origin request per interval. Settling is separate and
|
|
17
|
+
* explicit: `until` says when a read is the last one, and `onSettled` fires
|
|
18
|
+
* once on that read - so a page that wants to re-render through the server
|
|
19
|
+
* path that built it calls refresh('page') there, and a page that wants the
|
|
20
|
+
* value in hand reads `data`. Which of those is the page's to decide, not
|
|
21
|
+
* the hook's.
|
|
22
|
+
*
|
|
23
|
+
* Pauses while the tab is hidden, never overlaps two reads, and `refresh()`
|
|
24
|
+
* reads now - including after it settled, which starts it again.
|
|
25
|
+
*
|
|
26
|
+
* Polling against server-sent events: a stream sends bytes only when
|
|
27
|
+
* something changed and arrives at once, but holds a connection per tab
|
|
28
|
+
* and needs a source of change to yield from. Polling needs neither - it
|
|
29
|
+
* reuses the query you wrote - and is cacheable. Start here when you have
|
|
30
|
+
* no change feed yet; see useEvents when you do.
|
|
31
|
+
*/
|
|
32
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
33
|
+
export function usePolling(read, options) {
|
|
34
|
+
const { every, enabled = true, whenHidden = false } = options;
|
|
35
|
+
const latest = useRef({
|
|
36
|
+
read,
|
|
37
|
+
onData: options.onData,
|
|
38
|
+
until: options.until,
|
|
39
|
+
onSettled: options.onSettled,
|
|
40
|
+
onError: options.onError,
|
|
41
|
+
});
|
|
42
|
+
latest.current = {
|
|
43
|
+
read,
|
|
44
|
+
onData: options.onData,
|
|
45
|
+
until: options.until,
|
|
46
|
+
onSettled: options.onSettled,
|
|
47
|
+
onError: options.onError,
|
|
48
|
+
};
|
|
49
|
+
const [data, setData] = useState(null);
|
|
50
|
+
const [error, setError] = useState(null);
|
|
51
|
+
const failures = useRef(0);
|
|
52
|
+
const [status, setStatus] = useState(enabled ? "reading" : "idle");
|
|
53
|
+
const inFlight = useRef(null);
|
|
54
|
+
// Settled is a stop: the interval is cleared and stays cleared until
|
|
55
|
+
// refresh() or a change of inputs. Kept in a ref as well as state, so the
|
|
56
|
+
// interval callback sees it without a re-render in between.
|
|
57
|
+
const isSettled = useRef(false);
|
|
58
|
+
const refresh = useCallback(async () => {
|
|
59
|
+
// Never two at once: a slow answer and a fast interval would otherwise
|
|
60
|
+
// pile reads up, and the last to land wins whether or not it was newest.
|
|
61
|
+
if (inFlight.current)
|
|
62
|
+
return inFlight.current;
|
|
63
|
+
// A read after settling is asked for: refresh() starts the clock again.
|
|
64
|
+
if (isSettled.current) {
|
|
65
|
+
isSettled.current = false;
|
|
66
|
+
setStatus("reading");
|
|
67
|
+
}
|
|
68
|
+
inFlight.current = (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const next = await latest.current.read();
|
|
71
|
+
setData(next);
|
|
72
|
+
setError(null);
|
|
73
|
+
failures.current = 0;
|
|
74
|
+
latest.current.onData?.(next);
|
|
75
|
+
if (latest.current.until?.(next)) {
|
|
76
|
+
isSettled.current = true;
|
|
77
|
+
setStatus("settled");
|
|
78
|
+
latest.current.onSettled?.(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
failures.current += 1;
|
|
83
|
+
setError(e);
|
|
84
|
+
latest.current.onError?.(e, { failures: failures.current });
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
inFlight.current = null;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
return inFlight.current;
|
|
91
|
+
}, []);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!enabled) {
|
|
94
|
+
setStatus("idle");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let timer = null;
|
|
98
|
+
isSettled.current = false;
|
|
99
|
+
const start = () => {
|
|
100
|
+
if (timer)
|
|
101
|
+
return;
|
|
102
|
+
setStatus("reading");
|
|
103
|
+
void refresh();
|
|
104
|
+
timer = setInterval(() => {
|
|
105
|
+
if (!isSettled.current)
|
|
106
|
+
void refresh();
|
|
107
|
+
}, every);
|
|
108
|
+
};
|
|
109
|
+
const stop = () => {
|
|
110
|
+
if (timer)
|
|
111
|
+
clearInterval(timer);
|
|
112
|
+
timer = null;
|
|
113
|
+
};
|
|
114
|
+
const onVisibility = () => {
|
|
115
|
+
if (document.visibilityState === "hidden") {
|
|
116
|
+
stop();
|
|
117
|
+
setStatus("paused");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
start();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (whenHidden ||
|
|
124
|
+
typeof document === "undefined" ||
|
|
125
|
+
document.visibilityState !== "hidden")
|
|
126
|
+
start();
|
|
127
|
+
else
|
|
128
|
+
setStatus("paused");
|
|
129
|
+
if (!whenHidden && typeof document !== "undefined") {
|
|
130
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
131
|
+
}
|
|
132
|
+
return () => {
|
|
133
|
+
stop();
|
|
134
|
+
if (!whenHidden && typeof document !== "undefined") {
|
|
135
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}, [enabled, every, whenHidden, refresh]);
|
|
139
|
+
return { data, error, status, refresh };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=usePolling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePolling.js","sourceRoot":"","sources":["../../src/js/usePolling.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * A value read again on an interval, as state - until it settles, if asked.\n *\n * const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n *\n * // Until a job is done, then re-render the page that showed it.\n * usePolling(() => fetchQuery(jobStatus, [id]), {\n * every: 2_000,\n * until: (job) => job.state === 'done' || job.state === 'failed',\n * onSettled: () => refresh('page'),\n * })\n *\n * The result is the data: whatever `read` returns is what `data` holds, and\n * a query's Cache-Control lets a CDN absorb a thousand tabs polling the same\n * thing into one origin request per interval. Settling is separate and\n * explicit: `until` says when a read is the last one, and `onSettled` fires\n * once on that read - so a page that wants to re-render through the server\n * path that built it calls refresh('page') there, and a page that wants the\n * value in hand reads `data`. Which of those is the page's to decide, not\n * the hook's.\n *\n * Pauses while the tab is hidden, never overlaps two reads, and `refresh()`\n * reads now - including after it settled, which starts it again.\n *\n * Polling against server-sent events: a stream sends bytes only when\n * something changed and arrives at once, but holds a connection per tab\n * and needs a source of change to yield from. Polling needs neither - it\n * reuses the query you wrote - and is cacheable. Start here when you have\n * no change feed yet; see useEvents when you do.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface PollingOptions<T> {\n /** Milliseconds between reads. */\n every: number;\n enabled?: boolean;\n /** Every answer, as it arrives. */\n onData?: (data: T) => void;\n /** When a read is the last one: polling stops, and the answer is settled. */\n until?: (data: T) => boolean;\n /** Once, with the answer `until` accepted. */\n onSettled?: (data: T) => void;\n /**\n * Every read that failed. The next interval still reads; `error` is the\n * state. `failures` counts the failed reads in a row, so a third one can be\n * a toast where the first was a blip — a success resets it.\n */\n onError?: (error: unknown, info: { failures: number }) => void;\n /** Keep reading while the tab is hidden. Off by default. */\n whenHidden?: boolean;\n}\n\nexport interface PollingState<T> {\n data: T | null;\n error: unknown;\n status: \"idle\" | \"reading\" | \"paused\" | \"settled\";\n /** Read now, outside the interval - and start again after settling. */\n refresh: () => Promise<void>;\n}\n\nexport function usePolling<T>(\n read: () => Promise<T>,\n options: PollingOptions<T>,\n): PollingState<T> {\n const { every, enabled = true, whenHidden = false } = options;\n const latest = useRef({\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n });\n\n latest.current = {\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n };\n\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const failures = useRef(0);\n const [status, setStatus] = useState<PollingState<T>[\"status\"]>(\n enabled ? \"reading\" : \"idle\",\n );\n const inFlight = useRef<Promise<void> | null>(null);\n // Settled is a stop: the interval is cleared and stays cleared until\n // refresh() or a change of inputs. Kept in a ref as well as state, so the\n // interval callback sees it without a re-render in between.\n const isSettled = useRef(false);\n\n const refresh = useCallback(async () => {\n // Never two at once: a slow answer and a fast interval would otherwise\n // pile reads up, and the last to land wins whether or not it was newest.\n if (inFlight.current) return inFlight.current;\n\n // A read after settling is asked for: refresh() starts the clock again.\n if (isSettled.current) {\n isSettled.current = false;\n setStatus(\"reading\");\n }\n\n inFlight.current = (async () => {\n try {\n const next = await latest.current.read();\n\n setData(next);\n setError(null);\n failures.current = 0;\n latest.current.onData?.(next);\n\n if (latest.current.until?.(next)) {\n isSettled.current = true;\n setStatus(\"settled\");\n latest.current.onSettled?.(next);\n }\n } catch (e) {\n failures.current += 1;\n setError(e);\n latest.current.onError?.(e, { failures: failures.current });\n } finally {\n inFlight.current = null;\n }\n })();\n\n return inFlight.current;\n }, []);\n\n useEffect(() => {\n if (!enabled) {\n setStatus(\"idle\");\n\n return;\n }\n\n let timer: ReturnType<typeof setInterval> | null = null;\n\n isSettled.current = false;\n\n const start = () => {\n if (timer) return;\n\n setStatus(\"reading\");\n void refresh();\n timer = setInterval(() => {\n if (!isSettled.current) void refresh();\n }, every);\n };\n const stop = () => {\n if (timer) clearInterval(timer);\n\n timer = null;\n };\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") {\n stop();\n setStatus(\"paused\");\n } else {\n start();\n }\n };\n\n if (\n whenHidden ||\n typeof document === \"undefined\" ||\n document.visibilityState !== \"hidden\"\n )\n start();\n else setStatus(\"paused\");\n\n if (!whenHidden && typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n\n return () => {\n stop();\n if (!whenHidden && typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n }, [enabled, every, whenHidden, refresh]);\n\n return { data, error, status, refresh };\n}\n"]}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export interface RouteSegment {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `host`: a `[name]` directory at the top of app/. Bound from the request's
|
|
4
|
+
* host, never from a path segment - so `example.com/nope` is a 404 rather
|
|
5
|
+
* than a tenant called "nope". See hostRouting.
|
|
6
|
+
*/
|
|
7
|
+
type: "static" | "param" | "catchAll" | "host";
|
|
3
8
|
value: string;
|
|
4
9
|
}
|
|
5
10
|
export interface ManifestRoute {
|
|
@@ -96,6 +101,18 @@ export interface RouteManifest {
|
|
|
96
101
|
output: string;
|
|
97
102
|
exportPath: string;
|
|
98
103
|
payloadName: string;
|
|
104
|
+
/**
|
|
105
|
+
* The site's own hosts, bare and lower-case. A request from any other
|
|
106
|
+
* host is matched with that host's segment in front of its path - see
|
|
107
|
+
* hostRouting. Absent or empty: every request is the site's own.
|
|
108
|
+
*/
|
|
109
|
+
hosts?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* Whether every response says what built it: `X-Powered-By: rsc-kit` and
|
|
112
|
+
* a generator meta tag in the document. The name only, never the version.
|
|
113
|
+
* `X-RSC-Kit` (how a response was served) is sent regardless.
|
|
114
|
+
*/
|
|
115
|
+
identify?: boolean;
|
|
99
116
|
};
|
|
100
117
|
routes: ManifestRoute[];
|
|
101
118
|
intercepts: ManifestIntercept[];
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n type:
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
|
package/dist/metadataRoutes.d.ts
CHANGED
|
@@ -39,3 +39,23 @@ export declare function llmsText(llms: MetadataRoute.Llms, base: string | URL |
|
|
|
39
39
|
* build synthesised, never by an app directly.
|
|
40
40
|
*/
|
|
41
41
|
export declare function metadataResponse(kind: MetadataRouteKind, produce: () => unknown, base: string | URL | null | undefined): Promise<Response>;
|
|
42
|
+
/**
|
|
43
|
+
* The sitemap the build writes when the app has no sitemap.ts.
|
|
44
|
+
*
|
|
45
|
+
* The build already knows what belongs in one: every page it stored, and
|
|
46
|
+
* every url generateStaticParams listed, which is the set a crawler should
|
|
47
|
+
* be told about. Left out: a route with middleware above it (a guard means
|
|
48
|
+
* "not for everyone"), a page the build could not store (it varies per
|
|
49
|
+
* visitor, so it has no one url worth listing... it does, but no freshness
|
|
50
|
+
* to claim - listed without lastModified), the not-found page, and the
|
|
51
|
+
* metadata files themselves. lastModified is the build, which is when the
|
|
52
|
+
* stored copy changed. A sitemap.ts beside the root layout replaces this.
|
|
53
|
+
*/
|
|
54
|
+
export declare function automaticSitemap(results: readonly {
|
|
55
|
+
url: string;
|
|
56
|
+
type: string;
|
|
57
|
+
component: string;
|
|
58
|
+
}[], routes: readonly {
|
|
59
|
+
component: string;
|
|
60
|
+
middleware: readonly string[];
|
|
61
|
+
}[], base: string | URL, builtAt?: Date): string;
|
package/dist/metadataRoutes.js
CHANGED
|
@@ -135,4 +135,39 @@ export async function metadataResponse(kind, produce, base) {
|
|
|
135
135
|
headers: { "Content-Type": METADATA_ROUTES[kind].type },
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The sitemap the build writes when the app has no sitemap.ts.
|
|
140
|
+
*
|
|
141
|
+
* The build already knows what belongs in one: every page it stored, and
|
|
142
|
+
* every url generateStaticParams listed, which is the set a crawler should
|
|
143
|
+
* be told about. Left out: a route with middleware above it (a guard means
|
|
144
|
+
* "not for everyone"), a page the build could not store (it varies per
|
|
145
|
+
* visitor, so it has no one url worth listing... it does, but no freshness
|
|
146
|
+
* to claim - listed without lastModified), the not-found page, and the
|
|
147
|
+
* metadata files themselves. lastModified is the build, which is when the
|
|
148
|
+
* stored copy changed. A sitemap.ts beside the root layout replaces this.
|
|
149
|
+
*/
|
|
150
|
+
export function automaticSitemap(results, routes, base, builtAt = new Date()) {
|
|
151
|
+
const guarded = new Set(routes.filter((r) => r.middleware.length > 0).map((r) => r.component));
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const entries = [];
|
|
154
|
+
for (const result of results) {
|
|
155
|
+
if (guarded.has(result.component))
|
|
156
|
+
continue;
|
|
157
|
+
if (result.type === "error" || result.type === "blocked")
|
|
158
|
+
continue;
|
|
159
|
+
if (/(^|\/)not-found$|\/_/.test(result.url))
|
|
160
|
+
continue;
|
|
161
|
+
if (/\[.*\]/.test(result.url))
|
|
162
|
+
continue; // a pattern, not a url
|
|
163
|
+
if (seen.has(result.url))
|
|
164
|
+
continue;
|
|
165
|
+
seen.add(result.url);
|
|
166
|
+
entries.push(result.type === "frozen" || result.type === "shell"
|
|
167
|
+
? { url: result.url, lastModified: builtAt }
|
|
168
|
+
: { url: result.url });
|
|
169
|
+
}
|
|
170
|
+
entries.sort((a, b) => a.url.localeCompare(b.url));
|
|
171
|
+
return sitemapXml(entries, base);
|
|
172
|
+
}
|
|
138
173
|
//# sourceMappingURL=metadataRoutes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadataRoutes.js","sourceRoot":"","sources":["../src/metadataRoutes.ts"],"names":[],"mappings":"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","sourcesContent":["// robots.txt, sitemap.xml and llms.txt, from a file beside the root layout.\n//\n// Each convention file becomes an api route the build synthesises - so a\n// sitemap that reads nothing per request is stored at build like any frozen\n// route, one that reads the request stays dynamic, and the build's table says\n// which. What lives here is the formatting: the value the app's function\n// returns, written the way the crawler expects it.\n\nimport type { MetadataRoute, RobotsRule, SitemapEntry } from \"./metadata.js\";\n\nexport const METADATA_ROUTES = {\n robots: { file: \"robots.txt\", type: \"text/plain; charset=utf-8\" },\n sitemap: { file: \"sitemap.xml\", type: \"application/xml; charset=utf-8\" },\n llms: { file: \"llms.txt\", type: \"text/plain; charset=utf-8\" },\n \"llms-full\": { file: \"llms-full.txt\", type: \"text/plain; charset=utf-8\" },\n} as const;\n\nexport type MetadataRouteKind = keyof typeof METADATA_ROUTES;\n\n/**\n * Files an app may drop beside the root layout to be served at the root as\n * they are: the same three, hand-written, and the handful of other files a\n * site is asked for at its root.\n */\nexport const ROOT_FILES =\n /^(?:robots\\.txt|sitemap(?:-[\\w.-]+)?\\.xml|llms(?:-full)?\\.txt|humans\\.txt|security\\.txt|ads\\.txt|app-ads\\.txt)$/;\n\nexport function rootFileType(name: string): string {\n return name.endsWith(\".xml\")\n ? \"application/xml; charset=utf-8\"\n : \"text/plain; charset=utf-8\";\n}\n\nfunction list<T>(value: T | T[] | undefined): T[] {\n return value === undefined ? [] : Array.isArray(value) ? value : [value];\n}\n\n/**\n * A url made absolute. A relative one needs the root layout's metadataBase -\n * a crawler is handed these with no page to resolve them against.\n */\nexport function absoluteUrl(\n url: string,\n base: string | URL | null | undefined,\n file: string,\n): string {\n if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;\n\n if (!base) {\n throw new Error(\n `[rsc-kit] ${file} names a relative url, ${JSON.stringify(url)}, and the root layout has no metadataBase to make it absolute. ` +\n \"Add metadataBase: new URL('https://example.com') to the root layout's metadata, or write the url in full.\",\n );\n }\n\n return new URL(url, base).href;\n}\n\nexport function robotsText(\n robots: MetadataRoute.Robots,\n base: string | URL | null | undefined,\n): string {\n const blocks = list(robots.rules).map((rule: RobotsRule) => {\n const lines: string[] = [];\n\n for (const agent of list(rule.userAgent).length\n ? list(rule.userAgent)\n : [\"*\"])\n lines.push(`User-Agent: ${agent}`);\n for (const path of list(rule.allow)) lines.push(`Allow: ${path}`);\n for (const path of list(rule.disallow)) lines.push(`Disallow: ${path}`);\n if (rule.crawlDelay !== undefined)\n lines.push(`Crawl-delay: ${rule.crawlDelay}`);\n\n return lines.join(\"\\n\");\n });\n\n const tail: string[] = [];\n\n for (const sitemap of list(robots.sitemap))\n tail.push(`Sitemap: ${absoluteUrl(sitemap, base, \"robots.ts\")}`);\n if (robots.host) tail.push(`Host: ${robots.host}`);\n\n return (\n [...blocks, ...(tail.length ? [tail.join(\"\\n\")] : [])].join(\"\\n\\n\") + \"\\n\"\n );\n}\n\nfunction escapeXml(text: string): string {\n return text.replace(\n /[&<>\"']/g,\n (c) =>\n ({\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n })[c]!,\n );\n}\n\nexport function sitemapXml(\n entries: MetadataRoute.Sitemap,\n base: string | URL | null | undefined,\n): string {\n const hasAlternates = entries.some(\n (e) =>\n e.alternates?.languages && Object.keys(e.alternates.languages).length > 0,\n );\n const hasImages = entries.some((e) => e.images && e.images.length > 0);\n\n const urls = entries.map((entry: SitemapEntry) => {\n const parts = [\n `<loc>${escapeXml(absoluteUrl(entry.url, base, \"sitemap.ts\"))}</loc>`,\n ];\n\n if (entry.lastModified !== undefined) {\n const when =\n entry.lastModified instanceof Date\n ? entry.lastModified.toISOString()\n : entry.lastModified;\n\n parts.push(`<lastmod>${escapeXml(when)}</lastmod>`);\n }\n if (entry.changeFrequency)\n parts.push(`<changefreq>${entry.changeFrequency}</changefreq>`);\n if (entry.priority !== undefined)\n parts.push(`<priority>${entry.priority}</priority>`);\n for (const [lang, href] of Object.entries(\n entry.alternates?.languages ?? {},\n )) {\n parts.push(\n `<xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(lang)}\" href=\"${escapeXml(absoluteUrl(href, base, \"sitemap.ts\"))}\"/>`,\n );\n }\n for (const image of entry.images ?? []) {\n parts.push(\n `<image:image><image:loc>${escapeXml(absoluteUrl(image, base, \"sitemap.ts\"))}</image:loc></image:image>`,\n );\n }\n\n return `<url>${parts.join(\"\")}</url>`;\n });\n\n const namespaces = [\n 'xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"',\n ...(hasAlternates ? ['xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"'] : []),\n ...(hasImages\n ? ['xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"']\n : []),\n ];\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset ${namespaces.join(\" \")}>\\n${urls.join(\"\\n\")}\\n</urlset>\\n`;\n}\n\nexport function llmsText(\n llms: MetadataRoute.Llms,\n base: string | URL | null | undefined,\n): string {\n const out: string[] = [`# ${llms.title}`];\n\n if (llms.summary) out.push(\"\", `> ${llms.summary}`);\n for (const paragraph of list(llms.details)) out.push(\"\", paragraph);\n\n for (const section of llms.sections ?? []) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const link of section.links) {\n out.push(\n `- [${link.title}](${absoluteUrl(link.url, base, \"llms.ts\")})${link.description ? `: ${link.description}` : \"\"}`,\n );\n }\n }\n\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * The response for one of these routes: what the app's function returned,\n * formatted, unless it returned the text itself. Called by the route the\n * build synthesised, never by an app directly.\n */\nexport async function metadataResponse(\n kind: MetadataRouteKind,\n produce: () => unknown,\n base: string | URL | null | undefined,\n): Promise<Response> {\n const value = await produce();\n const body =\n typeof value === \"string\"\n ? value\n : kind === \"robots\"\n ? robotsText(value as MetadataRoute.Robots, base)\n : kind === \"sitemap\"\n ? sitemapXml(value as MetadataRoute.Sitemap, base)\n : llmsText(value as MetadataRoute.Llms, base);\n\n return new Response(body, {\n headers: { \"Content-Type\": METADATA_ROUTES[kind].type },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"metadataRoutes.js","sourceRoot":"","sources":["../src/metadataRoutes.ts"],"names":[],"mappings":"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","sourcesContent":["// robots.txt, sitemap.xml and llms.txt, from a file beside the root layout.\n//\n// Each convention file becomes an api route the build synthesises - so a\n// sitemap that reads nothing per request is stored at build like any frozen\n// route, one that reads the request stays dynamic, and the build's table says\n// which. What lives here is the formatting: the value the app's function\n// returns, written the way the crawler expects it.\n\nimport type { MetadataRoute, RobotsRule, SitemapEntry } from \"./metadata.js\";\n\nexport const METADATA_ROUTES = {\n robots: { file: \"robots.txt\", type: \"text/plain; charset=utf-8\" },\n sitemap: { file: \"sitemap.xml\", type: \"application/xml; charset=utf-8\" },\n llms: { file: \"llms.txt\", type: \"text/plain; charset=utf-8\" },\n \"llms-full\": { file: \"llms-full.txt\", type: \"text/plain; charset=utf-8\" },\n} as const;\n\nexport type MetadataRouteKind = keyof typeof METADATA_ROUTES;\n\n/**\n * Files an app may drop beside the root layout to be served at the root as\n * they are: the same three, hand-written, and the handful of other files a\n * site is asked for at its root.\n */\nexport const ROOT_FILES =\n /^(?:robots\\.txt|sitemap(?:-[\\w.-]+)?\\.xml|llms(?:-full)?\\.txt|humans\\.txt|security\\.txt|ads\\.txt|app-ads\\.txt)$/;\n\nexport function rootFileType(name: string): string {\n return name.endsWith(\".xml\")\n ? \"application/xml; charset=utf-8\"\n : \"text/plain; charset=utf-8\";\n}\n\nfunction list<T>(value: T | T[] | undefined): T[] {\n return value === undefined ? [] : Array.isArray(value) ? value : [value];\n}\n\n/**\n * A url made absolute. A relative one needs the root layout's metadataBase -\n * a crawler is handed these with no page to resolve them against.\n */\nexport function absoluteUrl(\n url: string,\n base: string | URL | null | undefined,\n file: string,\n): string {\n if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;\n\n if (!base) {\n throw new Error(\n `[rsc-kit] ${file} names a relative url, ${JSON.stringify(url)}, and the root layout has no metadataBase to make it absolute. ` +\n \"Add metadataBase: new URL('https://example.com') to the root layout's metadata, or write the url in full.\",\n );\n }\n\n return new URL(url, base).href;\n}\n\nexport function robotsText(\n robots: MetadataRoute.Robots,\n base: string | URL | null | undefined,\n): string {\n const blocks = list(robots.rules).map((rule: RobotsRule) => {\n const lines: string[] = [];\n\n for (const agent of list(rule.userAgent).length\n ? list(rule.userAgent)\n : [\"*\"])\n lines.push(`User-Agent: ${agent}`);\n for (const path of list(rule.allow)) lines.push(`Allow: ${path}`);\n for (const path of list(rule.disallow)) lines.push(`Disallow: ${path}`);\n if (rule.crawlDelay !== undefined)\n lines.push(`Crawl-delay: ${rule.crawlDelay}`);\n\n return lines.join(\"\\n\");\n });\n\n const tail: string[] = [];\n\n for (const sitemap of list(robots.sitemap))\n tail.push(`Sitemap: ${absoluteUrl(sitemap, base, \"robots.ts\")}`);\n if (robots.host) tail.push(`Host: ${robots.host}`);\n\n return (\n [...blocks, ...(tail.length ? [tail.join(\"\\n\")] : [])].join(\"\\n\\n\") + \"\\n\"\n );\n}\n\nfunction escapeXml(text: string): string {\n return text.replace(\n /[&<>\"']/g,\n (c) =>\n ({\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n })[c]!,\n );\n}\n\nexport function sitemapXml(\n entries: MetadataRoute.Sitemap,\n base: string | URL | null | undefined,\n): string {\n const hasAlternates = entries.some(\n (e) =>\n e.alternates?.languages && Object.keys(e.alternates.languages).length > 0,\n );\n const hasImages = entries.some((e) => e.images && e.images.length > 0);\n\n const urls = entries.map((entry: SitemapEntry) => {\n const parts = [\n `<loc>${escapeXml(absoluteUrl(entry.url, base, \"sitemap.ts\"))}</loc>`,\n ];\n\n if (entry.lastModified !== undefined) {\n const when =\n entry.lastModified instanceof Date\n ? entry.lastModified.toISOString()\n : entry.lastModified;\n\n parts.push(`<lastmod>${escapeXml(when)}</lastmod>`);\n }\n if (entry.changeFrequency)\n parts.push(`<changefreq>${entry.changeFrequency}</changefreq>`);\n if (entry.priority !== undefined)\n parts.push(`<priority>${entry.priority}</priority>`);\n for (const [lang, href] of Object.entries(\n entry.alternates?.languages ?? {},\n )) {\n parts.push(\n `<xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(lang)}\" href=\"${escapeXml(absoluteUrl(href, base, \"sitemap.ts\"))}\"/>`,\n );\n }\n for (const image of entry.images ?? []) {\n parts.push(\n `<image:image><image:loc>${escapeXml(absoluteUrl(image, base, \"sitemap.ts\"))}</image:loc></image:image>`,\n );\n }\n\n return `<url>${parts.join(\"\")}</url>`;\n });\n\n const namespaces = [\n 'xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"',\n ...(hasAlternates ? ['xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"'] : []),\n ...(hasImages\n ? ['xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"']\n : []),\n ];\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset ${namespaces.join(\" \")}>\\n${urls.join(\"\\n\")}\\n</urlset>\\n`;\n}\n\nexport function llmsText(\n llms: MetadataRoute.Llms,\n base: string | URL | null | undefined,\n): string {\n const out: string[] = [`# ${llms.title}`];\n\n if (llms.summary) out.push(\"\", `> ${llms.summary}`);\n for (const paragraph of list(llms.details)) out.push(\"\", paragraph);\n\n for (const section of llms.sections ?? []) {\n out.push(\"\", `## ${section.title}`, \"\");\n for (const link of section.links) {\n out.push(\n `- [${link.title}](${absoluteUrl(link.url, base, \"llms.ts\")})${link.description ? `: ${link.description}` : \"\"}`,\n );\n }\n }\n\n return out.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * The response for one of these routes: what the app's function returned,\n * formatted, unless it returned the text itself. Called by the route the\n * build synthesised, never by an app directly.\n */\nexport async function metadataResponse(\n kind: MetadataRouteKind,\n produce: () => unknown,\n base: string | URL | null | undefined,\n): Promise<Response> {\n const value = await produce();\n const body =\n typeof value === \"string\"\n ? value\n : kind === \"robots\"\n ? robotsText(value as MetadataRoute.Robots, base)\n : kind === \"sitemap\"\n ? sitemapXml(value as MetadataRoute.Sitemap, base)\n : llmsText(value as MetadataRoute.Llms, base);\n\n return new Response(body, {\n headers: { \"Content-Type\": METADATA_ROUTES[kind].type },\n });\n}\n\n/**\n * The sitemap the build writes when the app has no sitemap.ts.\n *\n * The build already knows what belongs in one: every page it stored, and\n * every url generateStaticParams listed, which is the set a crawler should\n * be told about. Left out: a route with middleware above it (a guard means\n * \"not for everyone\"), a page the build could not store (it varies per\n * visitor, so it has no one url worth listing... it does, but no freshness\n * to claim - listed without lastModified), the not-found page, and the\n * metadata files themselves. lastModified is the build, which is when the\n * stored copy changed. A sitemap.ts beside the root layout replaces this.\n */\nexport function automaticSitemap(\n results: readonly { url: string; type: string; component: string }[],\n routes: readonly { component: string; middleware: readonly string[] }[],\n base: string | URL,\n builtAt: Date = new Date(),\n): string {\n const guarded = new Set(\n routes.filter((r) => r.middleware.length > 0).map((r) => r.component),\n );\n const seen = new Set<string>();\n const entries: SitemapEntry[] = [];\n\n for (const result of results) {\n if (guarded.has(result.component)) continue;\n if (result.type === \"error\" || result.type === \"blocked\") continue;\n if (/(^|\\/)not-found$|\\/_/.test(result.url)) continue;\n if (/\\[.*\\]/.test(result.url)) continue; // a pattern, not a url\n if (seen.has(result.url)) continue;\n\n seen.add(result.url);\n entries.push(\n result.type === \"frozen\" || result.type === \"shell\"\n ? { url: result.url, lastModified: builtAt }\n : { url: result.url },\n );\n }\n\n entries.sort((a, b) => a.url.localeCompare(b.url));\n\n return sitemapXml(entries, base);\n}\n"]}
|
package/dist/prerender.js
CHANGED