@rsc-kit/core 0.29.0 → 0.29.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/testHost.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // becoming a 401 - and only the backend is stood in for.
8
8
  import { CHANGED_FUNCTION, memoryVersions } from './changed.js';
9
9
  const REPLY = Symbol.for('rsc-kit.test-host-reply');
10
+ /** Cookies a reply sets: sent as Set-Cookie headers, never in the body. */
11
+ const COOKIES = Symbol.for('rsc-kit.test-host-cookies');
10
12
  /** The name the engine asks a host's route guards under. */
11
13
  export const HOST_MIDDLEWARE = '__rsc.middleware';
12
14
  export function testChanges() {
@@ -46,6 +48,8 @@ export const hostReply = {
46
48
  fail: (message = 'Something went wrong.') => reply({ error: message }),
47
49
  /** A result, and the regions of the page the call says it changed. */
48
50
  revalidating: (result, ...targets) => reply({ result, revalidate: targets }),
51
+ /** A result, and cookies set on the response - a login's session: `'session=abc; Path=/; HttpOnly'`. */
52
+ settingCookies: (result, ...cookies) => reply({ result, [COOKIES]: cookies }),
49
53
  };
50
54
  function isReply(value) {
51
55
  return typeof value === 'object' && value !== null && value[REPLY] === true;
@@ -64,6 +68,8 @@ async function answer(host, name, args, headers) {
64
68
  }
65
69
  const value = await handler({ args, headers });
66
70
  if (isReply(value)) {
71
+ // Symbol keys survive the rest spread; COOKIES is read off by the fetch
72
+ // below, and JSON leaves it out of the body.
67
73
  const { [REPLY]: _, ...fields } = value;
68
74
  return fields;
69
75
  }
@@ -86,7 +92,12 @@ export function testHostFetch(host) {
86
92
  replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),
87
93
  });
88
94
  }
89
- return json(await answer(host, body.function, body.args ?? [], headers));
95
+ const single = await answer(host, body.function, body.args ?? [], headers);
96
+ const cookies = single[COOKIES] ?? [];
97
+ const response = json(single);
98
+ for (const cookie of cookies)
99
+ response.headers.append('Set-Cookie', cookie);
100
+ return response;
90
101
  });
91
102
  }
92
103
  //# sourceMappingURL=testHost.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testHost.js","sourceRoot":"","sources":["../src/testHost.ts"],"names":[],"mappings":"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","sourcesContent":["// A backend for tests: answers rpc() calls and host middleware the way a Go\n// or Laravel host answers them over HTTP, without one running.\n//\n// The app under test talks to it through the real client, httpHostCalls,\n// handed a fetch that answers in-process. So a test exercises what ships -\n// batching, a redirect raised as the engine's own, an unauthenticated reply\n// becoming a 401 - and only the backend is stood in for.\n\nimport type { HostCallReply } from './hostCalls.js'\nimport { CHANGED_FUNCTION, memoryVersions } from './changed.js'\nimport type { ChangedAnswer, ChangedQuery } from './changed.js'\n\nconst REPLY = Symbol.for('rsc-kit.test-host-reply')\n\n/** The name the engine asks a host's route guards under. */\nexport const HOST_MIDDLEWARE = '__rsc.middleware'\n\n/**\n * Tag versions for a test host, kept in the test.\n *\n * const names = testChanges()\n * const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })\n * names.changed('team:1:repos') // what the backend's webhook would say\n *\n * `host` answers `__rsc.changed` the way an adapter does - holding the call up\n * to `wait` for a name to move - and `changed` moves one.\n */\n/**\n * The test host's answer to `__rsc.changed`: given what any test host is\n * given, answering a `ChangedAnswer`.\n *\n * Both halves matter. It takes `HostCallInput`, so an untyped host of\n * `HostHandler`s accepts it; it returns a `ChangedAnswer`, so a host typed\n * from the app's rsc-host.json - whose `__rsc.changed` must answer one -\n * accepts it too. Typed as `HostHandler`, it returned `unknown`, and\n * spreading it into a typed host did not compile.\n */\nexport interface ChangedHost {\n [CHANGED_FUNCTION]: (call: HostCallInput) => Promise<ChangedAnswer>\n}\n\nexport function testChanges(): { host: ChangedHost; changed: (...names: string[]) => void } {\n const source = memoryVersions()\n\n return {\n host: {\n [CHANGED_FUNCTION]: async ({ args }) => {\n const query = args[0] as Partial<ChangedQuery> | undefined\n\n return { versions: await source.changed(query?.since ?? {}, Math.min(query?.wait ?? 0, 5_000)) }\n },\n },\n changed: (...names) => void source.bump(names),\n }\n}\n\n/** What a test handler is given: the call's arguments, and the headers the renderer forwarded. */\nexport interface HostCallInput {\n args: unknown[]\n /** The visitor's request headers the renderer passed on - the cookie, the Accept-Language. */\n headers: Headers\n}\n\n/** Answers one function. Return a value for its result, or one of `hostReply`'s answers. */\nexport type HostHandler = (call: HostCallInput) => unknown\n\ntype Reply = Omit<HostCallReply, 'result'> & { [REPLY]: true }\n\n/** One of the protocol's own answers, from `hostReply`. */\nexport type HostReply = Reply\n\ndeclare global {\n /**\n * The backend's functions, as the build read them from rsc-host.json:\n * each one's arguments and result. Written into .rsc-kit/rsc-env.d.ts;\n * empty without a manifest.\n */\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface RscHostFunctions {}\n}\n\ntype Answer<T> = T | HostReply | Promise<T | HostReply>\n\ntype Guards = {\n [HOST_MIDDLEWARE]?: (call: { args: [names: string[]]; headers: Headers }) => Answer<true>\n [CHANGED_FUNCTION]?: (call: { args: [query: ChangedQuery]; headers: Headers }) => Answer<ChangedAnswer>\n}\n\n/**\n * The backend's functions, by the name rpc() calls them with.\n *\n * Typed from the app's rsc-host.json when it has one: a handler may only\n * answer for a function the backend has, is given its arguments, and returns\n * its result or one of `hostReply`'s answers - never a shape the backend\n * would not send, which is how a fake drifts from the real thing while its\n * tests stay green. Without a manifest, any name and any value.\n *\n * Route guards declared for the host are asked as `__rsc.middleware`\n * (`HOST_MIDDLEWARE`), with the guard names as the first argument; answer\n * `true` to let the page render.\n */\nexport type TestHost = [keyof RscHostFunctions] extends [never]\n ? Record<string, HostHandler>\n : {\n [K in keyof RscHostFunctions]?: RscHostFunctions[K] extends { args: infer A extends unknown[]; result: infer R }\n ? (call: { args: A; headers: Headers }) => Answer<R>\n : never\n } & Guards\n\nconst reply = (fields: Omit<Reply, typeof REPLY>): Reply => ({ ...fields, [REPLY]: true })\n\n/**\n * The protocol's own answers, for a handler to return.\n *\n * host: {\n * 'Apps.list': ({ headers }) =>\n * headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),\n * 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),\n * }\n */\nexport const hostReply = {\n /** No session: the page answers 401, an action its auth error. */\n unauthenticated: (message?: string): Reply => reply({ unauthenticated: true, error: message }),\n /** A session that still may not: 403. */\n unauthorized: (message?: string): Reply => reply({ unauthorized: true, error: message }),\n /** Send the visitor somewhere else, as the host's redirect does. */\n redirect: (to: string, status = 307): Reply => reply({ redirect: to, redirectStatus: status }),\n /** Refuse with a status - a 404 for a record this caller cannot see, a 429. */\n refuse: (status: number, message = 'Refused.'): Reply => reply({ error: message, refusalStatus: status }),\n /** The input was refused, by field: an action returns these as validationErrors. */\n invalid: (errors: Record<string, string[]>): Reply => reply({ validationErrors: errors }),\n /** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */\n fail: (message = 'Something went wrong.'): Reply => reply({ error: message }),\n /** A result, and the regions of the page the call says it changed. */\n revalidating: (result: unknown, ...targets: string[]): Reply =>\n reply({ result, revalidate: targets } as Omit<Reply, typeof REPLY>),\n} as const\n\nfunction isReply(value: unknown): value is Reply {\n return typeof value === 'object' && value !== null && (value as Record<symbol, unknown>)[REPLY] === true\n}\n\nasync function answer(host: TestHost, name: string, args: unknown[], headers: Headers): Promise<HostCallReply> {\n // By a name only known at runtime: a typed host has no index signature.\n const handler = (host as Record<string, HostHandler | undefined>)[name]\n\n // Fails the call rather than answering null: a function the test forgot\n // is a test that would otherwise pass against nothing.\n if (!handler) {\n return {\n error:\n name === HOST_MIDDLEWARE\n ? `the page asked its host guards (${JSON.stringify(args[0])}) and the test host has no \"${HOST_MIDDLEWARE}\" handler`\n : `the test host has no handler for ${JSON.stringify(name)}`,\n }\n }\n\n const value = await handler({ args, headers })\n\n if (isReply(value)) {\n const { [REPLY]: _, ...fields } = value\n\n return fields\n }\n\n return { result: value === undefined ? null : value }\n}\n\n/**\n * A fetch that answers the host-call endpoint from `host`: one call as\n * itself, a batch as `{ replies }`, in order.\n */\nexport function testHostFetch(host: TestHost): typeof fetch {\n return (async (_input: RequestInfo | URL, init?: RequestInit) => {\n const headers = new Headers(init?.headers)\n const body = JSON.parse(String(init?.body ?? '{}')) as\n | { function: string; args?: unknown[] }\n | { calls: { function: string; args?: unknown[] }[] }\n\n // The renderer's own headers are not the visitor's.\n headers.delete('x-rsc-host-secret')\n headers.delete('content-type')\n\n const json = (value: unknown) =>\n new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } })\n\n if ('calls' in body) {\n return json({\n replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),\n })\n }\n\n return json(await answer(host, body.function, body.args ?? [], headers))\n }) as typeof fetch\n}\n"]}
1
+ {"version":3,"file":"testHost.js","sourceRoot":"","sources":["../src/testHost.ts"],"names":[],"mappings":"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","sourcesContent":["// A backend for tests: answers rpc() calls and host middleware the way a Go\n// or Laravel host answers them over HTTP, without one running.\n//\n// The app under test talks to it through the real client, httpHostCalls,\n// handed a fetch that answers in-process. So a test exercises what ships -\n// batching, a redirect raised as the engine's own, an unauthenticated reply\n// becoming a 401 - and only the backend is stood in for.\n\nimport type { HostCallReply } from './hostCalls.js'\nimport { CHANGED_FUNCTION, memoryVersions } from './changed.js'\nimport type { ChangedAnswer, ChangedQuery } from './changed.js'\n\nconst REPLY = Symbol.for('rsc-kit.test-host-reply')\n/** Cookies a reply sets: sent as Set-Cookie headers, never in the body. */\nconst COOKIES = Symbol.for('rsc-kit.test-host-cookies')\n\n/** The name the engine asks a host's route guards under. */\nexport const HOST_MIDDLEWARE = '__rsc.middleware'\n\n/**\n * Tag versions for a test host, kept in the test.\n *\n * const names = testChanges()\n * const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })\n * names.changed('team:1:repos') // what the backend's webhook would say\n *\n * `host` answers `__rsc.changed` the way an adapter does - holding the call up\n * to `wait` for a name to move - and `changed` moves one.\n */\n/**\n * The test host's answer to `__rsc.changed`: given what any test host is\n * given, answering a `ChangedAnswer`.\n *\n * Both halves matter. It takes `HostCallInput`, so an untyped host of\n * `HostHandler`s accepts it; it returns a `ChangedAnswer`, so a host typed\n * from the app's rsc-host.json - whose `__rsc.changed` must answer one -\n * accepts it too. Typed as `HostHandler`, it returned `unknown`, and\n * spreading it into a typed host did not compile.\n */\nexport interface ChangedHost {\n [CHANGED_FUNCTION]: (call: HostCallInput) => Promise<ChangedAnswer>\n}\n\nexport function testChanges(): { host: ChangedHost; changed: (...names: string[]) => void } {\n const source = memoryVersions()\n\n return {\n host: {\n [CHANGED_FUNCTION]: async ({ args }) => {\n const query = args[0] as Partial<ChangedQuery> | undefined\n\n return { versions: await source.changed(query?.since ?? {}, Math.min(query?.wait ?? 0, 5_000)) }\n },\n },\n changed: (...names) => void source.bump(names),\n }\n}\n\n/** What a test handler is given: the call's arguments, and the headers the renderer forwarded. */\nexport interface HostCallInput {\n args: unknown[]\n /** The visitor's request headers the renderer passed on - the cookie, the Accept-Language. */\n headers: Headers\n}\n\n/** Answers one function. Return a value for its result, or one of `hostReply`'s answers. */\nexport type HostHandler = (call: HostCallInput) => unknown\n\ntype Reply = Omit<HostCallReply, 'result'> & { [REPLY]: true }\n\n/** One of the protocol's own answers, from `hostReply`. */\nexport type HostReply = Reply\n\ndeclare global {\n /**\n * The backend's functions, as the build read them from rsc-host.json:\n * each one's arguments and result. Written into .rsc-kit/rsc-env.d.ts;\n * empty without a manifest.\n */\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface RscHostFunctions {}\n}\n\ntype Answer<T> = T | HostReply | Promise<T | HostReply>\n\ntype Guards = {\n [HOST_MIDDLEWARE]?: (call: { args: [names: string[]]; headers: Headers }) => Answer<true>\n [CHANGED_FUNCTION]?: (call: { args: [query: ChangedQuery]; headers: Headers }) => Answer<ChangedAnswer>\n}\n\n/**\n * The backend's functions, by the name rpc() calls them with.\n *\n * Typed from the app's rsc-host.json when it has one: a handler may only\n * answer for a function the backend has, is given its arguments, and returns\n * its result or one of `hostReply`'s answers - never a shape the backend\n * would not send, which is how a fake drifts from the real thing while its\n * tests stay green. Without a manifest, any name and any value.\n *\n * Route guards declared for the host are asked as `__rsc.middleware`\n * (`HOST_MIDDLEWARE`), with the guard names as the first argument; answer\n * `true` to let the page render.\n */\nexport type TestHost = [keyof RscHostFunctions] extends [never]\n ? Record<string, HostHandler>\n : {\n [K in keyof RscHostFunctions]?: RscHostFunctions[K] extends { args: infer A extends unknown[]; result: infer R }\n ? (call: { args: A; headers: Headers }) => Answer<R>\n : never\n } & Guards\n\nconst reply = (fields: Omit<Reply, typeof REPLY>): Reply => ({ ...fields, [REPLY]: true })\n\n/**\n * The protocol's own answers, for a handler to return.\n *\n * host: {\n * 'Apps.list': ({ headers }) =>\n * headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),\n * 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),\n * }\n */\nexport const hostReply = {\n /** No session: the page answers 401, an action its auth error. */\n unauthenticated: (message?: string): Reply => reply({ unauthenticated: true, error: message }),\n /** A session that still may not: 403. */\n unauthorized: (message?: string): Reply => reply({ unauthorized: true, error: message }),\n /** Send the visitor somewhere else, as the host's redirect does. */\n redirect: (to: string, status = 307): Reply => reply({ redirect: to, redirectStatus: status }),\n /** Refuse with a status - a 404 for a record this caller cannot see, a 429. */\n refuse: (status: number, message = 'Refused.'): Reply => reply({ error: message, refusalStatus: status }),\n /** The input was refused, by field: an action returns these as validationErrors. */\n invalid: (errors: Record<string, string[]>): Reply => reply({ validationErrors: errors }),\n /** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */\n fail: (message = 'Something went wrong.'): Reply => reply({ error: message }),\n /** A result, and the regions of the page the call says it changed. */\n revalidating: (result: unknown, ...targets: string[]): Reply =>\n reply({ result, revalidate: targets } as Omit<Reply, typeof REPLY>),\n /** A result, and cookies set on the response - a login's session: `'session=abc; Path=/; HttpOnly'`. */\n settingCookies: (result: unknown, ...cookies: string[]): Reply =>\n reply({ result, [COOKIES]: cookies } as unknown as Omit<Reply, typeof REPLY>),\n} as const\n\nfunction isReply(value: unknown): value is Reply {\n return typeof value === 'object' && value !== null && (value as Record<symbol, unknown>)[REPLY] === true\n}\n\nasync function answer(host: TestHost, name: string, args: unknown[], headers: Headers): Promise<HostCallReply> {\n // By a name only known at runtime: a typed host has no index signature.\n const handler = (host as Record<string, HostHandler | undefined>)[name]\n\n // Fails the call rather than answering null: a function the test forgot\n // is a test that would otherwise pass against nothing.\n if (!handler) {\n return {\n error:\n name === HOST_MIDDLEWARE\n ? `the page asked its host guards (${JSON.stringify(args[0])}) and the test host has no \"${HOST_MIDDLEWARE}\" handler`\n : `the test host has no handler for ${JSON.stringify(name)}`,\n }\n }\n\n const value = await handler({ args, headers })\n\n if (isReply(value)) {\n // Symbol keys survive the rest spread; COOKIES is read off by the fetch\n // below, and JSON leaves it out of the body.\n const { [REPLY]: _, ...fields } = value\n\n return fields\n }\n\n return { result: value === undefined ? null : value }\n}\n\n/**\n * A fetch that answers the host-call endpoint from `host`: one call as\n * itself, a batch as `{ replies }`, in order.\n */\nexport function testHostFetch(host: TestHost): typeof fetch {\n return (async (_input: RequestInfo | URL, init?: RequestInit) => {\n const headers = new Headers(init?.headers)\n const body = JSON.parse(String(init?.body ?? '{}')) as\n | { function: string; args?: unknown[] }\n | { calls: { function: string; args?: unknown[] }[] }\n\n // The renderer's own headers are not the visitor's.\n headers.delete('x-rsc-host-secret')\n headers.delete('content-type')\n\n const json = (value: unknown) =>\n new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } })\n\n if ('calls' in body) {\n return json({\n replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),\n })\n }\n\n const single = await answer(host, body.function, body.args ?? [], headers)\n const cookies = (single as { [COOKIES]?: string[] })[COOKIES] ?? []\n const response = json(single)\n\n for (const cookie of cookies) response.headers.append('Set-Cookie', cookie)\n\n return response\n }) as typeof fetch\n}\n"]}
package/dist/vite.js CHANGED
@@ -4796,8 +4796,14 @@ export async function handleAction(
4796
4796
  // to ask is the whole point — the answer carries what went stale with it.
4797
4797
  const targets = takeRevalidated?.() ?? []
4798
4798
 
4799
+ // flightOnError on both, as on every other render: it is what turns a
4800
+ // redirect() or notFound() into the digest the client's boundaries read.
4801
+ // Without it a re-render that reached a redirect inside a Suspense
4802
+ // boundary - the old address of a page the action just renamed - sent an
4803
+ // anonymous error, and the boundary showed the error page instead of
4804
+ // following it.
4799
4805
  if (targets.length === 0 || !page) {
4800
- return { stream: renderToReadableStream(result) }
4806
+ return { stream: renderToReadableStream(result, { onError: flightOnError }) }
4801
4807
  }
4802
4808
 
4803
4809
  // The render is of the world after the write. Everything cache() answered
@@ -4822,7 +4828,7 @@ export async function handleAction(
4822
4828
 
4823
4829
  // Marked, so an action whose own result happens to be an object with a
4824
4830
  // 'result' key is not mistaken for this envelope.
4825
- return { stream: renderToReadableStream({ __rscRevalidated: revalidated, result }) }
4831
+ return { stream: renderToReadableStream({ __rscRevalidated: revalidated, result }, { onError: flightOnError }) }
4826
4832
  }
4827
4833
 
4828
4834
  return page.href ? await withUrl(page.href, render) : await render()
@@ -5076,13 +5082,16 @@ export async function handleRscRevalidate(
5076
5082
  await runMiddleware(page.component, page.props)
5077
5083
 
5078
5084
  const rscPayload = await shareRender(page.component + ' ' + target + ' ' + share, async () =>
5079
- new Response(renderToReadableStream(await renderRevalidated(target, page, false, true))).text(),
5085
+ new Response(renderToReadableStream(await renderRevalidated(target, page, false, true), { onError: flightOnError })).text(),
5080
5086
  )
5081
5087
 
5082
5088
  return { rscPayload }
5083
5089
  }
5084
5090
 
5085
- const flight = renderToReadableStream(await renderRevalidated(target, page))
5091
+ // A redirect the refreshed region reaches has to arrive as one - a region
5092
+ // refreshed because refreshOn heard its data change, on a page that now
5093
+ // redirects, showed the error page without the digest.
5094
+ const flight = renderToReadableStream(await renderRevalidated(target, page), { onError: flightOnError })
5086
5095
 
5087
5096
  return { rscPayload: await new Response(flight).text() }
5088
5097
  }