counterfact 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/bin/README.md +44 -0
- package/bin/counterfact.js +22 -3
- package/dist/client/README.md +14 -0
- package/dist/server/counterfact-types/OpenApiHeader.ts +2 -1
- package/dist/server/counterfact-types/index.ts +17 -1
- package/dist/server/module-loader.js +18 -0
- package/dist/server/registry.js +4 -5
- package/dist/server/response-builder.js +28 -0
- package/dist/typescript-generator/generate.js +6 -0
- package/dist/typescript-generator/prune.js +101 -0
- package/dist/typescript-generator/response-type-coder.js +20 -0
- package/package.json +2 -2
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.
|
|
@@ -192,6 +202,7 @@ npx counterfact@latest [openapi.yaml] [destination] [options]
|
|
|
192
202
|
| `-w, --watch` | Generate and watch for spec changes |
|
|
193
203
|
| `-s, --serve` | Start the mock server |
|
|
194
204
|
| `-r, --repl` | Start the interactive REPL |
|
|
205
|
+
| `--spec <path>` | Path or URL to the OpenAPI document |
|
|
195
206
|
| `--proxy-url <url>` | Forward all requests to this URL by default |
|
|
196
207
|
| `--prefix <path>` | Base path prefix (e.g. `/api/v1`) |
|
|
197
208
|
|
package/bin/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
| `--spec <path>` | Path or URL to the OpenAPI document (alternative to positional argument) |
|
|
41
|
+
| `--proxy-url <url>` | Forward all unmatched requests to this upstream URL |
|
|
42
|
+
| `--prefix <path>` | Base path prefix for all routes (e.g. `/api/v1`) |
|
|
43
|
+
|
|
44
|
+
Run `npx counterfact --help` to see the full option list.
|
package/bin/counterfact.js
CHANGED
|
@@ -102,11 +102,20 @@ async function main(source, destination) {
|
|
|
102
102
|
|
|
103
103
|
const options = program.opts();
|
|
104
104
|
|
|
105
|
+
// --spec takes precedence over the positional [openapi.yaml] argument.
|
|
106
|
+
// When --spec is provided, the [openapi.yaml] positional slot shifts to
|
|
107
|
+
// become the [destination] argument (so `counterfact --spec api.yaml ./api`
|
|
108
|
+
// works the same as `counterfact api.yaml ./api`).
|
|
109
|
+
if (options.spec) {
|
|
110
|
+
if (source !== "_") {
|
|
111
|
+
destination = source;
|
|
112
|
+
}
|
|
113
|
+
source = options.spec;
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
const args = process.argv;
|
|
106
117
|
|
|
107
|
-
const destinationPath = nodePath
|
|
108
|
-
.join(process.cwd(), destination)
|
|
109
|
-
.replaceAll("\\", "/");
|
|
118
|
+
const destinationPath = nodePath.resolve(destination).replaceAll("\\", "/");
|
|
110
119
|
|
|
111
120
|
const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
|
|
112
121
|
|
|
@@ -155,6 +164,8 @@ async function main(source, destination) {
|
|
|
155
164
|
options.watch ||
|
|
156
165
|
options.watchTypes ||
|
|
157
166
|
options.buildCache,
|
|
167
|
+
|
|
168
|
+
prune: Boolean(options.prune),
|
|
158
169
|
},
|
|
159
170
|
|
|
160
171
|
openApiPath: source,
|
|
@@ -326,5 +337,13 @@ program
|
|
|
326
337
|
"--always-fake-optionals",
|
|
327
338
|
"random responses will include optional fields",
|
|
328
339
|
)
|
|
340
|
+
.option(
|
|
341
|
+
"--prune",
|
|
342
|
+
"remove route files that no longer exist in the OpenAPI spec",
|
|
343
|
+
)
|
|
344
|
+
.option(
|
|
345
|
+
"--spec <string>",
|
|
346
|
+
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
347
|
+
)
|
|
329
348
|
.action(main)
|
|
330
349
|
.parse(process.argv);
|
|
@@ -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,7 +37,8 @@ type OmitValueWhenNever<Base> = Pick<
|
|
|
37
37
|
|
|
38
38
|
interface OpenApiResponse {
|
|
39
39
|
content: { [key: MediaType]: OpenApiContent };
|
|
40
|
-
|
|
40
|
+
examples?: { [key: string]: unknown };
|
|
41
|
+
headers: { [key: string]: { schema: unknown } };
|
|
41
42
|
requiredHeaders: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -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
|
}>;
|
|
@@ -246,12 +257,16 @@ interface OpenApiOperation {
|
|
|
246
257
|
};
|
|
247
258
|
};
|
|
248
259
|
examples?: { [key: string]: unknown };
|
|
260
|
+
headers?: {
|
|
261
|
+
[name: string]: OpenApiHeader;
|
|
262
|
+
};
|
|
249
263
|
schema?: { [key: string]: unknown };
|
|
250
264
|
};
|
|
251
265
|
};
|
|
252
266
|
}
|
|
253
267
|
|
|
254
268
|
interface WideResponseBuilder {
|
|
269
|
+
example: (name: string) => WideResponseBuilder;
|
|
255
270
|
header: (body: unknown) => WideResponseBuilder;
|
|
256
271
|
html: (body: unknown) => WideResponseBuilder;
|
|
257
272
|
json: (body: unknown) => WideResponseBuilder;
|
|
@@ -274,6 +289,7 @@ interface WideOperationArgument {
|
|
|
274
289
|
export type { COUNTERFACT_RESPONSE };
|
|
275
290
|
|
|
276
291
|
export type {
|
|
292
|
+
ExampleNames,
|
|
277
293
|
HttpStatusCode,
|
|
278
294
|
MaybePromise,
|
|
279
295
|
MediaType,
|
|
@@ -110,10 +110,28 @@ export class ModuleLoader extends EventTarget {
|
|
|
110
110
|
if (basename(pathName).startsWith("_.context.") &&
|
|
111
111
|
isContextModule(endpoint)) {
|
|
112
112
|
const loadContext = (path) => this.contextRegistry.find(path);
|
|
113
|
+
const contextDir = nodePath.dirname(unescapePathForWindows(pathName));
|
|
114
|
+
const readJson = async (relativePath) => {
|
|
115
|
+
const absolutePath = nodePath.resolve(contextDir, relativePath);
|
|
116
|
+
let content;
|
|
117
|
+
try {
|
|
118
|
+
content = await fs.readFile(absolutePath, "utf8");
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new Error(`readJson: could not read file at "${absolutePath}" (resolved from "${relativePath}" relative to "${contextDir}")`);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(content);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error(`readJson: file at "${absolutePath}" does not contain valid JSON`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
113
130
|
this.contextRegistry.update(directory,
|
|
114
131
|
// @ts-expect-error TS says Context has no constructable signatures but that's not true?
|
|
115
132
|
new endpoint.Context({
|
|
116
133
|
loadContext,
|
|
134
|
+
readJson,
|
|
117
135
|
}));
|
|
118
136
|
return;
|
|
119
137
|
}
|
package/dist/server/registry.js
CHANGED
|
@@ -37,7 +37,7 @@ export class Registry {
|
|
|
37
37
|
moduleTree = new ModuleTree();
|
|
38
38
|
middlewares = new Map();
|
|
39
39
|
constructor() {
|
|
40
|
-
this.middlewares.set("
|
|
40
|
+
this.middlewares.set("", ($, respondTo) => respondTo($));
|
|
41
41
|
}
|
|
42
42
|
get routes() {
|
|
43
43
|
return this.moduleTree.routes;
|
|
@@ -46,7 +46,7 @@ export class Registry {
|
|
|
46
46
|
this.moduleTree.add(url, module);
|
|
47
47
|
}
|
|
48
48
|
addMiddleware(url, callback) {
|
|
49
|
-
this.middlewares.set(url, callback);
|
|
49
|
+
this.middlewares.set(url === "/" ? "" : url, callback);
|
|
50
50
|
}
|
|
51
51
|
remove(url) {
|
|
52
52
|
this.moduleTree.remove(url);
|
|
@@ -111,11 +111,10 @@ export class Registry {
|
|
|
111
111
|
};
|
|
112
112
|
const middlewares = this.middlewares;
|
|
113
113
|
function recurse(path, respondTo) {
|
|
114
|
+
debug("recursing path", path);
|
|
114
115
|
if (path === null)
|
|
115
116
|
return respondTo;
|
|
116
|
-
const nextPath = path === "
|
|
117
|
-
? null
|
|
118
|
-
: path.slice(0, path.lastIndexOf("/")) || "/";
|
|
117
|
+
const nextPath = path === "" ? null : path.slice(0, path.lastIndexOf("/"));
|
|
119
118
|
const middleware = middlewares.get(path);
|
|
120
119
|
if (middleware !== undefined) {
|
|
121
120
|
return recurse(nextPath, ($) => middleware($, respondTo));
|
|
@@ -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);
|
|
@@ -78,6 +96,12 @@ export function createResponseBuilder(operation, config) {
|
|
|
78
96
|
return unknownStatusCodeResponse(this.status);
|
|
79
97
|
}
|
|
80
98
|
const { content } = response;
|
|
99
|
+
const generatedHeaders = {};
|
|
100
|
+
for (const [name, header] of Object.entries(response.headers ?? {})) {
|
|
101
|
+
if (header.required && !(name in (this.headers ?? {}))) {
|
|
102
|
+
generatedHeaders[name] = JSONSchemaFaker.generate(header.schema ?? { type: "string" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
81
105
|
return {
|
|
82
106
|
...this,
|
|
83
107
|
content: Object.keys(content).map((type) => ({
|
|
@@ -86,6 +110,10 @@ export function createResponseBuilder(operation, config) {
|
|
|
86
110
|
: JSONSchemaFaker.generate(content[type]?.schema ?? { type: "object" }), content[type]?.schema),
|
|
87
111
|
type,
|
|
88
112
|
})),
|
|
113
|
+
headers: {
|
|
114
|
+
...generatedHeaders,
|
|
115
|
+
...this.headers,
|
|
116
|
+
},
|
|
89
117
|
};
|
|
90
118
|
},
|
|
91
119
|
randomLegacy() {
|
|
@@ -5,6 +5,7 @@ import nodePath from "node:path";
|
|
|
5
5
|
import createDebug from "debug";
|
|
6
6
|
import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
|
|
7
7
|
import { OperationCoder } from "./operation-coder.js";
|
|
8
|
+
import { pruneRoutes } from "./prune.js";
|
|
8
9
|
import { Repository } from "./repository.js";
|
|
9
10
|
import { Specification } from "./specification.js";
|
|
10
11
|
const debug = createDebug("counterfact:typescript-generator:generate");
|
|
@@ -41,6 +42,11 @@ export async function generate(source, destination, generateOptions, repository
|
|
|
41
42
|
debug("reading the #/paths from the specification");
|
|
42
43
|
const paths = await getPathsFromSpecification(specification);
|
|
43
44
|
debug("got %i paths", paths.size);
|
|
45
|
+
if (generateOptions.prune && generateOptions.routes) {
|
|
46
|
+
debug("pruning defunct route files");
|
|
47
|
+
await pruneRoutes(destination, paths.keys());
|
|
48
|
+
debug("done pruning");
|
|
49
|
+
}
|
|
44
50
|
const securityRequirement = specification.getRequirement("#/components/securitySchemes");
|
|
45
51
|
const securitySchemes = Object.values(securityRequirement?.data ?? {});
|
|
46
52
|
paths.forEach((pathDefinition, key) => {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import nodePath from "node:path";
|
|
3
|
+
import createDebug from "debug";
|
|
4
|
+
const debug = createDebug("counterfact:typescript-generator:prune");
|
|
5
|
+
/**
|
|
6
|
+
* Collects all .ts route files in a directory recursively.
|
|
7
|
+
* Context files (_.context.ts) are excluded.
|
|
8
|
+
* @param {string} routesDir - Path to routes directory
|
|
9
|
+
* @param {string} currentPath - Current subdirectory being processed (relative to routesDir)
|
|
10
|
+
* @returns {Promise<string[]>} - Array of relative paths (using forward slashes)
|
|
11
|
+
*/
|
|
12
|
+
async function collectRouteFiles(routesDir, currentPath = "") {
|
|
13
|
+
const files = [];
|
|
14
|
+
try {
|
|
15
|
+
const fullDir = currentPath
|
|
16
|
+
? nodePath.join(routesDir, currentPath)
|
|
17
|
+
: routesDir;
|
|
18
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const relativePath = currentPath
|
|
21
|
+
? `${currentPath}/${entry.name}`
|
|
22
|
+
: entry.name;
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
files.push(...(await collectRouteFiles(routesDir, relativePath)));
|
|
25
|
+
}
|
|
26
|
+
else if (entry.name.endsWith(".ts") && entry.name !== "_.context.ts") {
|
|
27
|
+
files.push(relativePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Recursively removes empty directories under rootDir, but not rootDir itself.
|
|
40
|
+
* @param {string} dir - Directory to check
|
|
41
|
+
* @param {string} rootDir - Root directory that should never be removed
|
|
42
|
+
*/
|
|
43
|
+
async function removeEmptyDirectories(dir, rootDir) {
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
await removeEmptyDirectories(nodePath.join(dir, entry.name), rootDir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (nodePath.resolve(dir) === nodePath.resolve(rootDir)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const remaining = await fs.readdir(dir);
|
|
60
|
+
if (remaining.length === 0) {
|
|
61
|
+
await fs.rmdir(dir);
|
|
62
|
+
debug("removed empty directory: %s", dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Converts an OpenAPI path to the expected route file path (relative to routesDir).
|
|
67
|
+
* e.g. "/pet/{id}" -> "pet/{id}.ts", "/" -> "index.ts"
|
|
68
|
+
* @param {string} openApiPath
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function openApiPathToRouteFile(openApiPath) {
|
|
72
|
+
const filePath = openApiPath === "/" ? "index" : openApiPath.slice(1);
|
|
73
|
+
return `${filePath}.ts`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Prunes route files that no longer correspond to any path in the OpenAPI spec.
|
|
77
|
+
* Context files (_.context.ts) are never pruned.
|
|
78
|
+
* @param {string} destination - Base destination directory (contains the routes/ sub-directory)
|
|
79
|
+
* @param {Iterable<string>} openApiPaths - Iterable of OpenAPI path strings (e.g. "/pet/{id}")
|
|
80
|
+
* @returns {Promise<number>} - Number of files removed
|
|
81
|
+
*/
|
|
82
|
+
export async function pruneRoutes(destination, openApiPaths) {
|
|
83
|
+
const routesDir = nodePath.join(destination, "routes");
|
|
84
|
+
const expectedFiles = new Set(Array.from(openApiPaths).map(openApiPathToRouteFile));
|
|
85
|
+
debug("expected route files: %o", Array.from(expectedFiles));
|
|
86
|
+
const actualFiles = await collectRouteFiles(routesDir);
|
|
87
|
+
debug("actual route files: %o", actualFiles);
|
|
88
|
+
let prunedCount = 0;
|
|
89
|
+
for (const file of actualFiles) {
|
|
90
|
+
const normalizedFile = file.replaceAll("\\", "/");
|
|
91
|
+
if (!expectedFiles.has(normalizedFile)) {
|
|
92
|
+
const fullPath = nodePath.join(routesDir, file);
|
|
93
|
+
debug("pruning %s", fullPath);
|
|
94
|
+
await fs.rm(fullPath);
|
|
95
|
+
prunedCount++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await removeEmptyDirectories(routesDir, routesDir);
|
|
99
|
+
debug("pruned %d files", prunedCount);
|
|
100
|
+
return prunedCount;
|
|
101
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "2.3.0",
|
|
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@types/debug": "^4.1.12",
|
|
88
88
|
"@types/jest": "30.0.0",
|
|
89
89
|
"@types/js-yaml": "4.0.9",
|
|
90
|
-
"@types/koa": "3.0.
|
|
90
|
+
"@types/koa": "3.0.2",
|
|
91
91
|
"@types/koa-bodyparser": "4.3.13",
|
|
92
92
|
"@types/koa-proxy": "1.0.8",
|
|
93
93
|
"@types/koa-static": "4.0.4",
|