@rsc-kit/core 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testHost.js CHANGED
@@ -9,16 +9,6 @@ import { CHANGED_FUNCTION, memoryVersions } from './changed.js';
9
9
  const REPLY = Symbol.for('rsc-kit.test-host-reply');
10
10
  /** The name the engine asks a host's route guards under. */
11
11
  export const HOST_MIDDLEWARE = '__rsc.middleware';
12
- /**
13
- * Tag versions for a test host, kept in the test.
14
- *
15
- * const names = testChanges()
16
- * const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })
17
- * names.changed('team:1:repos') // what the backend's webhook would say
18
- *
19
- * `host` answers `__rsc.changed` the way an adapter does - holding the call up
20
- * to `wait` for a name to move - and `changed` moves one.
21
- */
22
12
  export function testChanges() {
23
13
  const source = memoryVersions();
24
14
  return {
@@ -28,7 +18,7 @@ export function testChanges() {
28
18
  return { versions: await source.changed(query?.since ?? {}, Math.min(query?.wait ?? 0, 5_000)) };
29
19
  },
30
20
  },
31
- changed: (...tags) => void source.bump(tags),
21
+ changed: (...names) => void source.bump(names),
32
22
  };
33
23
  }
34
24
  const reply = (fields) => ({ ...fields, [REPLY]: true });
@@ -61,6 +51,7 @@ function isReply(value) {
61
51
  return typeof value === 'object' && value !== null && value[REPLY] === true;
62
52
  }
63
53
  async function answer(host, name, args, headers) {
54
+ // By a name only known at runtime: a typed host has no index signature.
64
55
  const handler = host[name];
65
56
  // Fails the call rather than answering null: a function the test forgot
66
57
  // is a test that would otherwise pass against nothing.
@@ -1 +1 @@
1
- {"version":3,"file":"testHost.js","sourceRoot":"","sources":["../src/testHost.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+DAA+D;AAC/D,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,4EAA4E;AAC5E,yDAAyD;AAGzD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAG/D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAA;AAEnD,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAA;AAEjD;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,MAAM,GAAG,cAAc,EAAE,CAAA;IAE/B,OAAO;QACL,IAAI,EAAE;YACJ,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;gBACrC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAsC,CAAA;gBAE1D,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,EAA0B,CAAA;YAC1H,CAAC;SACF;QACD,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;KAC7C,CAAA;AACH,CAAC;AAuDD,MAAM,KAAK,GAAG,CAAC,MAAiC,EAAS,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;AAE1F;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,kEAAkE;IAClE,eAAe,EAAE,CAAC,OAAgB,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC9F,yCAAyC;IACzC,YAAY,EAAE,CAAC,OAAgB,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACxF,oEAAoE;IACpE,QAAQ,EAAE,CAAC,EAAU,EAAE,MAAM,GAAG,GAAG,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IAC9F,+EAA+E;IAC/E,MAAM,EAAE,CAAC,MAAc,EAAE,OAAO,GAAG,UAAU,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;IACzG,oFAAoF;IACpF,OAAO,EAAE,CAAC,MAAgC,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IACzF,qFAAqF;IACrF,IAAI,EAAE,CAAC,OAAO,GAAG,uBAAuB,EAAS,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC7E,sEAAsE;IACtE,YAAY,EAAE,CAAC,MAAe,EAAE,GAAG,OAAiB,EAAS,EAAE,CAC7D,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAA+B,CAAC;CAC7D,CAAA;AAEV,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAAiC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAA;AAC1G,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAc,EAAE,IAAY,EAAE,IAAe,EAAE,OAAgB;IACnF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAA;IAE1B,wEAAwE;IACxE,uDAAuD;IACvD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EACH,IAAI,KAAK,eAAe;gBACtB,CAAC,CAAC,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,+BAA+B,eAAe,WAAW;gBACrH,CAAC,CAAC,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;SACjE,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IAE9C,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,KAAK,CAAA;QAEvC,OAAO,MAAM,CAAA;IACf,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAA;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAc;IAC1C,OAAO,CAAC,KAAK,EAAE,MAAyB,EAAE,IAAkB,EAAE,EAAE;QAC9D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAEK,CAAA;QAEvD,oDAAoD;QACpD,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;QACnC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;QAE9B,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE,CAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAA;QAE1F,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;gBACV,OAAO,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;aACnG,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IAC1E,CAAC,CAAiB,CAAA;AACpB,CAAC","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 */\nexport function testChanges(): { host: { [CHANGED_FUNCTION]: HostHandler }; changed: (...tags: 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)) } satisfies ChangedAnswer\n },\n },\n changed: (...tags) => void source.bump(tags),\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 const handler = host[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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsc-kit/core",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "React Server Components as a Vite plugin. Routing, streaming, server actions, typed urls and build-time prerendering — deploy to Bun, Node, Workers, Vercel or Deno.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -276,7 +276,7 @@
276
276
  },
277
277
  "scripts": {
278
278
  "test": "bun test --isolate tests/js",
279
- "typecheck": "tsc --noEmit && tsc --noEmit -p tests/js/routeTypes.tsconfig.json",
279
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tests/js/routeTypes.tsconfig.json && tsc --noEmit -p tests/js/testHostTypes.tsconfig.json",
280
280
  "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/typegen.js dist/conformance.js",
281
281
  "prepack": "bun run build"
282
282
  },