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.
Files changed (48) hide show
  1. package/README.md +36 -13
  2. package/bin/README.md +39 -14
  3. package/bin/counterfact.js +18 -547
  4. package/bin/ts-loader.mjs +1 -0
  5. package/dist/api-runner.js +202 -0
  6. package/dist/app.js +72 -138
  7. package/dist/cli/banner.js +81 -0
  8. package/dist/cli/check-for-updates.js +45 -0
  9. package/dist/cli/run.js +304 -0
  10. package/dist/cli/telemetry.js +50 -0
  11. package/dist/migrate/paths-to-routes.js +1 -0
  12. package/dist/migrate/update-route-types.js +2 -1
  13. package/dist/msw.js +78 -0
  14. package/dist/repl/raw-http-client.js +3 -1
  15. package/dist/repl/repl.js +228 -60
  16. package/dist/server/counterfact-types/generic-response-builder.ts +1 -2
  17. package/dist/server/counterfact-types/open-api-parameters.ts +3 -0
  18. package/dist/server/determine-module-kind.js +1 -0
  19. package/dist/server/dispatcher.js +45 -2
  20. package/dist/server/file-discovery.js +1 -0
  21. package/dist/server/module-loader.js +8 -0
  22. package/dist/server/request-validator.js +42 -1
  23. package/dist/server/transpiler.js +1 -0
  24. package/dist/server/{admin-api-middleware.js → web-server/admin-api-middleware.js} +19 -9
  25. package/dist/server/web-server/create-koa-app.js +68 -0
  26. package/dist/server/web-server/openapi-middleware.js +34 -0
  27. package/dist/server/{koa-middleware.js → web-server/routes-middleware.js} +11 -8
  28. package/dist/typescript-generator/code-generator.js +2 -1
  29. package/dist/typescript-generator/coder.js +4 -2
  30. package/dist/typescript-generator/operation-coder.js +4 -4
  31. package/dist/typescript-generator/operation-type-coder.js +15 -14
  32. package/dist/typescript-generator/parameter-export-type-coder.js +2 -2
  33. package/dist/typescript-generator/parameters-type-coder.js +3 -3
  34. package/dist/typescript-generator/prune.js +1 -0
  35. package/dist/typescript-generator/repository.js +1 -0
  36. package/dist/typescript-generator/response-type-coder.js +7 -6
  37. package/dist/typescript-generator/responses-type-coder.js +3 -3
  38. package/dist/typescript-generator/scenario-file-generator.js +1 -0
  39. package/dist/typescript-generator/schema-coder.js +2 -2
  40. package/dist/typescript-generator/schema-type-coder.js +7 -5
  41. package/dist/typescript-generator/script.js +58 -3
  42. package/dist/util/ensure-directory-exists.js +1 -0
  43. package/dist/util/load-config-file.js +2 -2
  44. package/dist/util/read-file.js +16 -2
  45. package/dist/util/runtime-can-execute-erasable-ts.js +1 -0
  46. package/package.json +3 -2
  47. package/dist/server/create-koa-app.js +0 -65
  48. package/dist/server/openapi-middleware.js +0 -48
@@ -0,0 +1,304 @@
1
+ import fs from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ /* eslint-disable security/detect-non-literal-fs-filename -- CLI migration cleanup operates on known child directories under config.basePath. */
6
+ import { Command } from "commander";
7
+ import createDebug from "debug";
8
+ import open from "open";
9
+ import { counterfact } from "../app.js";
10
+ import { pathsToRoutes } from "../migrate/paths-to-routes.js";
11
+ import { updateRouteTypes } from "../migrate/update-route-types.js";
12
+ import { pathResolve } from "../util/forward-slash-path.js";
13
+ import { loadConfigFile } from "../util/load-config-file.js";
14
+ import { createIntroduction } from "./banner.js";
15
+ import { checkForUpdates } from "./check-for-updates.js";
16
+ import { isTelemetryEnabled, sendTelemetry } from "./telemetry.js";
17
+ const debug = createDebug("counterfact:cli:run");
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const DEFAULT_PORT = 3100;
20
+ /**
21
+ * Normalises the `spec` option (as read from a config file or the `--spec`
22
+ * CLI flag) into an array of {@link SpecConfig} objects, or `undefined` when
23
+ * the option is a plain string (single OpenAPI document path).
24
+ *
25
+ * - **Array**: each entry is mapped to `{source, prefix, group}` with defaults.
26
+ * - **Object**: wrapped in a single-element array.
27
+ * - **String / undefined**: returns `undefined` — caller handles the string
28
+ * case (it shifts the positional argument) and the `undefined` case
29
+ * (single spec derived from config).
30
+ */
31
+ export function normalizeSpecOption(specOption) {
32
+ if (Array.isArray(specOption)) {
33
+ return specOption.map((entry) => ({
34
+ source: entry.source,
35
+ prefix: entry.prefix ?? "",
36
+ group: entry.group ?? "",
37
+ }));
38
+ }
39
+ if (typeof specOption === "object" &&
40
+ specOption !== null &&
41
+ "source" in specOption) {
42
+ return [
43
+ {
44
+ source: specOption.source,
45
+ prefix: specOption.prefix ?? "",
46
+ group: specOption.group ?? "",
47
+ },
48
+ ];
49
+ }
50
+ return undefined;
51
+ }
52
+ /**
53
+ * Builds the Commander program with all CLI options and the action handler.
54
+ * Factored out of `runCli` so it is easy to test or extend.
55
+ *
56
+ * @param version - Package version string shown in `--version` output.
57
+ * @param taglines - Array of random taglines for the startup banner.
58
+ */
59
+ function buildProgram(version, taglines) {
60
+ const program = new Command();
61
+ async function main(source, destination) {
62
+ debug("executing the main function");
63
+ const options = program.opts();
64
+ const updateCheckPromise = options.updateCheck === false
65
+ ? Promise.resolve()
66
+ : checkForUpdates(version);
67
+ // Load the config file (counterfact.yaml by default, or --config <path>).
68
+ // CLI options always take precedence over config file settings.
69
+ const configFilePath = resolve(options.config ?? "counterfact.yaml");
70
+ const fileConfig = await loadConfigFile(configFilePath, options.config !== undefined);
71
+ debug("fileConfig: %o", fileConfig);
72
+ // Apply config file values for any option that was not explicitly set on
73
+ // the command line (i.e. its source is "default" or it was never defined).
74
+ for (const [key, value] of Object.entries(fileConfig)) {
75
+ const optionSource = program.getOptionValueSource(key);
76
+ if (optionSource !== "cli") {
77
+ options[key] = value;
78
+ }
79
+ }
80
+ // If the config file specifies a destination and none was given on the CLI,
81
+ // use it (destination has no Commander option — it's a positional argument).
82
+ if (fileConfig["destination"] !== undefined && destination === ".") {
83
+ destination = String(fileConfig["destination"]);
84
+ }
85
+ // --spec takes precedence over the positional [openapi.yaml] argument.
86
+ // When --spec is provided as a string, the [openapi.yaml] positional slot
87
+ // shifts to become the [destination] argument (so `counterfact --spec
88
+ // api.yaml ./api` works the same as `counterfact api.yaml ./api`).
89
+ //
90
+ // When --spec / the config file's `spec` key is an object or array of
91
+ // objects ({source, prefix, group}), it describes multiple API specs and
92
+ // is passed directly to counterfact() as the `specs` argument.
93
+ const specs = normalizeSpecOption(options.spec);
94
+ if (specs === undefined && typeof options.spec === "string") {
95
+ // CLI --spec flag: a string path to a single OpenAPI document.
96
+ if (source !== "_") {
97
+ destination = source;
98
+ }
99
+ source = options.spec;
100
+ }
101
+ const destinationPath = pathResolve(destination);
102
+ const basePath = pathResolve(destinationPath);
103
+ // If no action-related option is provided, default to all options.
104
+ const actions = ["repl", "serve", "watch", "generate", "buildCache"];
105
+ if (!Object.keys(options).some((argument) => actions.some((action) => argument.startsWith(action)))) {
106
+ for (const action of actions) {
107
+ options[action] = true;
108
+ }
109
+ }
110
+ debug("options: %o", options);
111
+ debug("source: %s", source);
112
+ debug("destination: %s", destination);
113
+ const openBrowser = options.open;
114
+ const url = `http://localhost:${options.port}${options.prefix}`;
115
+ const guiUrl = `${url}/counterfact/`;
116
+ const swaggerUrl = `${url}/counterfact/swagger/`;
117
+ const config = {
118
+ adminApiToken: options.adminApiToken ??
119
+ process.env["COUNTERFACT_ADMIN_API_TOKEN"] ??
120
+ "",
121
+ alwaysFakeOptionals: options.alwaysFakeOptionals ?? false,
122
+ basePath,
123
+ generate: {
124
+ routes: Boolean(options.generate) ||
125
+ Boolean(options.generateRoutes) ||
126
+ Boolean(options.watch) ||
127
+ Boolean(options.watchRoutes) ||
128
+ Boolean(options.buildCache),
129
+ types: Boolean(options.generate) ||
130
+ Boolean(options.generateTypes) ||
131
+ Boolean(options.watch) ||
132
+ Boolean(options.watchTypes) ||
133
+ Boolean(options.buildCache),
134
+ prune: Boolean(options.prune),
135
+ },
136
+ openApiPath: source,
137
+ port: options.port,
138
+ proxyPaths: new Map([["", Boolean(options.proxyUrl)]]),
139
+ proxyUrl: options.proxyUrl ?? "",
140
+ prefix: options.prefix,
141
+ startAdminApi: options.adminApi,
142
+ startRepl: Boolean(options.repl),
143
+ startServer: Boolean(options.serve),
144
+ buildCache: Boolean(options.buildCache),
145
+ validateRequests: options.validateRequest !== false,
146
+ validateResponses: options.validateResponse !== false,
147
+ watch: {
148
+ routes: Boolean(options.watch) || Boolean(options.watchRoutes),
149
+ types: Boolean(options.watch) || Boolean(options.watchTypes),
150
+ },
151
+ };
152
+ const configForLogging = {
153
+ ...config,
154
+ adminApiToken: config.adminApiToken ? "[REDACTED]" : "",
155
+ };
156
+ debug("loading counterfact (%o)", configForLogging);
157
+ if (config.startAdminApi && !config.adminApiToken) {
158
+ process.stderr.write("⚠️ WARNING: The admin API is enabled without an authentication token.\n" +
159
+ " Any process on this machine can read and modify server state via /_counterfact/api/*.\n" +
160
+ " Set --admin-api-token or COUNTERFACT_ADMIN_API_TOKEN to restrict access.\n\n");
161
+ }
162
+ let didMigrate = false;
163
+ if (fs.existsSync(join(config.basePath, "paths"))) {
164
+ await pathsToRoutes(config.basePath);
165
+ await fs.promises.rmdir(join(config.basePath, "paths"), {
166
+ recursive: true,
167
+ });
168
+ await fs.promises.rmdir(join(config.basePath, "path-types"), {
169
+ recursive: true,
170
+ });
171
+ await fs.promises.rmdir(join(config.basePath, "components"), {
172
+ recursive: true,
173
+ });
174
+ didMigrate = true;
175
+ }
176
+ const { start, startRepl } = await (async () => {
177
+ try {
178
+ return await counterfact(config, specs);
179
+ }
180
+ catch (error) {
181
+ process.stderr.write(`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`);
182
+ process.exit(1);
183
+ }
184
+ })();
185
+ debug("loaded counterfact", configForLogging);
186
+ // Migrate route type imports if needed.
187
+ debug("checking if route type migration is needed");
188
+ const didMigrateRouteTypes = await updateRouteTypes(config.basePath, config.openApiPath);
189
+ debug("route type migration check complete: %s", didMigrateRouteTypes);
190
+ const isTelemetryDisabled = !isTelemetryEnabled();
191
+ process.stdout.write(createIntroduction({
192
+ config,
193
+ isTelemetryDisabled,
194
+ source,
195
+ swaggerUrl,
196
+ taglines,
197
+ url,
198
+ version,
199
+ }));
200
+ process.stdout.write("\n\n");
201
+ debug("starting server");
202
+ try {
203
+ await start(config);
204
+ }
205
+ catch (error) {
206
+ process.stderr.write(`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`);
207
+ process.exit(1);
208
+ }
209
+ debug("started server");
210
+ await updateCheckPromise;
211
+ if (config.startRepl) {
212
+ startRepl();
213
+ }
214
+ if (openBrowser) {
215
+ debug("opening browser");
216
+ await open(guiUrl);
217
+ debug("opened browser");
218
+ }
219
+ if (didMigrate) {
220
+ process.stdout.write("\n\n\n*******************************\n");
221
+ process.stdout.write("MIGRATING TO NEW FILE STRUCTURE\n\n");
222
+ process.stdout.write("In preparation for version 1.0, Counterfact has migrated to a new file structure.\n");
223
+ process.stdout.write("- The paths directory has been renamed to routes.\n");
224
+ process.stdout.write("- The path-types and components directories are now stored under types.\n");
225
+ process.stdout.write("Your files have automatically been migrated.\n");
226
+ process.stdout.write("Please report any issues to https://github.com/pmcelhaney/counterfact/issues\n");
227
+ process.stdout.write("*******************************\n\n\n");
228
+ }
229
+ if (didMigrateRouteTypes) {
230
+ process.stdout.write("\n\n\n*******************************\n");
231
+ process.stdout.write("MIGRATING ROUTE TYPE IMPORTS\n\n");
232
+ process.stdout.write("Operation types now use operationId from your OpenAPI spec when available.\n");
233
+ process.stdout.write("Your route files have been automatically updated to use the new type names.\n");
234
+ process.stdout.write("Example: 'HTTP_GET' may now be 'getPetById' if operationId is defined.\n");
235
+ process.stdout.write("Please review the changes and report any issues to:\n");
236
+ process.stdout.write("https://github.com/pmcelhaney/counterfact/issues\n");
237
+ process.stdout.write("*******************************\n\n\n");
238
+ }
239
+ }
240
+ program
241
+ .name("counterfact")
242
+ .description("Counterfact is a tool for mocking REST APIs in development. See https://counterfact.dev for more info.")
243
+ .version(version)
244
+ .argument("[openapi.yaml]", 'path or URL to OpenAPI document or "_" to run without OpenAPI', "_")
245
+ .argument("[destination]", "path to generated code", ".")
246
+ .option("-p, --port <number>", "server port number", (v) => Number(v), DEFAULT_PORT)
247
+ .option("-o, --open", "open a browser")
248
+ .option("-g, --generate", "generate all code for both routes and types")
249
+ .option("--generate-types", "generate types")
250
+ .option("--generate-routes", "generate routes")
251
+ .option("-w, --watch", "generate + watch all code for changes")
252
+ .option("--watch-types", "generate + watch types for changes")
253
+ .option("--watch-routes", "generate + watch routes for changes")
254
+ .option("-s, --serve", "start the server")
255
+ .option("-b, --build-cache", "builds the cache of compiled routes and types")
256
+ .option("--no-admin-api", "disable the admin API at /_counterfact/api/*")
257
+ .option("-r, --repl", "start the REPL")
258
+ .option("--proxy-url <string>", "proxy URL")
259
+ .option("--admin-api-token <string>", "bearer token required for /_counterfact/api/* endpoints (defaults to COUNTERFACT_ADMIN_API_TOKEN)")
260
+ .option("--prefix <string>", "base path from which routes will be served (e.g. /api/v1)", "")
261
+ .option("--always-fake-optionals", "random responses will include optional fields")
262
+ .option("--prune", "remove route files that no longer exist in the OpenAPI spec")
263
+ .option("--spec <string>", "path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)")
264
+ .option("--no-update-check", "disable the npm update check on startup")
265
+ .option("--no-validate-request", "disable request validation against the OpenAPI spec")
266
+ .option("--no-validate-response", "disable response validation against the OpenAPI spec")
267
+ .option("--config <path>", "path to a counterfact.yaml config file (default: counterfact.yaml in the current directory)")
268
+ .action(main);
269
+ return program;
270
+ }
271
+ /**
272
+ * Entry point for the Counterfact CLI.
273
+ *
274
+ * Reads the package version and taglines, fires telemetry (if enabled),
275
+ * then hands off to Commander to parse `argv` and invoke the action handler.
276
+ *
277
+ * @param argv - Raw argument vector, typically `process.argv`.
278
+ */
279
+ export async function runCli(argv) {
280
+ // Read version from package.json.
281
+ // src/cli/ and dist/cli/ are both two levels below the project root,
282
+ // so "../../package.json" resolves correctly in both environments.
283
+ const packageJson = JSON.parse(await readFile(resolve(__dirname, "../../package.json"), "utf8"));
284
+ const version = packageJson.version;
285
+ // Taglines live in bin/taglines.txt; both src/cli/ and dist/cli/ are two
286
+ // levels below the project root, so "../bin/taglines.txt" (or the equivalent
287
+ // from the root) resolves correctly. We go up two levels to the root and
288
+ // then into bin/.
289
+ let taglines;
290
+ try {
291
+ const taglinesFile = await readFile(resolve(__dirname, "../../bin/taglines.txt"), "utf8");
292
+ taglines = taglinesFile.split("\n").slice(0, -1);
293
+ }
294
+ catch {
295
+ taglines = ["counterfact — mock API server"];
296
+ }
297
+ // Fire telemetry once on startup — fire-and-forget, never blocks.
298
+ if (isTelemetryEnabled()) {
299
+ sendTelemetry(version);
300
+ }
301
+ debug("running counterfact CLI v%s", version);
302
+ const program = buildProgram(version, taglines);
303
+ await program.parseAsync(argv);
304
+ }
@@ -0,0 +1,50 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { PostHog } from "posthog-node";
3
+ const POSTHOG_API_KEY = "phc_msXmBxiL8FVugNMLCx9bnPQGqfEMqmyBjnVkKhHkN3m7";
4
+ const POSTHOG_HOST = "https://us.i.posthog.com";
5
+ /**
6
+ * Returns `true` when telemetry should be sent.
7
+ *
8
+ * Telemetry is disabled in CI, when `COUNTERFACT_TELEMETRY_DISABLED=true`,
9
+ * or before the May 2026 rollout date unless the user has explicitly opted
10
+ * in with `COUNTERFACT_TELEMETRY_DISABLED=false`.
11
+ */
12
+ export function isTelemetryEnabled() {
13
+ if (process.env["CI"])
14
+ return false;
15
+ const telemetryDisabledEnv = process.env["COUNTERFACT_TELEMETRY_DISABLED"];
16
+ if (telemetryDisabledEnv === "true")
17
+ return false;
18
+ const isBeforeRollout = new Date() < new Date("2026-05-01");
19
+ if (isBeforeRollout && telemetryDisabledEnv !== "false")
20
+ return false;
21
+ return true;
22
+ }
23
+ /**
24
+ * Fires a telemetry event to PostHog. Fire-and-forget — never blocks
25
+ * startup and never surfaces errors to the user.
26
+ */
27
+ export function sendTelemetry(version) {
28
+ const telemetryKey = process.env["POSTHOG_API_KEY"] ?? POSTHOG_API_KEY;
29
+ const telemetryHost = process.env["POSTHOG_HOST"] ?? POSTHOG_HOST;
30
+ try {
31
+ const posthog = new PostHog(telemetryKey, { host: telemetryHost });
32
+ posthog.capture({
33
+ distinctId: randomUUID(),
34
+ event: "counterfact_started",
35
+ properties: {
36
+ version,
37
+ nodeVersion: process.version,
38
+ platform: process.platform,
39
+ arch: process.arch,
40
+ source: "counterfact-cli",
41
+ },
42
+ });
43
+ posthog.flush().catch(() => {
44
+ // ignore errors — telemetry is best-effort
45
+ });
46
+ }
47
+ catch {
48
+ // ignore errors — telemetry must never surface to the user
49
+ }
50
+ }
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
+ /* eslint-disable security/detect-non-literal-fs-filename -- migration paths are derived from a selected project root and walked via fs Dirent entries. */
3
4
  async function copyAndModifyFiles(sourceDirectory, destinationDirectory) {
4
5
  try {
5
6
  const entries = await fs.readdir(sourceDirectory, { withFileTypes: true });
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
+ /* eslint-disable security/detect-non-literal-fs-filename -- migration reads/writes discovered route files under the configured basePath/routes tree. */
3
4
  import createDebug from "debug";
4
5
  import { toForwardSlashPath } from "../util/forward-slash-path.js";
5
6
  import { OperationTypeCoder, } from "../typescript-generator/operation-type-coder.js";
@@ -66,7 +67,7 @@ async function buildTypeNameMapping(specification) {
66
67
  return;
67
68
  }
68
69
  // Create the type coder to get the correct type name
69
- const typeCoder = new OperationTypeCoder(operation, requestMethod, securitySchemes);
70
+ const typeCoder = new OperationTypeCoder(operation, "", requestMethod, securitySchemes);
70
71
  // Get the type name (first from the names generator)
71
72
  const typeName = typeCoder.names().next().value;
72
73
  methodMap.set(requestMethod.toUpperCase(), typeName);
package/dist/msw.js ADDED
@@ -0,0 +1,78 @@
1
+ import fs 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 { pathJoin } from "./util/forward-slash-path.js";
8
+ const allowedMethods = [
9
+ "all",
10
+ "head",
11
+ "get",
12
+ "post",
13
+ "put",
14
+ "delete",
15
+ "patch",
16
+ "options",
17
+ ];
18
+ const mswHandlers = {};
19
+ /**
20
+ * Dispatches a single MSW (Mock Service Worker) intercepted request to the
21
+ * matching Counterfact route handler registered via {@link createMswHandlers}.
22
+ *
23
+ * @param request - The intercepted request, including the HTTP method, path,
24
+ * headers, query, body, and a `rawPath` that preserves the original URL
25
+ * before base-path stripping.
26
+ * @returns The response produced by the matching handler, or a 404 object when
27
+ * no handler has been registered for the given method and path.
28
+ */
29
+ export async function handleMswRequest(request) {
30
+ const { method, rawPath } = request;
31
+ const handler = mswHandlers[`${method}:${rawPath}`];
32
+ if (handler) {
33
+ return handler(request);
34
+ }
35
+ console.warn(`No handler found for ${method} ${rawPath}`);
36
+ return { error: `No handler found for ${method} ${rawPath}`, status: 404 };
37
+ }
38
+ /**
39
+ * Loads an OpenAPI document, registers all routes from it as MSW handlers, and
40
+ * returns the list of registered routes so callers (e.g. Vitest Browser mode)
41
+ * can mount them on their own request-interception layer.
42
+ *
43
+ * @param config - Counterfact configuration; `openApiPath` and `basePath` are
44
+ * the most important fields for this function.
45
+ * @param ModuleLoaderClass - Injectable module-loader constructor, primarily
46
+ * used in tests to substitute a test-friendly implementation.
47
+ * @returns An array of `{ method, path }` objects describing every registered
48
+ * MSW handler.
49
+ */
50
+ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader) {
51
+ // TODO: For some reason the Vitest Custom Commands needed by Vitest Browser mode fail on fs.readFile when they are called from the nested loadOpenApiDocument function.
52
+ // If we "pre-read" the file here it works. This is a workaround to avoid the issue.
53
+ await fs.readFile(config.openApiPath);
54
+ const openApiDocument = await loadOpenApiDocument(config.openApiPath);
55
+ const modulesPath = config.basePath;
56
+ const compiledPathsDirectory = pathJoin(modulesPath, ".cache");
57
+ const registry = new Registry();
58
+ const contextRegistry = new ContextRegistry();
59
+ const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
60
+ const moduleLoader = new ModuleLoaderClass(compiledPathsDirectory, registry, contextRegistry);
61
+ await moduleLoader.load();
62
+ const routes = registry.routes;
63
+ const handlers = routes.flatMap((route) => {
64
+ const { methods, path } = route;
65
+ return Object.keys(methods)
66
+ .filter((method) => allowedMethods.includes(method.toLowerCase()))
67
+ .map((method) => {
68
+ const lowerMethod = method.toLowerCase();
69
+ const apiPath = `${openApiDocument.basePath ?? ""}${path.replaceAll("{", ":").replaceAll("}", "")}`;
70
+ const handler = async (request) => {
71
+ return await dispatcher.request(request);
72
+ };
73
+ mswHandlers[`${method}:${apiPath}`] = handler;
74
+ return { method: lowerMethod, path: apiPath };
75
+ });
76
+ });
77
+ return handlers;
78
+ }
@@ -30,7 +30,9 @@ function highlightJson(text) {
30
30
  return text;
31
31
  }
32
32
  const pretty = JSON.stringify(obj, null, 2);
33
- return pretty.replace(/(?<str>"(?:\\.|[^"\\])*")(?<colon>\s*:)?|\b(?<boolOrNull>true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (match, str, colon, boolOrNull) => {
33
+ return pretty.replace(
34
+ // eslint-disable-next-line security/detect-unsafe-regex -- This alternation is linear: the quoted-string branch consumes either an escaped pair (`\\.`) or one non-quote/non-backslash char per iteration, so it cannot catastrophically backtrack.
35
+ /(?<str>"(?:\\.|[^"\\])*")(?<colon>\s*:)?|\b(?<boolOrNull>true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (match, str, colon, boolOrNull) => {
34
36
  if (str) {
35
37
  if (colon)
36
38
  return `${colors.blue}${str}${colors.reset}${colon}`;