@zerolith/client 0.0.0-canary.20261102.g8292c77

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/index.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @zerolith/client: a typed HTTP client over a neutral contract. The route
3
+ * table and the types come from `@zerolith/client/generate`, which reads an
4
+ * OpenAPI document; the client itself knows nothing about the server beyond
5
+ * its paths. Failures are RFC 9457 problems, thrown as `ProblemError`.
6
+ */
7
+ import { PROBLEM_MEDIA_TYPE, isProblem, resource } from "zerolith";
8
+ /** Thrown for a response outside 2xx, carrying the problem the server sent or one built from the status line. */
9
+ export class ProblemError extends Error {
10
+ /** The problem the server sent, with `status` filled from the response when the body omits it; a body that is not a JSON problem yields `status` and a `title` from the status line. */
11
+ problem;
12
+ /** The response outside 2xx, for its status and headers; its body has already been read when the content type is JSON. */
13
+ response;
14
+ /** Wraps `problem` and the `response` it came from; the message is the problem's `title`, or `HTTP <status>` when it has none. */
15
+ constructor(problem, response) {
16
+ super(problem.title ?? `HTTP ${response.status}`);
17
+ this.name = "ProblemError";
18
+ this.problem = problem;
19
+ this.response = response;
20
+ }
21
+ }
22
+ function isRawBody(body) {
23
+ return typeof body === "string" || body instanceof FormData || body instanceof URLSearchParams || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof ReadableStream;
24
+ }
25
+ function searchOf(query) {
26
+ const search = new URLSearchParams();
27
+ for (const [key, value] of Object.entries(query)) {
28
+ if (value === undefined || value === null) {
29
+ continue;
30
+ }
31
+ for (const item of Array.isArray(value) ? value : [value]) {
32
+ search.append(key, typeof item === "object" && item !== null ? JSON.stringify(item) : String(item));
33
+ }
34
+ }
35
+ return search.size > 0 ? `?${search.toString()}` : "";
36
+ }
37
+ function buildRequest(route, input, options, client) {
38
+ const path = route.path.replace(/\{([^{}]+)\}/g, (_match, name) => {
39
+ const value = input.params?.[name];
40
+ if (value === undefined) {
41
+ throw new TypeError(`zerolith/client: ${route.method.toUpperCase()} ${route.path} needs the path parameter "${name}"`);
42
+ }
43
+ const segment = encodeURIComponent(String(value));
44
+ if (segment === "." || segment === "..") {
45
+ throw new TypeError(`zerolith/client: ${route.method.toUpperCase()} ${route.path} got "${segment}" for the path parameter "${name}", which would move the request to another path`);
46
+ }
47
+ return segment;
48
+ });
49
+ const base = (client.baseUrl ?? "").replace(/\/$/, "");
50
+ const headers = new Headers(typeof client.headers === "function" ? client.headers() : client.headers);
51
+ new Headers(options.headers).forEach((value, key) => headers.set(key, value));
52
+ if (!headers.has("accept")) {
53
+ headers.set("accept", `application/json, ${PROBLEM_MEDIA_TYPE}`);
54
+ }
55
+ const init = { method: route.method.toUpperCase(), headers };
56
+ if (options.signal !== undefined) {
57
+ init.signal = options.signal;
58
+ }
59
+ if (input.body !== undefined) {
60
+ if (isRawBody(input.body)) {
61
+ init.body = input.body;
62
+ if (input.body instanceof ReadableStream) {
63
+ init.duplex = "half";
64
+ }
65
+ }
66
+ else {
67
+ init.body = JSON.stringify(input.body);
68
+ if (!headers.has("content-type")) {
69
+ headers.set("content-type", "application/json");
70
+ }
71
+ }
72
+ }
73
+ return new Request(`${base}${path}${searchOf(input.query ?? {})}`, init);
74
+ }
75
+ /** A successful response's body: JSON parsed, text for `text/*` or no type, a `Blob` for any other type, such as an image, and `undefined` when it is empty. */
76
+ async function readBody(response) {
77
+ if (response.status === 204 || response.status === 205) {
78
+ return undefined;
79
+ }
80
+ const type = (response.headers.get("content-type") ?? "").toLowerCase();
81
+ if (type.includes("json")) {
82
+ return response.json();
83
+ }
84
+ if (type === "" || type.startsWith("text/")) {
85
+ const text = await response.text();
86
+ return text === "" ? undefined : text;
87
+ }
88
+ return response.blob();
89
+ }
90
+ /** Calls `run` with a signal that aborts when either `own` or `caller` does, and stops listening to the caller once the call settles. */
91
+ async function withSignals(own, caller, run) {
92
+ if (caller === undefined) {
93
+ return run(own);
94
+ }
95
+ const combined = new AbortController();
96
+ const abort = () => combined.abort();
97
+ if (own.aborted || caller.aborted) {
98
+ combined.abort();
99
+ }
100
+ own.addEventListener("abort", abort);
101
+ caller.addEventListener("abort", abort);
102
+ try {
103
+ return await run(combined.signal);
104
+ }
105
+ finally {
106
+ own.removeEventListener("abort", abort);
107
+ caller.removeEventListener("abort", abort);
108
+ }
109
+ }
110
+ async function settle(response) {
111
+ if (response.ok) {
112
+ return readBody(response);
113
+ }
114
+ let problem;
115
+ if ((response.headers.get("content-type") ?? "").includes("json")) {
116
+ const body = await response.json().catch(() => undefined);
117
+ if (isProblem(body)) {
118
+ problem = { status: response.status, ...body };
119
+ }
120
+ }
121
+ throw new ProblemError(problem ?? { status: response.status, title: response.statusText === "" ? `HTTP ${response.status}` : response.statusText }, response);
122
+ }
123
+ /**
124
+ * Creates a client for a generated route table. `routes` and the `Api`
125
+ * type parameter come from the same generated module, so an operation
126
+ * name, its parameters, its body, and its output are checked together.
127
+ */
128
+ export function createClient(routes, options = {}) {
129
+ const transport = options.transport ?? ((request) => fetch(request));
130
+ const request = (name, input, callOptions) => {
131
+ const route = routes[name];
132
+ if (route === undefined) {
133
+ throw new TypeError(`zerolith/client: unknown operation "${String(name)}"`);
134
+ }
135
+ return buildRequest(route, input ?? {}, callOptions ?? {}, options);
136
+ };
137
+ const call = async (name, input, callOptions) => {
138
+ return settle(await transport(request(name, input, callOptions)));
139
+ };
140
+ return {
141
+ call(name, ...args) {
142
+ return call(name, args[0], args[1]);
143
+ },
144
+ query(name, input, callOptions = {}) {
145
+ return resource({
146
+ params: input,
147
+ loader: ({ params, abort }) => withSignals(abort, callOptions.signal, (signal) => call(name, params, { ...callOptions, signal }))
148
+ });
149
+ },
150
+ request(name, ...args) {
151
+ return request(name, args[0], args[1]);
152
+ }
153
+ };
154
+ }
155
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/port.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A transport over a message port, for the places `fetch` cannot reach the
3
+ * server: an Electron renderer behind `contextBridge`, a worker, a test.
4
+ * `portTransport` runs where the client runs; `servePort` runs where the
5
+ * handler runs. Bodies travel as text, which covers JSON and encoded forms.
6
+ */
7
+ import type { Transport } from "./index.js";
8
+ /** The port shapes supported: a DOM `MessagePort`, Node's, or Electron's `MessagePortMain`. */
9
+ export interface PortLike {
10
+ /** Sends a message to the other end; `portTransport` posts each `PortRequest` through it and `servePort` each `PortResponse`. */
11
+ postMessage(message: unknown): void;
12
+ /** Starts delivering messages; called, when present, right after the listener is attached, since a DOM `MessagePort` and Electron's `MessagePortMain` queue messages until it runs. */
13
+ start?(): void;
14
+ /** Receives messages the `EventTarget` way, as a DOM `MessagePort` does; used in preference to `on` when both exist. */
15
+ addEventListener?(type: "message", listener: (event: {
16
+ data: unknown;
17
+ }) => void): void;
18
+ /** Receives messages the `EventEmitter` way, as Electron's `MessagePortMain` does; used when `addEventListener` is absent, and a port with neither throws a `TypeError`. */
19
+ on?(type: "message", listener: (event: {
20
+ data: unknown;
21
+ }) => void): void;
22
+ }
23
+ /** What crosses the port for a request. */
24
+ export interface PortRequest {
25
+ /** Tags the message as a request, so `servePort` ignores other traffic on the port. */
26
+ kind: "zerolith:request";
27
+ /** Pairs the request with its response; each `portTransport` numbers its requests from 1. */
28
+ id: number;
29
+ /** The request's absolute URL, from which `servePort` builds the `Request` its handler receives. */
30
+ url: string;
31
+ /** The request method as `Request.method` reports it, such as `POST`. */
32
+ method: string;
33
+ /** The request headers as `[name, value]` pairs with lowercase names, as iterating a `Headers` yields them. */
34
+ headers: [string, string][];
35
+ /** The body as UTF-8 text, or `null` for a `GET` or `HEAD` request or an empty body. */
36
+ body: string | null;
37
+ }
38
+ /** What crosses the port for a response. */
39
+ export interface PortResponse {
40
+ /** Tags the message as a response, so `portTransport` ignores other traffic on the port. */
41
+ kind: "zerolith:response";
42
+ /** The `id` of the request this answers; an answer with no pending request, such as one whose call was aborted, is dropped. */
43
+ id: number;
44
+ /** The status code of the handler's response, or 500 when the handler threw. */
45
+ status: number;
46
+ /** The status text of the handler's response, empty when none was set. */
47
+ statusText: string;
48
+ /** The response headers as `[name, value]` pairs with lowercase names, as iterating a `Headers` yields them. */
49
+ headers: [string, string][];
50
+ /** The body as UTF-8 text, or `null` when it is empty, as for a 204. */
51
+ body: string | null;
52
+ }
53
+ /**
54
+ * A `Transport` that sends each request over the port as a message and
55
+ * resolves with the response that comes back under the same id. Aborting
56
+ * the request's signal rejects the call; the message already sent is
57
+ * answered and ignored.
58
+ */
59
+ export declare function portTransport(port: PortLike): Transport;
60
+ /**
61
+ * Answers the requests `portTransport` sends by building a `Request`,
62
+ * calling `handler`, and posting the response back. A handler that throws
63
+ * answers with a 500 problem so the client sees a `ProblemError`.
64
+ */
65
+ export declare function servePort(port: PortLike, handler: (request: Request) => Response | Promise<Response>): void;
66
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../src/port.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IAErB,iIAAiI;IACjI,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,uLAAuL;IACvL,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,wHAAwH;IACxH,gBAAgB,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IACvF,4KAA4K;IAC5K,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CAC5E;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAExB,uFAAuF;IACvF,IAAI,EAAE,kBAAkB,CAAC;IACzB,6FAA6F;IAC7F,EAAE,EAAE,MAAM,CAAC;IACX,oGAAoG;IACpG,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,+GAA+G;IAC/G,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5B,wFAAwF;IACxF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAEzB,4FAA4F;IAC5F,IAAI,EAAE,mBAAmB,CAAC;IAC1B,+HAA+H;IAC/H,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,gHAAgH;IAChH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5B,wEAAwE;IACxE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAmBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CA0CvD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAmC3G"}
package/dist/port.js ADDED
@@ -0,0 +1,86 @@
1
+ function listen(port, handler) {
2
+ if (typeof port.addEventListener === "function") {
3
+ port.addEventListener("message", (event) => handler(event.data));
4
+ }
5
+ else if (typeof port.on === "function") {
6
+ port.on("message", (event) => handler(event.data));
7
+ }
8
+ else {
9
+ throw new TypeError("zerolith/client: the port has neither addEventListener nor on");
10
+ }
11
+ port.start?.();
12
+ }
13
+ /**
14
+ * A `Transport` that sends each request over the port as a message and
15
+ * resolves with the response that comes back under the same id. Aborting
16
+ * the request's signal rejects the call; the message already sent is
17
+ * answered and ignored.
18
+ */
19
+ export function portTransport(port) {
20
+ const pending = new Map();
21
+ let next = 1;
22
+ listen(port, (data) => {
23
+ const message = data;
24
+ if (message === null || typeof message !== "object" || message.kind !== "zerolith:response" || typeof message.id !== "number") {
25
+ return;
26
+ }
27
+ const waiting = pending.get(message.id);
28
+ if (waiting === undefined) {
29
+ return;
30
+ }
31
+ pending.delete(message.id);
32
+ waiting.resolve(new Response(message.body ?? null, { status: message.status ?? 200, statusText: message.statusText ?? "", headers: message.headers ?? [] }));
33
+ });
34
+ return async (request) => {
35
+ const id = next++;
36
+ const body = request.method === "GET" || request.method === "HEAD" ? null : await request.text();
37
+ const message = { kind: "zerolith:request", id, url: request.url, method: request.method, headers: [...request.headers], body: body === "" ? null : body };
38
+ return new Promise((resolve, reject) => {
39
+ if (request.signal.aborted) {
40
+ reject(request.signal.reason ?? new DOMException("The request was aborted", "AbortError"));
41
+ return;
42
+ }
43
+ pending.set(id, { resolve, reject });
44
+ request.signal.addEventListener("abort", () => {
45
+ if (pending.delete(id)) {
46
+ reject(request.signal.reason ?? new DOMException("The request was aborted", "AbortError"));
47
+ }
48
+ });
49
+ port.postMessage(message);
50
+ });
51
+ };
52
+ }
53
+ /**
54
+ * Answers the requests `portTransport` sends by building a `Request`,
55
+ * calling `handler`, and posting the response back. A handler that throws
56
+ * answers with a 500 problem so the client sees a `ProblemError`.
57
+ */
58
+ export function servePort(port, handler) {
59
+ listen(port, (data) => {
60
+ const message = data;
61
+ if (message === null || typeof message !== "object" || message.kind !== "zerolith:request" || typeof message.id !== "number" || typeof message.url !== "string") {
62
+ return;
63
+ }
64
+ const id = message.id;
65
+ const answer = async () => {
66
+ let response;
67
+ try {
68
+ response = await handler(new Request(message.url ?? "", { method: message.method ?? "GET", headers: message.headers ?? [], body: message.body ?? null }));
69
+ }
70
+ catch (error) {
71
+ if (typeof reportError === "function") {
72
+ reportError(error);
73
+ }
74
+ else {
75
+ console.error("zerolith/client: a servePort handler threw", error);
76
+ }
77
+ response = new Response(JSON.stringify({ title: "The handler failed", status: 500 }), { status: 500, headers: { "content-type": "application/problem+json" } });
78
+ }
79
+ const text = await response.text();
80
+ const reply = { kind: "zerolith:response", id, status: response.status, statusText: response.statusText, headers: [...response.headers], body: text === "" ? null : text };
81
+ port.postMessage(reply);
82
+ };
83
+ void answer();
84
+ });
85
+ }
86
+ //# sourceMappingURL=port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.js","sourceRoot":"","sources":["../src/port.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@zerolith/client",
3
+ "version": "0.0.0-canary.20261102.g8292c77",
4
+ "description": "A typed HTTP client for zerolith over a neutral contract: generate the route table and types from an OpenAPI document, call operations with checked inputs, and read failures as RFC 9457 problems",
5
+ "license": "Apache-2.0",
6
+ "author": "molexxxx",
7
+ "homepage": "https://zerolith.dev/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/molexxxx/zerolith.git",
11
+ "directory": "packages/client"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/molexxxx/zerolith/issues"
15
+ },
16
+ "keywords": [
17
+ "zerolith",
18
+ "openapi",
19
+ "typed-client",
20
+ "problem-details"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "engines": {
25
+ "node": ">=24.0.0"
26
+ },
27
+ "bin": {
28
+ "zerolith-client": "dist/cli.js"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./generate": {
36
+ "types": "./dist/generate.d.ts",
37
+ "default": "./dist/generate.js"
38
+ },
39
+ "./port": {
40
+ "types": "./dist/port.d.ts",
41
+ "default": "./dist/port.js"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "src",
48
+ "LICENSE",
49
+ "README.md"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json"
53
+ },
54
+ "peerDependencies": {
55
+ "zerolith": "^0.0.0-canary.20261102.g8292c77"
56
+ },
57
+ "devDependencies": {
58
+ "zerolith": "^0.0.0-canary.20261102.g8292c77"
59
+ }
60
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `zerolith-client <openapi.json> [--out <file.ts>]`: writes the contract
4
+ * module for an OpenAPI document to a file, or to standard output.
5
+ */
6
+ import { readFile, writeFile } from "node:fs/promises";
7
+ import { realpathSync } from "node:fs";
8
+ import process from "node:process";
9
+ import { pathToFileURL } from "node:url";
10
+ import { type OpenApiDocument, generateClient } from "./generate.js";
11
+
12
+ const USAGE = "usage: zerolith-client <openapi.json> [--out <file.ts>] [--name <Api>]\n";
13
+
14
+ interface Arguments
15
+ {
16
+ input: string | undefined;
17
+ out: string | undefined;
18
+ name: string | undefined;
19
+ }
20
+
21
+ function parse(argv: readonly string[]): Arguments
22
+ {
23
+ const parsed: Arguments = { input: undefined, out: undefined, name: undefined };
24
+ for (let index = 0; index < argv.length; index++)
25
+ {
26
+ const argument = argv[index] ?? "";
27
+ if (argument === "--out" || argument === "--name")
28
+ {
29
+ const value = argv[index + 1];
30
+ if (value === undefined)
31
+ {
32
+ throw new Error(`${argument} needs a value`);
33
+ }
34
+ parsed[argument === "--out" ? "out" : "name"] = value;
35
+ index++;
36
+ }
37
+ else if (argument.startsWith("--"))
38
+ {
39
+ throw new Error(`unknown option ${argument}`);
40
+ }
41
+ else if (parsed.input === undefined)
42
+ {
43
+ parsed.input = argument;
44
+ }
45
+ else
46
+ {
47
+ throw new Error(`unexpected argument ${argument}`);
48
+ }
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ /** Runs the command with the given arguments and returns the exit code. */
54
+ export async function main(argv: readonly string[]): Promise<number>
55
+ {
56
+ let parsed: Arguments;
57
+ try
58
+ {
59
+ parsed = parse(argv);
60
+ }
61
+ catch (error)
62
+ {
63
+ process.stderr.write(`zerolith-client: ${error instanceof Error ? error.message : String(error)}\n${USAGE}`);
64
+ return 2;
65
+ }
66
+ if (parsed.input === undefined)
67
+ {
68
+ process.stderr.write(USAGE);
69
+ return 2;
70
+ }
71
+ try
72
+ {
73
+ const document = JSON.parse(await readFile(parsed.input, "utf8")) as OpenApiDocument;
74
+ const code = generateClient(document, parsed.name === undefined ? {} : { apiName: parsed.name });
75
+ if (parsed.out === undefined)
76
+ {
77
+ process.stdout.write(code);
78
+ }
79
+ else
80
+ {
81
+ await writeFile(parsed.out, code);
82
+ }
83
+ }
84
+ catch (error)
85
+ {
86
+ process.stderr.write(`zerolith-client: ${error instanceof Error ? error.message.replace(/^zerolith\/client: /, "") : String(error)}\n`);
87
+ return 1;
88
+ }
89
+ return 0;
90
+ }
91
+
92
+ /** True when node started this module, directly or through the bin link npm installs, which it resolves the way node resolves its entry. */
93
+ function started(): boolean
94
+ {
95
+ const entry = process.argv[1];
96
+ try
97
+ {
98
+ return entry !== undefined && pathToFileURL(realpathSync(entry)).href === import.meta.url;
99
+ }
100
+ catch
101
+ {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ if (started())
107
+ {
108
+ process.exitCode = await main(process.argv.slice(2));
109
+ }