counterfact 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,6 +96,16 @@ export const DELETE: HTTP_DELETE = ($) => {
96
96
  };
97
97
  ```
98
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
+
99
109
  ### State management with plain old objects
100
110
 
101
111
  Use a `_.context.ts` file to share in-memory state across routes. POST data and GET it back, just like a real API.
package/bin/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # `bin/` — CLI Entry Point
2
+
3
+ This directory contains the executable script that is run when a developer invokes `npx counterfact` (or `counterfact` after a global install).
4
+
5
+ ## Files
6
+
7
+ | File | Description |
8
+ |---|---|
9
+ | `counterfact.js` | Parses command-line arguments with [Commander](https://github.com/tj/commander.js), validates inputs, and calls `counterfact()` from `src/app.ts` to start the server, code generator, file watcher, and/or REPL |
10
+
11
+ ## How It Works
12
+
13
+ ```
14
+ npx counterfact openapi.yaml ./api [options]
15
+
16
+
17
+ ┌────────────────────────────┐
18
+ │ counterfact.js │
19
+ │ │
20
+ │ 1. Parse args (Commander) │
21
+ │ 2. Resolve paths │
22
+ │ 3. Build Config object │
23
+ │ 4. Run migrations if │
24
+ │ old layout detected │
25
+ │ 5. Call start(config) │
26
+ │ from src/app.ts │
27
+ └────────────────────────────┘
28
+ ```
29
+
30
+ ### Key CLI Options
31
+
32
+ | Option | Description |
33
+ |---|---|
34
+ | `--port <number>` | HTTP server port (default: `3100`) |
35
+ | `-o, --open` | Open the dashboard in a browser after startup |
36
+ | `-g, --generate` | Generate route and type files from the OpenAPI spec |
37
+ | `-w, --watch` | Re-generate whenever the spec changes |
38
+ | `-s, --serve` | Start the HTTP server |
39
+ | `-r, --repl` | Start the interactive REPL |
40
+ | `--proxy-url <url>` | Forward all unmatched requests to this upstream URL |
41
+ | `--prefix <path>` | Base path prefix for all routes (e.g. `/api/v1`) |
42
+
43
+ Run `npx counterfact --help` to see the full option list.
@@ -0,0 +1,14 @@
1
+ # `src/client/` — Built-in UI Templates
2
+
3
+ This directory contains [Handlebars](https://handlebarsjs.com/) (`.hbs`) templates that are rendered by `page-middleware.ts` to produce the browser-facing pages bundled with Counterfact.
4
+
5
+ ## Files
6
+
7
+ | File | Description |
8
+ |---|---|
9
+ | `index.html.hbs` | Template for the Counterfact dashboard (`/counterfact/`); lists registered routes and shows server status |
10
+ | `rapi-doc.html.hbs` | Template for the interactive API documentation page (`/counterfact/swagger/`); embeds the [RapiDoc](https://rapidocweb.com/) viewer and adds VSCode "open file" links |
11
+
12
+ ## How It Works
13
+
14
+ When a request arrives for `/counterfact/` or `/counterfact/swagger/`, `page-middleware.ts` compiles the appropriate template with runtime data (routes, port, base path, etc.) and sends the resulting HTML to the browser. No build step is required; templates are rendered on the fly.
@@ -37,6 +37,7 @@ type OmitValueWhenNever<Base> = Pick<
37
37
 
38
38
  interface OpenApiResponse {
39
39
  content: { [key: MediaType]: OpenApiContent };
40
+ examples?: { [key: string]: unknown };
40
41
  headers: { [key: string]: OpenApiHeader };
41
42
  requiredHeaders: string;
42
43
  }
@@ -105,9 +106,16 @@ type RandomFunction<Response extends OpenApiResponse> = <
105
106
  Header extends string & keyof Response["headers"],
106
107
  >() => COUNTERFACT_RESPONSE;
107
108
 
109
+ type ExampleNames<Response extends OpenApiResponse> = Response extends {
110
+ examples: infer E;
111
+ }
112
+ ? keyof E & string
113
+ : never;
114
+
108
115
  interface ResponseBuilder {
109
116
  [status: number | `${number} ${string}`]: ResponseBuilder;
110
117
  content?: { body: unknown; type: string }[];
118
+ example: (name: string) => ResponseBuilder;
111
119
  header: (name: string, value: string) => ResponseBuilder;
112
120
  headers: { [name: string]: string };
113
121
  html: (body: unknown) => ResponseBuilder;
@@ -143,6 +151,9 @@ export type GenericResponseBuilderInner<
143
151
  random: [keyof Response["content"]] extends [never]
144
152
  ? never
145
153
  : RandomFunction<Response>;
154
+ example: [ExampleNames<Response>] extends [never]
155
+ ? never
156
+ : (name: ExampleNames<Response>) => COUNTERFACT_RESPONSE;
146
157
  text: MaybeShortcut<["text/plain"], Response>;
147
158
  xml: MaybeShortcut<["application/xml", "text/xml"], Response>;
148
159
  }>;
@@ -252,6 +263,7 @@ interface OpenApiOperation {
252
263
  }
253
264
 
254
265
  interface WideResponseBuilder {
266
+ example: (name: string) => WideResponseBuilder;
255
267
  header: (body: unknown) => WideResponseBuilder;
256
268
  html: (body: unknown) => WideResponseBuilder;
257
269
  json: (body: unknown) => WideResponseBuilder;
@@ -274,6 +286,7 @@ interface WideOperationArgument {
274
286
  export type { COUNTERFACT_RESPONSE };
275
287
 
276
288
  export type {
289
+ ExampleNames,
277
290
  HttpStatusCode,
278
291
  MaybePromise,
279
292
  MediaType,
@@ -63,6 +63,24 @@ export function createResponseBuilder(operation, config) {
63
63
  ],
64
64
  };
65
65
  },
66
+ example(name) {
67
+ if (operation.produces) {
68
+ return unknownStatusCodeResponse(this.status);
69
+ }
70
+ const response = operation.responses[this.status ?? "default"] ??
71
+ operation.responses.default;
72
+ if (response?.content === undefined) {
73
+ return unknownStatusCodeResponse(this.status);
74
+ }
75
+ const { content } = response;
76
+ return {
77
+ ...this,
78
+ content: Object.keys(content).map((type) => ({
79
+ body: convertToXmlIfNecessary(type, content[type]?.examples?.[name]?.value, content[type]?.schema),
80
+ type,
81
+ })),
82
+ };
83
+ },
66
84
  random() {
67
85
  if (config?.alwaysFakeOptionals) {
68
86
  JSONSchemaFaker.option("alwaysFakeOptionals", true);
@@ -52,6 +52,25 @@ export class ResponseTypeCoder extends TypeCoder {
52
52
  .map(({ name }) => `"${name}"`);
53
53
  return requiredHeaders.length === 0 ? "never" : requiredHeaders.join(" | ");
54
54
  }
55
+ buildExamplesObjectType(response) {
56
+ if (!response.has("content")) {
57
+ return "{}";
58
+ }
59
+ const exampleNames = [];
60
+ response.get("content").forEach((content) => {
61
+ if (content.has("examples")) {
62
+ content.get("examples").forEach((_, name) => {
63
+ if (!exampleNames.includes(name)) {
64
+ exampleNames.push(name);
65
+ }
66
+ });
67
+ }
68
+ });
69
+ if (exampleNames.length === 0) {
70
+ return "{}";
71
+ }
72
+ return printObject(exampleNames.map((name) => [name, "unknown"]));
73
+ }
55
74
  modulePath() {
56
75
  return `types/${this.requirement.data.$ref}.ts`;
57
76
  }
@@ -60,6 +79,7 @@ export class ResponseTypeCoder extends TypeCoder {
60
79
  headers: ${this.printHeaders(script, this.requirement)};
61
80
  requiredHeaders: ${this.printRequiredHeaders(this.requirement)};
62
81
  content: ${this.printContentObjectType(script, this.requirement)};
82
+ examples: ${this.buildExamplesObjectType(this.requirement)};
63
83
  }`;
64
84
  }
65
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.",
5
5
  "type": "module",
6
6
  "main": "./dist/app.js",