counterfact 2.8.1 → 2.10.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 +36 -13
- package/bin/README.md +39 -14
- package/bin/counterfact.js +18 -547
- package/bin/ts-loader.mjs +1 -0
- package/dist/api-runner.js +202 -0
- package/dist/app.js +72 -138
- package/dist/cli/banner.js +81 -0
- package/dist/cli/check-for-updates.js +45 -0
- package/dist/cli/run.js +304 -0
- package/dist/cli/telemetry.js +50 -0
- package/dist/migrate/paths-to-routes.js +1 -0
- package/dist/migrate/update-route-types.js +2 -1
- package/dist/msw.js +78 -0
- package/dist/repl/raw-http-client.js +3 -1
- package/dist/repl/repl.js +228 -60
- package/dist/server/counterfact-types/generic-response-builder.ts +1 -2
- package/dist/server/counterfact-types/open-api-parameters.ts +3 -0
- package/dist/server/determine-module-kind.js +1 -0
- package/dist/server/dispatcher.js +45 -2
- package/dist/server/file-discovery.js +1 -0
- package/dist/server/module-loader.js +8 -0
- package/dist/server/request-validator.js +42 -1
- package/dist/server/transpiler.js +1 -0
- package/dist/server/{admin-api-middleware.js → web-server/admin-api-middleware.js} +19 -9
- package/dist/server/web-server/create-koa-app.js +68 -0
- package/dist/server/web-server/openapi-middleware.js +34 -0
- package/dist/server/{koa-middleware.js → web-server/routes-middleware.js} +11 -8
- package/dist/typescript-generator/code-generator.js +2 -1
- package/dist/typescript-generator/coder.js +4 -2
- package/dist/typescript-generator/operation-coder.js +4 -4
- package/dist/typescript-generator/operation-type-coder.js +15 -14
- package/dist/typescript-generator/parameter-export-type-coder.js +2 -2
- package/dist/typescript-generator/parameters-type-coder.js +3 -3
- package/dist/typescript-generator/prune.js +1 -0
- package/dist/typescript-generator/repository.js +1 -0
- package/dist/typescript-generator/response-type-coder.js +7 -6
- package/dist/typescript-generator/responses-type-coder.js +3 -3
- package/dist/typescript-generator/scenario-file-generator.js +1 -0
- package/dist/typescript-generator/schema-coder.js +2 -2
- package/dist/typescript-generator/schema-type-coder.js +7 -5
- package/dist/typescript-generator/script.js +58 -3
- package/dist/util/ensure-directory-exists.js +1 -0
- package/dist/util/load-config-file.js +2 -2
- package/dist/util/read-file.js +16 -2
- package/dist/util/runtime-can-execute-erasable-ts.js +1 -0
- package/package.json +3 -2
- package/dist/server/create-koa-app.js +0 -65
- package/dist/server/openapi-middleware.js +0 -48
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { ContextRegistry } from "./server/context-registry.js";
|
|
3
|
+
import { Dispatcher } from "./server/dispatcher.js";
|
|
4
|
+
import { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
5
|
+
import { ModuleLoader } from "./server/module-loader.js";
|
|
6
|
+
import { Registry } from "./server/registry.js";
|
|
7
|
+
import { ScenarioRegistry } from "./server/scenario-registry.js";
|
|
8
|
+
import { Transpiler } from "./server/transpiler.js";
|
|
9
|
+
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
10
|
+
import { ScenarioFileGenerator } from "./typescript-generator/scenario-file-generator.js";
|
|
11
|
+
import { pathJoin } from "./util/forward-slash-path.js";
|
|
12
|
+
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
|
|
13
|
+
/**
|
|
14
|
+
* Encapsulates the creation and lifecycle management of all Counterfact
|
|
15
|
+
* sub-systems for a single API specification.
|
|
16
|
+
*
|
|
17
|
+
* Use the static {@link ApiRunner.create} factory method to construct an
|
|
18
|
+
* instance — the constructor requires async initialisation (loading the
|
|
19
|
+
* OpenAPI document and probing for native TypeScript support) that cannot
|
|
20
|
+
* be done in a synchronous constructor.
|
|
21
|
+
*
|
|
22
|
+
* Each sub-system is exposed as a public property so callers can interact
|
|
23
|
+
* with individual components directly when needed. The composite methods
|
|
24
|
+
* {@link generate}, {@link load}, {@link watch}, and {@link stopWatching}
|
|
25
|
+
* coordinate across multiple sub-systems and encapsulate the conditional
|
|
26
|
+
* logic driven by the configuration.
|
|
27
|
+
*/
|
|
28
|
+
export class ApiRunner {
|
|
29
|
+
/** Stores loaded route handlers, keyed by path. */
|
|
30
|
+
registry;
|
|
31
|
+
/** Stores context objects per route path and dispatches change events. */
|
|
32
|
+
contextRegistry;
|
|
33
|
+
/** Registry of loaded scenario modules (used by the REPL). */
|
|
34
|
+
scenarioRegistry;
|
|
35
|
+
/** Generates `types/_.context.ts` and the default `scenarios/index.ts`. */
|
|
36
|
+
scenarioFileGenerator;
|
|
37
|
+
/** Reads the OpenAPI spec and writes TypeScript route/type stub files. */
|
|
38
|
+
codeGenerator;
|
|
39
|
+
/** Routes incoming HTTP requests to the matching handler in `registry`. */
|
|
40
|
+
dispatcher;
|
|
41
|
+
/**
|
|
42
|
+
* Compiles TypeScript route files to JavaScript when the runtime cannot
|
|
43
|
+
* execute TypeScript natively.
|
|
44
|
+
*/
|
|
45
|
+
transpiler;
|
|
46
|
+
/**
|
|
47
|
+
* Imports compiled route/context/scenario modules and hot-reloads them on
|
|
48
|
+
* file-system changes.
|
|
49
|
+
*/
|
|
50
|
+
moduleLoader;
|
|
51
|
+
/**
|
|
52
|
+
* The loaded OpenAPI document, or `undefined` when `config.openApiPath`
|
|
53
|
+
* is `"_"` (spec-less mode).
|
|
54
|
+
*/
|
|
55
|
+
openApiDocument;
|
|
56
|
+
/** `true` when the current Node.js runtime can execute TypeScript natively. */
|
|
57
|
+
nativeTs;
|
|
58
|
+
/** Path or URL to the OpenAPI document for this runner. */
|
|
59
|
+
openApiPath;
|
|
60
|
+
/** URL prefix that this runner intercepts (default `""`). */
|
|
61
|
+
prefix;
|
|
62
|
+
/**
|
|
63
|
+
* Optional group name that places generated code in a subdirectory.
|
|
64
|
+
* Defaults to `""` (no subdirectory).
|
|
65
|
+
*/
|
|
66
|
+
group;
|
|
67
|
+
/**
|
|
68
|
+
* The subdirectory path segment derived from {@link group}.
|
|
69
|
+
* Returns `""` when `group` is empty, otherwise `"/${group}"`.
|
|
70
|
+
*/
|
|
71
|
+
get subdirectory() {
|
|
72
|
+
return this.group ? `/${this.group}` : "";
|
|
73
|
+
}
|
|
74
|
+
config;
|
|
75
|
+
constructor(config, nativeTs, openApiDocument, group) {
|
|
76
|
+
this.group = group;
|
|
77
|
+
const modulesPath = this.group
|
|
78
|
+
? pathJoin(config.basePath, this.group)
|
|
79
|
+
: config.basePath;
|
|
80
|
+
const compiledPathsDirectory = pathJoin(modulesPath, nativeTs ? "routes" : ".cache");
|
|
81
|
+
this.config = config;
|
|
82
|
+
this.nativeTs = nativeTs;
|
|
83
|
+
this.openApiDocument = openApiDocument;
|
|
84
|
+
this.openApiPath = config.openApiPath;
|
|
85
|
+
this.prefix = config.prefix;
|
|
86
|
+
this.registry = new Registry();
|
|
87
|
+
this.contextRegistry = new ContextRegistry();
|
|
88
|
+
this.scenarioRegistry = new ScenarioRegistry();
|
|
89
|
+
this.scenarioFileGenerator = new ScenarioFileGenerator(modulesPath);
|
|
90
|
+
this.codeGenerator = new CodeGenerator(this.openApiPath, config.basePath + this.subdirectory, config.generate);
|
|
91
|
+
this.dispatcher = new Dispatcher(this.registry, this.contextRegistry, openApiDocument, config);
|
|
92
|
+
this.transpiler = new Transpiler(pathJoin(modulesPath, "routes"), compiledPathsDirectory, "commonjs");
|
|
93
|
+
this.moduleLoader = new ModuleLoader(compiledPathsDirectory, this.registry, this.contextRegistry, pathJoin(modulesPath, "scenarios"), this.scenarioRegistry);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates and returns a fully initialised `ApiRunner`.
|
|
97
|
+
*
|
|
98
|
+
* Probes for native TypeScript support, optionally cleans the compiled
|
|
99
|
+
* output directory, and loads the OpenAPI document before constructing
|
|
100
|
+
* the runner.
|
|
101
|
+
*
|
|
102
|
+
* @param config - Runtime configuration for this runner instance.
|
|
103
|
+
* @param group - Optional group name placing generated code in a subdirectory (default `""`).
|
|
104
|
+
*/
|
|
105
|
+
static async create(config, group = "") {
|
|
106
|
+
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
107
|
+
const modulesPath = group
|
|
108
|
+
? pathJoin(config.basePath, group)
|
|
109
|
+
: config.basePath;
|
|
110
|
+
const compiledPathsDirectory = pathJoin(modulesPath, nativeTs ? "routes" : ".cache");
|
|
111
|
+
if (!nativeTs) {
|
|
112
|
+
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
113
|
+
}
|
|
114
|
+
const openApiDocument = config.openApiPath === "_"
|
|
115
|
+
? undefined
|
|
116
|
+
: await loadOpenApiDocument(config.openApiPath);
|
|
117
|
+
return new ApiRunner(config, nativeTs, openApiDocument, group);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Generates TypeScript route stubs and type files from the OpenAPI spec.
|
|
121
|
+
*
|
|
122
|
+
* Respects the `config.generate.routes` and `config.generate.types` flags:
|
|
123
|
+
* - Routes and types are only generated when `config.openApiPath` is not `"_"`.
|
|
124
|
+
* - The scenario context type file is always generated when
|
|
125
|
+
* `config.generate.types` is `true`, even without a spec.
|
|
126
|
+
*/
|
|
127
|
+
async generate() {
|
|
128
|
+
if (this.config.openApiPath !== "_" &&
|
|
129
|
+
(this.config.generate.routes || this.config.generate.types)) {
|
|
130
|
+
await this.codeGenerator.generate();
|
|
131
|
+
}
|
|
132
|
+
if (this.config.generate.types) {
|
|
133
|
+
await this.scenarioFileGenerator.generate();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Loads all compiled route, context, and scenario modules into the
|
|
138
|
+
* appropriate registries.
|
|
139
|
+
*/
|
|
140
|
+
async load() {
|
|
141
|
+
await this.moduleLoader.load();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Starts watching the OpenAPI spec and scenario context files for changes
|
|
145
|
+
* and re-generates the corresponding TypeScript files on each change.
|
|
146
|
+
*
|
|
147
|
+
* - Re-generates route stubs when the spec changes (when
|
|
148
|
+
* `config.watch.routes` or `config.watch.types` is `true`).
|
|
149
|
+
* - Re-generates `types/_.context.ts` when a `_.context.ts` file changes
|
|
150
|
+
* (when `config.watch.types` is `true`).
|
|
151
|
+
*/
|
|
152
|
+
async watch() {
|
|
153
|
+
if (this.config.openApiPath !== "_" &&
|
|
154
|
+
(this.config.watch.routes || this.config.watch.types)) {
|
|
155
|
+
await this.codeGenerator.watch();
|
|
156
|
+
}
|
|
157
|
+
if (this.config.watch.types) {
|
|
158
|
+
await this.scenarioFileGenerator.watch();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Starts the server-related sub-systems based on the supplied options.
|
|
163
|
+
*
|
|
164
|
+
* When `startServer` is `true`:
|
|
165
|
+
* - Watches the OpenAPI document for live reloads.
|
|
166
|
+
* - Transpiles TypeScript route files (when the runtime does not support
|
|
167
|
+
* native TypeScript execution).
|
|
168
|
+
* - Loads all compiled modules into their registries.
|
|
169
|
+
* - Watches compiled modules for hot-reload.
|
|
170
|
+
*
|
|
171
|
+
* When `buildCache` is `true` (and `startServer` is `false`):
|
|
172
|
+
* - Runs the transpiler once to build the compiled-output cache, then stops.
|
|
173
|
+
*
|
|
174
|
+
* @param options - Subset of the runtime config that governs startup behaviour.
|
|
175
|
+
*/
|
|
176
|
+
async start(options) {
|
|
177
|
+
const { startServer, buildCache } = options;
|
|
178
|
+
if (startServer) {
|
|
179
|
+
await this.openApiDocument?.watch();
|
|
180
|
+
if (!this.nativeTs) {
|
|
181
|
+
await this.transpiler.watch();
|
|
182
|
+
}
|
|
183
|
+
await this.moduleLoader.load();
|
|
184
|
+
await this.moduleLoader.watch();
|
|
185
|
+
}
|
|
186
|
+
else if (buildCache) {
|
|
187
|
+
// Transpile once to populate the cache, then immediately stop watching.
|
|
188
|
+
await this.transpiler.watch();
|
|
189
|
+
await this.transpiler.stopWatching();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Stops all active file-system watchers across every sub-system.
|
|
194
|
+
*/
|
|
195
|
+
async stopWatching() {
|
|
196
|
+
await this.codeGenerator.stopWatching();
|
|
197
|
+
await this.scenarioFileGenerator.stopWatching();
|
|
198
|
+
await this.transpiler.stopWatching();
|
|
199
|
+
await this.moduleLoader.stopWatching();
|
|
200
|
+
await this.openApiDocument?.stopWatching();
|
|
201
|
+
}
|
|
202
|
+
}
|
package/dist/app.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import fs, { rm } from "node:fs/promises";
|
|
2
1
|
import { createHttpTerminator } from "http-terminator";
|
|
2
|
+
import { ApiRunner } from "./api-runner.js";
|
|
3
3
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
4
4
|
import { createRouteFunction } from "./repl/route-builder.js";
|
|
5
|
-
import {
|
|
6
|
-
import { createKoaApp } from "./server/create-koa-app.js";
|
|
7
|
-
import { Dispatcher } from "./server/dispatcher.js";
|
|
8
|
-
import { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
9
|
-
import { ModuleLoader } from "./server/module-loader.js";
|
|
10
|
-
import { Registry } from "./server/registry.js";
|
|
11
|
-
import { ScenarioRegistry } from "./server/scenario-registry.js";
|
|
12
|
-
import { Transpiler } from "./server/transpiler.js";
|
|
13
|
-
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
14
|
-
import { ScenarioFileGenerator } from "./typescript-generator/scenario-file-generator.js";
|
|
15
|
-
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
|
|
16
|
-
import { pathJoin } from "./util/forward-slash-path.js";
|
|
5
|
+
import { createKoaApp } from "./server/web-server/create-koa-app.js";
|
|
17
6
|
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
7
|
+
export { createMswHandlers, handleMswRequest, } from "./msw.js";
|
|
18
8
|
export async function runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument) {
|
|
19
9
|
const indexModule = scenarioRegistry.getModule("index");
|
|
20
10
|
if (!indexModule || typeof indexModule["startup"] !== "function") {
|
|
@@ -28,86 +18,59 @@ export async function runStartupScenario(scenarioRegistry, contextRegistry, conf
|
|
|
28
18
|
};
|
|
29
19
|
await indexModule["startup"](scenario$);
|
|
30
20
|
}
|
|
31
|
-
const allowedMethods = [
|
|
32
|
-
"all",
|
|
33
|
-
"head",
|
|
34
|
-
"get",
|
|
35
|
-
"post",
|
|
36
|
-
"put",
|
|
37
|
-
"delete",
|
|
38
|
-
"patch",
|
|
39
|
-
"options",
|
|
40
|
-
];
|
|
41
|
-
const mswHandlers = {};
|
|
42
21
|
/**
|
|
43
|
-
*
|
|
44
|
-
* matching Counterfact route handler registered via {@link createMswHandlers}.
|
|
22
|
+
* Normalises the spec configuration to an array.
|
|
45
23
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* no handler has been registered for the given method and path.
|
|
24
|
+
* When `specs` is provided it is returned as-is. When it is omitted, a
|
|
25
|
+
* single-entry array is constructed from `config.openApiPath`,
|
|
26
|
+
* `config.prefix`, and `group = ""` so that the rest of the code never
|
|
27
|
+
* needs to branch on single-vs-multiple specs.
|
|
51
28
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (handler) {
|
|
56
|
-
return handler(request);
|
|
29
|
+
function normalizeSpecs(config, specs) {
|
|
30
|
+
if (specs !== undefined) {
|
|
31
|
+
return specs;
|
|
57
32
|
}
|
|
58
|
-
|
|
59
|
-
return { error: `No handler found for ${method} ${rawPath}`, status: 404 };
|
|
33
|
+
return [{ source: config.openApiPath, prefix: config.prefix, group: "" }];
|
|
60
34
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const handlers = routes.flatMap((route) => {
|
|
87
|
-
const { methods, path } = route;
|
|
88
|
-
return Object.keys(methods)
|
|
89
|
-
.filter((method) => allowedMethods.includes(method.toLowerCase()))
|
|
90
|
-
.map((method) => {
|
|
91
|
-
const lowerMethod = method.toLowerCase();
|
|
92
|
-
const apiPath = `${openApiDocument.basePath ?? ""}${path.replaceAll("{", ":").replaceAll("}", "")}`;
|
|
93
|
-
const handler = async (request) => {
|
|
94
|
-
return await dispatcher.request(request);
|
|
95
|
-
};
|
|
96
|
-
mswHandlers[`${method}:${apiPath}`] = handler;
|
|
97
|
-
return { method: lowerMethod, path: apiPath };
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
return handlers;
|
|
35
|
+
function validateSpecGroups(specs) {
|
|
36
|
+
if (specs.length <= 1) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const invalidSpecNumbers = specs
|
|
40
|
+
.map((spec, index) => ({ group: spec.group.trim(), index }))
|
|
41
|
+
.filter(({ group }) => group === "")
|
|
42
|
+
.map(({ index }) => String(index + 1));
|
|
43
|
+
if (invalidSpecNumbers.length === 0) {
|
|
44
|
+
const seenGroups = new Set();
|
|
45
|
+
const duplicateGroupNames = new Set();
|
|
46
|
+
for (const spec of specs) {
|
|
47
|
+
const group = spec.group.trim();
|
|
48
|
+
if (seenGroups.has(group)) {
|
|
49
|
+
duplicateGroupNames.add(group);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
seenGroups.add(group);
|
|
53
|
+
}
|
|
54
|
+
if (duplicateGroupNames.size === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Each spec must define a unique group when multiple APIs are configured (duplicate groups: ${[...duplicateGroupNames].join(", ")}).`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Each spec must define a non-empty group when multiple APIs are configured (invalid spec entries: ${invalidSpecNumbers.join(", ")}).`);
|
|
101
60
|
}
|
|
102
61
|
/**
|
|
103
62
|
* Creates and configures a full Counterfact server instance.
|
|
104
63
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
64
|
+
* Supports one or more API specifications. Each spec produces its own
|
|
65
|
+
* {@link ApiRunner}. When `specs` is omitted a single runner is created from
|
|
66
|
+
* `config.openApiPath` and `config.prefix`.
|
|
67
|
+
*
|
|
68
|
+
* The returned object exposes handles for starting the server, stopping it,
|
|
69
|
+
* and launching the interactive REPL.
|
|
109
70
|
*
|
|
110
71
|
* @param config - Runtime configuration (port, paths, feature flags, etc.).
|
|
72
|
+
* @param specs - Optional array of spec entries. Omit to use a single spec
|
|
73
|
+
* derived from `config.openApiPath` and `config.prefix`.
|
|
111
74
|
* @returns An object containing the configured sub-systems and two entry-point
|
|
112
75
|
* functions:
|
|
113
76
|
* - `start(options)` — generates/watches code and optionally starts the HTTP
|
|
@@ -115,53 +78,23 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
115
78
|
* - `startRepl()` — launches the interactive Node.js REPL connected to the
|
|
116
79
|
* live server state.
|
|
117
80
|
*/
|
|
118
|
-
export async function counterfact(config) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
if (!nativeTs) {
|
|
123
|
-
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
124
|
-
}
|
|
125
|
-
const registry = new Registry();
|
|
126
|
-
const contextRegistry = new ContextRegistry();
|
|
127
|
-
const scenarioRegistry = new ScenarioRegistry();
|
|
128
|
-
const scenarioFileGenerator = new ScenarioFileGenerator(modulesPath);
|
|
129
|
-
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
|
|
130
|
-
const openApiDocument = config.openApiPath === "_"
|
|
131
|
-
? undefined
|
|
132
|
-
: await loadOpenApiDocument(config.openApiPath);
|
|
133
|
-
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
|
|
134
|
-
const transpiler = new Transpiler(pathJoin(modulesPath, "routes"), compiledPathsDirectory, "commonjs");
|
|
135
|
-
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry, pathJoin(modulesPath, "scenarios"), scenarioRegistry);
|
|
81
|
+
export async function counterfact(config, specs) {
|
|
82
|
+
const normalizedSpecs = normalizeSpecs({ openApiPath: config.openApiPath, prefix: config.prefix }, specs);
|
|
83
|
+
validateSpecGroups(normalizedSpecs);
|
|
84
|
+
const runners = await Promise.all(normalizedSpecs.map((spec) => ApiRunner.create({ ...config, openApiPath: spec.source, prefix: spec.prefix }, spec.group)));
|
|
136
85
|
const koaApp = createKoaApp({
|
|
86
|
+
runners,
|
|
137
87
|
config,
|
|
138
|
-
contextRegistry,
|
|
139
|
-
dispatcher,
|
|
140
|
-
registry,
|
|
141
88
|
});
|
|
89
|
+
// The REPL is configured using the first runner.
|
|
90
|
+
const primaryRunner = runners[0];
|
|
142
91
|
async function start(options) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (generate.types) {
|
|
148
|
-
await scenarioFileGenerator.generate();
|
|
149
|
-
}
|
|
150
|
-
if (config.openApiPath !== "_" && (watch.routes || watch.types)) {
|
|
151
|
-
await codeGenerator.watch();
|
|
152
|
-
}
|
|
153
|
-
if (watch.types) {
|
|
154
|
-
await scenarioFileGenerator.watch();
|
|
155
|
-
}
|
|
92
|
+
await Promise.all(runners.map((runner) => runner.generate()));
|
|
93
|
+
await Promise.all(runners.map((runner) => runner.watch()));
|
|
94
|
+
await Promise.all(runners.map((runner) => runner.start(options)));
|
|
156
95
|
let httpTerminator;
|
|
157
|
-
if (startServer) {
|
|
158
|
-
await openApiDocument
|
|
159
|
-
if (!nativeTs) {
|
|
160
|
-
await transpiler.watch();
|
|
161
|
-
}
|
|
162
|
-
await moduleLoader.load();
|
|
163
|
-
await moduleLoader.watch();
|
|
164
|
-
await runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument);
|
|
96
|
+
if (options.startServer) {
|
|
97
|
+
await runStartupScenario(primaryRunner.scenarioRegistry, primaryRunner.contextRegistry, { port: config.port }, primaryRunner.openApiDocument);
|
|
165
98
|
const server = koaApp.listen({
|
|
166
99
|
port: config.port,
|
|
167
100
|
});
|
|
@@ -169,28 +102,29 @@ export async function counterfact(config) {
|
|
|
169
102
|
server,
|
|
170
103
|
});
|
|
171
104
|
}
|
|
172
|
-
else if (buildCache) {
|
|
173
|
-
// If we are not starting the server, we still want to transpile and load modules
|
|
174
|
-
await transpiler.watch();
|
|
175
|
-
await transpiler.stopWatching();
|
|
176
|
-
}
|
|
177
105
|
return {
|
|
178
106
|
async stop() {
|
|
179
|
-
await
|
|
180
|
-
await scenarioFileGenerator.stopWatching();
|
|
181
|
-
await transpiler.stopWatching();
|
|
182
|
-
await moduleLoader.stopWatching();
|
|
183
|
-
await openApiDocument?.stopWatching();
|
|
107
|
+
await Promise.all(runners.map((runner) => runner.stopWatching()));
|
|
184
108
|
await httpTerminator?.terminate();
|
|
185
109
|
},
|
|
186
110
|
};
|
|
187
111
|
}
|
|
188
112
|
return {
|
|
189
|
-
contextRegistry,
|
|
113
|
+
contextRegistry: primaryRunner.contextRegistry,
|
|
190
114
|
koaApp,
|
|
191
|
-
registry,
|
|
115
|
+
registry: primaryRunner.registry,
|
|
192
116
|
start,
|
|
193
|
-
startRepl: () => startReplServer(contextRegistry, registry,
|
|
194
|
-
|
|
117
|
+
startRepl: () => startReplServer(primaryRunner.contextRegistry, primaryRunner.registry, {
|
|
118
|
+
port: config.port,
|
|
119
|
+
proxyPaths: config.proxyPaths,
|
|
120
|
+
proxyUrl: config.proxyUrl,
|
|
121
|
+
}, undefined, // use the default print function (stdout)
|
|
122
|
+
primaryRunner.openApiDocument, primaryRunner.scenarioRegistry, runners.map((runner) => ({
|
|
123
|
+
contextRegistry: runner.contextRegistry,
|
|
124
|
+
group: runner.group,
|
|
125
|
+
openApiDocument: runner.openApiDocument,
|
|
126
|
+
registry: runner.registry,
|
|
127
|
+
scenarioRegistry: runner.scenarioRegistry,
|
|
128
|
+
}))),
|
|
195
129
|
};
|
|
196
130
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centers a tag line within the fixed-width ASCII banner header.
|
|
3
|
+
*/
|
|
4
|
+
export function padTagLine(tagLine) {
|
|
5
|
+
const headerLength = 51;
|
|
6
|
+
const padding = " ".repeat((headerLength - tagLine.length) / 2);
|
|
7
|
+
return `${padding}${tagLine}`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns a short human-readable message describing which files are being
|
|
11
|
+
* watched or generated, or `undefined` when neither is active.
|
|
12
|
+
*/
|
|
13
|
+
export function createWatchMessage(config) {
|
|
14
|
+
switch (true) {
|
|
15
|
+
case config.watch.routes && config.watch.types: {
|
|
16
|
+
return " Watching for changes";
|
|
17
|
+
}
|
|
18
|
+
case config.watch.routes: {
|
|
19
|
+
return " Watching routes for changes";
|
|
20
|
+
}
|
|
21
|
+
case config.watch.types: {
|
|
22
|
+
return " Watching types for changes";
|
|
23
|
+
}
|
|
24
|
+
default: {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
switch (true) {
|
|
29
|
+
case config.generate.routes && config.generate.types: {
|
|
30
|
+
return "Generating routes and types";
|
|
31
|
+
}
|
|
32
|
+
case config.generate.routes: {
|
|
33
|
+
return "Generating routes";
|
|
34
|
+
}
|
|
35
|
+
case config.generate.types: {
|
|
36
|
+
return "Generating types";
|
|
37
|
+
}
|
|
38
|
+
default: {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds the startup introduction lines that are printed to stdout when
|
|
45
|
+
* Counterfact starts.
|
|
46
|
+
*/
|
|
47
|
+
export function createIntroduction(params) {
|
|
48
|
+
const { config, isTelemetryDisabled, source, swaggerUrl, taglines, url, version, } = params;
|
|
49
|
+
const watchMessage = createWatchMessage({
|
|
50
|
+
generate: config.generate,
|
|
51
|
+
watch: config.watch,
|
|
52
|
+
});
|
|
53
|
+
const telemetryWarning = isTelemetryDisabled
|
|
54
|
+
? []
|
|
55
|
+
: [
|
|
56
|
+
"⚠️ Telemetry will be enabled by default starting May 1, 2026.",
|
|
57
|
+
" Learn more and how to disable: https://counterfact.dev/telemetry-discussion",
|
|
58
|
+
"",
|
|
59
|
+
];
|
|
60
|
+
const lines = [
|
|
61
|
+
" ____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___",
|
|
62
|
+
String.raw ` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
63
|
+
" " +
|
|
64
|
+
padTagLine(taglines[Math.floor(Math.random() * taglines.length)] ?? ""),
|
|
65
|
+
"",
|
|
66
|
+
` Version ${version}`,
|
|
67
|
+
` API Base URL ${url}`,
|
|
68
|
+
source === "_" ? undefined : ` Swagger UI ${swaggerUrl}`,
|
|
69
|
+
"",
|
|
70
|
+
" Instructions https://counterfact.dev/docs/usage.html",
|
|
71
|
+
" Help/feedback https://github.com/pmcelhaney/counterfact/issues",
|
|
72
|
+
"",
|
|
73
|
+
...telemetryWarning,
|
|
74
|
+
watchMessage,
|
|
75
|
+
config.startServer ? " Starting server" : undefined,
|
|
76
|
+
config.startRepl
|
|
77
|
+
? " Starting REPL (type .help for more info)"
|
|
78
|
+
: undefined,
|
|
79
|
+
];
|
|
80
|
+
return lines.filter((line) => line !== undefined).join("\n");
|
|
81
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import createDebug from "debug";
|
|
2
|
+
const debug = createDebug("counterfact:cli:check-for-updates");
|
|
3
|
+
/**
|
|
4
|
+
* Returns `true` when `latest` is a strictly higher semver version than
|
|
5
|
+
* `current`.
|
|
6
|
+
*/
|
|
7
|
+
export function isOutdated(current, latest) {
|
|
8
|
+
const [cMajor = 0, cMinor = 0, cPatch = 0] = current.split(".").map(Number);
|
|
9
|
+
const [lMajor = 0, lMinor = 0, lPatch = 0] = latest.split(".").map(Number);
|
|
10
|
+
if (lMajor > cMajor)
|
|
11
|
+
return true;
|
|
12
|
+
if (lMajor === cMajor && lMinor > cMinor)
|
|
13
|
+
return true;
|
|
14
|
+
if (lMajor === cMajor && lMinor === cMinor && lPatch > cPatch)
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Checks the npm registry for a newer version of counterfact and prints a
|
|
20
|
+
* warning when one is available. Never throws — update checks are
|
|
21
|
+
* best-effort.
|
|
22
|
+
*/
|
|
23
|
+
export async function checkForUpdates(currentVersion) {
|
|
24
|
+
if (process.env["CI"]) {
|
|
25
|
+
debug("skipping update check in CI environment");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch("https://registry.npmjs.org/counterfact/latest");
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
debug("update check failed with status %d", response.status);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const data = (await response.json());
|
|
35
|
+
const latestVersion = data.version;
|
|
36
|
+
if (isOutdated(currentVersion, latestVersion)) {
|
|
37
|
+
process.stdout.write(`\n⚠️ You're running counterfact ${currentVersion}\n`);
|
|
38
|
+
process.stdout.write(` Latest version is ${latestVersion}\n`);
|
|
39
|
+
process.stdout.write(` Run: npx counterfact@latest\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
debug("update check error: %o", error);
|
|
44
|
+
}
|
|
45
|
+
}
|