@rsc-kit/core 0.24.1 → 0.24.3
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/README.md +69 -62
- package/dist/conformance.d.ts +68 -0
- package/dist/conformance.js +309 -0
- package/dist/conformance.js.map +1 -0
- package/dist/hostCalls.d.ts +10 -3
- package/dist/hostCalls.js +19 -0
- package/dist/hostCalls.js.map +1 -1
- package/dist/testHost.d.ts +39 -6
- package/dist/testHost.js +2 -0
- package/dist/testHost.js.map +1 -1
- package/dist/typegen.d.ts +10 -1
- package/dist/typegen.js +44 -7
- package/dist/typegen.js.map +1 -1
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +47 -2
- package/dist/vite.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
React Server Components as a Vite plugin: it discovers the route tree, renders it, and serves it. Nitro builds the server around it, so there is no server file to write.
|
|
4
4
|
|
|
5
|
-
This is the library. Most people
|
|
5
|
+
This is the library. Most people start with `bun create rsc-kit@latest my-app` (a new app) or `bunx rsc-kit@latest init` (an existing project).
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
bun create rsc-kit@latest my-app
|
|
@@ -10,90 +10,97 @@ bun create rsc-kit@latest my-app
|
|
|
10
10
|
|
|
11
11
|
## Why not just use Next?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
own
|
|
13
|
+
The routing conventions here *are* Next's — `app/`, `page`/`layout`/`loading`,
|
|
14
|
+
`@slot`, `(.)` interception, `"use client"`, server actions,
|
|
15
|
+
`generateMetadata` — so there is no translation table to learn. The difference
|
|
16
|
+
is underneath:
|
|
17
|
+
|
|
18
|
+
- **It is a Vite plugin, not a bundler.** You keep your `vite.config.ts` and
|
|
19
|
+
your plugins. Adding RSC is one entry in the plugins array.
|
|
20
|
+
- **It deploys wherever Nitro does.** [Nitro](https://nitro.build) builds the
|
|
21
|
+
server around the route tree, so the target is a preset — Bun, Node,
|
|
22
|
+
Cloudflare Workers, Vercel, Netlify, Deno.
|
|
23
|
+
- **A page ships no JavaScript until something on it needs some.** No client
|
|
24
|
+
component and no server action in its tree means stored HTML, with no React
|
|
25
|
+
in the browser ([guide](https://docs.rsc-kit.dev/guides/no-javascript)).
|
|
26
|
+
- **It can sit in front of a backend you already have.** With Laravel or Go
|
|
27
|
+
behind it, a server component calls the backend with
|
|
28
|
+
`await rpc('Orders.recent')`, typed from the backend's own signatures. See
|
|
29
|
+
[Backend-Answered Pages](https://docs.rsc-kit.dev/hosts/backend-answered-pages).
|
|
30
|
+
- **It compiles to a single binary** with `bun build --compile`, assets and
|
|
31
|
+
frozen pages inside it.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
29
34
|
|
|
30
35
|
```ts
|
|
31
|
-
|
|
36
|
+
// vite.config.ts
|
|
37
|
+
import { defineConfig } from 'vite'
|
|
38
|
+
import { nitro } from 'nitro/vite'
|
|
39
|
+
import react from '@vitejs/plugin-react'
|
|
40
|
+
import { rscKit } from '@rsc-kit/core/vite'
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
plugins: [nitro({ preset: 'bun', serveStatic: 'inline' }), rscKit(), react()],
|
|
44
|
+
})
|
|
32
45
|
```
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
70 kB gzipped back on a page with nothing to hydrate, of which react-dom alone
|
|
37
|
-
is ~54 kB. The build refuses the combination rather than shipping a button that
|
|
38
|
-
does nothing.
|
|
39
|
-
|
|
40
|
-
**And the framework itself is small, because most of what ships is React.**
|
|
41
|
-
Measured on the example app: a typical route is 85 kB gzipped, of which
|
|
42
|
-
**66 kB is React and react-dom** and this framework's own runtime is 6 kB.
|
|
43
|
-
Client components are chunked per module, so a route only downloads the ones it
|
|
44
|
-
renders — the three routes using TanStack Query are the only ones that pay for
|
|
45
|
-
it. Serving a page frozen at build time costs the server about 25 µs, because
|
|
46
|
-
it renders nothing.
|
|
47
|
+
React 19.2+ and Vite 8 are peer dependencies. The full walk-through is
|
|
48
|
+
[Installation](https://docs.rsc-kit.dev/installation).
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
With a backend, `rscKit({ hostManifest: { command, cwd, watch } })` runs the
|
|
51
|
+
backend's manifest command — `php artisan rsc:host-manifest`, or a Go program
|
|
52
|
+
calling `reg.WriteManifest` — when dev or a build starts, and again under dev
|
|
53
|
+
when something in a `watch` directory changes. It writes `rsc-host.json`
|
|
54
|
+
(`{ actions, functions, types, defs }`), from which the build generates typed
|
|
55
|
+
`rpc()` overloads and server action stubs.
|
|
50
56
|
|
|
51
57
|
## What you get
|
|
52
58
|
|
|
53
|
-
- File-based routing with layouts
|
|
54
|
-
route interception
|
|
59
|
+
- File-based routing with layouts (which receive `params`), loading
|
|
60
|
+
boundaries, parallel routes and route interception
|
|
55
61
|
- Streaming SSR with Suspense, and partial prerendering — the static shell is
|
|
56
62
|
stored, the part that needs the request is rendered per visit
|
|
57
|
-
- Server actions, with an optional builder
|
|
58
|
-
(any Standard Schema library — Zod, Valibot, ArkType) and
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
- Server actions, with an optional builder (`@rsc-kit/core/action`) for
|
|
64
|
+
validation (any Standard Schema library — Zod, Valibot, ArkType) and
|
|
65
|
+
middleware; `<Form>` with field errors and `formError`, and `useAction` for
|
|
66
|
+
calls without a form
|
|
67
|
+
- API routes in `app/**/route.ts` — a `Request` in, a `Response` out, frozen
|
|
68
|
+
at build time when they read nothing from the request
|
|
61
69
|
- Typed urls: `params`, `searchParams` and request bodies arrive parsed through
|
|
62
70
|
a schema you export beside the route
|
|
63
71
|
- Typed routes: every build writes the routes it found, so a link to a page
|
|
64
72
|
that does not exist stops compiling
|
|
65
|
-
- `middleware.ts` that runs before every render below it
|
|
73
|
+
- `middleware.ts` that runs before every render below it; `notFound()` there
|
|
74
|
+
answers 404
|
|
66
75
|
- Request and response access — `headers()`, `cookies()`, `responseHeaders()`
|
|
67
76
|
- Offline and installable: a generated service worker, a web manifest, icons
|
|
68
77
|
found by name, and a place to put your own push handlers
|
|
78
|
+
- Testing without a browser: `createTestApp({ host, backend })` from
|
|
79
|
+
`@rsc-kit/core/testing` fetches any url, with `hostReply` to fake backend
|
|
80
|
+
answers ([guide](https://docs.rsc-kit.dev/guides/testing))
|
|
69
81
|
- A build that explains itself — why each route is dynamic, what it ships, and
|
|
70
|
-
a report
|
|
71
|
-
|
|
72
|
-
## Status
|
|
73
|
-
|
|
74
|
-
**0.x. Early, and honest about it.** The API will change before 1.0, and
|
|
75
|
-
`@vitejs/plugin-rsc` — which this builds on — is itself pre-1.0.
|
|
82
|
+
a `build-report.json` that [`@rsc-kit/mcp`](https://www.npmjs.com/package/@rsc-kit/mcp)
|
|
83
|
+
answers questions from
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
it has had an adversarial security review, and it runs a real documentation
|
|
79
|
-
site in production. It has not been used by anyone but its author. Issues and
|
|
80
|
-
questions are welcome; promises are not being made yet.
|
|
85
|
+
## Commands
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
- `rsc-kit-typegen` writes the generated types without starting Vite;
|
|
88
|
+
`--check` exits non-zero when the committed `rsc-host.json` or types are
|
|
89
|
+
stale ([typed routes](https://docs.rsc-kit.dev/guides/typed-routes)).
|
|
90
|
+
- `rsc-kit-conformance` checks a backend adapter against the host-call
|
|
91
|
+
contract ([your own backend](https://docs.rsc-kit.dev/hosts/your-own-backend)):
|
|
84
92
|
|
|
85
|
-
|
|
93
|
+
```sh
|
|
94
|
+
npx -y -p @rsc-kit/core rsc-kit-conformance \
|
|
95
|
+
--endpoint http://127.0.0.1:8123/__rsc/host-call --secret test --manifest rsc-host.json
|
|
96
|
+
```
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
bun create rsc-kit@latest my-app # a new app
|
|
89
|
-
cd my-app && bun run dev
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Adding it to a project you already have:
|
|
98
|
+
## Status
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
**0.x.** The API will change before 1.0, and `@vitejs/plugin-rsc` — which
|
|
101
|
+
this builds on — is itself pre-1.0. It has had an adversarial security review
|
|
102
|
+
and runs a real documentation site in production. See
|
|
103
|
+
[SECURITY.md](./SECURITY.md) for the threat model and how to report something.
|
|
97
104
|
|
|
98
105
|
Docs: https://docs.rsc-kit.dev
|
|
99
106
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
type Schema = Record<string, unknown>;
|
|
3
|
+
/** A host-call client: the real one in a run, the test host's in core's own test. */
|
|
4
|
+
export type ConformanceCall = (name: string, ...args: unknown[]) => Promise<unknown>;
|
|
5
|
+
export interface ConformanceOptions {
|
|
6
|
+
/** The adapter's host-call endpoint, e.g. http://127.0.0.1:8080/__rsc/host-call. */
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/** The secret the adapter checks. */
|
|
9
|
+
secret: string;
|
|
10
|
+
/** The adapter's rsc-host.json, parsed or as a path: its types are checked against what arrives. */
|
|
11
|
+
manifest?: string | {
|
|
12
|
+
functions?: string[];
|
|
13
|
+
types?: Record<string, {
|
|
14
|
+
result?: Schema;
|
|
15
|
+
}>;
|
|
16
|
+
defs?: Record<string, Schema>;
|
|
17
|
+
};
|
|
18
|
+
/** For a test: the fetch the client uses. */
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
export interface ConformanceResult {
|
|
22
|
+
case: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
/** What went wrong, when it did. */
|
|
25
|
+
detail?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What the adapter registers, and what each must do. Names are fixed; how
|
|
29
|
+
* they are written is the adapter's - the point is that they are written the
|
|
30
|
+
* way an app's would be, with the adapter's ordinary API.
|
|
31
|
+
*/
|
|
32
|
+
export declare const CASES: {
|
|
33
|
+
readonly 'Conformance.echo': 'Return its one argument unchanged: any JSON value, nested.';
|
|
34
|
+
readonly 'Conformance.emptyList': 'Return an empty list of objects, the way the language makes "no rows" (Go: a nil slice).';
|
|
35
|
+
readonly 'Conformance.time': "Return the instant 2026-01-02T03:04:05Z as the language's own time value.";
|
|
36
|
+
readonly 'Conformance.noTime': "Return an absent time: a nil pointer, a null Carbon, typed as a time or nothing.";
|
|
37
|
+
readonly 'Conformance.unauthenticated': 'Refuse as not signed in, the way the framework does.';
|
|
38
|
+
readonly 'Conformance.unauthorized': 'Refuse as signed in but not allowed.';
|
|
39
|
+
readonly 'Conformance.notFound': "Refuse as not found (404), the way the framework does: Go's Refuse(404), Laravel's abort(404).";
|
|
40
|
+
readonly 'Conformance.refuse': 'Refuse with status 429 and the message "Slow down."';
|
|
41
|
+
readonly 'Conformance.invalid': 'Refuse the input: a validation error on the field "name".';
|
|
42
|
+
readonly 'Conformance.redirect': 'Send the visitor to /login.';
|
|
43
|
+
readonly 'Conformance.revalidate': 'Mark the region "orders" stale, and return "ok".';
|
|
44
|
+
readonly 'Conformance.fail': 'Fail unexpectedly: an ordinary error, not a refusal.';
|
|
45
|
+
readonly 'Conformance.authorization': "Return the request's Authorization header, as the renderer forwarded it.";
|
|
46
|
+
readonly '__rsc.middleware': 'Guards: "conformance-allow" passes; "conformance-deny" refuses; a name with no guard refuses.';
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Run the suite against a client. Every case runs; none stops another.
|
|
50
|
+
*
|
|
51
|
+
* `revalidated` is where the client records what each call marked stale -
|
|
52
|
+
* its onRevalidate - so the revalidation case can read it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function conformance(call: ConformanceCall, options: {
|
|
55
|
+
revalidated: string[][];
|
|
56
|
+
manifest?: ConformanceOptions['manifest'];
|
|
57
|
+
wrongSecret?: ConformanceCall;
|
|
58
|
+
}): Promise<ConformanceResult[]>;
|
|
59
|
+
/** Run the suite against an adapter's endpoint. */
|
|
60
|
+
export declare function runConformance(options: ConformanceOptions): Promise<ConformanceResult[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Whether a value fits the JSON Schema subset the manifest uses. Null when it
|
|
63
|
+
* does; otherwise where and why it does not.
|
|
64
|
+
*/
|
|
65
|
+
export declare function conformsTo(value: unknown, schema: Schema, defs: Record<string, Schema>, at: string): string | null;
|
|
66
|
+
/** Print a result table, and say whether the adapter conforms. */
|
|
67
|
+
export declare function report(results: ConformanceResult[]): boolean;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The contract every backend adapter keeps, as a suite it runs against itself.
|
|
3
|
+
//
|
|
4
|
+
// A backend answers rpc(), actions and guards over one endpoint, and every
|
|
5
|
+
// difference between how it and the engine read that wire is a bug in an
|
|
6
|
+
// app that nobody wrote: a nil slice arriving as null where the type said a
|
|
7
|
+
// list, a timestamp the type called unknown, a refusal that did not come out
|
|
8
|
+
// as the error a page checks for. Each was found by an app, one at a time.
|
|
9
|
+
// This finds them before an adapter ships.
|
|
10
|
+
//
|
|
11
|
+
// The adapter registers the Conformance.* functions described in CASES, in
|
|
12
|
+
// its own language, the way an app would write them; this calls them through
|
|
13
|
+
// the real client and checks what arrives. Where the adapter's manifest types
|
|
14
|
+
// a function, every value it sends is checked against that type too.
|
|
15
|
+
//
|
|
16
|
+
// rsc-kit-conformance --endpoint http://127.0.0.1:8080/__rsc/host-call \
|
|
17
|
+
// --secret test-secret --manifest rsc-host.json
|
|
18
|
+
//
|
|
19
|
+
// The test host's hostReply passes the same suite (conformance.test.ts), so
|
|
20
|
+
// a page test faking the backend fakes what a real adapter does.
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { argv, exit, stderr, stdout } from 'node:process';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
24
|
+
import { httpHostCalls } from './hostCalls.js';
|
|
25
|
+
import { isNotFoundSignal } from './notFound.js';
|
|
26
|
+
import { withRedirect } from './redirect.js';
|
|
27
|
+
import { withRequest } from './request.js';
|
|
28
|
+
import { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js';
|
|
29
|
+
/**
|
|
30
|
+
* What the adapter registers, and what each must do. Names are fixed; how
|
|
31
|
+
* they are written is the adapter's - the point is that they are written the
|
|
32
|
+
* way an app's would be, with the adapter's ordinary API.
|
|
33
|
+
*/
|
|
34
|
+
export const CASES = {
|
|
35
|
+
'Conformance.echo': 'Return its one argument unchanged: any JSON value, nested.',
|
|
36
|
+
'Conformance.emptyList': 'Return an empty list of objects, the way the language makes "no rows" (Go: a nil slice).',
|
|
37
|
+
'Conformance.time': "Return the instant 2026-01-02T03:04:05Z as the language's own time value.",
|
|
38
|
+
'Conformance.noTime': "Return an absent time: a nil pointer, a null Carbon, typed as a time or nothing.",
|
|
39
|
+
'Conformance.unauthenticated': 'Refuse as not signed in, the way the framework does.',
|
|
40
|
+
'Conformance.unauthorized': 'Refuse as signed in but not allowed.',
|
|
41
|
+
'Conformance.notFound': "Refuse as not found (404), the way the framework does: Go's Refuse(404), Laravel's abort(404).",
|
|
42
|
+
'Conformance.refuse': 'Refuse with status 429 and the message "Slow down."',
|
|
43
|
+
'Conformance.invalid': 'Refuse the input: a validation error on the field "name".',
|
|
44
|
+
'Conformance.redirect': 'Send the visitor to /login.',
|
|
45
|
+
'Conformance.revalidate': 'Mark the region "orders" stale, and return "ok".',
|
|
46
|
+
'Conformance.fail': 'Fail unexpectedly: an ordinary error, not a refusal.',
|
|
47
|
+
'Conformance.authorization': "Return the request's Authorization header, as the renderer forwarded it.",
|
|
48
|
+
'__rsc.middleware': 'Guards: "conformance-allow" passes; "conformance-deny" refuses; a name with no guard refuses.',
|
|
49
|
+
};
|
|
50
|
+
const INSTANT = Date.parse('2026-01-02T03:04:05Z');
|
|
51
|
+
const ECHOES = [null, 0, -1.5, 'héllo ✓ "quoted"', [1, 'a', null], { a: { b: [true, false] }, c: '' }];
|
|
52
|
+
/**
|
|
53
|
+
* Run the suite against a client. Every case runs; none stops another.
|
|
54
|
+
*
|
|
55
|
+
* `revalidated` is where the client records what each call marked stale -
|
|
56
|
+
* its onRevalidate - so the revalidation case can read it.
|
|
57
|
+
*/
|
|
58
|
+
export async function conformance(call, options) {
|
|
59
|
+
const { revalidated, manifest, wrongSecret } = options;
|
|
60
|
+
const results = [];
|
|
61
|
+
const types = typeof manifest === 'string' ? JSON.parse(readFileSync(manifest, 'utf-8')) : manifest;
|
|
62
|
+
const check = async (name, run) => {
|
|
63
|
+
try {
|
|
64
|
+
await run();
|
|
65
|
+
results.push({ case: name, ok: true });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
results.push({ case: name, ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const expect = (ok, detail) => {
|
|
72
|
+
if (!ok)
|
|
73
|
+
throw new Error(detail);
|
|
74
|
+
};
|
|
75
|
+
const rejection = async (run) => {
|
|
76
|
+
try {
|
|
77
|
+
const value = await run();
|
|
78
|
+
throw new Error('answered ' + JSON.stringify(value) + ' instead of refusing');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** The value against the type the adapter's manifest declares, when it declares one. */
|
|
85
|
+
const typed = (name, value) => {
|
|
86
|
+
const schema = types?.types?.[name]?.result;
|
|
87
|
+
if (!schema)
|
|
88
|
+
return;
|
|
89
|
+
const problem = conformsTo(value, schema, types?.defs ?? {}, 'result');
|
|
90
|
+
expect(problem === null, `the manifest types ${name}'s result, and what arrived does not fit it: ${problem}`);
|
|
91
|
+
};
|
|
92
|
+
await check('the manifest lists every Conformance function', async () => {
|
|
93
|
+
if (!types)
|
|
94
|
+
return;
|
|
95
|
+
const missing = Object.keys(CASES).filter((n) => !n.startsWith('__') && !types.functions?.includes(n));
|
|
96
|
+
expect(missing.length === 0, 'not in functions: ' + missing.join(', '));
|
|
97
|
+
});
|
|
98
|
+
await check('a value of any JSON shape comes back unchanged', async () => {
|
|
99
|
+
for (const value of ECHOES) {
|
|
100
|
+
const back = await call('Conformance.echo', value);
|
|
101
|
+
expect(JSON.stringify(back) === JSON.stringify(value), `sent ${JSON.stringify(value)}, got ${JSON.stringify(back)}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
await check('"no rows" is an empty list, never null', async () => {
|
|
105
|
+
const value = await call('Conformance.emptyList');
|
|
106
|
+
expect(Array.isArray(value) && value.length === 0, 'got ' + JSON.stringify(value));
|
|
107
|
+
typed('Conformance.emptyList', value);
|
|
108
|
+
});
|
|
109
|
+
await check('a time is an ISO 8601 string of the same instant', async () => {
|
|
110
|
+
const value = await call('Conformance.time');
|
|
111
|
+
expect(typeof value === 'string' && Date.parse(value) === INSTANT, 'got ' + JSON.stringify(value));
|
|
112
|
+
typed('Conformance.time', value);
|
|
113
|
+
});
|
|
114
|
+
await check('an absent time is null', async () => {
|
|
115
|
+
const value = await call('Conformance.noTime');
|
|
116
|
+
expect(value === null, 'got ' + JSON.stringify(value));
|
|
117
|
+
typed('Conformance.noTime', value);
|
|
118
|
+
});
|
|
119
|
+
await check('not signed in is the engine\'s ServerAuthenticationError', async () => {
|
|
120
|
+
const error = await rejection(() => call('Conformance.unauthenticated'));
|
|
121
|
+
expect(error instanceof ServerAuthenticationError, 'got ' + String(error));
|
|
122
|
+
});
|
|
123
|
+
await check('not allowed is the engine\'s ServerAuthorizationError', async () => {
|
|
124
|
+
const error = await rejection(() => call('Conformance.unauthorized'));
|
|
125
|
+
expect(error instanceof ServerAuthorizationError, 'got ' + String(error));
|
|
126
|
+
});
|
|
127
|
+
await check('not found is the engine\'s notFound()', async () => {
|
|
128
|
+
const error = await rejection(() => call('Conformance.notFound'));
|
|
129
|
+
expect(isNotFoundSignal(error), 'got ' + String(error));
|
|
130
|
+
});
|
|
131
|
+
await check('a refusal keeps its status and its message', async () => {
|
|
132
|
+
const error = (await rejection(() => call('Conformance.refuse')));
|
|
133
|
+
expect(error.refusalStatus === 429, 'status ' + String(error.refusalStatus));
|
|
134
|
+
expect(String(error.message).includes('Slow down.'), 'message ' + JSON.stringify(error.message));
|
|
135
|
+
});
|
|
136
|
+
await check('refused input names its field', async () => {
|
|
137
|
+
const error = (await rejection(() => call('Conformance.invalid')));
|
|
138
|
+
expect(Array.isArray(error.errors?.name) && error.errors.name.length > 0, 'errors ' + JSON.stringify(error.errors));
|
|
139
|
+
});
|
|
140
|
+
await check('a redirect is the engine\'s redirect, to where it said', async () => {
|
|
141
|
+
const taken = await withRedirect(async (seen) => {
|
|
142
|
+
await rejection(() => call('Conformance.redirect'));
|
|
143
|
+
return seen();
|
|
144
|
+
});
|
|
145
|
+
expect(taken?.location === '/login', 'redirect ' + JSON.stringify(taken));
|
|
146
|
+
expect(taken.status >= 300 && taken.status < 400, 'status ' + taken?.status);
|
|
147
|
+
});
|
|
148
|
+
await check('a revalidation rides with the result', async () => {
|
|
149
|
+
const value = await call('Conformance.revalidate');
|
|
150
|
+
expect(value === 'ok', 'got ' + JSON.stringify(value));
|
|
151
|
+
expect(revalidated.some((r) => r.includes('orders')), 'revalidated ' + JSON.stringify(revalidated));
|
|
152
|
+
});
|
|
153
|
+
await check('an unexpected failure is an error, not a refusal', async () => {
|
|
154
|
+
const error = (await rejection(() => call('Conformance.fail')));
|
|
155
|
+
expect(error instanceof Error && !(error instanceof ServerAuthenticationError) && !(error instanceof ServerAuthorizationError) && !error.refusalStatus && !isNotFoundSignal(error), 'got ' + String(error));
|
|
156
|
+
});
|
|
157
|
+
await check('the visitor\'s Authorization header is forwarded', async () => {
|
|
158
|
+
const value = await withRequest(new Request('https://app.test/', { headers: { authorization: 'Bearer conformance' } }), () => call('Conformance.authorization'));
|
|
159
|
+
expect(value === 'Bearer conformance', 'got ' + JSON.stringify(value));
|
|
160
|
+
});
|
|
161
|
+
await check('calls in one batch are each answered, a refusal only its own', async () => {
|
|
162
|
+
const [a, b, c] = await Promise.allSettled([
|
|
163
|
+
call('Conformance.echo', 1),
|
|
164
|
+
call('Conformance.unauthenticated'),
|
|
165
|
+
call('Conformance.echo', 2),
|
|
166
|
+
]);
|
|
167
|
+
expect(a.status === 'fulfilled' && a.value === 1, 'first ' + JSON.stringify(a));
|
|
168
|
+
expect(b.status === 'rejected' && b.reason instanceof ServerAuthenticationError, 'second ' + String(b.reason));
|
|
169
|
+
expect(c.status === 'fulfilled' && c.value === 2, 'third ' + JSON.stringify(c));
|
|
170
|
+
});
|
|
171
|
+
await check('a guard that passes answers true', async () => {
|
|
172
|
+
const value = await call('__rsc.middleware', ['conformance-allow']);
|
|
173
|
+
expect(value === true, 'got ' + JSON.stringify(value));
|
|
174
|
+
});
|
|
175
|
+
await check('a guard that refuses refuses', async () => {
|
|
176
|
+
const value = await call('__rsc.middleware', ['conformance-deny']).catch((e) => e);
|
|
177
|
+
expect(value !== true, 'it answered true');
|
|
178
|
+
});
|
|
179
|
+
await check('a guard nobody registered refuses: fail closed', async () => {
|
|
180
|
+
const value = await call('__rsc.middleware', ['conformance-nobody-registered-this']).catch((e) => e);
|
|
181
|
+
expect(value !== true, 'it answered true');
|
|
182
|
+
});
|
|
183
|
+
if (wrongSecret) {
|
|
184
|
+
await check('a call with the wrong secret is refused', async () => {
|
|
185
|
+
const value = await wrongSecret('Conformance.echo', 'should not run').catch((e) => e);
|
|
186
|
+
expect(value !== 'should not run', 'it ran');
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
/** Run the suite against an adapter's endpoint. */
|
|
192
|
+
export async function runConformance(options) {
|
|
193
|
+
const revalidated = [];
|
|
194
|
+
const client = (secret) => httpHostCalls({
|
|
195
|
+
endpoint: options.endpoint,
|
|
196
|
+
secret,
|
|
197
|
+
fetch: options.fetch,
|
|
198
|
+
onRevalidate: (targets) => revalidated.push(targets),
|
|
199
|
+
});
|
|
200
|
+
return await conformance(client(options.secret), {
|
|
201
|
+
revalidated,
|
|
202
|
+
manifest: options.manifest,
|
|
203
|
+
wrongSecret: client(options.secret + '-wrong'),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Whether a value fits the JSON Schema subset the manifest uses. Null when it
|
|
208
|
+
* does; otherwise where and why it does not.
|
|
209
|
+
*/
|
|
210
|
+
export function conformsTo(value, schema, defs, at) {
|
|
211
|
+
if (!schema || Object.keys(schema).length === 0)
|
|
212
|
+
return null;
|
|
213
|
+
const ref = schema.$ref;
|
|
214
|
+
if (typeof ref === 'string' && ref.startsWith('#/defs/')) {
|
|
215
|
+
const target = defs[ref.slice(7)];
|
|
216
|
+
return target ? conformsTo(value, target, defs, at) : `${at}: ${ref} is not in defs`;
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(schema.anyOf)) {
|
|
219
|
+
const fits = schema.anyOf.some((s) => conformsTo(value, s, defs, at) === null);
|
|
220
|
+
return fits ? null : `${at}: ${JSON.stringify(value)} is none of ${JSON.stringify(schema.anyOf)}`;
|
|
221
|
+
}
|
|
222
|
+
if (Array.isArray(schema.enum)) {
|
|
223
|
+
return schema.enum.includes(value) ? null : `${at}: ${JSON.stringify(value)} is not one of ${JSON.stringify(schema.enum)}`;
|
|
224
|
+
}
|
|
225
|
+
const kind = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value;
|
|
226
|
+
switch (schema.type) {
|
|
227
|
+
case 'string':
|
|
228
|
+
if (kind !== 'string')
|
|
229
|
+
return `${at}: expected a string, got ${JSON.stringify(value)}`;
|
|
230
|
+
if (schema.format === 'date-time' && Number.isNaN(Date.parse(value)))
|
|
231
|
+
return `${at}: ${JSON.stringify(value)} is not a date-time`;
|
|
232
|
+
return null;
|
|
233
|
+
case 'integer':
|
|
234
|
+
return Number.isInteger(value) ? null : `${at}: expected an integer, got ${JSON.stringify(value)}`;
|
|
235
|
+
case 'number':
|
|
236
|
+
return kind === 'number' ? null : `${at}: expected a number, got ${JSON.stringify(value)}`;
|
|
237
|
+
case 'boolean':
|
|
238
|
+
return kind === 'boolean' ? null : `${at}: expected a boolean, got ${JSON.stringify(value)}`;
|
|
239
|
+
case 'null':
|
|
240
|
+
return value === null ? null : `${at}: expected null, got ${JSON.stringify(value)}`;
|
|
241
|
+
case 'array': {
|
|
242
|
+
if (kind !== 'array')
|
|
243
|
+
return `${at}: expected an array, got ${JSON.stringify(value)}`;
|
|
244
|
+
for (const [i, item] of value.entries()) {
|
|
245
|
+
const problem = conformsTo(item, schema.items, defs, `${at}[${i}]`);
|
|
246
|
+
if (problem)
|
|
247
|
+
return problem;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
case 'object': {
|
|
252
|
+
if (kind !== 'object')
|
|
253
|
+
return `${at}: expected an object, got ${JSON.stringify(value)}`;
|
|
254
|
+
const object = value;
|
|
255
|
+
const properties = (schema.properties ?? {});
|
|
256
|
+
for (const name of schema.required ?? []) {
|
|
257
|
+
if (!(name in object))
|
|
258
|
+
return `${at}.${name}: required, and missing`;
|
|
259
|
+
}
|
|
260
|
+
for (const [name, s] of Object.entries(properties)) {
|
|
261
|
+
if (name in object) {
|
|
262
|
+
const problem = conformsTo(object[name], s, defs, `${at}.${name}`);
|
|
263
|
+
if (problem)
|
|
264
|
+
return problem;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const extra = schema.additionalProperties;
|
|
268
|
+
if (extra && !schema.properties) {
|
|
269
|
+
for (const [name, item] of Object.entries(object)) {
|
|
270
|
+
const problem = conformsTo(item, extra, defs, `${at}.${name}`);
|
|
271
|
+
if (problem)
|
|
272
|
+
return problem;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Print a result table, and say whether the adapter conforms. */
|
|
282
|
+
export function report(results) {
|
|
283
|
+
for (const r of results) {
|
|
284
|
+
stdout.write(` ${r.ok ? '✓' : '✗'} ${r.case}${r.ok ? '' : '\n ' + r.detail}\n`);
|
|
285
|
+
}
|
|
286
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
287
|
+
stdout.write(failed === 0 ? `\n conforms: ${results.length} cases\n` : `\n ${failed} of ${results.length} cases failed\n`);
|
|
288
|
+
return failed === 0;
|
|
289
|
+
}
|
|
290
|
+
// Run as a command, not when imported.
|
|
291
|
+
if (import.meta.url === pathToFileURL(argv[1] ?? '').href || argv[1]?.endsWith('rsc-kit-conformance')) {
|
|
292
|
+
const flag = (name) => {
|
|
293
|
+
const at = argv.indexOf('--' + name);
|
|
294
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
295
|
+
};
|
|
296
|
+
const endpoint = flag('endpoint');
|
|
297
|
+
const secret = flag('secret');
|
|
298
|
+
if (!endpoint || !secret) {
|
|
299
|
+
stderr.write('usage: rsc-kit-conformance --endpoint <url> --secret <secret> [--manifest rsc-host.json]\n');
|
|
300
|
+
exit(2);
|
|
301
|
+
}
|
|
302
|
+
runConformance({ endpoint, secret, manifest: flag('manifest') })
|
|
303
|
+
.then((results) => exit(report(results) ? 0 : 1))
|
|
304
|
+
.catch((error) => {
|
|
305
|
+
stderr.write('rsc-kit-conformance: ' + (error instanceof Error ? error.message : String(error)) + '\n');
|
|
306
|
+
exit(1);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// The contract every backend adapter keeps, as a suite it runs against itself.\n//\n// A backend answers rpc(), actions and guards over one endpoint, and every\n// difference between how it and the engine read that wire is a bug in an\n// app that nobody wrote: a nil slice arriving as null where the type said a\n// list, a timestamp the type called unknown, a refusal that did not come out\n// as the error a page checks for. Each was found by an app, one at a time.\n// This finds them before an adapter ships.\n//\n// The adapter registers the Conformance.* functions described in CASES, in\n// its own language, the way an app would write them; this calls them through\n// the real client and checks what arrives. Where the adapter's manifest types\n// a function, every value it sends is checked against that type too.\n//\n// rsc-kit-conformance --endpoint http://127.0.0.1:8080/__rsc/host-call \\\n// --secret test-secret --manifest rsc-host.json\n//\n// The test host's hostReply passes the same suite (conformance.test.ts), so\n// a page test faking the backend fakes what a real adapter does.\n\nimport { readFileSync } from 'node:fs'\nimport { argv, exit, stderr, stdout } from 'node:process'\nimport { pathToFileURL } from 'node:url'\nimport { httpHostCalls } from './hostCalls.js'\nimport { isNotFoundSignal } from './notFound.js'\nimport { withRedirect } from './redirect.js'\nimport { withRequest } from './request.js'\nimport { ServerAuthenticationError, ServerAuthorizationError } from './js/errors.js'\n\ntype Schema = Record<string, unknown>\n\n/** A host-call client: the real one in a run, the test host's in core's own test. */\nexport type ConformanceCall = (name: string, ...args: unknown[]) => Promise<unknown>\n\nexport interface ConformanceOptions {\n /** The adapter's host-call endpoint, e.g. http://127.0.0.1:8080/__rsc/host-call. */\n endpoint: string\n /** The secret the adapter checks. */\n secret: string\n /** The adapter's rsc-host.json, parsed or as a path: its types are checked against what arrives. */\n manifest?: string | { functions?: string[]; types?: Record<string, { result?: Schema }>; defs?: Record<string, Schema> }\n /** For a test: the fetch the client uses. */\n fetch?: typeof fetch\n}\n\nexport interface ConformanceResult {\n case: string\n ok: boolean\n /** What went wrong, when it did. */\n detail?: string\n}\n\n/**\n * What the adapter registers, and what each must do. Names are fixed; how\n * they are written is the adapter's - the point is that they are written the\n * way an app's would be, with the adapter's ordinary API.\n */\nexport const CASES = {\n 'Conformance.echo': 'Return its one argument unchanged: any JSON value, nested.',\n 'Conformance.emptyList': 'Return an empty list of objects, the way the language makes \"no rows\" (Go: a nil slice).',\n 'Conformance.time': \"Return the instant 2026-01-02T03:04:05Z as the language's own time value.\",\n 'Conformance.noTime': \"Return an absent time: a nil pointer, a null Carbon, typed as a time or nothing.\",\n 'Conformance.unauthenticated': 'Refuse as not signed in, the way the framework does.',\n 'Conformance.unauthorized': 'Refuse as signed in but not allowed.',\n 'Conformance.notFound': \"Refuse as not found (404), the way the framework does: Go's Refuse(404), Laravel's abort(404).\",\n 'Conformance.refuse': 'Refuse with status 429 and the message \"Slow down.\"',\n 'Conformance.invalid': 'Refuse the input: a validation error on the field \"name\".',\n 'Conformance.redirect': 'Send the visitor to /login.',\n 'Conformance.revalidate': 'Mark the region \"orders\" stale, and return \"ok\".',\n 'Conformance.fail': 'Fail unexpectedly: an ordinary error, not a refusal.',\n 'Conformance.authorization': \"Return the request's Authorization header, as the renderer forwarded it.\",\n '__rsc.middleware': 'Guards: \"conformance-allow\" passes; \"conformance-deny\" refuses; a name with no guard refuses.',\n} as const\n\nconst INSTANT = Date.parse('2026-01-02T03:04:05Z')\n\nconst ECHOES: unknown[] = [null, 0, -1.5, 'héllo ✓ \"quoted\"', [1, 'a', null], { a: { b: [true, false] }, c: '' }]\n\n/**\n * Run the suite against a client. Every case runs; none stops another.\n *\n * `revalidated` is where the client records what each call marked stale -\n * its onRevalidate - so the revalidation case can read it.\n */\nexport async function conformance(\n call: ConformanceCall,\n options: { revalidated: string[][]; manifest?: ConformanceOptions['manifest']; wrongSecret?: ConformanceCall },\n): Promise<ConformanceResult[]> {\n const { revalidated, manifest, wrongSecret } = options\n const results: ConformanceResult[] = []\n const types = typeof manifest === 'string' ? JSON.parse(readFileSync(manifest, 'utf-8')) : manifest\n\n const check = async (name: string, run: () => Promise<void>) => {\n try {\n await run()\n results.push({ case: name, ok: true })\n } catch (error) {\n results.push({ case: name, ok: false, detail: error instanceof Error ? error.message : String(error) })\n }\n }\n\n const expect = (ok: boolean, detail: string) => {\n if (!ok) throw new Error(detail)\n }\n\n const rejection = async (run: () => Promise<unknown>): Promise<unknown> => {\n try {\n const value = await run()\n\n throw new Error('answered ' + JSON.stringify(value) + ' instead of refusing')\n } catch (error) {\n return error\n }\n }\n\n /** The value against the type the adapter's manifest declares, when it declares one. */\n const typed = (name: string, value: unknown) => {\n const schema = types?.types?.[name]?.result\n\n if (!schema) return\n\n const problem = conformsTo(value, schema, types?.defs ?? {}, 'result')\n\n expect(problem === null, `the manifest types ${name}'s result, and what arrived does not fit it: ${problem}`)\n }\n\n await check('the manifest lists every Conformance function', async () => {\n if (!types) return\n\n const missing = Object.keys(CASES).filter((n) => !n.startsWith('__') && !types.functions?.includes(n))\n\n expect(missing.length === 0, 'not in functions: ' + missing.join(', '))\n })\n\n await check('a value of any JSON shape comes back unchanged', async () => {\n for (const value of ECHOES) {\n const back = await call('Conformance.echo', value)\n\n expect(JSON.stringify(back) === JSON.stringify(value), `sent ${JSON.stringify(value)}, got ${JSON.stringify(back)}`)\n }\n })\n\n await check('\"no rows\" is an empty list, never null', async () => {\n const value = await call('Conformance.emptyList')\n\n expect(Array.isArray(value) && value.length === 0, 'got ' + JSON.stringify(value))\n typed('Conformance.emptyList', value)\n })\n\n await check('a time is an ISO 8601 string of the same instant', async () => {\n const value = await call('Conformance.time')\n\n expect(typeof value === 'string' && Date.parse(value) === INSTANT, 'got ' + JSON.stringify(value))\n typed('Conformance.time', value)\n })\n\n await check('an absent time is null', async () => {\n const value = await call('Conformance.noTime')\n\n expect(value === null, 'got ' + JSON.stringify(value))\n typed('Conformance.noTime', value)\n })\n\n await check('not signed in is the engine\\'s ServerAuthenticationError', async () => {\n const error = await rejection(() => call('Conformance.unauthenticated'))\n\n expect(error instanceof ServerAuthenticationError, 'got ' + String(error))\n })\n\n await check('not allowed is the engine\\'s ServerAuthorizationError', async () => {\n const error = await rejection(() => call('Conformance.unauthorized'))\n\n expect(error instanceof ServerAuthorizationError, 'got ' + String(error))\n })\n\n await check('not found is the engine\\'s notFound()', async () => {\n const error = await rejection(() => call('Conformance.notFound'))\n\n expect(isNotFoundSignal(error), 'got ' + String(error))\n })\n\n await check('a refusal keeps its status and its message', async () => {\n const error = (await rejection(() => call('Conformance.refuse'))) as Error & { refusalStatus?: number }\n\n expect(error.refusalStatus === 429, 'status ' + String(error.refusalStatus))\n expect(String(error.message).includes('Slow down.'), 'message ' + JSON.stringify(error.message))\n })\n\n await check('refused input names its field', async () => {\n const error = (await rejection(() => call('Conformance.invalid'))) as { errors?: Record<string, string[]> }\n\n expect(Array.isArray(error.errors?.name) && error.errors.name.length > 0, 'errors ' + JSON.stringify(error.errors))\n })\n\n await check('a redirect is the engine\\'s redirect, to where it said', async () => {\n const taken = await withRedirect(async (seen) => {\n await rejection(() => call('Conformance.redirect'))\n\n return seen()\n })\n\n expect(taken?.location === '/login', 'redirect ' + JSON.stringify(taken))\n expect(taken!.status >= 300 && taken!.status < 400, 'status ' + taken?.status)\n })\n\n await check('a revalidation rides with the result', async () => {\n const value = await call('Conformance.revalidate')\n\n expect(value === 'ok', 'got ' + JSON.stringify(value))\n expect(revalidated.some((r) => r.includes('orders')), 'revalidated ' + JSON.stringify(revalidated))\n })\n\n await check('an unexpected failure is an error, not a refusal', async () => {\n const error = (await rejection(() => call('Conformance.fail'))) as Error & { refusalStatus?: number }\n\n expect(\n error instanceof Error && !(error instanceof ServerAuthenticationError) && !(error instanceof ServerAuthorizationError) && !error.refusalStatus && !isNotFoundSignal(error),\n 'got ' + String(error),\n )\n })\n\n await check('the visitor\\'s Authorization header is forwarded', async () => {\n const value = await withRequest(new Request('https://app.test/', { headers: { authorization: 'Bearer conformance' } }), () =>\n call('Conformance.authorization'),\n )\n\n expect(value === 'Bearer conformance', 'got ' + JSON.stringify(value))\n })\n\n await check('calls in one batch are each answered, a refusal only its own', async () => {\n const [a, b, c] = await Promise.allSettled([\n call('Conformance.echo', 1),\n call('Conformance.unauthenticated'),\n call('Conformance.echo', 2),\n ])\n\n expect(a.status === 'fulfilled' && a.value === 1, 'first ' + JSON.stringify(a))\n expect(b.status === 'rejected' && b.reason instanceof ServerAuthenticationError, 'second ' + String((b as PromiseRejectedResult).reason))\n expect(c.status === 'fulfilled' && c.value === 2, 'third ' + JSON.stringify(c))\n })\n\n await check('a guard that passes answers true', async () => {\n const value = await call('__rsc.middleware', ['conformance-allow'])\n\n expect(value === true, 'got ' + JSON.stringify(value))\n })\n\n await check('a guard that refuses refuses', async () => {\n const value = await call('__rsc.middleware', ['conformance-deny']).catch((e: unknown) => e)\n\n expect(value !== true, 'it answered true')\n })\n\n await check('a guard nobody registered refuses: fail closed', async () => {\n const value = await call('__rsc.middleware', ['conformance-nobody-registered-this']).catch((e: unknown) => e)\n\n expect(value !== true, 'it answered true')\n })\n\n if (wrongSecret) {\n await check('a call with the wrong secret is refused', async () => {\n const value = await wrongSecret('Conformance.echo', 'should not run').catch((e: unknown) => e)\n\n expect(value !== 'should not run', 'it ran')\n })\n }\n\n return results\n}\n\n/** Run the suite against an adapter's endpoint. */\nexport async function runConformance(options: ConformanceOptions): Promise<ConformanceResult[]> {\n const revalidated: string[][] = []\n const client = (secret: string) =>\n httpHostCalls({\n endpoint: options.endpoint,\n secret,\n fetch: options.fetch,\n onRevalidate: (targets) => revalidated.push(targets),\n })\n\n return await conformance(client(options.secret), {\n revalidated,\n manifest: options.manifest,\n wrongSecret: client(options.secret + '-wrong'),\n })\n}\n\n/**\n * Whether a value fits the JSON Schema subset the manifest uses. Null when it\n * does; otherwise where and why it does not.\n */\nexport function conformsTo(value: unknown, schema: Schema, defs: Record<string, Schema>, at: string): string | null {\n if (!schema || Object.keys(schema).length === 0) return null\n\n const ref = schema.$ref\n\n if (typeof ref === 'string' && ref.startsWith('#/defs/')) {\n const target = defs[ref.slice(7)]\n\n return target ? conformsTo(value, target, defs, at) : `${at}: ${ref} is not in defs`\n }\n\n if (Array.isArray(schema.anyOf)) {\n const fits = (schema.anyOf as Schema[]).some((s) => conformsTo(value, s, defs, at) === null)\n\n return fits ? null : `${at}: ${JSON.stringify(value)} is none of ${JSON.stringify(schema.anyOf)}`\n }\n\n if (Array.isArray(schema.enum)) {\n return (schema.enum as unknown[]).includes(value) ? null : `${at}: ${JSON.stringify(value)} is not one of ${JSON.stringify(schema.enum)}`\n }\n\n const kind = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value\n\n switch (schema.type) {\n case 'string':\n if (kind !== 'string') return `${at}: expected a string, got ${JSON.stringify(value)}`\n if (schema.format === 'date-time' && Number.isNaN(Date.parse(value as string))) return `${at}: ${JSON.stringify(value)} is not a date-time`\n return null\n case 'integer':\n return Number.isInteger(value) ? null : `${at}: expected an integer, got ${JSON.stringify(value)}`\n case 'number':\n return kind === 'number' ? null : `${at}: expected a number, got ${JSON.stringify(value)}`\n case 'boolean':\n return kind === 'boolean' ? null : `${at}: expected a boolean, got ${JSON.stringify(value)}`\n case 'null':\n return value === null ? null : `${at}: expected null, got ${JSON.stringify(value)}`\n case 'array': {\n if (kind !== 'array') return `${at}: expected an array, got ${JSON.stringify(value)}`\n for (const [i, item] of (value as unknown[]).entries()) {\n const problem = conformsTo(item, schema.items as Schema, defs, `${at}[${i}]`)\n if (problem) return problem\n }\n return null\n }\n case 'object': {\n if (kind !== 'object') return `${at}: expected an object, got ${JSON.stringify(value)}`\n const object = value as Record<string, unknown>\n const properties = (schema.properties ?? {}) as Record<string, Schema>\n for (const name of (schema.required as string[] | undefined) ?? []) {\n if (!(name in object)) return `${at}.${name}: required, and missing`\n }\n for (const [name, s] of Object.entries(properties)) {\n if (name in object) {\n const problem = conformsTo(object[name], s, defs, `${at}.${name}`)\n if (problem) return problem\n }\n }\n const extra = schema.additionalProperties as Schema | undefined\n if (extra && !schema.properties) {\n for (const [name, item] of Object.entries(object)) {\n const problem = conformsTo(item, extra, defs, `${at}.${name}`)\n if (problem) return problem\n }\n }\n return null\n }\n default:\n return null\n }\n}\n\n/** Print a result table, and say whether the adapter conforms. */\nexport function report(results: ConformanceResult[]): boolean {\n for (const r of results) {\n stdout.write(` ${r.ok ? '✓' : '✗'} ${r.case}${r.ok ? '' : '\\n ' + r.detail}\\n`)\n }\n\n const failed = results.filter((r) => !r.ok).length\n\n stdout.write(failed === 0 ? `\\n conforms: ${results.length} cases\\n` : `\\n ${failed} of ${results.length} cases failed\\n`)\n\n return failed === 0\n}\n\n// Run as a command, not when imported.\nif (import.meta.url === pathToFileURL(argv[1] ?? '').href || argv[1]?.endsWith('rsc-kit-conformance')) {\n const flag = (name: string) => {\n const at = argv.indexOf('--' + name)\n\n return at === -1 ? undefined : argv[at + 1]\n }\n\n const endpoint = flag('endpoint')\n const secret = flag('secret')\n\n if (!endpoint || !secret) {\n stderr.write('usage: rsc-kit-conformance --endpoint <url> --secret <secret> [--manifest rsc-host.json]\\n')\n exit(2)\n }\n\n runConformance({ endpoint, secret, manifest: flag('manifest') })\n .then((results) => exit(report(results) ? 0 : 1))\n .catch((error: unknown) => {\n stderr.write('rsc-kit-conformance: ' + (error instanceof Error ? error.message : String(error)) + '\\n')\n exit(1)\n })\n}\n"]}
|