@rsc-kit/core 0.22.9 → 0.23.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.d.ts +53 -0
- package/dist/testHost.js +76 -0
- package/dist/testHost.js.map +1 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +13 -0
- package/dist/testing.js.map +1 -1
- package/dist/typegen.js +0 -0
- package/dist/vite.d.ts +16 -0
- package/dist/vite.js +110 -35
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HostCallReply } from './hostCalls.js';
|
|
2
|
+
declare const REPLY: unique symbol;
|
|
3
|
+
/** The name the engine asks a host's route guards under. */
|
|
4
|
+
export declare const HOST_MIDDLEWARE = "__rsc.middleware";
|
|
5
|
+
/** What a test handler is given: the call's arguments, and the headers the renderer forwarded. */
|
|
6
|
+
export interface HostCallInput {
|
|
7
|
+
args: unknown[];
|
|
8
|
+
/** The visitor's request headers the renderer passed on - the cookie, the Accept-Language. */
|
|
9
|
+
headers: Headers;
|
|
10
|
+
}
|
|
11
|
+
/** Answers one function. Return a value for its result, or one of `hostReply`'s answers. */
|
|
12
|
+
export type HostHandler = (call: HostCallInput) => unknown;
|
|
13
|
+
/**
|
|
14
|
+
* The backend's functions, by the name rpc() calls them with.
|
|
15
|
+
*
|
|
16
|
+
* Route guards declared for the host are asked as `__rsc.middleware`
|
|
17
|
+
* (`HOST_MIDDLEWARE`), with the guard names as the first argument; answer
|
|
18
|
+
* `true` to let the page render. Anything else refuses, as a real host's
|
|
19
|
+
* answer would.
|
|
20
|
+
*/
|
|
21
|
+
export type TestHost = Record<string, HostHandler>;
|
|
22
|
+
type Reply = Omit<HostCallReply, 'result'> & {
|
|
23
|
+
[REPLY]: true;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The protocol's own answers, for a handler to return.
|
|
27
|
+
*
|
|
28
|
+
* host: {
|
|
29
|
+
* 'Apps.list': ({ headers }) =>
|
|
30
|
+
* headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),
|
|
31
|
+
* 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export declare const hostReply: {
|
|
35
|
+
/** No session: the page answers 401, an action its auth error. */
|
|
36
|
+
readonly unauthenticated: (message?: string) => Reply;
|
|
37
|
+
/** A session that still may not: 403. */
|
|
38
|
+
readonly unauthorized: (message?: string) => Reply;
|
|
39
|
+
/** Send the visitor somewhere else, as the host's redirect does. */
|
|
40
|
+
readonly redirect: (to: string, status?: number) => Reply;
|
|
41
|
+
/** Refuse with a status - a 404 for a record this caller cannot see, a 429. */
|
|
42
|
+
readonly refuse: (status: number, message?: string) => Reply;
|
|
43
|
+
/** The input was refused, by field: an action returns these as validationErrors. */
|
|
44
|
+
readonly invalid: (errors: Record<string, string[]>) => Reply;
|
|
45
|
+
/** A result, and the regions of the page the call says it changed. */
|
|
46
|
+
readonly revalidating: (result: unknown, ...targets: string[]) => Reply;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A fetch that answers the host-call endpoint from `host`: one call as
|
|
50
|
+
* itself, a batch as `{ replies }`, in order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function testHostFetch(host: TestHost): typeof fetch;
|
|
53
|
+
export {};
|
package/dist/testHost.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// A backend for tests: answers rpc() calls and host middleware the way a Go
|
|
2
|
+
// or Laravel host answers them over HTTP, without one running.
|
|
3
|
+
//
|
|
4
|
+
// The app under test talks to it through the real client, httpHostCalls,
|
|
5
|
+
// handed a fetch that answers in-process. So a test exercises what ships -
|
|
6
|
+
// batching, a redirect raised as the engine's own, an unauthenticated reply
|
|
7
|
+
// becoming a 401 - and only the backend is stood in for.
|
|
8
|
+
const REPLY = Symbol.for('rsc-kit.test-host-reply');
|
|
9
|
+
/** The name the engine asks a host's route guards under. */
|
|
10
|
+
export const HOST_MIDDLEWARE = '__rsc.middleware';
|
|
11
|
+
const reply = (fields) => ({ ...fields, [REPLY]: true });
|
|
12
|
+
/**
|
|
13
|
+
* The protocol's own answers, for a handler to return.
|
|
14
|
+
*
|
|
15
|
+
* host: {
|
|
16
|
+
* 'Apps.list': ({ headers }) =>
|
|
17
|
+
* headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),
|
|
18
|
+
* 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export const hostReply = {
|
|
22
|
+
/** No session: the page answers 401, an action its auth error. */
|
|
23
|
+
unauthenticated: (message) => reply({ unauthenticated: true, error: message }),
|
|
24
|
+
/** A session that still may not: 403. */
|
|
25
|
+
unauthorized: (message) => reply({ unauthorized: true, error: message }),
|
|
26
|
+
/** Send the visitor somewhere else, as the host's redirect does. */
|
|
27
|
+
redirect: (to, status = 307) => reply({ redirect: to, redirectStatus: status }),
|
|
28
|
+
/** Refuse with a status - a 404 for a record this caller cannot see, a 429. */
|
|
29
|
+
refuse: (status, message = 'Refused.') => reply({ error: message, refusalStatus: status }),
|
|
30
|
+
/** The input was refused, by field: an action returns these as validationErrors. */
|
|
31
|
+
invalid: (errors) => reply({ validationErrors: errors }),
|
|
32
|
+
/** A result, and the regions of the page the call says it changed. */
|
|
33
|
+
revalidating: (result, ...targets) => reply({ result, revalidate: targets }),
|
|
34
|
+
};
|
|
35
|
+
function isReply(value) {
|
|
36
|
+
return typeof value === 'object' && value !== null && value[REPLY] === true;
|
|
37
|
+
}
|
|
38
|
+
async function answer(host, name, args, headers) {
|
|
39
|
+
const handler = host[name];
|
|
40
|
+
// Fails the call rather than answering null: a function the test forgot
|
|
41
|
+
// is a test that would otherwise pass against nothing.
|
|
42
|
+
if (!handler) {
|
|
43
|
+
return {
|
|
44
|
+
error: name === HOST_MIDDLEWARE
|
|
45
|
+
? `the page asked its host guards (${JSON.stringify(args[0])}) and the test host has no "${HOST_MIDDLEWARE}" handler`
|
|
46
|
+
: `the test host has no handler for ${JSON.stringify(name)}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const value = await handler({ args, headers });
|
|
50
|
+
if (isReply(value)) {
|
|
51
|
+
const { [REPLY]: _, ...fields } = value;
|
|
52
|
+
return fields;
|
|
53
|
+
}
|
|
54
|
+
return { result: value === undefined ? null : value };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A fetch that answers the host-call endpoint from `host`: one call as
|
|
58
|
+
* itself, a batch as `{ replies }`, in order.
|
|
59
|
+
*/
|
|
60
|
+
export function testHostFetch(host) {
|
|
61
|
+
return (async (_input, init) => {
|
|
62
|
+
const headers = new Headers(init?.headers);
|
|
63
|
+
const body = JSON.parse(String(init?.body ?? '{}'));
|
|
64
|
+
// The renderer's own headers are not the visitor's.
|
|
65
|
+
headers.delete('x-rsc-host-secret');
|
|
66
|
+
headers.delete('content-type');
|
|
67
|
+
const json = (value) => new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } });
|
|
68
|
+
if ('calls' in body) {
|
|
69
|
+
return json({
|
|
70
|
+
replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return json(await answer(host, body.function, body.args ?? [], headers));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=testHost.js.map
|
|
@@ -0,0 +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'\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/** 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\n/**\n * The backend's functions, by the name rpc() calls them with.\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. Anything else refuses, as a real host's\n * answer would.\n */\nexport type TestHost = Record<string, HostHandler>\n\ntype Reply = Omit<HostCallReply, 'result'> & { [REPLY]: true }\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 /** 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"]}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type TestHost } from './testHost.js';
|
|
2
|
+
export { hostReply, HOST_MIDDLEWARE, type HostCallInput, type HostHandler, type TestHost } from './testHost.js';
|
|
1
3
|
export interface TestApp {
|
|
2
4
|
/** A path, not a url. The origin is whatever the app was told it is. */
|
|
3
5
|
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
@@ -18,6 +20,12 @@ export interface TestAppOptions {
|
|
|
18
20
|
build?: boolean;
|
|
19
21
|
/** The origin requests are made against. Nothing reads it; it is a url. */
|
|
20
22
|
origin?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The backend, answered in the test: rpc() calls and host guards by name.
|
|
25
|
+
* The app reaches it through the same client it uses against a real one,
|
|
26
|
+
* so nothing listens and RSC_BACKEND is not called. See `hostReply`.
|
|
27
|
+
*/
|
|
28
|
+
host?: TestHost;
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
23
31
|
* A file the build put in .output/public, answered the way Nitro's static
|
package/dist/testing.js
CHANGED
|
@@ -19,6 +19,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
20
20
|
import { extname, join, resolve, sep } from 'node:path';
|
|
21
21
|
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import { httpHostCalls } from './hostCalls.js';
|
|
23
|
+
import { testHostFetch } from './testHost.js';
|
|
24
|
+
export { hostReply, HOST_MIDDLEWARE } from './testHost.js';
|
|
22
25
|
/**
|
|
23
26
|
* Where the build put the server bundle.
|
|
24
27
|
*
|
|
@@ -169,10 +172,20 @@ export async function createTestApp(options = {}) {
|
|
|
169
172
|
if (typeof entry.default !== 'function') {
|
|
170
173
|
throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`);
|
|
171
174
|
}
|
|
175
|
+
const host = options.host
|
|
176
|
+
? httpHostCalls({ endpoint: 'http://test-host/__rsc/host-call', secret: 'test', fetch: testHostFetch(options.host) })
|
|
177
|
+
: null;
|
|
178
|
+
if (host && !entry.installHostFn) {
|
|
179
|
+
throw new Error(`[rsc-kit] ${bundle} cannot take a host. Rebuild against the current @rsc-kit/core.`);
|
|
180
|
+
}
|
|
172
181
|
return {
|
|
173
182
|
bundle,
|
|
174
183
|
fetch: async (path, init) => {
|
|
175
184
|
const request = new Request(new URL(path, origin), init);
|
|
185
|
+
// Per request, not once: the loaded bundle is shared by every app a
|
|
186
|
+
// run creates, and the host is this app's.
|
|
187
|
+
if (host)
|
|
188
|
+
entry.installHostFn(host);
|
|
176
189
|
return staticFile(root, request) ?? entry.default(request);
|
|
177
190
|
},
|
|
178
191
|
};
|
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'\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 /** 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\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 const bundle = await built.get(root)!\n const entry = (await import(pathToFileURL(bundle).href)) as {\n default: (request: Request) => Promise<Response>\n }\n\n if (typeof entry.default !== 'function') {\n throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`)\n }\n\n return {\n bundle,\n fetch: async (path, init) => {\n const request = new Request(new URL(path, origin), init)\n\n return staticFile(root, request) ?? entry.default(request)\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,sCAAsC;AACtC,oFAAoF;AACpF,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,+EAA+E;AAC/E,gBAAgB;AAChB,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,4EAA4E;AAC5E,gEAAgE;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAiB,MAAM,eAAe,CAAA;AAE5D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAuD,MAAM,eAAe,CAAA;AA+B/G;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,IAAI,EAAE,gDAAgD,CAAC;QAC5D,IAAI,CAAC,IAAI,EAAE,wBAAwB,CAAC;QACpC,IAAI,CAAC,IAAI,EAAE,yBAAyB,CAAC;KACtC,CAAA;IAED,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAA;AAC5D,CAAC;AAED,iFAAiF;AACjF,SAAS,MAAM,CAAC,GAAW;IACzB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAA;IAE9B,IAAI,MAAM,GAAG,CAAC,CAAA;IAEd,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAQ;QAEzE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAA;QAExE,IAAI,IAAI,GAAG,MAAM;YAAE,MAAM,GAAG,IAAI,CAAA;IAClC,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,mEAAmE;AACnE,MAAM,KAAK,GAAG,IAAI,GAAG,EAA2B,CAAA;AAEhD,sFAAsF;AACtF,MAAM,aAAa,GAA2B;IAC5C,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,gCAAgC;IACxC,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,iCAAiC;IAC1C,cAAc,EAAE,2BAA2B;IAC3C,MAAM,EAAE,iBAAiB;IACzB,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,cAAc;IACtB,OAAO,EAAE,WAAW;IACpB,QAAQ,EAAE,YAAY;IACtB,OAAO,EAAE,kBAAkB;IAC3B,MAAM,EAAE,kBAAkB;CAC3B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,uCAAuC;AACvC,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgB;IACvD,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAEtE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAA;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,QAAQ,CAAC,CAAA;IAE/C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE;QAAE,OAAO,IAAI,CAAA;IAEnG,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,0BAA0B,CAAA;IACrF,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IAElE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE;KAC3F,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,uCAAuC;AACvC,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,KAAK,QAAQ,CAAA;IAEtD,IAAI,OAAO,GAA2B,EAAE,CAAA;IAExC,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAA;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;IAC7D,CAAC;IAED,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AACjF,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,KAAc;IACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAEjC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,4CAA4C,IAAI,8EAA8E,CAC/H,CAAA;QACH,CAAC;QAED,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAEhF,IAAI,KAAK;QAAE,OAAO,QAAQ,CAAA;IAE1B,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACnC,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;QACjC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE;KAChD,CAAC,CAAA;IAEF,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,6DAA6D,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5F,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAE/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,qEAAqE,IAAI,+BAA+B,CACzG,CAAA;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAO,GAAmB,EAAE;IAC9D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACnD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,kBAAkB,CAAA;IAEnD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAA;IAE5E,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAE,CAAA;IACrC,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAGtD,CAAA;IAED,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,aAAa,MAAM,qCAAqC,CAAC,CAAA;IAC3E,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI;QACvB,CAAC,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,kCAAkC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrH,CAAC,CAAC,IAAI,CAAA;IAER,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,aAAa,MAAM,iEAAiE,CAAC,CAAA;IACvG,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;YAC1B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;YAExD,oEAAoE;YACpE,2CAA2C;YAC3C,IAAI,IAAI;gBAAE,KAAK,CAAC,aAAc,CAAC,IAAI,CAAC,CAAA;YAEpC,OAAO,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAC5D,CAAC;KACF,CAAA;AACH,CAAC","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 { testHostFetch, type TestHost } from './testHost.js'\n\nexport { hostReply, 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 /** 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\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 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 }\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 return {\n bundle,\n 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\n return staticFile(root, request) ?? entry.default(request)\n },\n }\n}\n"]}
|
package/dist/typegen.js
CHANGED
|
File without changes
|
package/dist/vite.d.ts
CHANGED
|
@@ -273,6 +273,22 @@ export interface RscKitOptions {
|
|
|
273
273
|
* A host whose functions are already JavaScript passes nothing.
|
|
274
274
|
*/
|
|
275
275
|
hostActions?: Record<string, string>;
|
|
276
|
+
/**
|
|
277
|
+
* The backend's command that writes its manifest, run as dev or a build
|
|
278
|
+
* starts so the manifest cannot go stale:
|
|
279
|
+
*
|
|
280
|
+
* rscKit({ hostManifest: { command: ['go', 'run', './cmd/api', 'rsc-manifest'] } })
|
|
281
|
+
* rscKit({ hostManifest: { command: ['php', 'artisan', 'rsc:host-manifest'] } })
|
|
282
|
+
*
|
|
283
|
+
* It writes rsc-host.json: the actions the build makes stubs for, and the
|
|
284
|
+
* names rpc() may be called with. Run once per process, from `cwd`
|
|
285
|
+
* (default: the project root). A failure fails a build; under dev it is
|
|
286
|
+
* reported and the manifest already on disk is used.
|
|
287
|
+
*/
|
|
288
|
+
hostManifest?: {
|
|
289
|
+
command: string[];
|
|
290
|
+
cwd?: string;
|
|
291
|
+
};
|
|
276
292
|
}
|
|
277
293
|
/**
|
|
278
294
|
* The service worker, written into the client output at build time.
|
package/dist/vite.js
CHANGED
|
@@ -199,6 +199,16 @@ let foundAssets = {
|
|
|
199
199
|
};
|
|
200
200
|
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
201
201
|
let hostActions;
|
|
202
|
+
/** What rpc() may be called with, from rsc-host-functions.json; null when the backend lists none. */
|
|
203
|
+
let hostFunctions = null;
|
|
204
|
+
/**
|
|
205
|
+
* The manifest commands run in this process, by project and command.
|
|
206
|
+
*
|
|
207
|
+
* Once, not on every config load: a dev server reloads its config when the
|
|
208
|
+
* manifest changes, and running the command again there rewrote the file it
|
|
209
|
+
* had just restarted for - a restart that never ended.
|
|
210
|
+
*/
|
|
211
|
+
const hostManifestRan = new Set();
|
|
202
212
|
/**
|
|
203
213
|
* This file's directory.
|
|
204
214
|
*
|
|
@@ -230,47 +240,62 @@ function envRouteConfig() {
|
|
|
230
240
|
return { file, dynamicPattern: new RegExp(pattern) };
|
|
231
241
|
}
|
|
232
242
|
/** The file a backend writes its action names into. */
|
|
233
|
-
const
|
|
243
|
+
const HOST_FILE = "rsc-host.json";
|
|
234
244
|
/**
|
|
235
|
-
*
|
|
245
|
+
* What the backend offers the app, read from a file it wrote:
|
|
246
|
+
*
|
|
247
|
+
* {
|
|
248
|
+
* "actions": { "ordersCreate": "Orders.create" },
|
|
249
|
+
* "functions": ["Orders.create", "Orders.recent"]
|
|
250
|
+
* }
|
|
251
|
+
*
|
|
252
|
+
* `actions` are the server actions the build writes "use server" stubs for,
|
|
253
|
+
* by JavaScript name. `functions` are the names rpc() may be called with,
|
|
254
|
+
* which the build turns into a type so a misspelt one fails the typecheck.
|
|
236
255
|
*
|
|
237
256
|
* A file rather than an environment variable, because the backend no longer
|
|
238
|
-
* drives the build
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
257
|
+
* drives the build - `vite build` does. Discovery has to stay where the code
|
|
258
|
+
* is (reflection through Composer's autoloader finds what a class inherits; a
|
|
259
|
+
* regex would silently miss every inherited action), but the handoff is just
|
|
260
|
+
* names, and a JSON file is something any language can write:
|
|
242
261
|
*
|
|
243
|
-
* php artisan rsc:
|
|
244
|
-
* go run ./
|
|
262
|
+
* php artisan rsc:host-manifest
|
|
263
|
+
* go run ./backend -manifest rsc-host.json
|
|
245
264
|
*
|
|
246
|
-
* Absent is not an error
|
|
247
|
-
*
|
|
265
|
+
* Absent is not an error: a backend-less app has no file. `hostManifest`
|
|
266
|
+
* runs the command as dev or a build starts.
|
|
248
267
|
*/
|
|
249
|
-
function
|
|
250
|
-
const path = join(root,
|
|
251
|
-
if (!existsSync(path))
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// An empty list is an empty map. PHP's json_encode writes an empty
|
|
256
|
-
// array as [] whatever it was declared as, so a backend with no
|
|
257
|
-
// actions yet wrote exactly that - and the first `dev` of a fresh
|
|
258
|
-
// install refused to start over a file that said, correctly, "none".
|
|
259
|
-
if (Array.isArray(parsed) && parsed.length === 0)
|
|
260
|
-
return {};
|
|
261
|
-
if (parsed === null ||
|
|
262
|
-
typeof parsed !== "object" ||
|
|
263
|
-
Array.isArray(parsed)) {
|
|
264
|
-
throw new Error('expected an object of { jsName: "Class.method" }');
|
|
268
|
+
function fileHostManifest(root) {
|
|
269
|
+
const path = join(root, HOST_FILE);
|
|
270
|
+
if (!existsSync(path)) {
|
|
271
|
+
if (existsSync(join(root, "rsc-host-actions.json"))) {
|
|
272
|
+
throw new Error("[rsc-kit] rsc-host-actions.json is the old manifest. The backend now writes " +
|
|
273
|
+
HOST_FILE + " - { actions, functions } - so update the adapter and run its manifest command.");
|
|
265
274
|
}
|
|
266
|
-
return
|
|
275
|
+
return { actions: {}, functions: null };
|
|
276
|
+
}
|
|
277
|
+
let parsed;
|
|
278
|
+
try {
|
|
279
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
267
280
|
}
|
|
268
281
|
catch (error) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
throw new Error(`[rsc-kit] Could not read ${HOST_FILE}: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
// An empty list is an empty map: PHP's json_encode writes an empty array
|
|
285
|
+
// as [] whatever it was declared as.
|
|
286
|
+
const actions = Array.isArray(parsed?.actions) && parsed.actions.length === 0 ? {} : parsed?.actions ?? {};
|
|
287
|
+
if (typeof actions !== "object" || actions === null || Array.isArray(actions)) {
|
|
288
|
+
throw new Error(`[rsc-kit] ${HOST_FILE}: "actions" must be an object of { jsName: "Function.name" }.`);
|
|
289
|
+
}
|
|
290
|
+
const functions = parsed?.functions;
|
|
291
|
+
if (functions !== undefined && (!Array.isArray(functions) || functions.some((n) => typeof n !== "string"))) {
|
|
292
|
+
throw new Error(`[rsc-kit] ${HOST_FILE}: "functions" must be an array of names.`);
|
|
273
293
|
}
|
|
294
|
+
return {
|
|
295
|
+
actions: actions,
|
|
296
|
+
// The reserved guard call is the engine's to make, not the app's.
|
|
297
|
+
functions: functions ? functions.filter((n) => !n.startsWith("__rsc.")).sort() : null,
|
|
298
|
+
};
|
|
274
299
|
}
|
|
275
300
|
/**
|
|
276
301
|
* The alias that lets a vendored copy of this package be imported by name.
|
|
@@ -402,7 +427,41 @@ function resolvePaths(options) {
|
|
|
402
427
|
screenshots: foundAssets.screenshots.map((shot) => shot.href),
|
|
403
428
|
};
|
|
404
429
|
}
|
|
405
|
-
|
|
430
|
+
loadHostManifest(options);
|
|
431
|
+
}
|
|
432
|
+
function loadHostManifest(options) {
|
|
433
|
+
const manifest = fileHostManifest(projectRoot);
|
|
434
|
+
hostActions = options.hostActions ?? manifest.actions;
|
|
435
|
+
hostFunctions = manifest.functions;
|
|
436
|
+
}
|
|
437
|
+
/** Run the backend's manifest command, once per process; see the option. */
|
|
438
|
+
function runHostManifest(options, isBuild) {
|
|
439
|
+
const spec = options.hostManifest;
|
|
440
|
+
if (!spec)
|
|
441
|
+
return;
|
|
442
|
+
const ran = projectRoot + "\0" + spec.command.join("\0");
|
|
443
|
+
if (hostManifestRan.has(ran))
|
|
444
|
+
return;
|
|
445
|
+
hostManifestRan.add(ran);
|
|
446
|
+
const [command, ...args] = spec.command;
|
|
447
|
+
if (!command)
|
|
448
|
+
throw new Error("[rsc-kit] hostManifest.command is empty.");
|
|
449
|
+
const run = spawnSync(command, args, {
|
|
450
|
+
cwd: spec.cwd ? resolve(projectRoot, spec.cwd) : projectRoot,
|
|
451
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
452
|
+
encoding: "utf-8",
|
|
453
|
+
});
|
|
454
|
+
if (run.status === 0) {
|
|
455
|
+
loadHostManifest(options);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const message = `the backend's manifest command failed (${spec.command.join(" ")}):\n` +
|
|
459
|
+
(run.error?.message ?? run.stderr ?? "").trim();
|
|
460
|
+
// A build from a stale manifest ships stubs for functions the backend no
|
|
461
|
+
// longer has; dev carries on with the one on disk, and says so.
|
|
462
|
+
if (isBuild)
|
|
463
|
+
throw new Error(`[rsc-kit] ${message}`);
|
|
464
|
+
log(`${message}\n Using the ${HOST_FILE} already on disk.`);
|
|
406
465
|
}
|
|
407
466
|
function log(...args) {
|
|
408
467
|
console.error("[rsc-kit]", ...args);
|
|
@@ -1994,9 +2053,19 @@ function renderHostGlobalTypes() {
|
|
|
1994
2053
|
"// Deliberately not a module — no import/export — so the declaration is",
|
|
1995
2054
|
"// global to the project without every file having to reference it.",
|
|
1996
2055
|
"",
|
|
2056
|
+
// The names the backend listed, so a misspelt one fails the typecheck
|
|
2057
|
+
// rather than the render. Without a list, any string.
|
|
2058
|
+
...(hostFunctions?.length
|
|
2059
|
+
? [
|
|
2060
|
+
"// From " + HOST_FILE + ", which the backend writes.",
|
|
2061
|
+
"type RscHostFunction =",
|
|
2062
|
+
...hostFunctions.map((name, i) => " | " + JSON.stringify(name) + (i === hostFunctions.length - 1 ? ";" : "")),
|
|
2063
|
+
"",
|
|
2064
|
+
]
|
|
2065
|
+
: []),
|
|
1997
2066
|
"declare function " +
|
|
1998
2067
|
hostGlobal +
|
|
1999
|
-
"<T = unknown>(name: string, ...args: unknown[]): Promise<T>;",
|
|
2068
|
+
"<T = unknown>(name: " + (hostFunctions?.length ? "RscHostFunction" : "string") + ", ...args: unknown[]): Promise<T>;",
|
|
2000
2069
|
"",
|
|
2001
2070
|
].join("\n");
|
|
2002
2071
|
}
|
|
@@ -2461,6 +2530,11 @@ function installHostCallsOnce(): void {
|
|
|
2461
2530
|
if (hostInstalled) return
|
|
2462
2531
|
hostInstalled = true
|
|
2463
2532
|
|
|
2533
|
+
// One installed already - a test's, through createTestApp({ host }) - is
|
|
2534
|
+
// the host. Replacing it with the backend in .env would send the test's
|
|
2535
|
+
// calls to a server that may not be running.
|
|
2536
|
+
if (currentHost) return
|
|
2537
|
+
|
|
2464
2538
|
const origin = process.env.RSC_BACKEND ?? process.env.APP_URL
|
|
2465
2539
|
const secret = process.env.RSC_HOST_CALL_SECRET
|
|
2466
2540
|
|
|
@@ -6224,6 +6298,7 @@ export function rscKit(options = {}) {
|
|
|
6224
6298
|
},
|
|
6225
6299
|
},
|
|
6226
6300
|
config(_config, env) {
|
|
6301
|
+
runHostManifest(options, env.command === "build");
|
|
6227
6302
|
if (!existsSync(appDir)) {
|
|
6228
6303
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
6229
6304
|
}
|
|
@@ -6674,11 +6749,11 @@ export function rscKit(options = {}) {
|
|
|
6674
6749
|
// dev server - rewrites the file, and until the server started again
|
|
6675
6750
|
// the stub was not there to import. Any event: the file appears on a
|
|
6676
6751
|
// first action, changes on the next, and goes when the last is removed.
|
|
6677
|
-
const hostActionsPath = join(projectRoot,
|
|
6752
|
+
const hostActionsPath = join(projectRoot, HOST_FILE);
|
|
6678
6753
|
const hostActionsChanged = (file) => {
|
|
6679
6754
|
if (file !== hostActionsPath)
|
|
6680
6755
|
return;
|
|
6681
|
-
server.config.logger.info(`[rsc-kit] ${
|
|
6756
|
+
server.config.logger.info(`[rsc-kit] ${relative(projectRoot, file)} changed — restarting`);
|
|
6682
6757
|
void server.restart();
|
|
6683
6758
|
};
|
|
6684
6759
|
server.watcher.add(hostActionsPath);
|