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.
Files changed (98) hide show
  1. package/README.md +14 -207
  2. package/bin/README.md +24 -4
  3. package/bin/counterfact.js +54 -3
  4. package/dist/app.js +81 -28
  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/migrate/update-route-types.js +2 -3
  27. package/dist/repl/raw-http-client.js +19 -0
  28. package/dist/repl/repl.js +116 -4
  29. package/dist/repl/route-builder.js +68 -0
  30. package/dist/server/constants.js +8 -0
  31. package/dist/server/context-registry.js +70 -1
  32. package/dist/server/counterfact-types/cookie-options.ts +14 -0
  33. package/dist/server/counterfact-types/counterfact-response.ts +15 -0
  34. package/dist/server/counterfact-types/example-names.ts +13 -0
  35. package/dist/server/counterfact-types/example.ts +10 -0
  36. package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
  37. package/dist/server/counterfact-types/http-status-code.ts +62 -0
  38. package/dist/server/counterfact-types/if-has-key.ts +19 -0
  39. package/dist/server/counterfact-types/index.ts +20 -338
  40. package/dist/server/counterfact-types/maybe-promise.ts +6 -0
  41. package/dist/server/counterfact-types/media-type.ts +6 -0
  42. package/dist/server/counterfact-types/omit-all.ts +11 -0
  43. package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
  44. package/dist/server/counterfact-types/open-api-content.ts +8 -0
  45. package/dist/server/counterfact-types/open-api-operation.ts +36 -0
  46. package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
  47. package/dist/server/counterfact-types/open-api-response.ts +22 -0
  48. package/dist/server/counterfact-types/random-function.ts +9 -0
  49. package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
  50. package/dist/server/counterfact-types/response-builder.ts +31 -0
  51. package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
  52. package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
  53. package/dist/server/create-koa-app.js +28 -24
  54. package/dist/server/determine-module-kind.js +13 -0
  55. package/dist/server/dispatcher.js +64 -5
  56. package/dist/server/file-discovery.js +20 -9
  57. package/dist/server/is-proxy-enabled-for-path.js +12 -0
  58. package/dist/server/json-to-xml.js +11 -1
  59. package/dist/server/koa-middleware.js +25 -2
  60. package/dist/server/load-openapi-document.js +6 -0
  61. package/dist/server/module-dependency-graph.js +25 -0
  62. package/dist/server/module-loader.js +112 -17
  63. package/dist/server/module-tree.js +36 -0
  64. package/dist/server/openapi-document.js +69 -0
  65. package/dist/server/openapi-middleware.js +34 -5
  66. package/dist/server/openapi-watcher.js +35 -0
  67. package/dist/server/registry.js +89 -0
  68. package/dist/server/request-validator.js +3 -7
  69. package/dist/server/response-builder.js +18 -0
  70. package/dist/server/response-validator.js +58 -0
  71. package/dist/server/scenario-registry.js +55 -0
  72. package/dist/server/tools.js +29 -2
  73. package/dist/server/transpiler.js +23 -9
  74. package/dist/typescript-generator/code-generator.js +117 -4
  75. package/dist/typescript-generator/coder.js +80 -2
  76. package/dist/typescript-generator/operation-coder.js +13 -5
  77. package/dist/typescript-generator/operation-type-coder.js +40 -53
  78. package/dist/typescript-generator/parameters-type-coder.js +2 -4
  79. package/dist/typescript-generator/prune.js +2 -1
  80. package/dist/typescript-generator/read-only-comments.js +1 -1
  81. package/dist/typescript-generator/repository.js +76 -20
  82. package/dist/typescript-generator/requirement.js +77 -1
  83. package/dist/typescript-generator/reserved-words.js +50 -0
  84. package/dist/typescript-generator/scenario-file-generator.js +235 -0
  85. package/dist/typescript-generator/script.js +70 -7
  86. package/dist/typescript-generator/specification.js +27 -0
  87. package/dist/util/ensure-directory-exists.js +7 -0
  88. package/dist/util/forward-slash-path.js +63 -0
  89. package/dist/util/load-config-file.js +44 -0
  90. package/dist/util/read-file.js +11 -0
  91. package/dist/util/runtime-can-execute-erasable-ts.js +11 -0
  92. package/dist/util/windows-escape.js +18 -0
  93. package/package.json +9 -10
  94. package/dist/client/README.md +0 -14
  95. package/dist/client/index.html.hbs +0 -244
  96. package/dist/client/rapi-doc.html.hbs +0 -36
  97. package/dist/server/page-middleware.js +0 -23
  98. 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
- **Counterfact instantly turns an [OpenAPI/Swagge](https://www.openapis.org) spec into a live, working API you can run locally.**
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
- 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**
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
- I’m currently available — not for long.
21
+ ## Go deeper
223
22
 
224
- https://patrickmcelhaney.org
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
- [Documentation](./docs/usage.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md)
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. 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,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 = nodePath.resolve(destination).replaceAll("\\", "/");
325
+ const destinationPath = pathResolve(destination);
284
326
 
285
- const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
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, } from "./server/dispatcher.js";
10
- import { koaMiddleware } from "./server/koa-middleware.js";
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
- const debug = createDebug("counterfact:app");
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 = nodePath
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 = nodePath
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(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
95
- const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
96
- const middleware = koaMiddleware(dispatcher, config);
97
- const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
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,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 {};
@@ -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);