counterfact 2.7.0 → 2.9.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 +5 -160
- package/bin/README.md +39 -14
- package/bin/counterfact.js +18 -539
- package/bin/ts-loader.mjs +1 -0
- package/dist/api-runner.js +202 -0
- package/dist/app.js +102 -114
- 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 +3 -3
- package/dist/msw.js +78 -0
- package/dist/repl/raw-http-client.js +22 -1
- package/dist/repl/repl.js +250 -63
- package/dist/repl/route-builder.js +68 -0
- package/dist/server/constants.js +8 -0
- package/dist/server/context-registry.js +54 -1
- package/dist/server/determine-module-kind.js +14 -0
- package/dist/server/dispatcher.js +46 -0
- package/dist/server/file-discovery.js +21 -9
- package/dist/server/is-proxy-enabled-for-path.js +12 -0
- package/dist/server/json-to-xml.js +10 -0
- package/dist/server/load-openapi-document.js +4 -11
- package/dist/server/module-dependency-graph.js +25 -0
- package/dist/server/module-loader.js +52 -21
- package/dist/server/module-tree.js +36 -0
- package/dist/server/openapi-document.js +69 -0
- package/dist/server/registry.js +89 -0
- package/dist/server/response-builder.js +15 -0
- package/dist/server/scenario-registry.js +26 -0
- package/dist/server/tools.js +27 -0
- package/dist/server/transpiler.js +24 -9
- 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} +26 -6
- package/dist/typescript-generator/code-generator.js +118 -4
- package/dist/typescript-generator/coder.js +76 -0
- package/dist/typescript-generator/operation-coder.js +12 -4
- package/dist/typescript-generator/operation-type-coder.js +39 -4
- package/dist/typescript-generator/parameters-type-coder.js +2 -4
- package/dist/typescript-generator/prune.js +3 -1
- package/dist/typescript-generator/repository.js +77 -20
- package/dist/typescript-generator/requirement.js +69 -0
- package/dist/typescript-generator/{generate.js → scenario-file-generator.js} +99 -81
- package/dist/typescript-generator/script.js +70 -7
- package/dist/typescript-generator/specification.js +27 -0
- package/dist/util/ensure-directory-exists.js +8 -0
- package/dist/util/forward-slash-path.js +63 -0
- package/dist/util/load-config-file.js +2 -2
- package/dist/util/read-file.js +27 -2
- package/dist/util/runtime-can-execute-erasable-ts.js +12 -0
- package/dist/util/windows-escape.js +18 -0
- package/package.json +5 -4
- package/dist/server/create-koa-app.js +0 -42
- package/dist/server/openapi-middleware.js +0 -19
|
@@ -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,113 +1,100 @@
|
|
|
1
|
-
import fs, { rm } from "node:fs/promises";
|
|
2
|
-
import nodePath from "node:path";
|
|
3
1
|
import { createHttpTerminator } from "http-terminator";
|
|
2
|
+
import { ApiRunner } from "./api-runner.js";
|
|
4
3
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
5
|
-
import {
|
|
6
|
-
import { createKoaApp } from "./server/create-koa-app.js";
|
|
7
|
-
import { Dispatcher } from "./server/dispatcher.js";
|
|
8
|
-
import { koaMiddleware } from "./server/koa-middleware.js";
|
|
9
|
-
import { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
10
|
-
import { ModuleLoader } from "./server/module-loader.js";
|
|
11
|
-
import { OpenApiWatcher } from "./server/openapi-watcher.js";
|
|
12
|
-
import { Registry } from "./server/registry.js";
|
|
13
|
-
import { ScenarioRegistry } from "./server/scenario-registry.js";
|
|
14
|
-
import { Transpiler } from "./server/transpiler.js";
|
|
15
|
-
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
16
|
-
import { writeApplyContextType } from "./typescript-generator/generate.js";
|
|
17
|
-
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
|
|
4
|
+
import { createRouteFunction } from "./repl/route-builder.js";
|
|
5
|
+
import { createKoaApp } from "./server/web-server/create-koa-app.js";
|
|
18
6
|
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"put",
|
|
25
|
-
"delete",
|
|
26
|
-
"patch",
|
|
27
|
-
"options",
|
|
28
|
-
];
|
|
29
|
-
const mswHandlers = {};
|
|
30
|
-
export async function handleMswRequest(request) {
|
|
31
|
-
const { method, rawPath } = request;
|
|
32
|
-
const handler = mswHandlers[`${method}:${rawPath}`];
|
|
33
|
-
if (handler) {
|
|
34
|
-
return handler(request);
|
|
7
|
+
export { createMswHandlers, handleMswRequest, } from "./msw.js";
|
|
8
|
+
export async function runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument) {
|
|
9
|
+
const indexModule = scenarioRegistry.getModule("index");
|
|
10
|
+
if (!indexModule || typeof indexModule["startup"] !== "function") {
|
|
11
|
+
return;
|
|
35
12
|
}
|
|
36
|
-
|
|
37
|
-
|
|
13
|
+
const scenario$ = {
|
|
14
|
+
context: contextRegistry.find("/"),
|
|
15
|
+
loadContext: (path) => contextRegistry.find(path),
|
|
16
|
+
route: createRouteFunction(config.port, "localhost", openApiDocument),
|
|
17
|
+
routes: {},
|
|
18
|
+
};
|
|
19
|
+
await indexModule["startup"](scenario$);
|
|
38
20
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await moduleLoader.load();
|
|
53
|
-
const routes = registry.routes;
|
|
54
|
-
const handlers = routes.flatMap((route) => {
|
|
55
|
-
const { methods, path } = route;
|
|
56
|
-
return Object.keys(methods)
|
|
57
|
-
.filter((method) => allowedMethods.includes(method.toLowerCase()))
|
|
58
|
-
.map((method) => {
|
|
59
|
-
const lowerMethod = method.toLowerCase();
|
|
60
|
-
const apiPath = `${openApiDocument.basePath ?? ""}${path.replaceAll("{", ":").replaceAll("}", "")}`;
|
|
61
|
-
const handler = async (request) => {
|
|
62
|
-
return await dispatcher.request(request);
|
|
63
|
-
};
|
|
64
|
-
mswHandlers[`${method}:${apiPath}`] = handler;
|
|
65
|
-
return { method: lowerMethod, path: apiPath };
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
return handlers;
|
|
21
|
+
/**
|
|
22
|
+
* Normalises the spec configuration to an array.
|
|
23
|
+
*
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeSpecs(config, specs) {
|
|
30
|
+
if (specs !== undefined) {
|
|
31
|
+
return specs;
|
|
32
|
+
}
|
|
33
|
+
return [{ source: config.openApiPath, prefix: config.prefix, group: "" }];
|
|
69
34
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const compiledPathsDirectory = nodePath
|
|
74
|
-
.join(modulesPath, nativeTs ? "routes" : ".cache")
|
|
75
|
-
.replaceAll("\\", "/");
|
|
76
|
-
if (!nativeTs) {
|
|
77
|
-
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
35
|
+
function validateSpecGroups(specs) {
|
|
36
|
+
if (specs.length <= 1) {
|
|
37
|
+
return;
|
|
78
38
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
|
|
94
|
-
const openApiWatcher = new OpenApiWatcher(config.openApiPath, dispatcher);
|
|
95
|
-
async function start(options) {
|
|
96
|
-
const { generate, startServer, watch, buildCache } = options;
|
|
97
|
-
if (config.openApiPath !== "_" && (generate.routes || generate.types)) {
|
|
98
|
-
await codeGenerator.generate();
|
|
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);
|
|
99
53
|
}
|
|
100
|
-
if (
|
|
101
|
-
|
|
54
|
+
if (duplicateGroupNames.size === 0) {
|
|
55
|
+
return;
|
|
102
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(", ")}).`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates and configures a full Counterfact server instance.
|
|
63
|
+
*
|
|
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.
|
|
70
|
+
*
|
|
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`.
|
|
74
|
+
* @returns An object containing the configured sub-systems and two entry-point
|
|
75
|
+
* functions:
|
|
76
|
+
* - `start(options)` — generates/watches code and optionally starts the HTTP
|
|
77
|
+
* server; returns a `stop()` handle.
|
|
78
|
+
* - `startRepl()` — launches the interactive Node.js REPL connected to the
|
|
79
|
+
* live server state.
|
|
80
|
+
*/
|
|
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)));
|
|
85
|
+
const koaApp = createKoaApp({
|
|
86
|
+
runners,
|
|
87
|
+
config,
|
|
88
|
+
});
|
|
89
|
+
// The REPL is configured using the first runner.
|
|
90
|
+
const primaryRunner = runners[0];
|
|
91
|
+
async function start(options) {
|
|
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)));
|
|
103
95
|
let httpTerminator;
|
|
104
|
-
if (startServer) {
|
|
105
|
-
await
|
|
106
|
-
if (!nativeTs) {
|
|
107
|
-
await transpiler.watch();
|
|
108
|
-
}
|
|
109
|
-
await moduleLoader.load();
|
|
110
|
-
await moduleLoader.watch();
|
|
96
|
+
if (options.startServer) {
|
|
97
|
+
await runStartupScenario(primaryRunner.scenarioRegistry, primaryRunner.contextRegistry, { port: config.port }, primaryRunner.openApiDocument);
|
|
111
98
|
const server = koaApp.listen({
|
|
112
99
|
port: config.port,
|
|
113
100
|
});
|
|
@@ -115,28 +102,29 @@ export async function counterfact(config) {
|
|
|
115
102
|
server,
|
|
116
103
|
});
|
|
117
104
|
}
|
|
118
|
-
else if (buildCache) {
|
|
119
|
-
// If we are not starting the server, we still want to transpile and load modules
|
|
120
|
-
await transpiler.watch();
|
|
121
|
-
await transpiler.stopWatching();
|
|
122
|
-
}
|
|
123
105
|
return {
|
|
124
106
|
async stop() {
|
|
125
|
-
await
|
|
126
|
-
await transpiler.stopWatching();
|
|
127
|
-
await moduleLoader.stopWatching();
|
|
128
|
-
await openApiWatcher.stopWatching();
|
|
107
|
+
await Promise.all(runners.map((runner) => runner.stopWatching()));
|
|
129
108
|
await httpTerminator?.terminate();
|
|
130
109
|
},
|
|
131
110
|
};
|
|
132
111
|
}
|
|
133
112
|
return {
|
|
134
|
-
contextRegistry,
|
|
113
|
+
contextRegistry: primaryRunner.contextRegistry,
|
|
135
114
|
koaApp,
|
|
136
|
-
|
|
137
|
-
registry,
|
|
115
|
+
registry: primaryRunner.registry,
|
|
138
116
|
start,
|
|
139
|
-
startRepl: () => startReplServer(contextRegistry, registry,
|
|
140
|
-
|
|
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
|
+
}))),
|
|
141
129
|
};
|
|
142
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
|
+
}
|