diff --git a/.changeset/refactor-docs-ia.md b/.changeset/refactor-docs-ia.md new file mode 100644 index 000000000..d5b52d21d --- /dev/null +++ b/.changeset/refactor-docs-ia.md @@ -0,0 +1,5 @@ +--- +"counterfact": patch +--- + +Refactor docs information architecture: `docs/usage.md` is now a central hub page linking to individual feature pages under `docs/features/`, and pattern pages are consolidated under `docs/patterns/index.md`. diff --git a/.github/issue-proposals/docs-pattern-persistent-state.md b/.github/issue-proposals/docs-pattern-persistent-state.md index 98d2869b2..2af80f145 100644 --- a/.github/issue-proposals/docs-pattern-persistent-state.md +++ b/.github/issue-proposals/docs-pattern-persistent-state.md @@ -39,5 +39,5 @@ A `Persistent State` pattern document would describe: - [ ] `--persist-state ` CLI flag (or equivalent) is implemented - [ ] Context classes can opt in to persistence by implementing a documented interface - [ ] `docs/patterns/persistent-state.md` is added following the established pattern format -- [ ] The new pattern is linked in `docs/usage-patterns.md` +- [ ] The new pattern is linked in `docs/patterns/index.md` - [ ] The reference doc is updated to describe the new CLI flag and context interface diff --git a/.github/issue-proposals/docs-pattern-record-and-replay.md b/.github/issue-proposals/docs-pattern-record-and-replay.md index 1021a0375..bdc542ccc 100644 --- a/.github/issue-proposals/docs-pattern-record-and-replay.md +++ b/.github/issue-proposals/docs-pattern-record-and-replay.md @@ -30,5 +30,5 @@ A `Record and Replay` usage pattern document would describe: - [ ] A `--record` (or equivalent) CLI flag is implemented that writes captured responses to handler files - [ ] `docs/patterns/record-and-replay.md` is added following the established pattern format (Context, Problem, Solution, Example, Consequences, Related Patterns) -- [ ] The new pattern is linked in `docs/usage-patterns.md` +- [ ] The new pattern is linked in `docs/patterns/index.md` - [ ] The reference doc is updated to describe the new CLI flag diff --git a/.github/issue-proposals/docs-pattern-webhook-simulation.md b/.github/issue-proposals/docs-pattern-webhook-simulation.md index e35744fc2..5002cb6bf 100644 --- a/.github/issue-proposals/docs-pattern-webhook-simulation.md +++ b/.github/issue-proposals/docs-pattern-webhook-simulation.md @@ -34,5 +34,5 @@ A `Webhook Simulation` pattern document would describe: - [ ] An outbound-call mechanism (e.g., `$.webhook()` or `$.emit()`) is implemented and documented - [ ] `docs/patterns/webhook-simulation.md` is added following the established pattern format -- [ ] The new pattern is linked in `docs/usage-patterns.md` +- [ ] The new pattern is linked in `docs/patterns/index.md` - [ ] The reference doc is updated to describe the new API diff --git a/README.md b/README.md index 1742d26e7..6c9f73e90 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,8 @@ In five minutes, you turned a static spec into a working system: | | | |---|---| | [Getting started](./docs/getting-started.md) | Detailed walkthrough with state, REPL, and proxy | -| [Usage guide](./docs/usage.md) | Routes, context, REPL, proxy, middleware, programmatic API | -| [Usage patterns](./docs/usage-patterns.md) | Failures, latency, AI sandboxes, integration tests | +| [Usage](./docs/usage.md) | Feature index: routes, context, REPL, proxy, middleware, and more | +| [Patterns](./docs/patterns/index.md) | Failures, latency, AI sandboxes, integration tests | | [Reference](./docs/reference.md) | `$` API, CLI flags, architecture | | [How it compares](./docs/comparison.md) | json-server, WireMock, Prism, Microcks, MSW | | [FAQ](./docs/faq.md) | State, types, regeneration | diff --git a/docs/comparison.md b/docs/comparison.md index 2822badbc..de8fc0b4e 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -102,7 +102,7 @@ npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api ## See also - [Getting started](./getting-started.md) -- [Usage patterns](./usage-patterns.md) +- [Patterns](./patterns/index.md) - [Reference](./reference.md) - [FAQ](./faq.md) -- [Usage guide](./usage.md) +- [Usage](./usage.md) diff --git a/docs/context-change.md b/docs/context-change.md deleted file mode 100644 index 7f0ec9efe..000000000 --- a/docs/context-change.md +++ /dev/null @@ -1,42 +0,0 @@ -# Context Switch - -Version 0.36 has a minor breaking change in the way context objects are defined. - -## The file is now named `_.context.ts` instead of `$.context.ts`. - -This change was made because putting a `$` in a file name can be confusing in Mac/Linux/Unix systems. - -## Instead of exporting a default _value_, it the file should now export a class named `Context` - -### Old - -```ts -class Context { - // ... -} - -export default new Context(); -``` - -### New - -```ts -export class Context { - // ... -} -``` - -In the future, the context class will be able to have a constructor that takes one argument, an instance of the _parent_ context. - -```ts -// paths/accounts/_context.ts -import { CounterfactContext } from "../../types.ts"; - -export class Context extends CounterfactContext { - constructor(registry) { - this.mainContext = registry.find("/"); - this.productsContext = registry.find("/products"); - } - // ... -} -``` diff --git a/docs/faq-generated-code.md b/docs/faq-generated-code.md deleted file mode 100644 index b97ae9e51..000000000 --- a/docs/faq-generated-code.md +++ /dev/null @@ -1,60 +0,0 @@ -# Generated Code FAQ - -## Am I meant to edit the generated code? - -Yes, you're meant to edit the _imperative_ code under the `routes` directory in order to customize your mock server's behavior. Don't touch the _types_ under `types`. If you do, Counterfact will overwrite your changes anyway. - -## Should I commit generated code to source control? - -Short answer: yes, commit everything. - -Use your Git client to add and commit the directory containing Counterfact's output. It's best not get bogged down in the details. But if you really want to know, here are the details. - -You absolutely _should_ put the files under the `routes` directory under version control. That's _your_ code. Counterfact generates starter files and after that doesn't touch the code again. - -In theory, code under `types` can be reproduced exactly given the same input (OpenAPI file), so it might make sense to put those directories in your `.gitignore`. However, that depends on whether the OpenAPI file is in the same repository. That's not always the case, especially if the API is owned by a different team -- or a different organization. - -Note that Counterfact creates `.gitignore` file that excludes the `.cache` directory. Do commit `.gitignore`, don't commit `.cache`. - -## What's with these `never` types? - -If you peek into the type definitions under `types/paths` you may notice a bunch of references to `never`. For example, you might see `query: never`. - -The upshot is you won't see `query` offered as an autocomplete when you type `$.` when you're writing code for that operation. And if you have code like `$.query.id`, TypeScript will yell at you. - -This is by design. The code is generated from an OpenAPI description. If the OpenAPI description doesn't specify any query parameters, Counterfact assumes there's no reason to look for query parameters in the URL. If the OpenAPI description is incomplete, it's better to fix it at the source. - -## When do I need to restart? - -If your OpenAPI file is local, Counterfact will watch the file and automatically regenerate types whenever it changes. No restart required. - -If you point to a URL like https://petstore3.swagger.io/api/v3/openapi.yaml, Counterfact only looks up the URL once at startup. So you would need to restart to pick up any changes. - -If you have control over the OpenAPI (i.e. you're not getting it from a third party), we recommend working with a local file for a great developer experience. - -## Do I need to restart after changing a file in `routes`? - -No. The fact that you can change the code while the server is running is what makes mocking in Counterfact so pleasant. It watches for file changes and updates automatically. And the state of your context objects is preserved. You can even change the definition of a context object (`_.context.js`) and Counterfact will preserve the values of any properties you didn't explicitly change. - -## Can I have more granular control over when the generated code is created? - -Yes. Maybe you're working on a feature and a recent Counterfact bump has caused your types to get updated and it's cluttering your staging area. No problem. You can use the `--generate`, `--generate-types` and `--generate-routes` options to have more control over when the generated code is created. This allows you to generate the types and routes separately and to run the server without generating the code. - -```bash -npx counterfact@latest my-api.yml --generate-types -``` - -You can use the `--watch`, `--watch-types` and `--watch-routes` flags to have Counterfact watch the types and routes files for changes and regenerate the code when they change. Watching will always include generating the code. - -## After reading this FAQ I have even more questions! - -Please reach out by [creating an issue](https://github.com/pmcelhaney/counterfact/issues). - -We love feedback, so even if we've answered all your questions, please let us know in a [comment](https://github.com/pmcelhaney/counterfact/pull/796). - -## See also - -- [FAQ](./faq.md) — broader questions about Counterfact -- [Getting started](./getting-started.md) — step-by-step walkthrough -- [Usage guide](./usage.md) — full documentation for routes, context, REPL, and more -- [Reference](./reference.md) — `$` API, CLI flags, architecture diff --git a/docs/faq.md b/docs/faq.md index 6e164042a..2a4634a22 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -287,8 +287,7 @@ Wherever you tell it. The second argument to the CLI is the output directory. Us ## See also - [Getting started](./getting-started.md) -- [Usage patterns](./usage-patterns.md) +- [Patterns](./patterns/index.md) - [Reference](./reference.md) - [How it compares](./comparison.md) -- [Generated code FAQ](./faq-generated-code.md) -- [Usage guide](./usage.md) +- [Usage](./usage.md) diff --git a/docs/features/generated-code.md b/docs/features/generated-code.md new file mode 100644 index 000000000..29bf8d702 --- /dev/null +++ b/docs/features/generated-code.md @@ -0,0 +1,16 @@ +# Generated Code + +Counterfact generates two directories from your OpenAPI document: + +- 📂 **`types/`** — fully typed request/response interfaces, auto-regenerated whenever the OpenAPI document changes. Don't edit these by hand. +- 📂 **`routes/`** — one TypeScript file per API path. These are yours to edit. Out of the box each file returns a random, schema-valid response. You can leave them as-is or customize as much as you like. + +See the [FAQ](../faq.md) for common questions about source control, editing, and regeneration. + +No OpenAPI document? See [using Counterfact without OpenAPI](./without-openapi.md). + +## See also + +- [Routes](./routes.md) — writing route handlers, reading request data, building responses +- [State](./state.md) — sharing state across routes with context objects +- [Usage](../usage.md) diff --git a/docs/features/hot-reload.md b/docs/features/hot-reload.md new file mode 100644 index 000000000..061461d76 --- /dev/null +++ b/docs/features/hot-reload.md @@ -0,0 +1,19 @@ +# Hot Reload 🔥 + +Save a file — any route or context file — and the running server picks it up immediately. No restart needed, and in-memory state is preserved across reloads. + +This makes it fast to set up edge cases like: + +- What does the UI do 8 clicks deep when the server returns a 500? +- What if there are zero results? What if there are 10,000? +- What if the server is slow? + +Find the file corresponding to the route, change behavior by editing the TypeScript code, and continue testing. + +Depending on the scenario, you may want to commit your changes to source control or throw them away. + +## See also + +- [State](./state.md) — in-memory state that survives reloads +- [REPL](./repl.md) — make changes without touching files at all +- [Usage](../usage.md) diff --git a/docs/features/middleware.md b/docs/features/middleware.md new file mode 100644 index 000000000..3db83a8d9 --- /dev/null +++ b/docs/features/middleware.md @@ -0,0 +1,20 @@ +# Middleware + +Place a `_.middleware.ts` file in any `routes/` subdirectory to intercept requests and responses for that subtree. Middleware applies from the root down — a `_.middleware.ts` at the root runs for every request. + +```ts +// routes/_.middleware.ts +export async function middleware($, respondTo) { + const response = await respondTo($); + return response.header("X-Custom-Header", "Custom Value"); +} +``` + +`respondTo($)` passes the request to the next middleware layer or the route handler, and returns the response. You can modify `$` before calling `respondTo`, modify the response after, or both. + +## See also + +- [Patterns: Custom Middleware](../patterns/custom-middleware.md) — authentication, headers, and logging across route groups +- [Routes](./routes.md) +- [Reference](../reference.md) +- [Usage](../usage.md) diff --git a/docs/features/programmatic-api.md b/docs/features/programmatic-api.md new file mode 100644 index 000000000..e55aab2ab --- /dev/null +++ b/docs/features/programmatic-api.md @@ -0,0 +1,106 @@ +# Programmatic API + +Counterfact can be used as a library — for example, from [Playwright](https://playwright.dev/) or [Cypress](https://www.cypress.io/) tests. This lets you manipulate context state directly in test code without relying on special magic values in mock logic. + +```ts +import { counterfact } from "counterfact"; + +const config = { + basePath: "./api", // directory containing your routes/ + openApiPath: "./api.yaml", // optional; pass "_" to run without a spec + port: 8100, + alwaysFakeOptionals: false, + generate: { routes: false, types: false }, + proxyPaths: new Map(), + proxyUrl: "", + routePrefix: "", + startAdminApi: false, + startRepl: false, // do not auto-start the REPL + startServer: true, + watch: { routes: false, types: false }, +}; + +const { contextRegistry, start } = await counterfact(config); +const { stop } = await start(config); + +// Get the root context — the object your routes see as $.context +const rootContext = contextRegistry.find("/"); +``` + +Once you have `rootContext` you can read and write any state that your route handlers expose. + +## Example: parameterised auth scenario with Playwright + +Given this route handler: + +```ts +// routes/auth/login.ts +export const POST: HTTP_POST = ($) => { + if ($.context.passwordResponse === "ok") return $.response[200]; + if ($.context.passwordResponse === "expired") + return $.response[403].header("reason", "expired-password"); + return $.response[401]; +}; +``` + +A Playwright test can flip between scenarios without hard-coded usernames: + +```ts +import { counterfact } from "counterfact"; +import { chromium } from "playwright"; + +let page; +let rootContext; +let stop; +let browser; + +beforeAll(async () => { + browser = await chromium.launch({ headless: true }); + page = await (await browser.newContext()).newPage(); + + const { contextRegistry, start } = await counterfact(config); + ({ stop } = await start(config)); + rootContext = contextRegistry.find("/"); +}); + +afterAll(async () => { + await stop(); + await browser.close(); +}); + +it("rejects an incorrect password", async () => { + rootContext.passwordResponse = "incorrect"; + await attemptToLogIn(); + expect(await page.isVisible("#authentication-error")).toBe(true); +}); + +it("loads the dashboard on success", async () => { + rootContext.passwordResponse = "ok"; + await attemptToLogIn(); + expect(await page.isVisible("#dashboard")).toBe(true); +}); + +it("prompts for a password change when the password has expired", async () => { + rootContext.passwordResponse = "expired"; + await attemptToLogIn(); + expect(await page.isVisible("#password-change-form")).toBe(true); +}); +``` + +## Return value of `counterfact()` + +| Property | Type | Description | +| ----------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | +| `contextRegistry` | `ContextRegistry` | Registry of all context objects keyed by path. Call `.find(path)` to get the context for a given route prefix. | +| `registry` | `Registry` | Registry of all loaded route modules. | +| `koaApp` | `Koa` | The underlying Koa application. | +| `koaMiddleware` | `Koa.Middleware` | The Counterfact request-dispatch middleware. | +| `start(config)` | `async (config) => { stop() }` | Starts the server (and optionally the file watcher and code generator). Returns a `stop()` function to gracefully shut down. | +| `startRepl()` | `() => REPLServer` | Starts the interactive REPL. Returns the REPL server instance. | + +## See also + +- [State](./state.md) — the context objects you manipulate from test code +- [Patterns: Automated Integration Tests](../patterns/automated-integration-tests.md) — using the programmatic API in a CI-friendly test suite +- [Reference](../reference.md) — CLI flags, architecture +- [Usage](../usage.md) diff --git a/docs/features/proxy.md b/docs/features/proxy.md new file mode 100644 index 000000000..6f71a12be --- /dev/null +++ b/docs/features/proxy.md @@ -0,0 +1,35 @@ +# Proxy 🔀 + +You can mix real backend calls with mocks — useful when some endpoints are not finished or you need to test edge cases like 500 errors. + +To proxy a single endpoint from within a route file: + +```ts +// routes/pet/{petId}.ts +export const GET: HTTP_GET = ($) => { + return $.proxy("https://uat.petstore.example.com/pet"); +}; +``` + +To set a proxy for the entire API at startup, pass `--proxy-url` on the CLI: + +```sh +npx counterfact@latest openapi.yaml api --proxy-url https://uat.petstore.example.com +``` + +From the REPL, you can toggle proxying for the whole API or specific routes: + +``` +⬣> .proxy on /payments # forward /payments to the real API +⬣> .proxy off /payments # let Counterfact handle /payments +⬣> .proxy off # stop proxying everything +``` + +Type `.proxy help` in the REPL for the full list of proxy commands. + +## See also + +- [Patterns: Hybrid Proxy](../patterns/hybrid-proxy.md) — when to mix real and mocked endpoints +- [REPL](./repl.md) — toggle proxying at runtime +- [Reference](../reference.md) — CLI flags +- [Usage](../usage.md) diff --git a/docs/features/repl.md b/docs/features/repl.md new file mode 100644 index 000000000..df98c6db2 --- /dev/null +++ b/docs/features/repl.md @@ -0,0 +1,108 @@ +# REPL ⬣ + +The REPL is a JavaScript prompt connected directly to the running server — like the browser DevTools console, but for your mock API. After starting Counterfact you'll see: + +``` +____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___ +|___ [__] |__| |\| | |=== |--< |--- |--| |___ | + Storybook for the back-end + +| API Base URL ==> http://localhost:3100 +| Admin Console ==> http://localhost:3100/counterfact/ + +⬣> +``` + +At the prompt you can interact with the live context: + +```js +// add a single pet +context.addPet({ name: "Fluffy", photoUrls: [] }); + +// add 100 pets +for (let i = 0; i < 100; i++) + context.addPet({ name: `Pet ${i}`, photoUrls: [] }); + +// query state +context.pets.filter((pet) => pet.name.startsWith("F")); +``` + +To access context from a subdirectory: + +```js +const petsContext = loadContext("/pets"); +``` + +The built-in `client` object lets you make HTTP requests from the prompt without leaving the terminal: + +```js +client.get("/users"); +client.post("/users", { name: "bob" }); +client.put("/users/1", { name: "robert" }, { "x-api-version": "2" }); +``` + +All standard HTTP methods are supported. Arguments are: path, body (where applicable), headers. + +The built-in `route()` function creates a fluent request builder that validates required parameters against your OpenAPI document before sending: + +```js +// Build and inspect before sending +const req = route("/pet/{petId}").method("get").path({ petId: 42 }) +req.ready() // true / false +req.missing() // lists missing required parameters +req.help() // prints OpenAPI docs for the operation +await req.send() +``` + +See the [Route Builder guide](./route-builder.md) for full documentation. + +## Scenario scripts with `.apply` + +For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.apply`: + +``` +⬣> .apply soldPets +``` + +**Path resolution:** the argument to `.apply` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `/scenarios/` (with `index.ts` as the default file). + +| Command | File | Function | +|---|---|---| +| `.apply foo` | `scenarios/index.ts` | `foo` | +| `.apply foo/bar` | `scenarios/foo.ts` | `bar` | +| `.apply foo/bar/baz` | `scenarios/foo/bar.ts` | `baz` | + +A scenario function receives a single argument with `{ context, loadContext, routes, route }`: + +```ts +// scenarios/index.ts +import type { Scenario } from "../types/scenario-context.js"; + +export const soldPets: Scenario = ($) => { + // Mutate context directly — same as typing in the REPL + $.context.petService.reset(); + $.context.petService.addPet({ id: 1, status: "sold" }); + $.context.petService.addPet({ id: 2, status: "available" }); + + // Store a pre-configured route builder for later use in the REPL + $.routes.findSold = $ + .route("/pet/findByStatus") + .method("get") + .query({ status: "sold" }); +} +``` + +After the command runs you can immediately use anything stored in `$.routes`: + +```js +⬣> routes.findSold.send() +``` + +The `Scenario` type and `ApplyContext` interface are generated automatically into `types/scenario-context.ts` when you run Counterfact with type generation enabled. + +## See also + +- [Route Builder](./route-builder.md) — fluent request builder with OpenAPI introspection +- [State](./state.md) — the context objects you interact with from the REPL +- [Patterns: Live Server Inspection with the REPL](../patterns/repl-inspection.md) +- [Usage](../usage.md) diff --git a/docs/route-builder.md b/docs/features/route-builder.md similarity index 98% rename from docs/route-builder.md rename to docs/features/route-builder.md index 9bab2533f..064d8fda6 100644 --- a/docs/route-builder.md +++ b/docs/features/route-builder.md @@ -159,5 +159,5 @@ Use `client` for quick one-off requests. Use `route()` when you want to inspect, ## See also -- [REPL documentation](./usage.md#repl-) -- [Usage guide](./usage.md) +- [REPL](./repl.md) +- [Usage](../usage.md) diff --git a/docs/features/routes.md b/docs/features/routes.md new file mode 100644 index 000000000..55b9663d2 --- /dev/null +++ b/docs/features/routes.md @@ -0,0 +1,163 @@ +# Routes + +Each file in `routes/` corresponds to an API path. For example, `/users/{userId}` maps to `routes/users/{userId}.ts`. The root path `/` maps to `routes/index.ts`. + +A freshly generated route file looks like this: + +```ts +export const GET: HTTP_GET = ($) => { + return $.response[200].random(); +}; + +export const POST: HTTP_POST = ($) => { + return $.response[200].random(); +}; +``` + +Each exported function handles one HTTP method. The single argument `$` gives you everything you need: request data, response builders, server state, and utilities. + +> [!TIP] +> If you know Express, think of `$` as a type-safe combination of `req` and `res`. + +## Building responses with `$.response` + +`$.response` is a fluent builder for HTTP responses. Start by picking a status code, then chain one or more methods: + +| Method | Description | +| ------------------------------ | --------------------------------------------------------------------------------------- | +| `.random()` | Returns random data generated from the OpenAPI schema (uses `examples` where available) | +| `.example(name)` | Returns a specific named example from the OpenAPI spec | +| `.json(content)` | Returns a JSON body (also converts to XML automatically when the client requests it) | +| `.text(content)` | Returns a plain-text body | +| `.html(content)` | Returns an HTML body | +| `.xml(content)` | Returns an XML body | +| `.match(contentType, content)` | Returns a body with an explicit content type; chain multiple for content negotiation | +| `.header(name, value)` | Adds a response header | + +```ts +return $.response[200].header("x-request-id", "abc123").json({ ok: true }); +``` + +### Setting cookies + +Use `.cookie(name, value, options?)` to set one or more cookies. Each call appends a new `Set-Cookie` header and returns the builder for chaining. + +```ts +return $.response[200] + .cookie("sessionId", "abc123", { + httpOnly: true, + secure: true, + sameSite: "lax", + path: "/", + maxAge: 3600, + }) + .json({ ok: true }); +``` + +Multiple cookies: + +```ts +return $.response[200] + .cookie("sessionId", "abc123") + .cookie("theme", "dark") + .json({ ok: true }); +``` + +Supported options: + +| Option | Description | +|--------|-------------| +| `path` | Cookie path (e.g. `"/"`) | +| `domain` | Cookie domain | +| `maxAge` | Max age in seconds (`Max-Age`) | +| `expires` | Expiry `Date` object (`Expires`) | +| `httpOnly` | Sets the `HttpOnly` flag when `true` | +| `secure` | Sets the `Secure` flag when `true` | +| `sameSite` | `"lax"`, `"strict"`, or `"none"` (`SameSite`) | + +### Using named examples + +If your OpenAPI spec defines named examples for a response, you can return a specific one by name using `.example()`. The name is autocompleted and type-checked — passing an unknown name is a compile error. + +```ts +// Return a specific named example +return $.response[200].example("successResponse"); + +// Chain additional decorations after selecting an example +return $.response[200] + .example("successResponse") + .header("x-request-id", "abc123"); +``` + +> [!TIP] +> Your IDE's autocomplete knows which status codes, headers, and response shapes are valid for each endpoint — based directly on your OpenAPI spec. If you omit a required header, TypeScript will tell you. When the spec changes and types are regenerated, TypeScript will surface any mismatches. + +## Reading request data + +The request is exposed through four typed properties: + +| Property | Contents | +| ----------- | ------------------------------------------------------------ | +| `$.path` | Path parameters (e.g. `$.path.userId` for `/users/{userId}`) | +| `$.query` | Query string parameters | +| `$.headers` | Request headers | +| `$.body` | Request body | + +```ts +export const GET: HTTP_GET = ($) => { + if ($.headers["x-token"] !== "super-secret") { + return $.response[401].text("Unauthorized"); + } + + const content = + `Results for "${$.query.keyword}" in ${$.path.groupName}` + + ` with tags: ${$.body.tags.join(", ")}`; + + return $.response[200].text(content); +}; +``` + +All four objects are typed from your OpenAPI spec, so autocomplete works for parameter names and values. + +## Basic auth: `$.auth` + +When a request includes HTTP Basic credentials, they're available at `$.auth.username` and `$.auth.password`. + +Support for other security schemes (API key, OAuth 2, OpenID Connect, mutual TLS) is planned. [Open an issue](https://github.com/pmcelhaney/counterfact/issues) to help prioritize. + +## Simulating latency: `$.delay()` + +Counterfact responds much faster than a real server. To test loading states and timeouts, use `$.delay()`: + +```ts +// pause for exactly one second +await $.delay(1000); + +// pause for a random duration between 1 and 5 seconds +await $.delay(1000, 5000); +``` + +## Escaping the type system: `$.x` + +Counterfact translates your OpenAPI spec into strict TypeScript types. If your spec is incomplete, or you need to return something outside the spec (like a `500` error that isn't documented), the strict types can get in the way. + +`$.x` is an alias for `$` with all types widened to `any`, giving you an escape hatch: + +```ts +export const GET: HTTP_GET = ($) => { + // header not defined in OpenAPI spec + $.headers["my-undocumented-header"]; // TypeScript error + $.x.headers["my-undocumented-header"]; // ok + + // status code not defined in OpenAPI spec + return $.response[500].text("Error!"); // TypeScript error + return $.x.response[500].text("Error!"); // ok +}; +``` + +## See also + +- [State](./state.md) — sharing state across routes with context objects +- [Middleware](./middleware.md) — cross-cutting request/response logic +- [Reference](../reference.md) — complete `$` parameter and response builder API reference +- [Usage](../usage.md) diff --git a/docs/features/state.md b/docs/features/state.md new file mode 100644 index 000000000..29b06507c --- /dev/null +++ b/docs/features/state.md @@ -0,0 +1,81 @@ +# State: Context Objects + +The `$.context` object is how routes share in-memory state. It's an instance of the `Context` class exported from `_.context.ts` in the same directory (or the nearest parent directory that has one). + +```ts +// routes/pet.ts +export const POST: HTTP_POST = ($) => { + return $.response[200].json($.context.addPet($.body)); +}; + +// routes/pet/{petId}.ts +export const GET: HTTP_GET = ($) => { + const pet = $.context.getPetById($.path.petId); + if (pet === undefined) + return $.response[404].text(`Pet ${$.path.petId} not found.`); + return $.response[200].json(pet); +}; +``` + +Customize `_.context.ts` to hold whatever state and business logic your mock needs: + +```ts +// routes/_.context.ts +export class Context { + pets: Pet[] = []; + + addPet(pet: Pet) { + const id = this.pets.length; + this.pets.push({ ...pet, id }); + return this.pets[id]; + } + + getPetById(id: number) { + return this.pets[id]; + } +} +``` + +> [!IMPORTANT] +> Keep context in memory. Counterfact is a development tool — starting fresh each time is a feature, not a bug. In-memory state also makes the server very fast. + +## Nested contexts + +For large APIs you can nest context objects. Any subdirectory can have its own `_.context.ts`. One context can access another via the `loadContext` function passed to its constructor: + +```ts +// routes/users/_.context.ts +export class Context { + constructor({ loadContext }) { + this.rootContext = loadContext("/"); + this.petsContext = loadContext("/pets"); + } +} +``` + +## Loading JSON data with `readJson` + +Use the `readJson` function (also passed to the constructor) to load static JSON data into your context. The path is resolved relative to the `_.context.ts` file. + +```ts +// routes/_.context.ts +export class Context { + private readonly readJson: (path: string) => Promise; + + constructor({ readJson }: { readJson: (path: string) => Promise }) { + this.readJson = readJson; + } + + async getSeeds() { + return this.readJson("../mocks/seeds.json"); + } +} +``` + +## See also + +- [Hot reload](./hot-reload.md) — state is preserved across hot reloads +- [REPL](./repl.md) — inspect and mutate state interactively at runtime +- [Patterns: Federated Context Files](../patterns/federated-context.md) — managing state across multiple context files +- [Patterns: Test the Context, Not the Handlers](../patterns/test-context-not-handlers.md) — unit-testing context logic +- [Usage](../usage.md) diff --git a/docs/features/typescript-native-mode.md b/docs/features/typescript-native-mode.md new file mode 100644 index 000000000..dbf5ccbad --- /dev/null +++ b/docs/features/typescript-native-mode.md @@ -0,0 +1,59 @@ +# TypeScript Native Mode + +By default Counterfact compiles your route files into a `.cache/` directory before loading them. When you run Counterfact under a TypeScript-aware runtime it detects this automatically and skips compilation, loading `.ts` source files directly. The result is the same hot-reload experience with no build step. + +## How detection works + +At startup Counterfact writes a small temporary TypeScript file to a system temp directory and attempts to import it. If the import succeeds the runtime is TypeScript-capable and the transpiler is skipped. No configuration is needed. + +## With tsx + +Invoke the `counterfact` binary through [tsx](https://tsx.is/): + +```sh +# one-off via npx +npx tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch + +# or in package.json scripts +"mock": "tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" +``` + +tsx is available as a dev dependency (`npm install --save-dev tsx`). + +## With plain Node.js + +Node 22.6+ ships with `--experimental-strip-types`. A small module hook bundled with Counterfact (`bin/register-ts-loader.mjs`) adds the `.js` → `.ts` import remapping that Node doesn't do on its own: + +```sh +node \ + --experimental-strip-types \ + --import ./node_modules/counterfact/bin/register-ts-loader.mjs \ + ./node_modules/counterfact/bin/counterfact.js \ + openapi.yaml api --serve --watch +``` + +In `package.json`: + +```json +"scripts": { + "mock": "node --experimental-strip-types --import ./node_modules/counterfact/bin/register-ts-loader.mjs ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" +} +``` + +> **Note:** `--experimental-strip-types` is stable enough for development use but the flag name may change before it graduates from experimental status. + +## What changes in native mode + +| | Default (compiled) | Native TS | +| ------------------- | -------------------------------------- | -------------------------- | +| Startup | Compiles routes to `.cache/` first | Loads `.ts` files directly | +| `.cache/` directory | Created and managed automatically | Not used | +| Dependencies | None extra | tsx _or_ Node 22.6+ | +| Route file format | Generated `.ts` files (same as always) | Same | +| Hot reload | ✅ | ✅ | + +## See also + +- [Hot reload](./hot-reload.md) +- [Reference](../reference.md) — CLI flags +- [Usage](../usage.md) diff --git a/docs/usage-without-openapi.md b/docs/features/without-openapi.md similarity index 89% rename from docs/usage-without-openapi.md rename to docs/features/without-openapi.md index f15d7eadd..a078ef9a5 100644 --- a/docs/usage-without-openapi.md +++ b/docs/features/without-openapi.md @@ -46,6 +46,6 @@ Not many people love writing documentation. Fewer people still love working on A ## See also -- [Getting started](./getting-started.md) — start with an OpenAPI spec for type-safe, auto-generated routes -- [Usage guide](./usage.md) — full documentation for routes, context, REPL, and more -- [FAQ](./faq.md) — common questions about state, types, and regeneration +- [Getting started](../getting-started.md) — start with an OpenAPI spec for type-safe, auto-generated routes +- [Usage](../usage.md) — full documentation for routes, context, REPL, and more +- [FAQ](../faq.md) — common questions about state, types, and regeneration diff --git a/docs/getting-started.md b/docs/getting-started.md index ae9637b31..89ab3df4c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -206,9 +206,9 @@ Either way, Counterfact only scaffolds route files that don't exist yet. Your ex ## Next steps -- [Usage patterns](./usage-patterns.md) — explore an API, simulate failures, hybrid proxy, agentic coding, and more +- [Patterns](./patterns/index.md) — explore an API, simulate failures, hybrid proxy, agentic coding, and more - [Reference](./reference.md) — `$` parameter, response builder methods, full CLI flags, architecture overview - [FAQ](./faq.md) — common questions about state, type safety, regeneration, and programmatic use - [How it compares](./comparison.md) — side-by-side with json-server, WireMock, Prism, Microcks, and MSW - [Petstore example](https://github.com/counterfact/example-petstore) — a complete worked example of the Swagger Petstore, fully implemented with Counterfact -- [Usage guide](./usage.md) — complete documentation +- [Usage](./usage.md) — full feature documentation diff --git a/docs/patterns/index.md b/docs/patterns/index.md new file mode 100644 index 000000000..e6f51b31b --- /dev/null +++ b/docs/patterns/index.md @@ -0,0 +1,30 @@ +# Usage Patterns + +A pattern is a reusable solution to a recurring problem when building API simulations with Counterfact. Each pattern below describes a context, the problem it addresses, the solution, and its consequences. + +Most projects start with [Explore a New API](./explore-new-api.md) or [Executable Spec](./executable-spec.md) to get a running server from an OpenAPI spec with no code. From there, [Mock APIs with Dummy Data](./mock-with-dummy-data.md) and [AI-Assisted Implementation](./ai-assisted-implementation.md) are the natural next steps for adding realistic responses — the former by hand, the latter with an AI agent doing the heavy lifting. As the mock grows, [Federated Context Files](./federated-context.md) and [Test the Context, Not the Handlers](./test-context-not-handlers.md) keep the stateful logic organized and reliable. Throughout all of this, [Live Server Inspection with the REPL](./repl-inspection.md) is Counterfact's most distinctive feature: it lets you seed data, send requests, and toggle behavior in real time without restarting. [Simulate Failures and Edge Cases](./simulate-failures.md) and [Simulate Realistic Latency](./simulate-latency.md) extend any mock to cover error paths and performance characteristics that real services exhibit. [Reference Implementation](./reference-implementation.md) and [Executable Spec](./executable-spec.md) make the mock a first-class artifact that teams can rely on as the API evolves. Finally, [Agentic Sandbox](./agentic-sandbox.md) and [Hybrid Proxy](./hybrid-proxy.md) address the two common integration strategies — isolating an AI agent from the real service, or blending mock and live traffic across endpoints. [Automated Integration Tests](./automated-integration-tests.md) shows how to embed the mock server in a test suite using the programmatic API, while [Custom Middleware](./custom-middleware.md) covers cross-cutting concerns like authentication and response headers without touching individual handlers. + +| Pattern | When to use it | +|---|---| +| [Explore a New API](./explore-new-api.md) | You have a spec but no running backend or production access | +| [Executable Spec](./executable-spec.md) | You want immediate feedback on how spec changes affect the running server during API design | +| [Mock APIs with Dummy Data](./mock-with-dummy-data.md) | You need realistic-looking responses to build a UI, run a demo, or write assertions | +| [AI-Assisted Implementation](./ai-assisted-implementation.md) | You want an AI agent to replace random responses with working handler logic | +| [Federated Context Files](./federated-context.md) | You want each domain to own its state, with explicit cross-domain dependencies | +| [Test the Context, Not the Handlers](./test-context-not-handlers.md) | You want to keep shared stateful logic reliable as the mock grows | +| [Live Server Inspection with the REPL](./repl-inspection.md) | You want to seed data, send requests, and toggle behavior without restarting the server | +| [Simulate Failures and Edge Cases](./simulate-failures.md) | You need reproducible, on-demand error conditions for development or testing | +| [Simulate Realistic Latency](./simulate-latency.md) | You want to test how clients and UIs behave under realistic response times | +| [Reference Implementation](./reference-implementation.md) | You want a working, executable implementation that expresses intended API behavior in code | +| [Agentic Sandbox](./agentic-sandbox.md) | You are building an AI coding agent and want to avoid rate limits and costs during development | +| [Hybrid Proxy](./hybrid-proxy.md) | Some endpoints exist in the real backend; others need to be mocked | +| [Automated Integration Tests](./automated-integration-tests.md) | You want to run real HTTP tests against the mock in a CI-friendly test suite | +| [Custom Middleware](./custom-middleware.md) | You want authentication, headers, or logging applied uniformly across a group of routes | + +## See also + +- [Getting started](../getting-started.md) +- [Reference](../reference.md) +- [FAQ](../faq.md) +- [How it compares](../comparison.md) +- [Usage](../usage.md) diff --git a/docs/quick-start.md b/docs/quick-start.md deleted file mode 100644 index cd124fadd..000000000 --- a/docs/quick-start.md +++ /dev/null @@ -1,18 +0,0 @@ -# Got 60 Seconds? Try me! - -Copy the following command into your terminal. The only prerequisite is Node 16+. - -```sh copy -npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api -``` - -## What does that command do? - -1. installs the `@latest` version of `counterfact` -2. reads an [OpenAPI 3](https://oai.github.io/Documentation/) document (`https://petstore3.swagger.io/api/v3/openapi.json`) -3. generates TypeScript files in the `api` directory -4. starts a server which implements the API - -You can use [Swagger UI](https://swagger.io/tools/swagger-ui/) to try out the auto-generated API. Out of the box, it returns random responses using metadata from the OpenAPI document. Edit the files under `./api/routes` to add more realistic behavior. There's no need to restart the server. - -To learn more, see the [Usage Guide](./usage.md). diff --git a/docs/reference.md b/docs/reference.md index 5f11db7a0..0df3fcf8e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -319,7 +319,7 @@ Run `npx counterfact@latest --help` for the full list. ## See also - [Getting started](./getting-started.md) -- [Usage patterns](./usage-patterns.md) +- [Patterns](./patterns/index.md) - [FAQ](./faq.md) - [How it compares](./comparison.md) -- [Usage guide](./usage.md) +- [Usage](./usage.md) diff --git a/docs/usage-patterns.md b/docs/usage-patterns.md deleted file mode 100644 index 4d6cb2939..000000000 --- a/docs/usage-patterns.md +++ /dev/null @@ -1,29 +0,0 @@ -# Usage Patterns - -A pattern is a reusable solution to a recurring problem when building API simulations with Counterfact. Each pattern below describes a context, the problem it addresses, the solution, and its consequences. - -Most projects start with [Explore a New API](./patterns/explore-new-api.md) or [Executable Spec](./patterns/executable-spec.md) to get a running server from an OpenAPI spec with no code. From there, [Mock APIs with Dummy Data](./patterns/mock-with-dummy-data.md) and [AI-Assisted Implementation](./patterns/ai-assisted-implementation.md) are the natural next steps for adding realistic responses — the former by hand, the latter with an AI agent doing the heavy lifting. As the mock grows, [Federated Context Files](./patterns/federated-context.md) and [Test the Context, Not the Handlers](./patterns/test-context-not-handlers.md) keep the stateful logic organized and reliable. Throughout all of this, [Live Server Inspection with the REPL](./patterns/repl-inspection.md) is Counterfact's most distinctive feature: it lets you seed data, send requests, and toggle behavior in real time without restarting. [Simulate Failures and Edge Cases](./patterns/simulate-failures.md) and [Simulate Realistic Latency](./patterns/simulate-latency.md) extend any mock to cover error paths and performance characteristics that real services exhibit. [Reference Implementation](./patterns/reference-implementation.md) and [Executable Spec](./patterns/executable-spec.md) make the mock a first-class artifact that teams can rely on as the API evolves. Finally, [Agentic Sandbox](./patterns/agentic-sandbox.md) and [Hybrid Proxy](./patterns/hybrid-proxy.md) address the two common integration strategies — isolating an AI agent from the real service, or blending mock and live traffic across endpoints. [Automated Integration Tests](./patterns/automated-integration-tests.md) shows how to embed the mock server in a test suite using the programmatic API, while [Custom Middleware](./patterns/custom-middleware.md) covers cross-cutting concerns like authentication and response headers without touching individual handlers. - -| Pattern | When to use it | -|---|---| -| [Explore a New API](./patterns/explore-new-api.md) | You have a spec but no running backend or production access | -| [Executable Spec](./patterns/executable-spec.md) | You want immediate feedback on how spec changes affect the running server during API design | -| [Mock APIs with Dummy Data](./patterns/mock-with-dummy-data.md) | You need realistic-looking responses to build a UI, run a demo, or write assertions | -| [AI-Assisted Implementation](./patterns/ai-assisted-implementation.md) | You want an AI agent to replace random responses with working handler logic | -| [Federated Context Files](./patterns/federated-context.md) | You want each domain to own its state, with explicit cross-domain dependencies | -| [Test the Context, Not the Handlers](./patterns/test-context-not-handlers.md) | You want to keep shared stateful logic reliable as the mock grows | -| [Live Server Inspection with the REPL](./patterns/repl-inspection.md) | You want to seed data, send requests, and toggle behavior without restarting the server | -| [Simulate Failures and Edge Cases](./patterns/simulate-failures.md) | You need reproducible, on-demand error conditions for development or testing | -| [Simulate Realistic Latency](./patterns/simulate-latency.md) | You want to test how clients and UIs behave under realistic response times | -| [Reference Implementation](./patterns/reference-implementation.md) | You want a working, executable implementation that expresses intended API behavior in code | -| [Agentic Sandbox](./patterns/agentic-sandbox.md) | You are building an AI coding agent and want to avoid rate limits and costs during development | -| [Hybrid Proxy](./patterns/hybrid-proxy.md) | Some endpoints exist in the real backend; others need to be mocked | -| [Automated Integration Tests](./patterns/automated-integration-tests.md) | You want to run real HTTP tests against the mock in a CI-friendly test suite | -| [Custom Middleware](./patterns/custom-middleware.md) | You want authentication, headers, or logging applied uniformly across a group of routes | - -## See also - -- [Getting started](./getting-started.md) -- [Reference](./reference.md) -- [FAQ](./faq.md) -- [How it compares](./comparison.md) diff --git a/docs/usage.md b/docs/usage.md index 15b263067..434e025d0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,746 +6,59 @@ Counterfact is three tools in one: - a **mock server** optimized for front-end development workflows - a **live REPL** for inspecting and manipulating server state at runtime ---- - -## Contents - -- [Quick Start](#quick-start) -- [Generated Code](#generated-code) -- [Routes](#routes) -- [State: Context Objects](#state-context-objects) -- [TypeScript Native Mode](#typescript-native-mode) -- [Hot Reload](#hot-reload-) -- [REPL](#repl-) -- [Proxy](#proxy-) -- [Middleware](#middleware) -- [Programmatic API](#programmatic-api) - ---- - -## Quick Start - -```sh -npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api -``` - -This generates TypeScript route files under `api/` for the Swagger Petstore and starts the server. Swap in your own OpenAPI document URL (or local path) and change `api` to whatever directory you prefer. - -> **Requires Node ≥ 17.0.0** - -
-Full CLI reference - -``` -Usage: counterfact [options] [openapi.yaml] [destination] - -Arguments: - openapi.yaml path or URL to OpenAPI document, or "_" to skip (default: "_") - destination path where code is generated (default: ".") - -Options: - --port server port (default: 3100) - -o, --open open a browser after starting - -g, --generate generate route and type files - --generate-types generate types only - --generate-routes generate routes only - -w, --watch generate + watch for spec changes - --watch-types watch types only - --watch-routes watch routes only - -s, --serve start the mock server - -r, --repl start the REPL - --spec path or URL to OpenAPI document (alternative to positional argument) - --proxy-url forward all unhandled requests to this URL - --prefix base path prefix (e.g. /api/v1) - --always-fake-optionals include optional fields in random responses - -b, --build-cache pre-compile routes and types without starting the server - --config path to a counterfact.yaml config file - -h, --help display help -``` - -
- -
-Using a counterfact.yaml config file - -Any CLI option can also be placed in a `counterfact.yaml` file in the current working directory. Command-line flags always take precedence over values in the file. - -```yaml -# counterfact.yaml -spec: ./openapi.yaml -port: 8080 -serve: true -repl: true -watch: true -proxy-url: https://api.example.com -prefix: /api/v1 -``` - -Kebab-case keys (e.g. `proxy-url`, `always-fake-optionals`) are accepted and normalised automatically. - -Use `--config ` to load a config file from a non-default location: - -```sh -npx counterfact@latest --config ./config/counterfact.yaml -``` - -If `--config` points to a file that does not exist, Counterfact exits with an error. If no `--config` flag is given and there is no `counterfact.yaml` in the current directory, startup proceeds normally using CLI options and defaults. - -
- -
-Using with npm or yarn instead of npx - -Pin a specific version by adding Counterfact as a dev dependency: - -```json -"scripts": { - "mock": "counterfact https://petstore3.swagger.io/api/v3/openapi.json api" -}, -"devDependencies": { - "counterfact": "^2.0.0" -} -``` - -Then run `npm run mock` or `yarn mock`. This ensures every developer on the team uses the same version. - -
- -
-Running in TypeScript native mode — no build step required - -By default Counterfact pre-compiles your route files into a `.cache/` directory using the TypeScript compiler. If you run Counterfact under a TypeScript-aware runtime, it detects this automatically and skips compilation entirely — loading the `.ts` source files directly. - -**With [tsx](https://tsx.is/) (recommended for simplicity)** - -Install tsx as a dev dependency: - -```sh -npm install --save-dev tsx -# or -yarn add --dev tsx -``` - -Then invoke `counterfact` via tsx: - -```sh -npx tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch -``` - -Or in `package.json`: - -```json -"scripts": { - "mock": "tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" -} -``` - -**With plain Node.js (Node 22.6+ required)** - -Node's built-in `--experimental-strip-types` removes TypeScript type annotations without any extra dependency. A small loader included with Counterfact handles the `.js` → `.ts` import remapping that the TypeScript codebase relies on: - -```sh -node \ - --experimental-strip-types \ - --import ./node_modules/counterfact/bin/register-ts-loader.mjs \ - ./node_modules/counterfact/bin/counterfact.js \ - openapi.yaml api --serve --watch -``` - -Or in `package.json`: - -```json -"scripts": { - "mock": "node --experimental-strip-types --import ./node_modules/counterfact/bin/register-ts-loader.mjs ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" -} -``` - -> **Note:** `--experimental-strip-types` is stable enough for development use but the flag name may change before it graduates from experimental status. - -
- ---- - -## Generated Code - -Counterfact generates two directories from your OpenAPI document: - -- 📂 **`types/`** — fully typed request/response interfaces, auto-regenerated whenever the OpenAPI document changes. Don't edit these by hand. -- 📂 **`routes/`** — one TypeScript file per API path. These are yours to edit. Out of the box each file returns a random, schema-valid response. You can leave them as-is or customize as much as you like. - -See [Generated Code FAQ](./faq-generated-code.md) for questions about source control, editing, and regeneration. - -No OpenAPI document? See [using Counterfact without OpenAPI](./usage-without-openapi.md). - ---- - -## Routes - -Each file in `routes/` corresponds to an API path. For example, `/users/{userId}` maps to `routes/users/{userId}.ts`. The root path `/` maps to `routes/index.ts`. - -A freshly generated route file looks like this: - -```ts -export const GET: HTTP_GET = ($) => { - return $.response[200].random(); -}; - -export const POST: HTTP_POST = ($) => { - return $.response[200].random(); -}; -``` - -Each exported function handles one HTTP method. The single argument `$` gives you everything you need: request data, response builders, server state, and utilities. - -> [!TIP] -> If you know Express, think of `$` as a type-safe combination of `req` and `res`. - -### Building responses with `$.response` - -`$.response` is a fluent builder for HTTP responses. Start by picking a status code, then chain one or more methods: - -| Method | Description | -| ------------------------------ | --------------------------------------------------------------------------------------- | -| `.random()` | Returns random data generated from the OpenAPI schema (uses `examples` where available) | -| `.example(name)` | Returns a specific named example from the OpenAPI spec | -| `.json(content)` | Returns a JSON body (also converts to XML automatically when the client requests it) | -| `.text(content)` | Returns a plain-text body | -| `.html(content)` | Returns an HTML body | -| `.xml(content)` | Returns an XML body | -| `.match(contentType, content)` | Returns a body with an explicit content type; chain multiple for content negotiation | -| `.header(name, value)` | Adds a response header | - -```ts -return $.response[200].header("x-request-id", "abc123").json({ ok: true }); -``` - -#### Setting cookies - -Use `.cookie(name, value, options?)` to set one or more cookies. Each call appends a new `Set-Cookie` header and returns the builder for chaining. - -```ts -return $.response[200] - .cookie("sessionId", "abc123", { - httpOnly: true, - secure: true, - sameSite: "lax", - path: "/", - maxAge: 3600, - }) - .json({ ok: true }); -``` - -Multiple cookies: - -```ts -return $.response[200] - .cookie("sessionId", "abc123") - .cookie("theme", "dark") - .json({ ok: true }); -``` - -Supported options: - -| Option | Description | -|--------|-------------| -| `path` | Cookie path (e.g. `"/"`) | -| `domain` | Cookie domain | -| `maxAge` | Max age in seconds (`Max-Age`) | -| `expires` | Expiry `Date` object (`Expires`) | -| `httpOnly` | Sets the `HttpOnly` flag when `true` | -| `secure` | Sets the `Secure` flag when `true` | -| `sameSite` | `"lax"`, `"strict"`, or `"none"` (`SameSite`) | - -#### Using named examples - -If your OpenAPI spec defines named examples for a response, you can return a specific one by name using `.example()`. The name is autocompleted and type-checked — passing an unknown name is a compile error. - -```ts -// Return a specific named example -return $.response[200].example("successResponse"); - -// Chain additional decorations after selecting an example -return $.response[200] - .example("successResponse") - .header("x-request-id", "abc123"); -``` - -> [!TIP] -> Your IDE's autocomplete knows which status codes, headers, and response shapes are valid for each endpoint — based directly on your OpenAPI spec. If you omit a required header, TypeScript will tell you. When the spec changes and types are regenerated, TypeScript will surface any mismatches. - -### Reading request data - -The request is exposed through four typed properties: - -| Property | Contents | -| ----------- | ------------------------------------------------------------ | -| `$.path` | Path parameters (e.g. `$.path.userId` for `/users/{userId}`) | -| `$.query` | Query string parameters | -| `$.headers` | Request headers | -| `$.body` | Request body | - -```ts -export const GET: HTTP_GET = ($) => { - if ($.headers["x-token"] !== "super-secret") { - return $.response[401].text("Unauthorized"); - } - - const content = - `Results for "${$.query.keyword}" in ${$.path.groupName}` + - ` with tags: ${$.body.tags.join(", ")}`; - - return $.response[200].text(content); -}; -``` - -All four objects are typed from your OpenAPI spec, so autocomplete works for parameter names and values. - -### Basic auth: `$.auth` - -When a request includes HTTP Basic credentials, they're available at `$.auth.username` and `$.auth.password`. - -Support for other security schemes (API key, OAuth 2, OpenID Connect, mutual TLS) is planned. [Open an issue](https://github.com/pmcelhaney/counterfact/issues) to help prioritize. - -### Simulating latency: `$.delay()` - -Counterfact responds much faster than a real server. To test loading states and timeouts, use `$.delay()`: - -```ts -// pause for exactly one second -await $.delay(1000); - -// pause for a random duration between 1 and 5 seconds -await $.delay(1000, 5000); -``` - -### Escaping the type system: `$.x` - -Counterfact translates your OpenAPI spec into strict TypeScript types. If your spec is incomplete, or you need to return something outside the spec (like a `500` error that isn't documented), the strict types can get in the way. - -`$.x` is an alias for `$` with all types widened to `any`, giving you an escape hatch: - -```ts -export const GET: HTTP_GET = ($) => { - // header not defined in OpenAPI spec - $.headers["my-undocumented-header"]; // TypeScript error - $.x.headers["my-undocumented-header"]; // ok - - // status code not defined in OpenAPI spec - return $.response[500].text("Error!"); // TypeScript error - return $.x.response[500].text("Error!"); // ok -}; -``` +This page is your map to the documentation. --- -## State: Context Objects +## Getting started - - -The `$.context` object is how routes share in-memory state. It's an instance of the `Context` class exported from `_.context.ts` in the same directory (or the nearest parent directory that has one). - -```ts -// routes/pet.ts -export const POST: HTTP_POST = ($) => { - return $.response[200].json($.context.addPet($.body)); -}; - -// routes/pet/{petId}.ts -export const GET: HTTP_GET = ($) => { - const pet = $.context.getPetById($.path.petId); - if (pet === undefined) - return $.response[404].text(`Pet ${$.path.petId} not found.`); - return $.response[200].json(pet); -}; -``` - -Customize `_.context.ts` to hold whatever state and business logic your mock needs: - -```ts -// routes/_.context.ts -export class Context { - pets: Pet[] = []; - - addPet(pet: Pet) { - const id = this.pets.length; - this.pets.push({ ...pet, id }); - return this.pets[id]; - } - - getPetById(id: number) { - return this.pets[id]; - } -} -``` - -> [!IMPORTANT] -> Keep context in memory. Counterfact is a development tool — starting fresh each time is a feature, not a bug. In-memory state also makes the server very fast. - -For large APIs you can nest context objects. Any subdirectory can have its own `_.context.ts`. One context can access another via the `loadContext` function passed to its constructor: - -```ts -// routes/users/_.context.ts -export class Context { - constructor({ loadContext }) { - this.rootContext = loadContext("/"); - this.petsContext = loadContext("/pets"); - } -} -``` - -### Loading JSON data with `readJson` - -Use the `readJson` function (also passed to the constructor) to load static JSON data into your context. The path is resolved relative to the `_.context.ts` file. - -```ts -// routes/_.context.ts -export class Context { - private readonly readJson: (path: string) => Promise; - - constructor({ readJson }: { readJson: (path: string) => Promise }) { - this.readJson = readJson; - } - - async getSeeds() { - return this.readJson("../mocks/seeds.json"); - } -} -``` +New to Counterfact? Begin with the [Getting Started guide](./getting-started.md) for a step-by-step walkthrough. --- -## TypeScript Native Mode - -By default Counterfact compiles your route files into a `.cache/` directory before loading them. When you run Counterfact under a TypeScript-aware runtime it detects this automatically and skips compilation, loading `.ts` source files directly. The result is the same hot-reload experience with no build step. - -### How detection works - -At startup Counterfact writes a small temporary TypeScript file to a system temp directory and attempts to import it. If the import succeeds the runtime is TypeScript-capable and the transpiler is skipped. No configuration is needed. - -### With tsx - -Invoke the `counterfact` binary through [tsx](https://tsx.is/): - -```sh -# one-off via npx -npx tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch - -# or in package.json scripts -"mock": "tsx ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" -``` - -tsx is available as a dev dependency (`npm install --save-dev tsx`). - -### With plain Node.js - -Node 22.6+ ships with `--experimental-strip-types`. A small module hook bundled with Counterfact (`bin/register-ts-loader.mjs`) adds the `.js` → `.ts` import remapping that Node doesn't do on its own: - -```sh -node \ - --experimental-strip-types \ - --import ./node_modules/counterfact/bin/register-ts-loader.mjs \ - ./node_modules/counterfact/bin/counterfact.js \ - openapi.yaml api --serve --watch -``` - -In `package.json`: - -```json -"scripts": { - "mock": "node --experimental-strip-types --import ./node_modules/counterfact/bin/register-ts-loader.mjs ./node_modules/counterfact/bin/counterfact.js openapi.yaml api --serve --watch" -} -``` - -### What changes in native mode - -| | Default (compiled) | Native TS | -| ------------------- | -------------------------------------- | -------------------------- | -| Startup | Compiles routes to `.cache/` first | Loads `.ts` files directly | -| `.cache/` directory | Created and managed automatically | Not used | -| Dependencies | None extra | tsx _or_ Node 22.6+ | -| Route file format | Generated `.ts` files (same as always) | Same | -| Hot reload | ✅ | ✅ | +## Features + +| Feature | Description | +|---|---| +| [Generated code](./features/generated-code.md) | How Counterfact generates TypeScript from your OpenAPI spec | +| [Routes](./features/routes.md) | Writing route handlers, building responses, reading request data | +| [State (context objects)](./features/state.md) | Sharing in-memory state across routes | +| [Hot reload](./features/hot-reload.md) | Live file updates without restarting the server | +| [REPL](./features/repl.md) | Interactive terminal for runtime inspection and control | +| [Proxy](./features/proxy.md) | Mix real backend calls with mocked endpoints | +| [Middleware](./features/middleware.md) | Cross-cutting request/response logic | +| [TypeScript native mode](./features/typescript-native-mode.md) | Run route files directly without a compilation step | +| [Programmatic API](./features/programmatic-api.md) | Embed Counterfact in test suites with Playwright, Cypress, etc. | +| [Without OpenAPI](./features/without-openapi.md) | Use Counterfact without an OpenAPI document | --- -## Hot Reload 🔥 - -Save a file — any route or context file — and the running server picks it up immediately. No restart needed, and in-memory state is preserved across reloads. - -This makes it fast to set up edge cases like: - -- What does the UI do 8 clicks deep when the server returns a 500? -- What if there are zero results? What if there are 10,000? -- What if the server is slow? - -Find the file corresponding to the route, change behavior by editing the TypeScript code, and continue testing. - -Depending on the scenario, you may want to commmit your changes to source control or throw them away. - ---- - -## REPL ⬣ - -The REPL is a JavaScript prompt connected directly to the running server — like the browser DevTools console, but for your mock API. After starting Counterfact you'll see: - -``` -____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___ -|___ [__] |__| |\| | |=== |--< |--- |--| |___ | - Storybook for the back-end - -| API Base URL ==> http://localhost:3100 -| Admin Console ==> http://localhost:3100/counterfact/ - -⬣> -``` - -At the prompt you can interact with the live context: - -```js -// add a single pet -context.addPet({ name: "Fluffy", photoUrls: [] }); - -// add 100 pets -for (let i = 0; i < 100; i++) - context.addPet({ name: `Pet ${i}`, photoUrls: [] }); - -// query state -context.pets.filter((pet) => pet.name.startsWith("F")); -``` - -To access context from a subdirectory: +## Patterns -```js -const petsContext = loadContext("/pets"); -``` +Patterns are reusable solutions to recurring problems when building API simulations. -The built-in `client` object lets you make HTTP requests from the prompt without leaving the terminal: - -```js -client.get("/users"); -client.post("/users", { name: "bob" }); -client.put("/users/1", { name: "robert" }, { "x-api-version": "2" }); -``` - -All standard HTTP methods are supported. Arguments are: path, body (where applicable), headers. - -The built-in `route()` function creates a fluent request builder that validates required parameters against your OpenAPI document before sending: - -```js -// Build and inspect before sending -const req = route("/pet/{petId}").method("get").path({ petId: 42 }) -req.ready() // true / false -req.missing() // lists missing required parameters -req.help() // prints OpenAPI docs for the operation -await req.send() -``` - -See the [Route Builder guide](./route-builder.md) for full documentation. - -### Scenario scripts with `.apply` - -For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.apply`: - -``` -⬣> .apply soldPets -``` - -**Path resolution:** the argument to `.apply` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `/scenarios/` (with `index.ts` as the default file). - -| Command | File | Function | -|---|---|---| -| `.apply foo` | `scenarios/index.ts` | `foo` | -| `.apply foo/bar` | `scenarios/foo.ts` | `bar` | -| `.apply foo/bar/baz` | `scenarios/foo/bar.ts` | `baz` | - -A scenario function receives a single argument with `{ context, loadContext, routes, route }`: - -```ts -// scenarios/index.ts -import type { Scenario } from "../types/scenario-context.js"; - -export const soldPets: Scenario = ($) => { - // Mutate context directly — same as typing in the REPL - $.context.petService.reset(); - $.context.petService.addPet({ id: 1, status: "sold" }); - $.context.petService.addPet({ id: 2, status: "available" }); - - // Store a pre-configured route builder for later use in the REPL - $.routes.findSold = $ - .route("/pet/findByStatus") - .method("get") - .query({ status: "sold" }); -} -``` - -After the command runs you can immediately use anything stored in `$.routes`: - -```js -⬣> routes.findSold.send() -``` - -The `Scenario` type and `ApplyContext` interface are generated automatically into `types/scenario-context.ts` when you run Counterfact with type generation enabled. +See the [patterns index](./patterns/index.md) for the full list. --- -## Proxy 🔀 - -You can mix real backend calls with mocks — useful when some endpoints are not finished or you need to test edge cases like 500 errors. +## Reference -To proxy a single endpoint from within a route file: +Complete technical reference: the `$` parameter API, CLI flags, architecture overview, and more. -```ts -// routes/pet/{petId}.ts -export const GET: HTTP_GET = ($) => { - return $.proxy("https://uat.petstore.example.com/pet"); -}; -``` - -To set a proxy for then entire API at runtime pass `--proxy-url` on the CLI: - -```sh -npx counterfact@latest openapi.yaml api --proxy-url https://uat.petstore.example.com -``` - -From the REPL, you can toggle proxying for the whole API or specific routes: - -``` -⬣> .proxy on /payments # forward /payments to the real API -⬣> .proxy off /payments # let Counterfact handle /payments -⬣> .proxy off # stop proxying everything -``` - -Type `.proxy help` in the REPL for the full list of proxy commands. +→ [Reference](./reference.md) --- -## Middleware - -Place a `_.middleware.ts` file in any `routes/` subdirectory to intercept requests and responses for that subtree. Middleware applies from the root down — a `_.middleware.ts` at the root runs for every request. +## FAQ -```ts -// routes/_.middleware.ts -export async function middleware($, respondTo) { - const response = await respondTo($); - return response.header("X-Custom-Header", "Custom Value"); -} -``` +Common questions about state, types, hot reload, code generation, and regeneration. -`respondTo($)` passes the request to the next middleware layer or the route handler, and returns the response. You can modify `$` before calling `respondTo`, modify the response after, or both. +→ [FAQ](./faq.md) --- -## Programmatic API - -Counterfact can be used as a library — for example, from [Playwright](https://playwright.dev/) or [Cypress](https://www.cypress.io/) tests. This lets you manipulate context state directly in test code without relying on special magic values in mock logic. - -```ts -import { counterfact } from "counterfact"; - -const config = { - basePath: "./api", // directory containing your routes/ - openApiPath: "./api.yaml", // optional; pass "_" to run without a spec - port: 8100, - alwaysFakeOptionals: false, - generate: { routes: false, types: false }, - proxyPaths: new Map(), - proxyUrl: "", - routePrefix: "", - startAdminApi: false, - startRepl: false, // do not auto-start the REPL - startServer: true, - watch: { routes: false, types: false }, -}; - -const { contextRegistry, start } = await counterfact(config); -const { stop } = await start(config); - -// Get the root context — the object your routes see as $.context -const rootContext = contextRegistry.find("/"); -``` - -Once you have `rootContext` you can read and write any state that your route handlers expose. - -### Example: parameterised auth scenario with Playwright - -Given this route handler: - -```ts -// routes/auth/login.ts -export const POST: HTTP_POST = ($) => { - if ($.context.passwordResponse === "ok") return $.response[200]; - if ($.context.passwordResponse === "expired") - return $.response[403].header("reason", "expired-password"); - return $.response[401]; -}; -``` - -A Playwright test can flip between scenarios without hard-coded usernames: - -```ts -import { counterfact } from "counterfact"; -import { chromium } from "playwright"; - -let page; -let rootContext; - -let page; -let rootContext; -let stop; -let browser; - -beforeAll(async () => { - browser = await chromium.launch({ headless: true }); - page = await (await browser.newContext()).newPage(); - - const { contextRegistry, start } = await counterfact(config); - ({ stop } = await start(config)); - rootContext = contextRegistry.find("/"); -}); - -afterAll(async () => { - await stop(); - await browser.close(); -}); - -it("rejects an incorrect password", async () => { - rootContext.passwordResponse = "incorrect"; - await attemptToLogIn(); - expect(await page.isVisible("#authentication-error")).toBe(true); -}); - -it("loads the dashboard on success", async () => { - rootContext.passwordResponse = "ok"; - await attemptToLogIn(); - expect(await page.isVisible("#dashboard")).toBe(true); -}); - -it("prompts for a password change when the password has expired", async () => { - rootContext.passwordResponse = "expired"; - await attemptToLogIn(); - expect(await page.isVisible("#password-change-form")).toBe(true); -}); -``` - -### Return value of `counterfact()` - -| Property | Type | Description | -| ----------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| `contextRegistry` | `ContextRegistry` | Registry of all context objects keyed by path. Call `.find(path)` to get the context for a given route prefix. | -| `registry` | `Registry` | Registry of all loaded route modules. | -| `koaApp` | `Koa` | The underlying Koa application. | -| `koaMiddleware` | `Koa.Middleware` | The Counterfact request-dispatch middleware. | -| `start(config)` | `async (config) => { stop() }` | Starts the server (and optionally the file watcher and code generator). Returns a `stop()` function to gracefully shut down. | -| `startRepl()` | `() => REPLServer` | Starts the interactive REPL. Returns the REPL server instance. | - ---- +## How it compares -## See also +Side-by-side comparison with json-server, WireMock, Prism, Microcks, and MSW. -- [Getting started](./getting-started.md) -- [Usage patterns](./usage-patterns.md) — explore an API, simulate failures, hybrid proxy, agentic coding, and more -- [Reference](./reference.md) — `$` parameter, response builder methods, full CLI flags, architecture overview -- [FAQ](./faq.md) — common questions about state, type safety, regeneration, and programmatic use -- [How it compares](./comparison.md) — side-by-side with json-server, WireMock, Prism, Microcks, and MSW -- [Generated code FAQ](./faq-generated-code.md) — questions about source control, editing, and regeneration -- [Petstore example](https://github.com/counterfact/example-petstore) — a complete worked example +→ [How it compares](./comparison.md) diff --git a/src/server/module-loader.ts b/src/server/module-loader.ts index 80aad3d83..e6be7fb10 100644 --- a/src/server/module-loader.ts +++ b/src/server/module-loader.ts @@ -82,7 +82,7 @@ export class ModuleLoader extends EventTarget { if (pathName.includes("$.context") && eventName === "add") { process.stdout.write( - `\n\n!!! The file at ${pathName} needs a minor update.\n See https://github.com/pmcelhaney/counterfact/blob/main/docs/context-change.md\n\n\n`, + `\n\n!!! The file at ${pathName} needs a minor update.\n See https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md\n\n\n`, ); return; diff --git a/src/typescript-generator/read-only-comments.ts b/src/typescript-generator/read-only-comments.ts index a19d62d72..41f6e70a7 100644 --- a/src/typescript-generator/read-only-comments.ts +++ b/src/typescript-generator/read-only-comments.ts @@ -1,5 +1,5 @@ export const READ_ONLY_COMMENTS = [ "This code was automatically generated from an OpenAPI description.", "Do not edit this file. Edit the OpenAPI file instead.", - "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md", + "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md", ]; diff --git a/test/typescript-generator/__snapshots__/generate.test.ts.snap b/test/typescript-generator/__snapshots__/generate.test.ts.snap index 082b3bad2..567127e01 100644 --- a/test/typescript-generator/__snapshots__/generate.test.ts.snap +++ b/test/typescript-generator/__snapshots__/generate.test.ts.snap @@ -17,7 +17,7 @@ export const PUT: updatePet = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 2`] = ` "types/paths/pet.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; @@ -171,7 +171,7 @@ export const GET: findPetsByStatus = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 4`] = ` "types/paths/pet/findByStatus.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -258,7 +258,7 @@ export const GET: findPetsByTags = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 6`] = ` "types/paths/pet/findByTags.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -355,7 +355,7 @@ export const DELETE: deletePet = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 8`] = ` "types/paths/pet/{petId}.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -538,7 +538,7 @@ export const POST: uploadFile = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 10`] = ` "types/paths/pet/{petId}/uploadImage.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; @@ -612,7 +612,7 @@ export const GET: getInventory = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 12`] = ` "types/paths/store/inventory.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -674,7 +674,7 @@ export const POST: placeOrder = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 14`] = ` "types/paths/store/order.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -751,7 +751,7 @@ export const DELETE: deleteOrder = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 16`] = ` "types/paths/store/order/{orderId}.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts"; @@ -896,7 +896,7 @@ export const POST: createUser = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 18`] = ` "types/paths/user.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; @@ -968,7 +968,7 @@ export const POST: createUsersWithListInput = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 20`] = ` "types/paths/user/createWithList.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1049,7 +1049,7 @@ export const GET: loginUser = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 22`] = ` "types/paths/user/login.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1139,7 +1139,7 @@ export const GET: logoutUser = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 24`] = ` "types/paths/user/logout.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1203,7 +1203,7 @@ export const DELETE: deleteUser = async ($) => { exports[`end-to-end test generates the same code for pet store that it did on the last test run 26`] = ` "types/paths/user/{username}.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1531,7 +1531,7 @@ export const GET: HTTP_GET = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 2`] = ` "types/paths/index.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; @@ -1596,7 +1596,7 @@ export const GET: HTTP_GET = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 4`] = ` "types/paths/count.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; @@ -1661,7 +1661,7 @@ export const GET: helloKitty = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 6`] = ` "types/paths/hello/kitty.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1726,7 +1726,7 @@ export const GET: HTTP_GET = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 8`] = ` "types/paths/hello/{name}.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../counterfact-types/index.ts"; @@ -1801,7 +1801,7 @@ export const GET: HTTP_GET = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 10`] = ` "types/paths/path-one.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../counterfact-types/index.ts"; @@ -1869,7 +1869,7 @@ export const GET: HTTP_GET = async ($) => { exports[`end-to-end test generates the same code for the example that it did on the last test run 12`] = ` "types/paths/weird/path/with:colon.types.ts:// This code was automatically generated from an OpenAPI description. // Do not edit this file. Edit the OpenAPI file instead. -// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md +// For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md import type { WideOperationArgument } from "../../../../counterfact-types/index.ts"; import type { OmitValueWhenNever } from "../../../../counterfact-types/index.ts";