@rsc-kit/core 0.30.0 → 0.30.2
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/action.js +32 -7
- package/dist/action.js.map +1 -1
- package/dist/conformance.js +3 -0
- package/dist/conformance.js.map +1 -1
- package/dist/host.d.ts +1 -0
- package/dist/host.js +29 -1
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +11 -14
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/Form.d.ts +16 -5
- package/dist/js/Form.js +1 -1
- package/dist/js/Form.js.map +1 -1
- package/dist/js/queryClient.js +4 -0
- package/dist/js/queryClient.js.map +1 -1
- package/dist/query.d.ts +15 -0
- package/dist/query.js +25 -0
- package/dist/query.js.map +1 -1
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +30 -1
- package/dist/testing.js.map +1 -1
- package/dist/vite.js +11 -2
- package/dist/vite.js.map +1 -1
- package/package.json +1 -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>(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"]}
|
|
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[]>; refusal?: unknown }\n const error = new Error(parsed.message || `Query failed: ${res.status}`) as Error & {\n errors?: unknown\n refusal?: unknown\n status?: number\n }\n\n if (parsed.errors) error.errors = parsed.errors\n // The data a refusal declared with .refusal(schema), checked on the server.\n if (parsed.refusal !== undefined) error.refusal = parsed.refusal\n error.status = res.status\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/query.d.ts
CHANGED
|
@@ -57,3 +57,18 @@ export declare function isQuery(fn: unknown): boolean;
|
|
|
57
57
|
export declare function queryOptions(fn: unknown): QueryOptions;
|
|
58
58
|
/** The `Cache-Control` a query's answer goes out with. */
|
|
59
59
|
export declare function queryCacheControl(fn: unknown): string;
|
|
60
|
+
/**
|
|
61
|
+
* The status and message a read that was turned away answers with, or null
|
|
62
|
+
* when it failed.
|
|
63
|
+
*
|
|
64
|
+
* Signed out, not allowed, nothing there, or a refusal the backend gave a
|
|
65
|
+
* status of its own - each message was written to be seen, and as a 500 it
|
|
66
|
+
* was "Query failed.", with a fetcher unable to tell a 429 that says wait
|
|
67
|
+
* from a broken server. Matched by name and mark, never by class: the query
|
|
68
|
+
* was bundled apart from the engine.
|
|
69
|
+
*/
|
|
70
|
+
export declare function queryRefusal(error: unknown): {
|
|
71
|
+
status: number;
|
|
72
|
+
message: string;
|
|
73
|
+
refusal?: unknown;
|
|
74
|
+
} | null;
|
package/dist/query.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// checks before invoking anything. Without that check, `/_rsc/query?id=…`
|
|
20
20
|
// would run any registered action over GET, which is the classic
|
|
21
21
|
// a-crawler-emptied-the-database bug.
|
|
22
|
+
import { isNotFoundSignal } from './notFound.js';
|
|
22
23
|
/**
|
|
23
24
|
* Marks a function as safe to invoke over GET.
|
|
24
25
|
*
|
|
@@ -113,4 +114,28 @@ export function queryCacheControl(fn) {
|
|
|
113
114
|
// direction.
|
|
114
115
|
return age > 0 ? `${cache}, max-age=${age}` : `${cache}, max-age=0, must-revalidate`;
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The status and message a read that was turned away answers with, or null
|
|
119
|
+
* when it failed.
|
|
120
|
+
*
|
|
121
|
+
* Signed out, not allowed, nothing there, or a refusal the backend gave a
|
|
122
|
+
* status of its own - each message was written to be seen, and as a 500 it
|
|
123
|
+
* was "Query failed.", with a fetcher unable to tell a 429 that says wait
|
|
124
|
+
* from a broken server. Matched by name and mark, never by class: the query
|
|
125
|
+
* was bundled apart from the engine.
|
|
126
|
+
*/
|
|
127
|
+
export function queryRefusal(error) {
|
|
128
|
+
if (!(error instanceof Error))
|
|
129
|
+
return null;
|
|
130
|
+
const { name, message, refusalStatus, refusal } = error;
|
|
131
|
+
if (name === 'ServerAuthenticationError')
|
|
132
|
+
return { status: 401, message };
|
|
133
|
+
if (name === 'ServerAuthorizationError')
|
|
134
|
+
return { status: 403, message };
|
|
135
|
+
if (isNotFoundSignal(error))
|
|
136
|
+
return { status: 404, message };
|
|
137
|
+
if (typeof refusalStatus !== 'number' || refusalStatus < 400 || refusalStatus > 599)
|
|
138
|
+
return null;
|
|
139
|
+
return refusal === undefined ? { status: refusalStatus, message } : { status: refusalStatus, message, refusal };
|
|
140
|
+
}
|
|
116
141
|
//# sourceMappingURL=query.js.map
|
package/dist/query.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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","sourcesContent":["// Reads from the server, over GET, without an endpoint to write.\n//\n// // listings.ts\n// \"use server\"\n// import { query } from '@rsc-kit/core/query'\n//\n// export const getListings = query(async (filters: Filters) =>\n// listingService.getFiltered(filters))\n//\n// A server function is a POST, and a POST is cacheable by nothing: not an HTTP\n// cache, not a CDN, not the service worker this package generates. Routing\n// reads through one makes every dynamic read the single thing that cannot\n// survive without a network. That is the whole reason this exists alongside\n// actions rather than on top of them.\n//\n// The module still carries \"use server\", because that is what registers the\n// function and gives it an id — this does not invent a second registry. What\n// it adds is a mark saying the function is a READ, which the GET endpoint\n// checks before invoking anything. Without that check, `/_rsc/query?id=…`\n// would run any registered action over GET, which is the classic\n// a-crawler-emptied-the-database bug.\n\n/**\n * Marks a function as safe to invoke over GET.\n *\n * A property rather than a WeakSet or `instanceof`, for the reason this\n * project keeps meeting: an app's server functions are bundled separately from\n * the engine, so each side gets its own copy of this module. Anything holding\n * identity across that seam — a set, a class — is simply empty on the other\n * side, and every query would answer \"not a query\" with nothing logged.\n *\n * Symbol.for, so both copies agree on the key as well as the value.\n */\nconst QUERY_MARK = Symbol.for('@rsc-kit/core.query')\nconst QUERY_OPTIONS = Symbol.for('@rsc-kit/core.query-options')\n\n/** How the answer may be stored. Conservative by default — see `QueryOptions`. */\nexport interface QueryOptions {\n /**\n * What `Cache-Control` the answer carries.\n *\n * The default is `private, no-store`, and it is deliberately the safe one: a\n * query may read the session, and a cacheable answer to a personal read is\n * how one visitor is served another's data. Widen it per query, once you\n * have looked at what that query returns.\n *\n * `'private'` lets the visitor's own browser and service worker keep it and\n * nothing else. `'public'` lets a CDN keep it, and is only ever right for a\n * read whose answer does not depend on who is asking.\n */\n cache?: 'no-store' | 'private' | 'public'\n /** Seconds the answer stays fresh. Ignored when `cache` is `'no-store'`. */\n maxAge?: number\n}\n\n/** A function that may be read over GET. */\nexport type QueryFn<Args extends unknown[], Data> = (...args: Args) => Promise<Data>\n\n/**\n * Declare a read.\n *\n * The wrapper is what gets registered and what the id points at, so the mark\n * travels with the thing the endpoint actually loads. Wrapping rather than\n * annotating the original also keeps `query(fn)` honest when someone exports\n * the raw `fn` beside it: that export is an action, not a query, and the GET\n * endpoint will refuse it.\n */\nexport function query<Args extends unknown[], Data>(\n fn: (...args: Args) => Promise<Data> | Data,\n options: QueryOptions = {},\n): QueryFn<Args, Data> {\n const read = async (...args: Args): Promise<Data> => await fn(...args)\n\n return markQuery(read, options)\n}\n\n/**\n * Mark a function a read, without wrapping it again.\n *\n * For a builder that has already assembled the function it wants registered —\n * `createActionClient().query()` — where wrapping once more would only add a\n * frame to every stack trace.\n */\nexport function markQuery<F extends (...args: never[]) => unknown>(\n fn: F,\n options: QueryOptions = {},\n): F {\n // Non-enumerable, so the mark does not show up in anything that walks the\n // function's own keys — a bundler's export analysis, a test's snapshot.\n Object.defineProperty(fn, QUERY_MARK, { value: true })\n Object.defineProperty(fn, QUERY_OPTIONS, { value: options })\n\n return fn\n}\n\n/**\n * A refusal a query can send back with its fields intact.\n *\n * Thrown rather than returned, because a query is handed to a cache library as\n * a fetcher and every one of them reports failure by rejection. The endpoint\n * turns this into a 422 so the message and the fields survive the boundary —\n * React strips a thrown error's message in production, which is the reason an\n * ACTION returns its failures instead.\n */\nexport class QueryValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'QueryValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a query refusal, whichever copy of the class built it. */\nexport function isQueryValidationError(error: unknown): error is QueryValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * Symbol.for for the same reason the query mark is: an app's functions are\n * bundled apart from the engine, so each side gets its own copy of this class\n * and `instanceof` is simply false between them.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.query-validation')\n\n/** Whether this function was declared with `query()`, whichever copy declared it. */\nexport function isQuery(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[QUERY_MARK] === true\n}\n\n/** What the query asked for, or the safe default when it asked for nothing. */\nexport function queryOptions(fn: unknown): QueryOptions {\n if (typeof fn !== 'function') return {}\n\n return ((fn as unknown as Record<symbol, unknown>)[QUERY_OPTIONS] as QueryOptions) ?? {}\n}\n\n/** The `Cache-Control` a query's answer goes out with. */\nexport function queryCacheControl(fn: unknown): string {\n const { cache = 'no-store', maxAge } = queryOptions(fn)\n\n if (cache === 'no-store') return 'private, no-store'\n\n const age = typeof maxAge === 'number' && maxAge > 0 ? Math.floor(maxAge) : 0\n\n // must-revalidate at zero rather than omitting max-age: with no directive at\n // all, whether an intermediary keeps it comes down to that intermediary's\n // heuristics, and this package does not leave that to chance in either\n // direction.\n return age > 0 ? `${cache}, max-age=${age}` : `${cache}, max-age=0, must-revalidate`\n}\n"]}
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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","sourcesContent":["// Reads from the server, over GET, without an endpoint to write.\n//\n// // listings.ts\n// \"use server\"\n// import { query } from '@rsc-kit/core/query'\n//\n// export const getListings = query(async (filters: Filters) =>\n// listingService.getFiltered(filters))\n//\n// A server function is a POST, and a POST is cacheable by nothing: not an HTTP\n// cache, not a CDN, not the service worker this package generates. Routing\n// reads through one makes every dynamic read the single thing that cannot\n// survive without a network. That is the whole reason this exists alongside\n// actions rather than on top of them.\n//\n// The module still carries \"use server\", because that is what registers the\n// function and gives it an id — this does not invent a second registry. What\n// it adds is a mark saying the function is a READ, which the GET endpoint\n// checks before invoking anything. Without that check, `/_rsc/query?id=…`\n// would run any registered action over GET, which is the classic\n// a-crawler-emptied-the-database bug.\n\nimport { isNotFoundSignal } from './notFound.js'\n\n/**\n * Marks a function as safe to invoke over GET.\n *\n * A property rather than a WeakSet or `instanceof`, for the reason this\n * project keeps meeting: an app's server functions are bundled separately from\n * the engine, so each side gets its own copy of this module. Anything holding\n * identity across that seam — a set, a class — is simply empty on the other\n * side, and every query would answer \"not a query\" with nothing logged.\n *\n * Symbol.for, so both copies agree on the key as well as the value.\n */\nconst QUERY_MARK = Symbol.for('@rsc-kit/core.query')\nconst QUERY_OPTIONS = Symbol.for('@rsc-kit/core.query-options')\n\n/** How the answer may be stored. Conservative by default — see `QueryOptions`. */\nexport interface QueryOptions {\n /**\n * What `Cache-Control` the answer carries.\n *\n * The default is `private, no-store`, and it is deliberately the safe one: a\n * query may read the session, and a cacheable answer to a personal read is\n * how one visitor is served another's data. Widen it per query, once you\n * have looked at what that query returns.\n *\n * `'private'` lets the visitor's own browser and service worker keep it and\n * nothing else. `'public'` lets a CDN keep it, and is only ever right for a\n * read whose answer does not depend on who is asking.\n */\n cache?: 'no-store' | 'private' | 'public'\n /** Seconds the answer stays fresh. Ignored when `cache` is `'no-store'`. */\n maxAge?: number\n}\n\n/** A function that may be read over GET. */\nexport type QueryFn<Args extends unknown[], Data> = (...args: Args) => Promise<Data>\n\n/**\n * Declare a read.\n *\n * The wrapper is what gets registered and what the id points at, so the mark\n * travels with the thing the endpoint actually loads. Wrapping rather than\n * annotating the original also keeps `query(fn)` honest when someone exports\n * the raw `fn` beside it: that export is an action, not a query, and the GET\n * endpoint will refuse it.\n */\nexport function query<Args extends unknown[], Data>(\n fn: (...args: Args) => Promise<Data> | Data,\n options: QueryOptions = {},\n): QueryFn<Args, Data> {\n const read = async (...args: Args): Promise<Data> => await fn(...args)\n\n return markQuery(read, options)\n}\n\n/**\n * Mark a function a read, without wrapping it again.\n *\n * For a builder that has already assembled the function it wants registered —\n * `createActionClient().query()` — where wrapping once more would only add a\n * frame to every stack trace.\n */\nexport function markQuery<F extends (...args: never[]) => unknown>(\n fn: F,\n options: QueryOptions = {},\n): F {\n // Non-enumerable, so the mark does not show up in anything that walks the\n // function's own keys — a bundler's export analysis, a test's snapshot.\n Object.defineProperty(fn, QUERY_MARK, { value: true })\n Object.defineProperty(fn, QUERY_OPTIONS, { value: options })\n\n return fn\n}\n\n/**\n * A refusal a query can send back with its fields intact.\n *\n * Thrown rather than returned, because a query is handed to a cache library as\n * a fetcher and every one of them reports failure by rejection. The endpoint\n * turns this into a 422 so the message and the fields survive the boundary —\n * React strips a thrown error's message in production, which is the reason an\n * ACTION returns its failures instead.\n */\nexport class QueryValidationError extends Error {\n public readonly errors: Record<string, string[]>\n\n constructor(errors: Record<string, string[]>) {\n super('Validation failed')\n this.name = 'QueryValidationError'\n this.errors = errors\n ;(this as unknown as Record<symbol, boolean>)[VALIDATION_MARK] = true\n }\n}\n\n/** Whether this is a query refusal, whichever copy of the class built it. */\nexport function isQueryValidationError(error: unknown): error is QueryValidationError {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as Record<symbol, unknown>)[VALIDATION_MARK] === true\n )\n}\n\n/**\n * Marks a refusal so it survives a bundle seam.\n *\n * Symbol.for for the same reason the query mark is: an app's functions are\n * bundled apart from the engine, so each side gets its own copy of this class\n * and `instanceof` is simply false between them.\n */\nconst VALIDATION_MARK = Symbol.for('@rsc-kit/core.query-validation')\n\n/** Whether this function was declared with `query()`, whichever copy declared it. */\nexport function isQuery(fn: unknown): boolean {\n return typeof fn === 'function' && (fn as unknown as Record<symbol, unknown>)[QUERY_MARK] === true\n}\n\n/** What the query asked for, or the safe default when it asked for nothing. */\nexport function queryOptions(fn: unknown): QueryOptions {\n if (typeof fn !== 'function') return {}\n\n return ((fn as unknown as Record<symbol, unknown>)[QUERY_OPTIONS] as QueryOptions) ?? {}\n}\n\n/** The `Cache-Control` a query's answer goes out with. */\nexport function queryCacheControl(fn: unknown): string {\n const { cache = 'no-store', maxAge } = queryOptions(fn)\n\n if (cache === 'no-store') return 'private, no-store'\n\n const age = typeof maxAge === 'number' && maxAge > 0 ? Math.floor(maxAge) : 0\n\n // must-revalidate at zero rather than omitting max-age: with no directive at\n // all, whether an intermediary keeps it comes down to that intermediary's\n // heuristics, and this package does not leave that to chance in either\n // direction.\n return age > 0 ? `${cache}, max-age=${age}` : `${cache}, max-age=0, must-revalidate`\n}\n\n/**\n * The status and message a read that was turned away answers with, or null\n * when it failed.\n *\n * Signed out, not allowed, nothing there, or a refusal the backend gave a\n * status of its own - each message was written to be seen, and as a 500 it\n * was \"Query failed.\", with a fetcher unable to tell a 429 that says wait\n * from a broken server. Matched by name and mark, never by class: the query\n * was bundled apart from the engine.\n */\nexport function queryRefusal(error: unknown): { status: number; message: string; refusal?: unknown } | null {\n if (!(error instanceof Error)) return null\n\n const { name, message, refusalStatus, refusal } = error as Error & { refusalStatus?: unknown; refusal?: unknown }\n\n if (name === 'ServerAuthenticationError') return { status: 401, message }\n if (name === 'ServerAuthorizationError') return { status: 403, message }\n if (isNotFoundSignal(error)) return { status: 404, message }\n if (typeof refusalStatus !== 'number' || refusalStatus < 400 || refusalStatus > 599) return null\n\n return refusal === undefined ? { status: refusalStatus, message } : { status: refusalStatus, message, refusal }\n}\n"]}
|
package/dist/testing.d.ts
CHANGED
|
@@ -15,6 +15,22 @@ export interface TestApp {
|
|
|
15
15
|
* The names a tab would be handed, without reading them out of the payload.
|
|
16
16
|
*/
|
|
17
17
|
watched(path: string, init?: RequestInit): Promise<Record<string, string[]>>;
|
|
18
|
+
/**
|
|
19
|
+
* The page at `path` as markup: the whole streamed document, with every
|
|
20
|
+
* `<script>` taken out.
|
|
21
|
+
*
|
|
22
|
+
* The document carries the page's payload for the browser to hydrate from,
|
|
23
|
+
* and the payload holds everything the page was given - a client
|
|
24
|
+
* component's props included, shown or not. A test asserting that something
|
|
25
|
+
* is NOT on the page reads this, not `fetch`:
|
|
26
|
+
*
|
|
27
|
+
* expect(await app.markup('/teams/1')).not.toContain('internal note')
|
|
28
|
+
*
|
|
29
|
+
* Regions that streamed in later are included where they arrived; a
|
|
30
|
+
* Suspense fallback they replaced is still in the markup too, as the
|
|
31
|
+
* server sent it.
|
|
32
|
+
*/
|
|
33
|
+
markup(path: string, init?: RequestInit): Promise<string>;
|
|
18
34
|
/** Where the build was read from, for a test that wants to look. */
|
|
19
35
|
bundle: string;
|
|
20
36
|
}
|
package/dist/testing.js
CHANGED
|
@@ -33,11 +33,34 @@ export { hostReply, testChanges, HOST_MIDDLEWARE } from './testHost.js';
|
|
|
33
33
|
function findBundle(root) {
|
|
34
34
|
const candidates = [
|
|
35
35
|
join(root, 'node_modules/.nitro/vite/services/rsc/index.js'),
|
|
36
|
+
// The default outDir since 0.29.8; .rsc before it.
|
|
37
|
+
join(root, '.rsc-kit/dist/rsc/index.js'),
|
|
36
38
|
join(root, '.rsc/dist/rsc/index.js'),
|
|
37
39
|
join(root, 'build/dist/rsc/index.js'),
|
|
38
40
|
];
|
|
39
41
|
return candidates.find((path) => existsSync(path)) ?? null;
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* When anything a build is made from last changed: the app's source, its
|
|
45
|
+
* Vite config, its dependencies - package.json, the lockfile, and the
|
|
46
|
+
* installed engine itself. Only src/ used to count, so an app that upgraded
|
|
47
|
+
* @rsc-kit/core kept testing the build the old one made, and passed.
|
|
48
|
+
*/
|
|
49
|
+
function newestInput(root) {
|
|
50
|
+
const files = [
|
|
51
|
+
'vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs',
|
|
52
|
+
'package.json', 'bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock',
|
|
53
|
+
];
|
|
54
|
+
let latest = newest(join(root, 'src'));
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const path = join(root, file);
|
|
57
|
+
if (existsSync(path))
|
|
58
|
+
latest = Math.max(latest, statSync(path).mtimeMs);
|
|
59
|
+
}
|
|
60
|
+
// The engine as installed: a workspace link changes without the lockfile.
|
|
61
|
+
const engine = join(root, 'node_modules/@rsc-kit/core/dist');
|
|
62
|
+
return Math.max(latest, newest(engine));
|
|
63
|
+
}
|
|
41
64
|
/** The newest mtime under a directory, for deciding whether a build is stale. */
|
|
42
65
|
function newest(dir) {
|
|
43
66
|
if (!existsSync(dir))
|
|
@@ -135,7 +158,7 @@ async function ensureBuilt(root, build) {
|
|
|
135
158
|
}
|
|
136
159
|
return existing;
|
|
137
160
|
}
|
|
138
|
-
const fresh = existing && statSync(existing).mtimeMs >
|
|
161
|
+
const fresh = existing && statSync(existing).mtimeMs > newestInput(root);
|
|
139
162
|
if (fresh)
|
|
140
163
|
return existing;
|
|
141
164
|
// The user's own build command, so what is tested is what ships. A test
|
|
@@ -199,6 +222,12 @@ export async function createTestApp(options = {}) {
|
|
|
199
222
|
return {
|
|
200
223
|
bundle,
|
|
201
224
|
fetch,
|
|
225
|
+
markup: async (path, init) => {
|
|
226
|
+
const html = await (await fetch(path, init)).text();
|
|
227
|
+
// A script's body never holds its own closing tag: the payload writes
|
|
228
|
+
// "<" as \u003c, and React's scripts are its own.
|
|
229
|
+
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
230
|
+
},
|
|
202
231
|
watched: async (path, init) => {
|
|
203
232
|
// The render records what each region resolves to while this is set:
|
|
204
233
|
// the app's bundle runs in this process, so it sees the same global.
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["// The whole app as a function, for tests.\n//\n// import { createTestApp } from '@rsc-kit/core/testing'\n//\n// const app = await createTestApp()\n// const res = await app.fetch('/api/orders', { headers: { Cookie: 'user=ada' } })\n//\n// No port, no browser, no server process. The built entry already exports the\n// same Request → Response handler the dev server and the production server\n// both call, so a test can hand it a Request and read the Response — through\n// the real router, the real middleware, the real api routes, and the pages the\n// build stored.\n//\n// That is the tier between a unit test and a browser. An action or a route is\n// a function and can be imported and called; a browser test proves the page\n// works; this proves the app answers a url the way it is deployed, which is\n// where a route that renders fine and is served wrong shows up.\n\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { extname, join, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { httpHostCalls } from './hostCalls.js'\nimport { configureChanged } from './changed.js'\nimport { testHostFetch, type TestHost } from './testHost.js'\n\nexport { hostReply, testChanges, HOST_MIDDLEWARE, type HostCallInput, type HostHandler, type TestHost } from './testHost.js'\n\nexport interface TestApp {\n /** A path, not a url. The origin is whatever the app was told it is. */\n fetch(path: string, init?: RequestInit): Promise<Response>\n /**\n * What each region of the page at `path` refreshes on, rendered for this\n * request: `{ page: ['ledger'], repos: ['team:1:repos'] }` - `page` for the\n * page's own `refreshOn`, a section's name for each section's. A region\n * whose refreshOn gave no names is there with `[]`; one with no refreshOn\n * is not there at all.\n *\n * expect(await app.watched('/teams/1')).toMatchObject({ repos: ['team:1:repos'] })\n *\n * The names a tab would be handed, without reading them out of the payload.\n */\n watched(path: string, init?: RequestInit): Promise<Record<string, string[]>>\n /** Where the build was read from, for a test that wants to look. */\n bundle: string\n}\n\nexport interface TestAppOptions {\n /** The project. Defaults to the working directory. */\n root?: string\n /**\n * Whether to build first.\n *\n * `true` builds when the source is newer than the last build, which is what\n * a test run wants: the first run pays for it, the rest do not, and an edit\n * is picked up. `false` never builds and fails loudly if there is nothing to\n * read — for a ci step that already built.\n */\n build?: boolean\n /** The origin requests are made against. Nothing reads it; it is a url. */\n origin?: string\n /**\n * The backend, answered in the test: rpc() calls and host guards by name.\n * The app reaches it through the same client it uses against a real one,\n * so nothing listens and RSC_BACKEND is not called. See `hostReply`.\n */\n host?: TestHost\n /**\n * What answers a url this app does not own - a Go route, a Laravel page,\n * /login - in place of the backend the app forwards it to. Handed the\n * request as the backend would receive it. Without one, such a url is\n * forwarded to RSC_BACKEND, and in a test that is usually a 502.\n */\n backend?: (request: Request) => Response | Promise<Response>\n}\n\n/**\n * Where the build put the server bundle.\n *\n * Two layouts, because two ways of building. Under Nitro — every scaffolded\n * app — it is inside Nitro's own directory. On its own the plugin writes\n * <outDir>/dist/rsc. The first that exists wins.\n */\nfunction findBundle(root: string): string | null {\n const candidates = [\n join(root, 'node_modules/.nitro/vite/services/rsc/index.js'),\n join(root, '.rsc/dist/rsc/index.js'),\n join(root, 'build/dist/rsc/index.js'),\n ]\n\n return candidates.find((path) => existsSync(path)) ?? null\n}\n\n/** The newest mtime under a directory, for deciding whether a build is stale. */\nfunction newest(dir: string): number {\n if (!existsSync(dir)) return 0\n\n let latest = 0\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n\n const path = join(dir, entry.name)\n const time = entry.isDirectory() ? newest(path) : statSync(path).mtimeMs\n\n if (time > latest) latest = time\n }\n\n return latest\n}\n\n/** One build per process per root, however many test files ask. */\nconst built = new Map<string, Promise<string>>()\n\n/** The content types a test is likely to assert on; anything else is octet-stream. */\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.webmanifest': 'application/manifest+json',\n '.xml': 'application/xml',\n '.txt': 'text/plain; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.gif': 'image/gif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.wasm': 'application/wasm',\n '.map': 'application/json',\n}\n\n/**\n * A file the build put in .output/public, answered the way Nitro's static\n * layer would - which the handler this harness loads does not include. The\n * handler is the rsc service: routing, pages, actions, api routes. Assets,\n * the service worker, the manifest and the icons are files Nitro serves in\n * production from .output/public, and a test that asked for /sw.js used to\n * get the router's 404 for a file the deployment serves fine - the one\n * shape this harness exists to catch, in the other direction.\n *\n * Files only, for GET and HEAD, within the directory: a path that escapes it\n * is a request for the handler, not a file.\n */\n/** @internal Exported for its test. */\nexport function staticFile(root: string, request: Request): Response | null {\n if (request.method !== 'GET' && request.method !== 'HEAD') return null\n\n const publicDir = join(root, '.output/public')\n const pathname = decodeURIComponent(new URL(request.url).pathname)\n const file = resolve(publicDir, '.' + pathname)\n\n if (!file.startsWith(publicDir + sep) || !existsSync(file) || !statSync(file).isFile()) return null\n\n const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream'\n const body = request.method === 'HEAD' ? null : readFileSync(file)\n\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-RSC-Kit-Test': 'static' },\n })\n}\n\n/**\n * How this project builds: its own `build` script, run by the package manager\n * this test is running under. Without a script, Vite directly - on Bun's\n * runtime when the tests are, since a bin's node shebang would otherwise\n * start it under Node.\n */\n/** @internal Exported for its test. */\nexport function buildCommand(root: string): [string, string[]] {\n const onBun = typeof process.versions.bun === 'string'\n\n let scripts: Record<string, string> = {}\n\n try {\n scripts = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).scripts ?? {}\n } catch {\n // No package.json, or not JSON: there is no script to run.\n }\n\n if (typeof scripts.build === 'string') {\n return onBun ? ['bun', ['run', 'build']] : ['npm', ['run', 'build']]\n }\n\n return onBun ? ['bun', ['--bun', 'vite', 'build']] : ['npx', ['vite', 'build']]\n}\n\nasync function ensureBuilt(root: string, build: boolean): Promise<string> {\n const existing = findBundle(root)\n\n if (!build) {\n if (!existing) {\n throw new Error(\n `[rsc-kit] No build to test against under ${root}. Run the build first, or let createTestApp() do it by leaving \\`build\\` on.`,\n )\n }\n\n return existing\n }\n\n const fresh = existing && statSync(existing).mtimeMs > newest(join(root, 'src'))\n\n if (fresh) return existing\n\n // The user's own build command, so what is tested is what ships. A test\n // that built some other way would pass against a bundle nobody deploys -\n // and used to: this ran `npx vite build` whatever package.json said, which\n // on a Bun project built under Node and failed on the first `import 'bun'`.\n const [command, args] = buildCommand(root)\n const run = spawnSync(command, args, {\n cwd: root,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: { ...process.env, NODE_ENV: 'production' },\n })\n\n if (run.status !== 0) {\n throw new Error(`[rsc-kit] The build failed, so there is nothing to test:\\n${run.stderr}`)\n }\n\n const bundle = findBundle(root)\n\n if (!bundle) {\n throw new Error(\n `[rsc-kit] The build finished but no server bundle was found under ${root}. Is rscKit() in vite.config?`,\n )\n }\n\n return bundle\n}\n\n/**\n * Build the app if it needs it, load it, and hand back something to fetch from.\n *\n * Memoised per root: every test file in a run shares one build and one loaded\n * module, which is both the fast path and the correct one — two copies of the\n * server bundle in one process would be two client-reference registries.\n */\nexport async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {\n const root = resolve(options.root ?? process.cwd())\n const origin = options.origin ?? 'https://app.test'\n\n built.set(root, built.get(root) ?? ensureBuilt(root, options.build ?? true))\n\n // The app is built for production, which refuses to sign refreshOn names\n // without a key. One process, one key: a fixed one, unless the test set its own.\n if (!process.env.RSC_SIGNING_SECRET) {\n configureChanged({ secret: 'rsc-kit-test' })\n }\n\n const bundle = await built.get(root)!\n const entry = (await import(pathToFileURL(bundle).href)) as {\n default: (request: Request) => Promise<Response>\n installHostFn?: (fn: (name: string, ...args: unknown[]) => Promise<unknown>) => () => void\n installBackendForward?: (fn: ((request: Request) => Response | Promise<Response>) | null) => void\n }\n\n if (typeof entry.default !== 'function') {\n throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`)\n }\n\n const host = options.host\n ? httpHostCalls({ endpoint: 'http://test-host/__rsc/host-call', secret: 'test', fetch: testHostFetch(options.host) })\n : null\n\n if (host && !entry.installHostFn) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a host. Rebuild against the current @rsc-kit/core.`)\n }\n\n const fetch: TestApp['fetch'] = async (path, init) => {\n const request = new Request(new URL(path, origin), init)\n\n // Per request, not once: the loaded bundle is shared by every app a\n // run creates, and the host is this app's.\n if (host) entry.installHostFn!(host)\n if (options.backend && !entry.installBackendForward) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a backend. Rebuild against the current @rsc-kit/core.`)\n }\n entry.installBackendForward?.(options.backend ?? null)\n\n return staticFile(root, request) ?? entry.default(request)\n }\n\n return {\n bundle,\n fetch,\n watched: async (path, init) => {\n // The render records what each region resolves to while this is set:\n // the app's bundle runs in this process, so it sees the same global.\n const recording: Record<string, string[]> = {}\n const into = globalThis as { [WATCHED]?: Record<string, string[]> }\n\n into[WATCHED] = recording\n\n try {\n // Read to the end: the names resolve under their own Suspense, so\n // they are in the stream's tail, not its head.\n await (await fetch(path, init)).text()\n } finally {\n delete into[WATCHED]\n }\n\n return recording\n },\n }\n}\n\n/** Where a render records what its regions refresh on, while watched() asks (js/refreshOn). */\nconst WATCHED = Symbol.for('rsc-kit.test-watched')\n"]}
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["// The whole app as a function, for tests.\n//\n// import { createTestApp } from '@rsc-kit/core/testing'\n//\n// const app = await createTestApp()\n// const res = await app.fetch('/api/orders', { headers: { Cookie: 'user=ada' } })\n//\n// No port, no browser, no server process. The built entry already exports the\n// same Request → Response handler the dev server and the production server\n// both call, so a test can hand it a Request and read the Response — through\n// the real router, the real middleware, the real api routes, and the pages the\n// build stored.\n//\n// That is the tier between a unit test and a browser. An action or a route is\n// a function and can be imported and called; a browser test proves the page\n// works; this proves the app answers a url the way it is deployed, which is\n// where a route that renders fine and is served wrong shows up.\n\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { extname, join, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { httpHostCalls } from './hostCalls.js'\nimport { configureChanged } from './changed.js'\nimport { testHostFetch, type TestHost } from './testHost.js'\n\nexport { hostReply, testChanges, HOST_MIDDLEWARE, type HostCallInput, type HostHandler, type TestHost } from './testHost.js'\n\nexport interface TestApp {\n /** A path, not a url. The origin is whatever the app was told it is. */\n fetch(path: string, init?: RequestInit): Promise<Response>\n /**\n * What each region of the page at `path` refreshes on, rendered for this\n * request: `{ page: ['ledger'], repos: ['team:1:repos'] }` - `page` for the\n * page's own `refreshOn`, a section's name for each section's. A region\n * whose refreshOn gave no names is there with `[]`; one with no refreshOn\n * is not there at all.\n *\n * expect(await app.watched('/teams/1')).toMatchObject({ repos: ['team:1:repos'] })\n *\n * The names a tab would be handed, without reading them out of the payload.\n */\n watched(path: string, init?: RequestInit): Promise<Record<string, string[]>>\n /**\n * The page at `path` as markup: the whole streamed document, with every\n * `<script>` taken out.\n *\n * The document carries the page's payload for the browser to hydrate from,\n * and the payload holds everything the page was given - a client\n * component's props included, shown or not. A test asserting that something\n * is NOT on the page reads this, not `fetch`:\n *\n * expect(await app.markup('/teams/1')).not.toContain('internal note')\n *\n * Regions that streamed in later are included where they arrived; a\n * Suspense fallback they replaced is still in the markup too, as the\n * server sent it.\n */\n markup(path: string, init?: RequestInit): Promise<string>\n /** Where the build was read from, for a test that wants to look. */\n bundle: string\n}\n\nexport interface TestAppOptions {\n /** The project. Defaults to the working directory. */\n root?: string\n /**\n * Whether to build first.\n *\n * `true` builds when the source is newer than the last build, which is what\n * a test run wants: the first run pays for it, the rest do not, and an edit\n * is picked up. `false` never builds and fails loudly if there is nothing to\n * read — for a ci step that already built.\n */\n build?: boolean\n /** The origin requests are made against. Nothing reads it; it is a url. */\n origin?: string\n /**\n * The backend, answered in the test: rpc() calls and host guards by name.\n * The app reaches it through the same client it uses against a real one,\n * so nothing listens and RSC_BACKEND is not called. See `hostReply`.\n */\n host?: TestHost\n /**\n * What answers a url this app does not own - a Go route, a Laravel page,\n * /login - in place of the backend the app forwards it to. Handed the\n * request as the backend would receive it. Without one, such a url is\n * forwarded to RSC_BACKEND, and in a test that is usually a 502.\n */\n backend?: (request: Request) => Response | Promise<Response>\n}\n\n/**\n * Where the build put the server bundle.\n *\n * Two layouts, because two ways of building. Under Nitro — every scaffolded\n * app — it is inside Nitro's own directory. On its own the plugin writes\n * <outDir>/dist/rsc. The first that exists wins.\n */\nfunction findBundle(root: string): string | null {\n const candidates = [\n join(root, 'node_modules/.nitro/vite/services/rsc/index.js'),\n // The default outDir since 0.29.8; .rsc before it.\n join(root, '.rsc-kit/dist/rsc/index.js'),\n join(root, '.rsc/dist/rsc/index.js'),\n join(root, 'build/dist/rsc/index.js'),\n ]\n\n return candidates.find((path) => existsSync(path)) ?? null\n}\n\n/**\n * When anything a build is made from last changed: the app's source, its\n * Vite config, its dependencies - package.json, the lockfile, and the\n * installed engine itself. Only src/ used to count, so an app that upgraded\n * @rsc-kit/core kept testing the build the old one made, and passed.\n */\nfunction newestInput(root: string): number {\n const files = [\n 'vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs',\n 'package.json', 'bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock',\n ]\n let latest = newest(join(root, 'src'))\n\n for (const file of files) {\n const path = join(root, file)\n\n if (existsSync(path)) latest = Math.max(latest, statSync(path).mtimeMs)\n }\n\n // The engine as installed: a workspace link changes without the lockfile.\n const engine = join(root, 'node_modules/@rsc-kit/core/dist')\n\n return Math.max(latest, newest(engine))\n}\n\n/** The newest mtime under a directory, for deciding whether a build is stale. */\nfunction newest(dir: string): number {\n if (!existsSync(dir)) return 0\n\n let latest = 0\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n\n const path = join(dir, entry.name)\n const time = entry.isDirectory() ? newest(path) : statSync(path).mtimeMs\n\n if (time > latest) latest = time\n }\n\n return latest\n}\n\n/** One build per process per root, however many test files ask. */\nconst built = new Map<string, Promise<string>>()\n\n/** The content types a test is likely to assert on; anything else is octet-stream. */\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.webmanifest': 'application/manifest+json',\n '.xml': 'application/xml',\n '.txt': 'text/plain; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.gif': 'image/gif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.wasm': 'application/wasm',\n '.map': 'application/json',\n}\n\n/**\n * A file the build put in .output/public, answered the way Nitro's static\n * layer would - which the handler this harness loads does not include. The\n * handler is the rsc service: routing, pages, actions, api routes. Assets,\n * the service worker, the manifest and the icons are files Nitro serves in\n * production from .output/public, and a test that asked for /sw.js used to\n * get the router's 404 for a file the deployment serves fine - the one\n * shape this harness exists to catch, in the other direction.\n *\n * Files only, for GET and HEAD, within the directory: a path that escapes it\n * is a request for the handler, not a file.\n */\n/** @internal Exported for its test. */\nexport function staticFile(root: string, request: Request): Response | null {\n if (request.method !== 'GET' && request.method !== 'HEAD') return null\n\n const publicDir = join(root, '.output/public')\n const pathname = decodeURIComponent(new URL(request.url).pathname)\n const file = resolve(publicDir, '.' + pathname)\n\n if (!file.startsWith(publicDir + sep) || !existsSync(file) || !statSync(file).isFile()) return null\n\n const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream'\n const body = request.method === 'HEAD' ? null : readFileSync(file)\n\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-RSC-Kit-Test': 'static' },\n })\n}\n\n/**\n * How this project builds: its own `build` script, run by the package manager\n * this test is running under. Without a script, Vite directly - on Bun's\n * runtime when the tests are, since a bin's node shebang would otherwise\n * start it under Node.\n */\n/** @internal Exported for its test. */\nexport function buildCommand(root: string): [string, string[]] {\n const onBun = typeof process.versions.bun === 'string'\n\n let scripts: Record<string, string> = {}\n\n try {\n scripts = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).scripts ?? {}\n } catch {\n // No package.json, or not JSON: there is no script to run.\n }\n\n if (typeof scripts.build === 'string') {\n return onBun ? ['bun', ['run', 'build']] : ['npm', ['run', 'build']]\n }\n\n return onBun ? ['bun', ['--bun', 'vite', 'build']] : ['npx', ['vite', 'build']]\n}\n\nasync function ensureBuilt(root: string, build: boolean): Promise<string> {\n const existing = findBundle(root)\n\n if (!build) {\n if (!existing) {\n throw new Error(\n `[rsc-kit] No build to test against under ${root}. Run the build first, or let createTestApp() do it by leaving \\`build\\` on.`,\n )\n }\n\n return existing\n }\n\n const fresh = existing && statSync(existing).mtimeMs > newestInput(root)\n\n if (fresh) return existing\n\n // The user's own build command, so what is tested is what ships. A test\n // that built some other way would pass against a bundle nobody deploys -\n // and used to: this ran `npx vite build` whatever package.json said, which\n // on a Bun project built under Node and failed on the first `import 'bun'`.\n const [command, args] = buildCommand(root)\n const run = spawnSync(command, args, {\n cwd: root,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: { ...process.env, NODE_ENV: 'production' },\n })\n\n if (run.status !== 0) {\n throw new Error(`[rsc-kit] The build failed, so there is nothing to test:\\n${run.stderr}`)\n }\n\n const bundle = findBundle(root)\n\n if (!bundle) {\n throw new Error(\n `[rsc-kit] The build finished but no server bundle was found under ${root}. Is rscKit() in vite.config?`,\n )\n }\n\n return bundle\n}\n\n/**\n * Build the app if it needs it, load it, and hand back something to fetch from.\n *\n * Memoised per root: every test file in a run shares one build and one loaded\n * module, which is both the fast path and the correct one — two copies of the\n * server bundle in one process would be two client-reference registries.\n */\nexport async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {\n const root = resolve(options.root ?? process.cwd())\n const origin = options.origin ?? 'https://app.test'\n\n built.set(root, built.get(root) ?? ensureBuilt(root, options.build ?? true))\n\n // The app is built for production, which refuses to sign refreshOn names\n // without a key. One process, one key: a fixed one, unless the test set its own.\n if (!process.env.RSC_SIGNING_SECRET) {\n configureChanged({ secret: 'rsc-kit-test' })\n }\n\n const bundle = await built.get(root)!\n const entry = (await import(pathToFileURL(bundle).href)) as {\n default: (request: Request) => Promise<Response>\n installHostFn?: (fn: (name: string, ...args: unknown[]) => Promise<unknown>) => () => void\n installBackendForward?: (fn: ((request: Request) => Response | Promise<Response>) | null) => void\n }\n\n if (typeof entry.default !== 'function') {\n throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`)\n }\n\n const host = options.host\n ? httpHostCalls({ endpoint: 'http://test-host/__rsc/host-call', secret: 'test', fetch: testHostFetch(options.host) })\n : null\n\n if (host && !entry.installHostFn) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a host. Rebuild against the current @rsc-kit/core.`)\n }\n\n const fetch: TestApp['fetch'] = async (path, init) => {\n const request = new Request(new URL(path, origin), init)\n\n // Per request, not once: the loaded bundle is shared by every app a\n // run creates, and the host is this app's.\n if (host) entry.installHostFn!(host)\n if (options.backend && !entry.installBackendForward) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a backend. Rebuild against the current @rsc-kit/core.`)\n }\n entry.installBackendForward?.(options.backend ?? null)\n\n return staticFile(root, request) ?? entry.default(request)\n }\n\n return {\n bundle,\n fetch,\n markup: async (path, init) => {\n const html = await (await fetch(path, init)).text()\n\n // A script's body never holds its own closing tag: the payload writes\n // \"<\" as \\u003c, and React's scripts are its own.\n return html.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n },\n watched: async (path, init) => {\n // The render records what each region resolves to while this is set:\n // the app's bundle runs in this process, so it sees the same global.\n const recording: Record<string, string[]> = {}\n const into = globalThis as { [WATCHED]?: Record<string, string[]> }\n\n into[WATCHED] = recording\n\n try {\n // Read to the end: the names resolve under their own Suspense, so\n // they are in the stream's tail, not its head.\n await (await fetch(path, init)).text()\n } finally {\n delete into[WATCHED]\n }\n\n return recording\n },\n }\n}\n\n/** Where a render records what its regions refresh on, while watched() asks (js/refreshOn). */\nconst WATCHED = Symbol.for('rsc-kit.test-watched')\n"]}
|
package/dist/vite.js
CHANGED
|
@@ -3035,7 +3035,7 @@ import { backendVersions, installBackendVersionSource } from ${JSON.stringify(jo
|
|
|
3035
3035
|
import { RefreshOn } from ${JSON.stringify(join(packageDir, "js/refreshOn"))}
|
|
3036
3036
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
3037
3037
|
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
3038
|
-
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
3038
|
+
import { isQuery, queryCacheControl, isQueryValidationError, queryRefusal } from ${JSON.stringify(join(packageDir, "query"))}
|
|
3039
3039
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
3040
3040
|
import { forgetCached } from ${JSON.stringify(join(packageDir, "cache"))}
|
|
3041
3041
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -4859,7 +4859,7 @@ export async function handleQuery(
|
|
|
4859
4859
|
report?: (error: unknown) => string,
|
|
4860
4860
|
): Promise<
|
|
4861
4861
|
| { stream: ReadableStream; cacheControl: string }
|
|
4862
|
-
| { status: number; message: string; errors?: Record<string, string[]
|
|
4862
|
+
| { status: number; message: string; errors?: Record<string, string[]>; refusal?: unknown }
|
|
4863
4863
|
| null
|
|
4864
4864
|
> {
|
|
4865
4865
|
await instrumented()
|
|
@@ -4893,6 +4893,15 @@ export async function handleQuery(
|
|
|
4893
4893
|
return { status: 422, errors: error.errors, message: error.message }
|
|
4894
4894
|
}
|
|
4895
4895
|
|
|
4896
|
+
// Turned away rather than failed, and said with the status a page would
|
|
4897
|
+
// have answered: signed out, not allowed, nothing there, or a refusal the
|
|
4898
|
+
// backend gave a status of its own. Each message was written to be seen;
|
|
4899
|
+
// as a 500 it was "Query failed." and a fetcher could not tell a 429 to
|
|
4900
|
+
// wait from a broken server.
|
|
4901
|
+
const turnedAway = queryRefusal(error)
|
|
4902
|
+
|
|
4903
|
+
if (turnedAway) return turnedAway
|
|
4904
|
+
|
|
4896
4905
|
return { status: 500, message: report ? report(error) : 'Query failed.' }
|
|
4897
4906
|
}
|
|
4898
4907
|
|