counterfact 2.6.0 → 2.8.1
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 +14 -207
- package/bin/README.md +24 -4
- package/bin/counterfact.js +54 -3
- package/dist/app.js +81 -28
- package/dist/counterfact-types/cookie-options.js +1 -0
- package/dist/counterfact-types/counterfact-response.js +7 -0
- package/dist/counterfact-types/example-names.js +1 -0
- package/dist/counterfact-types/example.js +1 -0
- package/dist/counterfact-types/generic-response-builder.js +1 -0
- package/dist/counterfact-types/http-status-code.js +1 -0
- package/dist/counterfact-types/if-has-key.js +1 -0
- package/dist/counterfact-types/index.js +0 -1
- package/dist/counterfact-types/maybe-promise.js +1 -0
- package/dist/counterfact-types/media-type.js +1 -0
- package/dist/counterfact-types/omit-all.js +1 -0
- package/dist/counterfact-types/omit-value-when-never.js +1 -0
- package/dist/counterfact-types/open-api-content.js +1 -0
- package/dist/counterfact-types/open-api-operation.js +1 -0
- package/dist/counterfact-types/open-api-parameters.js +1 -0
- package/dist/counterfact-types/open-api-response.js +1 -0
- package/dist/counterfact-types/random-function.js +1 -0
- package/dist/counterfact-types/response-builder-factory.js +1 -0
- package/dist/counterfact-types/response-builder.js +1 -0
- package/dist/counterfact-types/wide-operation-argument.js +1 -0
- package/dist/counterfact-types/wide-response-builder.js +1 -0
- package/dist/migrate/update-route-types.js +2 -3
- package/dist/repl/raw-http-client.js +19 -0
- package/dist/repl/repl.js +116 -4
- package/dist/repl/route-builder.js +68 -0
- package/dist/server/constants.js +8 -0
- package/dist/server/context-registry.js +70 -1
- package/dist/server/counterfact-types/cookie-options.ts +14 -0
- package/dist/server/counterfact-types/counterfact-response.ts +15 -0
- package/dist/server/counterfact-types/example-names.ts +13 -0
- package/dist/server/counterfact-types/example.ts +10 -0
- package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
- package/dist/server/counterfact-types/http-status-code.ts +62 -0
- package/dist/server/counterfact-types/if-has-key.ts +19 -0
- package/dist/server/counterfact-types/index.ts +20 -338
- package/dist/server/counterfact-types/maybe-promise.ts +6 -0
- package/dist/server/counterfact-types/media-type.ts +6 -0
- package/dist/server/counterfact-types/omit-all.ts +11 -0
- package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/server/counterfact-types/open-api-content.ts +8 -0
- package/dist/server/counterfact-types/open-api-operation.ts +36 -0
- package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
- package/dist/server/counterfact-types/open-api-response.ts +22 -0
- package/dist/server/counterfact-types/random-function.ts +9 -0
- package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/server/counterfact-types/response-builder.ts +31 -0
- package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
- package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
- package/dist/server/create-koa-app.js +28 -24
- package/dist/server/determine-module-kind.js +13 -0
- package/dist/server/dispatcher.js +64 -5
- package/dist/server/file-discovery.js +20 -9
- package/dist/server/is-proxy-enabled-for-path.js +12 -0
- package/dist/server/json-to-xml.js +11 -1
- package/dist/server/koa-middleware.js +25 -2
- package/dist/server/load-openapi-document.js +6 -0
- package/dist/server/module-dependency-graph.js +25 -0
- package/dist/server/module-loader.js +112 -17
- package/dist/server/module-tree.js +36 -0
- package/dist/server/openapi-document.js +69 -0
- package/dist/server/openapi-middleware.js +34 -5
- package/dist/server/openapi-watcher.js +35 -0
- package/dist/server/registry.js +89 -0
- package/dist/server/request-validator.js +3 -7
- package/dist/server/response-builder.js +18 -0
- package/dist/server/response-validator.js +58 -0
- package/dist/server/scenario-registry.js +55 -0
- package/dist/server/tools.js +29 -2
- package/dist/server/transpiler.js +23 -9
- package/dist/typescript-generator/code-generator.js +117 -4
- package/dist/typescript-generator/coder.js +80 -2
- package/dist/typescript-generator/operation-coder.js +13 -5
- package/dist/typescript-generator/operation-type-coder.js +40 -53
- package/dist/typescript-generator/parameters-type-coder.js +2 -4
- package/dist/typescript-generator/prune.js +2 -1
- package/dist/typescript-generator/read-only-comments.js +1 -1
- package/dist/typescript-generator/repository.js +76 -20
- package/dist/typescript-generator/requirement.js +77 -1
- package/dist/typescript-generator/reserved-words.js +50 -0
- package/dist/typescript-generator/scenario-file-generator.js +235 -0
- package/dist/typescript-generator/script.js +70 -7
- package/dist/typescript-generator/specification.js +27 -0
- package/dist/util/ensure-directory-exists.js +7 -0
- package/dist/util/forward-slash-path.js +63 -0
- package/dist/util/load-config-file.js +44 -0
- package/dist/util/read-file.js +11 -0
- package/dist/util/runtime-can-execute-erasable-ts.js +11 -0
- package/dist/util/windows-escape.js +18 -0
- package/package.json +9 -10
- package/dist/client/README.md +0 -14
- package/dist/client/index.html.hbs +0 -244
- package/dist/client/rapi-doc.html.hbs +0 -36
- package/dist/server/page-middleware.js +0 -23
- package/dist/typescript-generator/generate.js +0 -63
package/README.md
CHANGED
|
@@ -8,223 +8,30 @@
|
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
You've used mock servers. You know where they stop being useful: static responses, no shared state, no way to inject a failure mid-run, no control without restarting. Counterfact picks up where they leave off.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Instead of waiting for a backend—or wiring up brittle mocks—it generates a server where every endpoint is backed by TypeScript code. Responses are valid by default, but fully customizable, and the system is stateful, interactive, and hot-reloading.
|
|
16
|
-
|
|
17
|
-
It’s not just a mock server.
|
|
18
|
-
|
|
19
|
-
It’s a controllable API environment you can shape in real time.
|
|
20
|
-
|
|
21
|
-
> Built by Patrick McElhaney · Currently available for the right opportunity → https://patrickmcelhaney.org
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Quick Start
|
|
13
|
+
Point it at an OpenAPI spec and it generates TypeScript handlers for every endpoint—type-safe, hot-reloading, sharing state across routes. A built-in REPL gives you a live control surface: seed data, trigger error conditions, proxy individual routes to a real backend, all on a running server. Whether you're a frontend developer waiting on a backend, a test engineer who needs clean reproducible state, or an AI agent that needs a stable API to work against, Counterfact is the simulator that doesn't plateau.
|
|
26
14
|
|
|
27
15
|
```sh
|
|
28
16
|
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
|
|
29
17
|
```
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
> **Requires Node ≥ 17.0.0**
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Features
|
|
38
|
-
|
|
39
|
-
- ⚡ **Zero config** — one command to generate and start a simulated api
|
|
40
|
-
- 🔒 **Type-safe by default** — route handlers are typed directly from your OpenAPI spec
|
|
41
|
-
- 🔄 **Hot reload** — edit route files while the server is running; state is preserved
|
|
42
|
-
- 🧠 **State management** — POST data and GET it back; share state across routes with context objects
|
|
43
|
-
- 🖥 **Live REPL** — inspect and modify server state from your terminal without touching files
|
|
44
|
-
- 🔀 **Hybrid proxy** — route some paths to the real API while mocking others
|
|
45
|
-
- 🎲 **Smart random data** — uses OpenAPI examples and schema metadata to generate realistic responses
|
|
46
|
-
- 📖 **Built-in Swagger UI** — browse and test your mock API in a browser automatically
|
|
47
|
-
- 🔌 **Middleware support** — add custom middleware with `_.middleware.ts` files
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## How It Works
|
|
52
|
-
|
|
53
|
-
1. **Generate** — Counterfact reads your OpenAPI spec and creates a `routes/` directory with a `.ts` file for each path, plus a `types/` directory with fully typed request/response interfaces.
|
|
54
|
-
2. **Customize** — Edit the route files to return exactly the data your frontend needs. The full power of TypeScript is at your disposal.
|
|
55
|
-
3. **Run** — The server hot-reloads on every save. No restart, no lost state.
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Examples
|
|
60
|
-
|
|
61
|
-
### Zero effort: random responses out of the box
|
|
62
|
-
|
|
63
|
-
Generated route files return random, schema-valid responses immediately — no editing required.
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
// mock-api/routes/store/order/{orderID}.ts
|
|
67
|
-
import type { HTTP_GET } from "../../../types/paths/store/order/{orderId}.types.js";
|
|
68
|
-
|
|
69
|
-
export const GET: HTTP_GET = ($) => {
|
|
70
|
-
return $.response[200].random();
|
|
71
|
-
};
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Typed custom responses
|
|
75
|
-
|
|
76
|
-
Replace `.random()` with `.json()` to return specific data. TypeScript (via your IDE's autocomplete) guides you to a valid response.
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import type { HTTP_GET } from "../../../types/paths/store/order/{orderId}.types.js";
|
|
80
|
-
import type { HTTP_DELETE } from "../../../types/paths/store/order/{orderId}.types.js";
|
|
81
|
-
|
|
82
|
-
export const GET: HTTP_GET = ($) => {
|
|
83
|
-
const orders: Record<number, Order> = {
|
|
84
|
-
1: { petId: 100, status: "placed" },
|
|
85
|
-
2: { petId: 999, status: "approved" },
|
|
86
|
-
3: { petId: 1234, status: "delivered" },
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const order = orders[$.path.orderID];
|
|
90
|
-
if (order === undefined) return $.response[404];
|
|
91
|
-
return $.response[200].json(order);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export const DELETE: HTTP_DELETE = ($) => {
|
|
95
|
-
return $.response[200];
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Returning named examples
|
|
100
|
-
|
|
101
|
-
If your OpenAPI spec defines named examples, use `.example(name)` to return a specific one. The name is autocompleted and type-checked from your spec:
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
export const GET: HTTP_GET = ($) => {
|
|
105
|
-
return $.response[200].example("successResponse");
|
|
106
|
-
};
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### State management with plain old objects
|
|
110
|
-
|
|
111
|
-
Use a `_.context.ts` file to share in-memory state across routes. POST data and GET it back, just like a real API.
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
// mock-api/routes/_.context.ts
|
|
115
|
-
export class Context {
|
|
116
|
-
pets: Pet[] = [];
|
|
117
|
-
|
|
118
|
-
addPet(pet: Pet) {
|
|
119
|
-
const id = this.pets.length;
|
|
120
|
-
this.pets.push({ ...pet, id });
|
|
121
|
-
return this.pets[id];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getPetById(id: number) {
|
|
125
|
-
return this.pets[id];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// mock-api/routes/pet.ts
|
|
132
|
-
export const POST: HTTP_POST = ($) => {
|
|
133
|
-
return $.response[200].json($.context.addPet($.body));
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// mock-api/routes/pet/{petId}.ts
|
|
137
|
-
export const GET: HTTP_GET = ($) => {
|
|
138
|
-
const pet = $.context.getPetById($.path.petId);
|
|
139
|
-
if (!pet) return $.response[404].text(`Pet ${$.path.petId} not found.`);
|
|
140
|
-
return $.response[200].json(pet);
|
|
141
|
-
};
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
You can also interact with the context object using a REPL. It's like DevTools on the server side. (See "Live REPL" below.)
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## Key Capabilities
|
|
149
|
-
|
|
150
|
-
### 🔄 Hot Reload
|
|
151
|
-
|
|
152
|
-
Save a route file and the server picks it up instantly — no restart, no lost state. Your in-memory context survives every reload.
|
|
153
|
-
|
|
154
|
-
### 🖥 Live REPL
|
|
155
|
-
|
|
156
|
-
The REPL gives you a JavaScript prompt connected directly to your running server. Inspect state, trigger edge cases, or adjust proxy settings without touching a file.
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
⬣> context.pets.length
|
|
160
|
-
3
|
|
161
|
-
⬣> context.addPet({ name: "Fluffy", photoUrls: [] })
|
|
162
|
-
⬣> client.get("/pet/3")
|
|
163
|
-
⬣> .proxy on /payments # forward /payments to the real API
|
|
164
|
-
⬣> .proxy off # stop all proxying
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### 🔀 Hybrid Proxy
|
|
168
|
-
|
|
169
|
-
Mock the paths that aren't ready yet while forwarding everything else to the real backend. See [Proxying](./docs/usage.md#proxy-peek-a-boo-) for details.
|
|
170
|
-
|
|
171
|
-
```sh
|
|
172
|
-
npx counterfact@latest openapi.yaml mock-api --proxy-url https://api.example.com
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### 🔒 Type Safety
|
|
176
|
-
|
|
177
|
-
Every route handler is typed to match your OpenAPI spec. When the spec changes, regenerating the types surfaces any mismatches at compile time — before they become bugs.
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
export const GET: HTTP_GET = ($) => {
|
|
181
|
-
return $.response[200]
|
|
182
|
-
.header("x-request-id", $.headers["x-request-id"])
|
|
183
|
-
.json({
|
|
184
|
-
id: $.path.userId,
|
|
185
|
-
});
|
|
186
|
-
};
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## CLI Reference
|
|
192
|
-
|
|
193
|
-
```sh
|
|
194
|
-
npx counterfact@latest [openapi.yaml] [destination] [options]
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
| Option | Description |
|
|
198
|
-
| ------------------- | ------------------------------------------- |
|
|
199
|
-
| `--port <number>` | Server port (default: `3100`) |
|
|
200
|
-
| `-o, --open` | Open browser automatically |
|
|
201
|
-
| `-g, --generate` | Generate route and type files |
|
|
202
|
-
| `-w, --watch` | Generate and watch for spec changes |
|
|
203
|
-
| `-s, --serve` | Start the mock server |
|
|
204
|
-
| `-r, --repl` | Start the interactive REPL |
|
|
205
|
-
| `--spec <path>` | Path or URL to the OpenAPI document |
|
|
206
|
-
| `--proxy-url <url>` | Forward all requests to this URL by default |
|
|
207
|
-
| `--prefix <path>` | Base path prefix (e.g. `/api/v1`) |
|
|
208
|
-
| `--no-validate-request` | Disable request validation against the OpenAPI spec |
|
|
209
|
-
|
|
210
|
-
Run `npx counterfact@latest --help` for the full list of options.
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## About the Author
|
|
215
|
-
|
|
216
|
-
Counterfact came out of a pattern I kept seeing: teams are slowed down more by coordination than by code.
|
|
217
|
-
|
|
218
|
-
I’ve spent 25+ years building software and improving how engineering organizations operate across large enterprises, regulated industries, and complex systems. Most of that time, the real constraint wasn’t technology—it was dependency and coordination.
|
|
219
|
-
|
|
220
|
-
Counterfact is one way of removing that friction.
|
|
19
|
+
> Requires Node ≥ 22.0.0
|
|
221
20
|
|
|
222
|
-
|
|
21
|
+
## Go deeper
|
|
223
22
|
|
|
224
|
-
|
|
23
|
+
| | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| [Getting started](./docs/getting-started.md) | Detailed walkthrough with state, REPL, and proxy |
|
|
26
|
+
| [Usage](./docs/usage.md) | Feature index: routes, context, REPL, proxy, middleware, and more |
|
|
27
|
+
| [Patterns](./docs/patterns/index.md) | Failures, latency, AI sandboxes, integration tests |
|
|
28
|
+
| [Reference](./docs/reference.md) | `$` API, CLI flags, architecture |
|
|
29
|
+
| [How it compares](./docs/comparison.md) | json-server, WireMock, Prism, Microcks, MSW |
|
|
30
|
+
| [FAQ](./docs/faq.md) | State, types, regeneration |
|
|
31
|
+
| [Petstore example](https://github.com/counterfact/example-petstore) | Full working example |
|
|
225
32
|
|
|
226
33
|
<div align="center" markdown="1">
|
|
227
34
|
|
|
228
|
-
[
|
|
35
|
+
[Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md)
|
|
229
36
|
|
|
230
37
|
</div>
|
package/bin/README.md
CHANGED
|
@@ -18,11 +18,13 @@ npx counterfact@latest openapi.yaml ./api [options]
|
|
|
18
18
|
│ counterfact.js │
|
|
19
19
|
│ │
|
|
20
20
|
│ 1. Parse args (Commander) │
|
|
21
|
-
│ 2.
|
|
22
|
-
│ 3.
|
|
23
|
-
│ 4.
|
|
21
|
+
│ 2. Load counterfact.yaml │
|
|
22
|
+
│ 3. Merge config + args │
|
|
23
|
+
│ 4. Resolve paths │
|
|
24
|
+
│ 5. Build Config object │
|
|
25
|
+
│ 6. Run migrations if │
|
|
24
26
|
│ old layout detected │
|
|
25
|
-
│
|
|
27
|
+
│ 7. Call start(config) │
|
|
26
28
|
│ from src/app.ts │
|
|
27
29
|
└────────────────────────────┘
|
|
28
30
|
```
|
|
@@ -42,5 +44,23 @@ npx counterfact@latest openapi.yaml ./api [options]
|
|
|
42
44
|
| `--prefix <path>` | Base path prefix for all routes (e.g. `/api/v1`) |
|
|
43
45
|
| `--no-update-check` | Disable the npm update check on startup |
|
|
44
46
|
| `--no-validate-request` | Disable request validation against the OpenAPI spec |
|
|
47
|
+
| `--config <path>` | Path to a `counterfact.yaml` config file (default: `counterfact.yaml` in the current directory) |
|
|
45
48
|
|
|
46
49
|
Run `npx counterfact@latest --help` to see the full option list.
|
|
50
|
+
|
|
51
|
+
### Config File
|
|
52
|
+
|
|
53
|
+
Any CLI option can also be specified in a `counterfact.yaml` file in the current working directory. Command-line options always take precedence.
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
# counterfact.yaml
|
|
57
|
+
spec: ./openapi.yaml
|
|
58
|
+
port: 8080
|
|
59
|
+
serve: true
|
|
60
|
+
repl: true
|
|
61
|
+
watch: true
|
|
62
|
+
proxy-url: https://api.example.com
|
|
63
|
+
prefix: /api/v1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `--config <path>` to load a config file from a non-default location.
|
package/bin/counterfact.js
CHANGED
|
@@ -41,7 +41,9 @@ import { PostHog } from "posthog-node";
|
|
|
41
41
|
|
|
42
42
|
const MIN_NODE_VERSION = 17;
|
|
43
43
|
|
|
44
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
Number.parseInt(process.versions.node.split(".")[0], 10) < MIN_NODE_VERSION
|
|
46
|
+
) {
|
|
45
47
|
process.stdout.write(
|
|
46
48
|
`Counterfact works with Node version ${MIN_NODE_VERSION}+. You are running version ${process.version}`,
|
|
47
49
|
);
|
|
@@ -152,6 +154,21 @@ const { updateRouteTypes } = await import(
|
|
|
152
154
|
: "../dist/migrate/update-route-types.js",
|
|
153
155
|
)
|
|
154
156
|
);
|
|
157
|
+
const { loadConfigFile } = await import(
|
|
158
|
+
resolve(
|
|
159
|
+
nativeTs
|
|
160
|
+
? "../src/util/load-config-file.js"
|
|
161
|
+
: "../dist/util/load-config-file.js",
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const { pathResolve } = await import(
|
|
166
|
+
resolve(
|
|
167
|
+
nativeTs
|
|
168
|
+
? "../src/util/forward-slash-path.js"
|
|
169
|
+
: "../dist/util/forward-slash-path.js",
|
|
170
|
+
)
|
|
171
|
+
);
|
|
155
172
|
|
|
156
173
|
const DEFAULT_PORT = 3100;
|
|
157
174
|
|
|
@@ -269,6 +286,31 @@ async function main(source, destination) {
|
|
|
269
286
|
? Promise.resolve()
|
|
270
287
|
: checkForUpdates(CURRENT_VERSION);
|
|
271
288
|
|
|
289
|
+
// Load the config file (counterfact.yaml by default, or --config <path>).
|
|
290
|
+
// CLI options always take precedence over config file settings.
|
|
291
|
+
const configFilePath = nodePath.resolve(options.config ?? "counterfact.yaml");
|
|
292
|
+
const fileConfig = await loadConfigFile(
|
|
293
|
+
configFilePath,
|
|
294
|
+
options.config !== undefined,
|
|
295
|
+
);
|
|
296
|
+
debug("fileConfig: %o", fileConfig);
|
|
297
|
+
|
|
298
|
+
// Apply config file values for any option that was not explicitly set on the
|
|
299
|
+
// command line (i.e. its source is "default" or it was never defined).
|
|
300
|
+
for (const [key, value] of Object.entries(fileConfig)) {
|
|
301
|
+
const optionSource = program.getOptionValueSource(key);
|
|
302
|
+
|
|
303
|
+
if (optionSource !== "cli") {
|
|
304
|
+
options[key] = value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// If the config file specifies a destination and none was given on the CLI,
|
|
309
|
+
// use it (destination has no Commander option — it's a positional argument).
|
|
310
|
+
if (fileConfig.destination !== undefined && destination === ".") {
|
|
311
|
+
destination = String(fileConfig.destination);
|
|
312
|
+
}
|
|
313
|
+
|
|
272
314
|
// --spec takes precedence over the positional [openapi.yaml] argument.
|
|
273
315
|
// When --spec is provided, the [openapi.yaml] positional slot shifts to
|
|
274
316
|
// become the [destination] argument (so `counterfact --spec api.yaml ./api`
|
|
@@ -280,9 +322,9 @@ async function main(source, destination) {
|
|
|
280
322
|
source = options.spec;
|
|
281
323
|
}
|
|
282
324
|
|
|
283
|
-
const destinationPath =
|
|
325
|
+
const destinationPath = pathResolve(destination);
|
|
284
326
|
|
|
285
|
-
const basePath =
|
|
327
|
+
const basePath = pathResolve(destinationPath);
|
|
286
328
|
|
|
287
329
|
// If no action-related option is provided, default to all options
|
|
288
330
|
|
|
@@ -343,6 +385,7 @@ async function main(source, destination) {
|
|
|
343
385
|
startServer: options.serve,
|
|
344
386
|
buildCache: options.buildCache || false,
|
|
345
387
|
validateRequests: options.validateRequest !== false,
|
|
388
|
+
validateResponses: options.validateResponse !== false,
|
|
346
389
|
|
|
347
390
|
watch: {
|
|
348
391
|
routes: options.watch || options.watchRoutes,
|
|
@@ -550,5 +593,13 @@ program
|
|
|
550
593
|
"--no-validate-request",
|
|
551
594
|
"disable request validation against the OpenAPI spec",
|
|
552
595
|
)
|
|
596
|
+
.option(
|
|
597
|
+
"--no-validate-response",
|
|
598
|
+
"disable response validation against the OpenAPI spec",
|
|
599
|
+
)
|
|
600
|
+
.option(
|
|
601
|
+
"--config <path>",
|
|
602
|
+
"path to a counterfact.yaml config file (default: counterfact.yaml in the current directory)",
|
|
603
|
+
)
|
|
553
604
|
.action(main)
|
|
554
605
|
.parse(process.argv);
|
package/dist/app.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import fs, { rm } from "node:fs/promises";
|
|
2
|
-
import nodePath from "node:path";
|
|
3
|
-
import { dereference } from "@apidevtools/json-schema-ref-parser";
|
|
4
|
-
import createDebug from "debug";
|
|
5
2
|
import { createHttpTerminator } from "http-terminator";
|
|
6
3
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
4
|
+
import { createRouteFunction } from "./repl/route-builder.js";
|
|
7
5
|
import { ContextRegistry } from "./server/context-registry.js";
|
|
8
6
|
import { createKoaApp } from "./server/create-koa-app.js";
|
|
9
|
-
import { Dispatcher
|
|
10
|
-
import {
|
|
7
|
+
import { Dispatcher } from "./server/dispatcher.js";
|
|
8
|
+
import { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
11
9
|
import { ModuleLoader } from "./server/module-loader.js";
|
|
12
10
|
import { Registry } from "./server/registry.js";
|
|
11
|
+
import { ScenarioRegistry } from "./server/scenario-registry.js";
|
|
13
12
|
import { Transpiler } from "./server/transpiler.js";
|
|
14
13
|
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
14
|
+
import { ScenarioFileGenerator } from "./typescript-generator/scenario-file-generator.js";
|
|
15
15
|
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
|
|
16
|
-
|
|
16
|
+
import { pathJoin } from "./util/forward-slash-path.js";
|
|
17
|
+
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
18
|
+
export async function runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument) {
|
|
19
|
+
const indexModule = scenarioRegistry.getModule("index");
|
|
20
|
+
if (!indexModule || typeof indexModule["startup"] !== "function") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const scenario$ = {
|
|
24
|
+
context: contextRegistry.find("/"),
|
|
25
|
+
loadContext: (path) => contextRegistry.find(path),
|
|
26
|
+
route: createRouteFunction(config.port, "localhost", openApiDocument),
|
|
27
|
+
routes: {},
|
|
28
|
+
};
|
|
29
|
+
await indexModule["startup"](scenario$);
|
|
30
|
+
}
|
|
17
31
|
const allowedMethods = [
|
|
18
32
|
"all",
|
|
19
33
|
"head",
|
|
@@ -24,17 +38,17 @@ const allowedMethods = [
|
|
|
24
38
|
"patch",
|
|
25
39
|
"options",
|
|
26
40
|
];
|
|
27
|
-
export async function loadOpenApiDocument(source) {
|
|
28
|
-
try {
|
|
29
|
-
return (await dereference(source));
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
debug("could not load OpenAPI document from %s: %o", source, error);
|
|
33
|
-
const details = error instanceof Error ? error.message : String(error);
|
|
34
|
-
throw new Error(`Could not load the OpenAPI spec from "${source}".\n${details}`, { cause: error });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
41
|
const mswHandlers = {};
|
|
42
|
+
/**
|
|
43
|
+
* Dispatches a single MSW (Mock Service Worker) intercepted request to the
|
|
44
|
+
* matching Counterfact route handler registered via {@link createMswHandlers}.
|
|
45
|
+
*
|
|
46
|
+
* @param request - The intercepted request, including the HTTP method, path,
|
|
47
|
+
* headers, query, body, and a `rawPath` that preserves the original URL
|
|
48
|
+
* before base-path stripping.
|
|
49
|
+
* @returns The response produced by the matching handler, or a 404 object when
|
|
50
|
+
* no handler has been registered for the given method and path.
|
|
51
|
+
*/
|
|
38
52
|
export async function handleMswRequest(request) {
|
|
39
53
|
const { method, rawPath } = request;
|
|
40
54
|
const handler = mswHandlers[`${method}:${rawPath}`];
|
|
@@ -44,15 +58,25 @@ export async function handleMswRequest(request) {
|
|
|
44
58
|
console.warn(`No handler found for ${method} ${rawPath}`);
|
|
45
59
|
return { error: `No handler found for ${method} ${rawPath}`, status: 404 };
|
|
46
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Loads an OpenAPI document, registers all routes from it as MSW handlers, and
|
|
63
|
+
* returns the list of registered routes so callers (e.g. Vitest Browser mode)
|
|
64
|
+
* can mount them on their own request-interception layer.
|
|
65
|
+
*
|
|
66
|
+
* @param config - Counterfact configuration; `openApiPath` and `basePath` are
|
|
67
|
+
* the most important fields for this function.
|
|
68
|
+
* @param ModuleLoaderClass - Injectable module-loader constructor, primarily
|
|
69
|
+
* used in tests to substitute a test-friendly implementation.
|
|
70
|
+
* @returns An array of `{ method, path }` objects describing every registered
|
|
71
|
+
* MSW handler.
|
|
72
|
+
*/
|
|
47
73
|
export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader) {
|
|
48
74
|
// TODO: For some reason the Vitest Custom Commands needed by Vitest Browser mode fail on fs.readFile when they are called from the nested loadOpenApiDocument function.
|
|
49
75
|
// If we "pre-read" the file here it works. This is a workaround to avoid the issue.
|
|
50
76
|
await fs.readFile(config.openApiPath);
|
|
51
77
|
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
|
|
52
78
|
const modulesPath = config.basePath;
|
|
53
|
-
const compiledPathsDirectory =
|
|
54
|
-
.join(modulesPath, ".cache")
|
|
55
|
-
.replaceAll("\\", "/");
|
|
79
|
+
const compiledPathsDirectory = pathJoin(modulesPath, ".cache");
|
|
56
80
|
const registry = new Registry();
|
|
57
81
|
const contextRegistry = new ContextRegistry();
|
|
58
82
|
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
|
|
@@ -75,41 +99,69 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
75
99
|
});
|
|
76
100
|
return handlers;
|
|
77
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Creates and configures a full Counterfact server instance.
|
|
104
|
+
*
|
|
105
|
+
* Sets up the route registry, context registry, scenario registry, code
|
|
106
|
+
* generator, transpiler, module loader, Koa application, and OpenAPI watcher.
|
|
107
|
+
* The returned object exposes handles for starting the server, stopping it, and
|
|
108
|
+
* launching the interactive REPL.
|
|
109
|
+
*
|
|
110
|
+
* @param config - Runtime configuration (port, paths, feature flags, etc.).
|
|
111
|
+
* @returns An object containing the configured sub-systems and two entry-point
|
|
112
|
+
* functions:
|
|
113
|
+
* - `start(options)` — generates/watches code and optionally starts the HTTP
|
|
114
|
+
* server; returns a `stop()` handle.
|
|
115
|
+
* - `startRepl()` — launches the interactive Node.js REPL connected to the
|
|
116
|
+
* live server state.
|
|
117
|
+
*/
|
|
78
118
|
export async function counterfact(config) {
|
|
79
119
|
const modulesPath = config.basePath;
|
|
80
120
|
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
81
|
-
const compiledPathsDirectory =
|
|
82
|
-
.join(modulesPath, nativeTs ? "routes" : ".cache")
|
|
83
|
-
.replaceAll("\\", "/");
|
|
121
|
+
const compiledPathsDirectory = pathJoin(modulesPath, nativeTs ? "routes" : ".cache");
|
|
84
122
|
if (!nativeTs) {
|
|
85
123
|
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
86
124
|
}
|
|
87
125
|
const registry = new Registry();
|
|
88
126
|
const contextRegistry = new ContextRegistry();
|
|
127
|
+
const scenarioRegistry = new ScenarioRegistry();
|
|
128
|
+
const scenarioFileGenerator = new ScenarioFileGenerator(modulesPath);
|
|
89
129
|
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
|
|
90
130
|
const openApiDocument = config.openApiPath === "_"
|
|
91
131
|
? undefined
|
|
92
132
|
: await loadOpenApiDocument(config.openApiPath);
|
|
93
133
|
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
|
|
94
|
-
const transpiler = new Transpiler(
|
|
95
|
-
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
|
|
96
|
-
const
|
|
97
|
-
|
|
134
|
+
const transpiler = new Transpiler(pathJoin(modulesPath, "routes"), compiledPathsDirectory, "commonjs");
|
|
135
|
+
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry, pathJoin(modulesPath, "scenarios"), scenarioRegistry);
|
|
136
|
+
const koaApp = createKoaApp({
|
|
137
|
+
config,
|
|
138
|
+
contextRegistry,
|
|
139
|
+
dispatcher,
|
|
140
|
+
registry,
|
|
141
|
+
});
|
|
98
142
|
async function start(options) {
|
|
99
143
|
const { generate, startServer, watch, buildCache } = options;
|
|
100
144
|
if (config.openApiPath !== "_" && (generate.routes || generate.types)) {
|
|
101
145
|
await codeGenerator.generate();
|
|
102
146
|
}
|
|
147
|
+
if (generate.types) {
|
|
148
|
+
await scenarioFileGenerator.generate();
|
|
149
|
+
}
|
|
103
150
|
if (config.openApiPath !== "_" && (watch.routes || watch.types)) {
|
|
104
151
|
await codeGenerator.watch();
|
|
105
152
|
}
|
|
153
|
+
if (watch.types) {
|
|
154
|
+
await scenarioFileGenerator.watch();
|
|
155
|
+
}
|
|
106
156
|
let httpTerminator;
|
|
107
157
|
if (startServer) {
|
|
158
|
+
await openApiDocument?.watch();
|
|
108
159
|
if (!nativeTs) {
|
|
109
160
|
await transpiler.watch();
|
|
110
161
|
}
|
|
111
162
|
await moduleLoader.load();
|
|
112
163
|
await moduleLoader.watch();
|
|
164
|
+
await runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument);
|
|
113
165
|
const server = koaApp.listen({
|
|
114
166
|
port: config.port,
|
|
115
167
|
});
|
|
@@ -125,8 +177,10 @@ export async function counterfact(config) {
|
|
|
125
177
|
return {
|
|
126
178
|
async stop() {
|
|
127
179
|
await codeGenerator.stopWatching();
|
|
180
|
+
await scenarioFileGenerator.stopWatching();
|
|
128
181
|
await transpiler.stopWatching();
|
|
129
182
|
await moduleLoader.stopWatching();
|
|
183
|
+
await openApiDocument?.stopWatching();
|
|
130
184
|
await httpTerminator?.terminate();
|
|
131
185
|
},
|
|
132
186
|
};
|
|
@@ -134,10 +188,9 @@ export async function counterfact(config) {
|
|
|
134
188
|
return {
|
|
135
189
|
contextRegistry,
|
|
136
190
|
koaApp,
|
|
137
|
-
koaMiddleware: middleware,
|
|
138
191
|
registry,
|
|
139
192
|
start,
|
|
140
193
|
startRepl: () => startReplServer(contextRegistry, registry, config, undefined, // use the default print function (stdout)
|
|
141
|
-
openApiDocument),
|
|
194
|
+
openApiDocument, scenarioRegistry),
|
|
142
195
|
};
|
|
143
196
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import createDebug from "debug";
|
|
4
|
+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
|
|
4
5
|
import { OperationTypeCoder, } from "../typescript-generator/operation-type-coder.js";
|
|
5
6
|
import { Specification } from "../typescript-generator/specification.js";
|
|
6
7
|
const debug = createDebug("counterfact:migrate:update-route-types");
|
|
@@ -180,9 +181,7 @@ async function processRouteDirectory(routesDir, currentPath, mapping) {
|
|
|
180
181
|
}
|
|
181
182
|
else if (entry.name.endsWith(".ts") && entry.name !== "_.context.ts") {
|
|
182
183
|
// Process TypeScript route files (skip context files)
|
|
183
|
-
const routePath = relativePath
|
|
184
|
-
.replace(/\.ts$/, "")
|
|
185
|
-
.replaceAll("\\", "/");
|
|
184
|
+
const routePath = toForwardSlashPath(relativePath.replace(/\.ts$/, ""));
|
|
186
185
|
const methodMap = mapping.get(routePath);
|
|
187
186
|
if (methodMap) {
|
|
188
187
|
const wasUpdated = await updateRouteFile(absolutePath, methodMap);
|