@crvouga/mockingbird-adapter-node 0.1.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/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @crvouga/mockingbird-adapter-node
2
+
3
+ Serve any Mockingbird `FetchAPI` (a provider mock such as `StripeAPI`, or your own) as a real HTTP
4
+ server over `node:http`. Use it when the code under test needs a URL (a subprocess, a browser, an
5
+ SDK you cannot hand a `fetch`). If you can inject `fetch`, call the mock's `fetch` directly instead;
6
+ on Bun, `@crvouga/mockingbird-adapter-bun` is lighter.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @crvouga/mockingbird-adapter-node
12
+ ```
13
+
14
+ Requires Node >=22 (also runs on Bun, which implements `node:http`). ESM only.
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import type { AddressInfo } from "node:net"
20
+ import { serve } from "@crvouga/mockingbird-adapter-node"
21
+ import type { FetchAPI } from "@crvouga/mockingbird-core"
22
+
23
+ // Any FetchAPI works, e.g. `new StripeAPI()` from @crvouga/mockingbird-service-stripe.
24
+ const api: FetchAPI = {
25
+ fetch: async (request) =>
26
+ Response.json({ method: request.method, url: request.url, body: await request.text() }),
27
+ }
28
+
29
+ // Port defaults to 0: the OS picks a free port.
30
+ const server = await serve(api, { host: "127.0.0.1" })
31
+ const { port } = server.address() as AddressInfo
32
+ const baseUrl = `http://127.0.0.1:${port}`
33
+
34
+ const response = await fetch(`${baseUrl}/v1/customers`, { method: "POST", body: "email=a@b.c" })
35
+ console.log(await response.json())
36
+
37
+ // Tear down (e.g. in afterAll). closeAllConnections drops keep-alive sockets so the process exits.
38
+ server.close()
39
+ server.closeAllConnections()
40
+ ```
41
+
42
+ ## API
43
+
44
+ | Export | Signature | Description |
45
+ | --- | --- | --- |
46
+ | `serve` | `(api: FetchAPI, options?: NodeServeOptions) => Promise<http.Server>` | Start listening; resolves once bound, rejects on listen errors (e.g. `EADDRINUSE`). |
47
+
48
+ Types:
49
+
50
+ - `NodeServeOptions`: `{ port?: number; host?: string }`. `port` defaults to `0` (ephemeral; read it
51
+ from `server.address()`); `host` is passed to `server.listen` (Node's default when omitted).
52
+
53
+ Behavior:
54
+
55
+ - Each incoming request is buffered and converted to a Fetch `Request`. The URL is built from the
56
+ `Host` header; leading repeated slashes in the path collapse to one.
57
+ - The body is forwarded only for methods other than `GET`/`HEAD`, and only when non-empty.
58
+ - The `Response` status, headers and body are written back in full (no streaming). Repeated
59
+ `Set-Cookie` headers are preserved, one header line per cookie.
60
+ - Errors thrown by `api.fetch` are not caught by the adapter; return error responses from your
61
+ `FetchAPI` instead.
62
+
63
+ ## Related
64
+
65
+ - `@crvouga/mockingbird-core`: the `FetchAPI` contract.
66
+ - `@crvouga/mockingbird-adapter-bun`: the same adapter for `Bun.serve`.
67
+
68
+ Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).
@@ -0,0 +1,12 @@
1
+ import type { FetchAPI } from "@crvouga/mockingbird-core";
2
+ /** Options for {@link serve}. */
3
+ export type NodeServeOptions = {
4
+ port?: number;
5
+ host?: string;
6
+ };
7
+ /**
8
+ * Serve any Mockingbird {@link FetchAPI} over `node:http`.
9
+ * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).
10
+ */
11
+ export declare const serve: (api: FetchAPI, options?: NodeServeOptions) => Promise<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>>;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAEzD,iCAAiC;AACjC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,GAAU,KAAK,QAAQ,EAAE,UAAS,gBAAqB,gHA+BxE,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ import { createServer } from "node:http";
2
+ /**
3
+ * Serve any Mockingbird {@link FetchAPI} over `node:http`.
4
+ * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).
5
+ */
6
+ export const serve = async (api, options = {}) => {
7
+ const server = createServer(async (req, res) => {
8
+ const chunks = [];
9
+ for await (const chunk of req) {
10
+ chunks.push(Buffer.from(chunk));
11
+ }
12
+ const body = Buffer.concat(chunks);
13
+ const address = server.address();
14
+ const port = typeof address === "object" && address !== null ? address.port : undefined;
15
+ const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
16
+ const raw = req.url ?? "/";
17
+ const url = new URL(raw.replace(/^\/+/, "/"), base);
18
+ const method = req.method ?? "GET";
19
+ const init = { method, headers: req.headers };
20
+ if (method !== "GET" && method !== "HEAD" && body.length > 0) {
21
+ init.body = body;
22
+ }
23
+ const request = new Request(url, init);
24
+ const response = await api.fetch(request);
25
+ // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.
26
+ const headers = Object.fromEntries(response.headers);
27
+ const cookies = response.headers.getSetCookie();
28
+ if (cookies.length > 0)
29
+ headers["set-cookie"] = cookies;
30
+ res.writeHead(response.status, headers);
31
+ res.end(Buffer.from(await response.arrayBuffer()));
32
+ });
33
+ await new Promise((resolve, reject) => {
34
+ server.once("error", reject);
35
+ server.listen(options.port ?? 0, options.host, resolve);
36
+ });
37
+ return server;
38
+ };
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AASxC;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,EAAE,GAAa,EAAE,UAA4B,EAAE,EAAE,EAAE;IAC3E,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC7C,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACjC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAA;QAChC,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;QACvF,MAAM,IAAI,GAAG,UAAU,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,aAAa,IAAI,IAAI,EAAE,EAAE,EAAE,CAAA;QACtE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAA;QAC1B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAA;QACnD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,KAAK,CAAA;QAClC,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAiC,EAAE,CAAA;QACpF,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAClB,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACtC,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACzC,wFAAwF;QACxF,MAAM,OAAO,GAAsC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QACvF,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;QAC/C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAA;QACvD,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACvC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IACF,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC5B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;IACF,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@crvouga/mockingbird-adapter-node",
3
+ "version": "0.1.0",
4
+ "description": "Serve any Mockingbird FetchAPI over node:http.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": true
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/crvouga/mockingbird.git",
25
+ "directory": "packages/adapters/node"
26
+ },
27
+ "homepage": "https://github.com/crvouga/mockingbird/tree/main/packages/adapters/node#readme",
28
+ "keywords": [
29
+ "mockingbird",
30
+ "adapter",
31
+ "node"
32
+ ],
33
+ "mockingbird": {
34
+ "runtime": "node",
35
+ "layer": "adapter"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "lint": "biome check .",
41
+ "test": "bun test",
42
+ "portability": "bun ../../../scripts/portability.ts",
43
+ "pack:check": "bun ../../../scripts/pack-check.ts"
44
+ },
45
+ "dependencies": {
46
+ "@crvouga/mockingbird-core": "0.0.0-development"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "22.20.1",
50
+ "fast-check": "4.9.0",
51
+ "@crvouga/mockingbird-testing": "0.1.0"
52
+ }
53
+ }