counterfact 2.6.0 → 2.7.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.
Files changed (73) hide show
  1. package/README.md +103 -141
  2. package/bin/README.md +24 -4
  3. package/bin/counterfact.js +44 -1
  4. package/dist/app.js +15 -16
  5. package/dist/counterfact-types/cookie-options.js +1 -0
  6. package/dist/counterfact-types/counterfact-response.js +7 -0
  7. package/dist/counterfact-types/example-names.js +1 -0
  8. package/dist/counterfact-types/example.js +1 -0
  9. package/dist/counterfact-types/generic-response-builder.js +1 -0
  10. package/dist/counterfact-types/http-status-code.js +1 -0
  11. package/dist/counterfact-types/if-has-key.js +1 -0
  12. package/dist/counterfact-types/index.js +0 -1
  13. package/dist/counterfact-types/maybe-promise.js +1 -0
  14. package/dist/counterfact-types/media-type.js +1 -0
  15. package/dist/counterfact-types/omit-all.js +1 -0
  16. package/dist/counterfact-types/omit-value-when-never.js +1 -0
  17. package/dist/counterfact-types/open-api-content.js +1 -0
  18. package/dist/counterfact-types/open-api-operation.js +1 -0
  19. package/dist/counterfact-types/open-api-parameters.js +1 -0
  20. package/dist/counterfact-types/open-api-response.js +1 -0
  21. package/dist/counterfact-types/random-function.js +1 -0
  22. package/dist/counterfact-types/response-builder-factory.js +1 -0
  23. package/dist/counterfact-types/response-builder.js +1 -0
  24. package/dist/counterfact-types/wide-operation-argument.js +1 -0
  25. package/dist/counterfact-types/wide-response-builder.js +1 -0
  26. package/dist/repl/repl.js +96 -3
  27. package/dist/server/context-registry.js +17 -1
  28. package/dist/server/counterfact-types/cookie-options.ts +14 -0
  29. package/dist/server/counterfact-types/counterfact-response.ts +15 -0
  30. package/dist/server/counterfact-types/example-names.ts +13 -0
  31. package/dist/server/counterfact-types/example.ts +10 -0
  32. package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
  33. package/dist/server/counterfact-types/http-status-code.ts +62 -0
  34. package/dist/server/counterfact-types/if-has-key.ts +19 -0
  35. package/dist/server/counterfact-types/index.ts +20 -338
  36. package/dist/server/counterfact-types/maybe-promise.ts +6 -0
  37. package/dist/server/counterfact-types/media-type.ts +6 -0
  38. package/dist/server/counterfact-types/omit-all.ts +11 -0
  39. package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
  40. package/dist/server/counterfact-types/open-api-content.ts +8 -0
  41. package/dist/server/counterfact-types/open-api-operation.ts +36 -0
  42. package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
  43. package/dist/server/counterfact-types/open-api-response.ts +22 -0
  44. package/dist/server/counterfact-types/random-function.ts +9 -0
  45. package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
  46. package/dist/server/counterfact-types/response-builder.ts +31 -0
  47. package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
  48. package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
  49. package/dist/server/create-koa-app.js +1 -20
  50. package/dist/server/dispatcher.js +18 -5
  51. package/dist/server/json-to-xml.js +1 -1
  52. package/dist/server/koa-middleware.js +7 -1
  53. package/dist/server/load-openapi-document.js +13 -0
  54. package/dist/server/module-loader.js +76 -4
  55. package/dist/server/openapi-watcher.js +35 -0
  56. package/dist/server/request-validator.js +3 -7
  57. package/dist/server/response-builder.js +3 -0
  58. package/dist/server/response-validator.js +58 -0
  59. package/dist/server/scenario-registry.js +29 -0
  60. package/dist/server/tools.js +2 -2
  61. package/dist/typescript-generator/coder.js +4 -2
  62. package/dist/typescript-generator/generate.js +155 -0
  63. package/dist/typescript-generator/operation-coder.js +1 -1
  64. package/dist/typescript-generator/operation-type-coder.js +1 -49
  65. package/dist/typescript-generator/read-only-comments.js +1 -1
  66. package/dist/typescript-generator/requirement.js +8 -1
  67. package/dist/typescript-generator/reserved-words.js +50 -0
  68. package/dist/util/load-config-file.js +44 -0
  69. package/package.json +7 -8
  70. package/dist/client/README.md +0 -14
  71. package/dist/client/index.html.hbs +0 -244
  72. package/dist/client/rapi-doc.html.hbs +0 -36
  73. package/dist/server/page-middleware.js +0 -23
package/README.md CHANGED
@@ -4,227 +4,189 @@
4
4
 
5
5
  <br>
6
6
 
7
- ![MIT License](https://img.shields.io/badge/license-MIT-blue) [![TypeScript](./typescript-badge.png)](https://github.com/ellerbrock/typescript-badges/) [![Coverage Status](https://coveralls.io/repos/github/pmcelhaney/counterfact/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact)
7
+ **Your backend isn't ready. Your frontend can't wait.**
8
8
 
9
- </div>
9
+ **Counterfact turns your OpenAPI spec into a live, stateful API you can program in TypeScript.**
10
10
 
11
- ---
11
+ <br>
12
12
 
13
- **Counterfact instantly turns an [OpenAPI/Swagge](https://www.openapis.org) spec into a live, working API you can run locally.**
13
+ ![MIT License](https://img.shields.io/badge/license-MIT-blue) [![TypeScript](./typescript-badge.png)](https://github.com/ellerbrock/typescript-badges/) [![Coverage Status](https://coveralls.io/repos/github/pmcelhaney/counterfact/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact)
14
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.
15
+ </div>
16
16
 
17
- It’s not just a mock server.
17
+ This is a five-minute walkthrough. By the end, youll have a **stateful, type-safe, hot-reloading API simulator** running locally—and you’ll understand why it’s different from traditional mock servers.
18
18
 
19
- It’s a controllable API environment you can shape in real time.
19
+ Built for frontend developers, test engineers, and AI agents that need a predictable API to work against.
20
20
 
21
- > Built by Patrick McElhaney · Currently available for the right opportunity → https://patrickmcelhaney.org
22
21
 
23
- ---
24
22
 
25
- ## Quick Start
23
+ ## Minute 1 — Start the server
26
24
 
27
25
  ```sh
28
26
  npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
29
27
  ```
30
28
 
31
- That's it. Counterfact reads your OpenAPI spec, generates TypeScript route files in `api/`, and starts a mock server — all in one command. Point it at your own spec instead of the Petstore whenever you're ready.
32
-
33
- > **Requires Node ≥ 17.0.0**
29
+ > **Requires Node 22.0.0**
34
30
 
35
- ---
31
+ That’s it.
36
32
 
37
- ## Features
33
+ Counterfact reads your spec, generates a TypeScript handler for every endpoint, and starts a server at `http://localhost:3100`.
38
34
 
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
35
+ Open `http://localhost:3100/counterfact/swagger/`.
48
36
 
49
- ---
37
+ Every endpoint is already live, returning random, schema-valid responses. No code written yet.
50
38
 
51
- ## How It Works
52
39
 
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
40
 
57
- ---
41
+ ## Minute 2 — Make a route return real data
58
42
 
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.
43
+ Open the generated file for `GET /pet/{petId}`:
64
44
 
65
45
  ```ts
66
- // mock-api/routes/store/order/{orderID}.ts
67
- import type { HTTP_GET } from "../../../types/paths/store/order/{orderId}.types.js";
46
+ import type { HTTP_GET } from "../../types/paths/pet/{petId}.types.js";
68
47
 
69
- export const GET: HTTP_GET = ($) => {
70
- return $.response[200].random();
71
- };
48
+ export const GET: HTTP_GET = ($) => $.response[200].random();
72
49
  ```
73
50
 
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.
51
+ Replace `.random()` with your own logic:
77
52
 
78
53
  ```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
54
  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];
55
+ if ($.path.petId === 99) {
56
+ return $.response[404].text("Pet not found");
57
+ }
58
+ return $.response[200].json({
59
+ id: $.path.petId,
60
+ name: "Fluffy",
61
+ status: "available",
62
+ photoUrls: []
63
+ });
96
64
  };
97
65
  ```
98
66
 
99
- ### Returning named examples
67
+ Save the file. The server reloads instantly—no restart, no lost state.
100
68
 
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:
69
+ TypeScript enforces the contract. If your response doesn’t match the spec, you’ll know before you make the request.
102
70
 
103
- ```ts
104
- export const GET: HTTP_GET = ($) => {
105
- return $.response[200].example("successResponse");
106
- };
107
- ```
71
+ ## Minute 3 — Add state that survives across requests
108
72
 
109
- ### State management with plain old objects
73
+ Real APIs have memory. Yours should too.
110
74
 
111
- Use a `_.context.ts` file to share in-memory state across routes. POST data and GET it back, just like a real API.
75
+ Create `api/routes/_.context.ts`:
112
76
 
113
77
  ```ts
114
- // mock-api/routes/_.context.ts
78
+ import type { Pet } from "../types/components/pet.types.js";
79
+
115
80
  export class Context {
116
- pets: Pet[] = [];
81
+ private pets = new Map<number, Pet>();
82
+ private nextId = 1;
117
83
 
118
- addPet(pet: Pet) {
119
- const id = this.pets.length;
120
- this.pets.push({ ...pet, id });
121
- return this.pets[id];
84
+ add(data: Omit<Pet, "id">): Pet {
85
+ const pet = { ...data, id: this.nextId++ };
86
+ this.pets.set(pet.id, pet);
87
+ return pet;
122
88
  }
123
89
 
124
- getPetById(id: number) {
125
- return this.pets[id];
126
- }
90
+ get(id: number): Pet | undefined { return this.pets.get(id); }
91
+ list(): Pet[] { return [...this.pets.values()]; }
92
+ remove(id: number): void { this.pets.delete(id); }
127
93
  }
128
94
  ```
129
95
 
130
- ```ts
131
- // mock-api/routes/pet.ts
132
- export const POST: HTTP_POST = ($) => {
133
- return $.response[200].json($.context.addPet($.body));
134
- };
96
+ Use it in your routes:
135
97
 
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
- };
98
+ ```ts
99
+ export const GET: HTTP_GET = ($) => $.response[200].json($.context.list());
100
+ export const POST: HTTP_POST = ($) => $.response[200].json($.context.add($.body));
142
101
  ```
143
102
 
144
- You can also interact with the context object using a REPL. It's like DevTools on the server side. (See "Live REPL" below.)
103
+ Now your API behaves like a real system:
104
+ - POST creates data
105
+ - GET returns it
106
+ - DELETE removes it
107
+
108
+ State survives hot reloads. Restarting resets everything—perfect for clean test runs.
145
109
 
146
- ---
147
110
 
148
- ## Key Capabilities
149
111
 
150
- ### 🔄 Hot Reload
112
+ ## Minute 4 — Control the system at runtime (REPL)
151
113
 
152
- Save a route file and the server picks it up instantly — no restart, no lost state. Your in-memory context survives every reload.
114
+ This is where Counterfact becomes more than a mock.
153
115
 
154
- ### 🖥 Live REPL
116
+ The built-in REPL lets you inspect and control the system while it’s running.
155
117
 
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.
118
+ Seed data:
157
119
 
158
120
  ```
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
121
+ ⬣> context.add({ name: "Fluffy", status: "available", photoUrls: [] })
122
+ ⬣> context.add({ name: "Rex", status: "pending", photoUrls: [] })
165
123
  ```
166
124
 
167
- ### 🔀 Hybrid Proxy
125
+ Make requests:
168
126
 
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.
127
+ ```
128
+ ⬣> client.get("/pet/1")
129
+ ```
170
130
 
171
- ```sh
172
- npx counterfact@latest openapi.yaml mock-api --proxy-url https://api.example.com
131
+ Simulate failures instantly:
132
+
133
+ ```
134
+ ⬣> context.rateLimitExceeded = true
135
+ ⬣> client.get("/pet/1")
136
+ { status: 429, body: "Too Many Requests" }
173
137
  ```
174
138
 
175
- ### 🔒 Type Safety
139
+ No HTTP scripts. No restarts. Just direct control.
176
140
 
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
141
 
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
142
 
189
- ---
143
+ ## Minute 5 — Proxy to the real backend
144
+
145
+ When parts of your backend are ready, forward them through.
190
146
 
191
- ## CLI Reference
147
+ Everything else stays simulated.
192
148
 
193
149
  ```sh
194
- npx counterfact@latest [openapi.yaml] [destination] [options]
150
+ npx counterfact@latest openapi.yaml api --proxy-url https://api.example.com
195
151
  ```
196
152
 
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 |
153
+ Toggle paths live:
154
+
155
+ ```
156
+ ⬣> .proxy on /payments
157
+ ⬣> .proxy on /auth
158
+ ⬣> .proxy off
159
+ ```
209
160
 
210
- Run `npx counterfact@latest --help` for the full list of options.
211
161
 
212
- ---
213
162
 
214
- ## About the Author
163
+ ## What you just built
215
164
 
216
- Counterfact came out of a pattern I kept seeing: teams are slowed down more by coordination than by code.
165
+ In five minutes, you turned a static spec into a working system:
217
166
 
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.
167
+ - **Schema-valid responses** from the moment it starts
168
+ - **Type-safe handlers** generated from your spec
169
+ - **Shared state** across all routes
170
+ - **Hot reloading** without losing that state
171
+ - A **live control surface (REPL)** for runtime behavior
172
+ - **Selective proxying** to real services
219
173
 
220
- Counterfact is one way of removing that friction.
221
174
 
222
- I’m currently available — not for long.
223
175
 
224
- https://patrickmcelhaney.org
176
+ ## Go deeper
177
+
178
+ | | |
179
+ |---|---|
180
+ | [Getting started](./docs/getting-started.md) | Detailed walkthrough with state, REPL, and proxy |
181
+ | [Usage](./docs/usage.md) | Feature index: routes, context, REPL, proxy, middleware, and more |
182
+ | [Patterns](./docs/patterns/index.md) | Failures, latency, AI sandboxes, integration tests |
183
+ | [Reference](./docs/reference.md) | `$` API, CLI flags, architecture |
184
+ | [How it compares](./docs/comparison.md) | json-server, WireMock, Prism, Microcks, MSW |
185
+ | [FAQ](./docs/faq.md) | State, types, regeneration |
186
+ | [Petstore example](https://github.com/counterfact/example-petstore) | Full working example |
225
187
 
226
188
  <div align="center" markdown="1">
227
189
 
228
- [Documentation](./docs/usage.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md)
190
+ [Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md)
229
191
 
230
- </div>
192
+ </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. Resolve paths
22
- │ 3. Build Config object
23
- │ 4. Run migrations if
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
- 5. Call start(config) │
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.
@@ -41,7 +41,9 @@ import { PostHog } from "posthog-node";
41
41
 
42
42
  const MIN_NODE_VERSION = 17;
43
43
 
44
- if (Number.parseInt(process.versions.node.split("."), 10) < MIN_NODE_VERSION) {
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,13 @@ 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
+ );
155
164
 
156
165
  const DEFAULT_PORT = 3100;
157
166
 
@@ -269,6 +278,31 @@ async function main(source, destination) {
269
278
  ? Promise.resolve()
270
279
  : checkForUpdates(CURRENT_VERSION);
271
280
 
281
+ // Load the config file (counterfact.yaml by default, or --config <path>).
282
+ // CLI options always take precedence over config file settings.
283
+ const configFilePath = nodePath.resolve(options.config ?? "counterfact.yaml");
284
+ const fileConfig = await loadConfigFile(
285
+ configFilePath,
286
+ options.config !== undefined,
287
+ );
288
+ debug("fileConfig: %o", fileConfig);
289
+
290
+ // Apply config file values for any option that was not explicitly set on the
291
+ // command line (i.e. its source is "default" or it was never defined).
292
+ for (const [key, value] of Object.entries(fileConfig)) {
293
+ const optionSource = program.getOptionValueSource(key);
294
+
295
+ if (optionSource !== "cli") {
296
+ options[key] = value;
297
+ }
298
+ }
299
+
300
+ // If the config file specifies a destination and none was given on the CLI,
301
+ // use it (destination has no Commander option — it's a positional argument).
302
+ if (fileConfig.destination !== undefined && destination === ".") {
303
+ destination = String(fileConfig.destination);
304
+ }
305
+
272
306
  // --spec takes precedence over the positional [openapi.yaml] argument.
273
307
  // When --spec is provided, the [openapi.yaml] positional slot shifts to
274
308
  // become the [destination] argument (so `counterfact --spec api.yaml ./api`
@@ -343,6 +377,7 @@ async function main(source, destination) {
343
377
  startServer: options.serve,
344
378
  buildCache: options.buildCache || false,
345
379
  validateRequests: options.validateRequest !== false,
380
+ validateResponses: options.validateResponse !== false,
346
381
 
347
382
  watch: {
348
383
  routes: options.watch || options.watchRoutes,
@@ -550,5 +585,13 @@ program
550
585
  "--no-validate-request",
551
586
  "disable request validation against the OpenAPI spec",
552
587
  )
588
+ .option(
589
+ "--no-validate-response",
590
+ "disable response validation against the OpenAPI spec",
591
+ )
592
+ .option(
593
+ "--config <path>",
594
+ "path to a counterfact.yaml config file (default: counterfact.yaml in the current directory)",
595
+ )
553
596
  .action(main)
554
597
  .parse(process.argv);
package/dist/app.js CHANGED
@@ -1,19 +1,21 @@
1
1
  import fs, { rm } from "node:fs/promises";
2
2
  import nodePath from "node:path";
3
- import { dereference } from "@apidevtools/json-schema-ref-parser";
4
- import createDebug from "debug";
5
3
  import { createHttpTerminator } from "http-terminator";
6
4
  import { startRepl as startReplServer } from "./repl/repl.js";
7
5
  import { ContextRegistry } from "./server/context-registry.js";
8
6
  import { createKoaApp } from "./server/create-koa-app.js";
9
- import { Dispatcher, } from "./server/dispatcher.js";
7
+ import { Dispatcher } from "./server/dispatcher.js";
10
8
  import { koaMiddleware } from "./server/koa-middleware.js";
9
+ import { loadOpenApiDocument } from "./server/load-openapi-document.js";
11
10
  import { ModuleLoader } from "./server/module-loader.js";
11
+ import { OpenApiWatcher } from "./server/openapi-watcher.js";
12
12
  import { Registry } from "./server/registry.js";
13
+ import { ScenarioRegistry } from "./server/scenario-registry.js";
13
14
  import { Transpiler } from "./server/transpiler.js";
14
15
  import { CodeGenerator } from "./typescript-generator/code-generator.js";
16
+ import { writeApplyContextType } from "./typescript-generator/generate.js";
15
17
  import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
16
- const debug = createDebug("counterfact:app");
18
+ export { loadOpenApiDocument } from "./server/load-openapi-document.js";
17
19
  const allowedMethods = [
18
20
  "all",
19
21
  "head",
@@ -24,16 +26,6 @@ const allowedMethods = [
24
26
  "patch",
25
27
  "options",
26
28
  ];
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
29
  const mswHandlers = {};
38
30
  export async function handleMswRequest(request) {
39
31
  const { method, rawPath } = request;
@@ -86,15 +78,20 @@ export async function counterfact(config) {
86
78
  }
87
79
  const registry = new Registry();
88
80
  const contextRegistry = new ContextRegistry();
81
+ const scenarioRegistry = new ScenarioRegistry();
89
82
  const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
90
83
  const openApiDocument = config.openApiPath === "_"
91
84
  ? undefined
92
85
  : await loadOpenApiDocument(config.openApiPath);
93
86
  const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
94
87
  const transpiler = new Transpiler(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
95
- const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
88
+ const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry, nodePath.join(modulesPath, "scenarios").replaceAll("\\", "/"), scenarioRegistry);
89
+ contextRegistry.addEventListener("context-changed", () => {
90
+ void writeApplyContextType(modulesPath);
91
+ });
96
92
  const middleware = koaMiddleware(dispatcher, config);
97
93
  const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
94
+ const openApiWatcher = new OpenApiWatcher(config.openApiPath, dispatcher);
98
95
  async function start(options) {
99
96
  const { generate, startServer, watch, buildCache } = options;
100
97
  if (config.openApiPath !== "_" && (generate.routes || generate.types)) {
@@ -105,6 +102,7 @@ export async function counterfact(config) {
105
102
  }
106
103
  let httpTerminator;
107
104
  if (startServer) {
105
+ await openApiWatcher.watch();
108
106
  if (!nativeTs) {
109
107
  await transpiler.watch();
110
108
  }
@@ -127,6 +125,7 @@ export async function counterfact(config) {
127
125
  await codeGenerator.stopWatching();
128
126
  await transpiler.stopWatching();
129
127
  await moduleLoader.stopWatching();
128
+ await openApiWatcher.stopWatching();
130
129
  await httpTerminator?.terminate();
131
130
  },
132
131
  };
@@ -138,6 +137,6 @@ export async function counterfact(config) {
138
137
  registry,
139
138
  start,
140
139
  startRepl: () => startReplServer(contextRegistry, registry, config, undefined, // use the default print function (stdout)
141
- openApiDocument),
140
+ openApiDocument, scenarioRegistry),
142
141
  };
143
142
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type.
3
+ * This prevents arbitrary objects from being accidentally treated as a
4
+ * completed response value.
5
+ */
6
+ const counterfactResponse = Symbol("Counterfact Response");
7
+ 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,2 +1 @@
1
- const counterfactResponse = Symbol("Counterfact Response");
2
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 {};