counterfact 2.5.0 → 2.6.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 +1 -0
- package/bin/README.md +1 -0
- package/bin/counterfact.js +164 -23
- package/bin/register-ts-loader.mjs +17 -0
- package/bin/ts-loader.mjs +31 -0
- package/dist/app.js +23 -12
- package/dist/migrate/update-route-types.js +47 -29
- package/dist/repl/raw-http-client.js +14 -14
- package/dist/repl/repl.js +24 -2
- package/dist/repl/route-builder.js +270 -0
- package/dist/server/config.js +1 -1
- package/dist/server/context-registry.js +27 -3
- package/dist/server/counterfact-types/index.ts +11 -1
- package/dist/server/determine-module-kind.js +1 -1
- package/dist/server/dispatcher.js +21 -10
- package/dist/server/file-discovery.js +34 -0
- package/dist/server/middleware-detector.js +8 -0
- package/dist/server/module-dependency-graph.js +4 -1
- package/dist/server/module-loader.js +7 -31
- package/dist/server/module-tree.js +26 -23
- package/dist/server/openapi-middleware.js +2 -2
- package/dist/server/registry.js +2 -2
- package/dist/server/request-validator.js +61 -0
- package/dist/server/transpiler.js +13 -5
- package/dist/typescript-generator/coder.js +8 -4
- package/dist/typescript-generator/generate.js +3 -3
- package/dist/typescript-generator/jsdoc.js +45 -0
- package/dist/typescript-generator/operation-coder.js +8 -5
- package/dist/typescript-generator/operation-type-coder.js +21 -11
- package/dist/typescript-generator/parameter-export-type-coder.js +4 -1
- package/dist/typescript-generator/parameters-type-coder.js +6 -1
- package/dist/typescript-generator/prune.js +11 -11
- package/dist/typescript-generator/repository.js +1 -1
- package/dist/typescript-generator/requirement.js +10 -5
- package/dist/typescript-generator/response-type-coder.js +10 -5
- package/dist/typescript-generator/responses-type-coder.js +1 -0
- package/dist/typescript-generator/schema-coder.js +5 -5
- package/dist/typescript-generator/schema-type-coder.js +23 -12
- package/dist/typescript-generator/script.js +18 -5
- package/dist/typescript-generator/specification.js +13 -4
- package/dist/util/ensure-directory-exists.js +1 -1
- package/dist/util/runtime-can-execute-erasable-ts.js +22 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -205,6 +205,7 @@ npx counterfact@latest [openapi.yaml] [destination] [options]
|
|
|
205
205
|
| `--spec <path>` | Path or URL to the OpenAPI document |
|
|
206
206
|
| `--proxy-url <url>` | Forward all requests to this URL by default |
|
|
207
207
|
| `--prefix <path>` | Base path prefix (e.g. `/api/v1`) |
|
|
208
|
+
| `--no-validate-request` | Disable request validation against the OpenAPI spec |
|
|
208
209
|
|
|
209
210
|
Run `npx counterfact@latest --help` for the full list of options.
|
|
210
211
|
|
package/bin/README.md
CHANGED
|
@@ -41,5 +41,6 @@ npx counterfact@latest openapi.yaml ./api [options]
|
|
|
41
41
|
| `--proxy-url <url>` | Forward all unmatched requests to this upstream URL |
|
|
42
42
|
| `--prefix <path>` | Base path prefix for all routes (e.g. `/api/v1`) |
|
|
43
43
|
| `--no-update-check` | Disable the npm update check on startup |
|
|
44
|
+
| `--no-validate-request` | Disable request validation against the OpenAPI spec |
|
|
44
45
|
|
|
45
46
|
Run `npx counterfact@latest --help` to see the full option list.
|
package/bin/counterfact.js
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* bin/counterfact.js — CLI entry point for the `counterfact` command.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* 1. Parse CLI arguments and build a `Config` object via Commander.
|
|
8
|
+
* 2. Run any pending migrations (paths → routes directory layout).
|
|
9
|
+
* 3. Delegate to `counterfact()` from `src/app.ts` to start the server,
|
|
10
|
+
* code generator, transpiler, module loader, and optional REPL.
|
|
11
|
+
* 4. Print the startup banner and open the browser when requested.
|
|
12
|
+
* 5. Check for available updates against the npm registry.
|
|
13
|
+
*
|
|
14
|
+
* Architecture (high-level data flow):
|
|
15
|
+
*
|
|
16
|
+
* CLI args ──▶ Commander ──▶ Config
|
|
17
|
+
* │
|
|
18
|
+
* ┌───────────▼───────────┐
|
|
19
|
+
* │ counterfact() │
|
|
20
|
+
* │ (src/app.ts) │
|
|
21
|
+
* │ │
|
|
22
|
+
* │ CodeGenerator │ reads OpenAPI spec, emits .ts route/type files
|
|
23
|
+
* │ Transpiler │ compiles .ts → .cjs and watches for changes
|
|
24
|
+
* │ ModuleLoader │ loads compiled modules into Registry
|
|
25
|
+
* │ Dispatcher + KoaApp │ handles HTTP requests
|
|
26
|
+
* │ REPL (optional) │ interactive terminal session
|
|
27
|
+
* └────────────────────────┘
|
|
28
|
+
*/
|
|
29
|
+
|
|
3
30
|
import fs from "node:fs";
|
|
4
31
|
import { readFile } from "node:fs/promises";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
5
33
|
import nodePath from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
|
+
import { randomUUID } from "node:crypto";
|
|
7
36
|
|
|
8
37
|
import { program } from "commander";
|
|
9
38
|
import createDebug from "debug";
|
|
10
39
|
import open from "open";
|
|
11
|
-
|
|
12
|
-
import { counterfact } from "../dist/app.js";
|
|
13
|
-
import { pathsToRoutes } from "../dist/migrate/paths-to-routes.js";
|
|
14
|
-
import { updateRouteTypes } from "../dist/migrate/update-route-types.js";
|
|
40
|
+
import { PostHog } from "posthog-node";
|
|
15
41
|
|
|
16
42
|
const MIN_NODE_VERSION = 17;
|
|
17
43
|
|
|
@@ -23,28 +49,110 @@ if (Number.parseInt(process.versions.node.split("."), 10) < MIN_NODE_VERSION) {
|
|
|
23
49
|
process.exit(1);
|
|
24
50
|
}
|
|
25
51
|
|
|
52
|
+
const __binDir = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
|
|
26
54
|
const packageJson = JSON.parse(
|
|
27
|
-
await readFile(
|
|
28
|
-
nodePath.join(
|
|
29
|
-
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
30
|
-
"../package.json",
|
|
31
|
-
),
|
|
32
|
-
"utf8",
|
|
33
|
-
),
|
|
55
|
+
await readFile(nodePath.join(__binDir, "../package.json"), "utf8"),
|
|
34
56
|
);
|
|
35
57
|
|
|
36
58
|
const CURRENT_VERSION = packageJson.version;
|
|
37
59
|
|
|
60
|
+
// Telemetry — fire-and-forget, never blocks startup
|
|
61
|
+
const POSTHOG_API_KEY = "phc_msXmBxiL8FVugNMLCx9bnPQGqfEMqmyBjnVkKhHkN3m7";
|
|
62
|
+
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
63
|
+
|
|
64
|
+
const telemetryKey = process.env.POSTHOG_API_KEY ?? POSTHOG_API_KEY;
|
|
65
|
+
const telemetryHost = process.env.POSTHOG_HOST ?? POSTHOG_HOST;
|
|
66
|
+
|
|
67
|
+
const isCI = Boolean(process.env.CI);
|
|
68
|
+
const isBeforeRollout = new Date() < new Date("2026-05-01");
|
|
69
|
+
const telemetryDisabledEnv = process.env.COUNTERFACT_TELEMETRY_DISABLED;
|
|
70
|
+
|
|
71
|
+
const isTelemetryDisabled =
|
|
72
|
+
isCI ||
|
|
73
|
+
telemetryDisabledEnv === "true" ||
|
|
74
|
+
(isBeforeRollout && telemetryDisabledEnv !== "false");
|
|
75
|
+
|
|
76
|
+
if (!isTelemetryDisabled) {
|
|
77
|
+
try {
|
|
78
|
+
const posthog = new PostHog(telemetryKey, { host: telemetryHost });
|
|
79
|
+
|
|
80
|
+
posthog.capture({
|
|
81
|
+
distinctId: randomUUID(),
|
|
82
|
+
event: "counterfact_started",
|
|
83
|
+
properties: {
|
|
84
|
+
version: CURRENT_VERSION,
|
|
85
|
+
nodeVersion: process.version,
|
|
86
|
+
platform: process.platform,
|
|
87
|
+
arch: process.arch,
|
|
88
|
+
source: "counterfact-cli",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
posthog.shutdownAsync().catch(() => {
|
|
93
|
+
// ignore errors — telemetry is best-effort
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore errors — telemetry must never surface to the user
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
const taglinesFile = await readFile(
|
|
39
|
-
nodePath.join(
|
|
40
|
-
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
41
|
-
"taglines.txt",
|
|
42
|
-
),
|
|
101
|
+
nodePath.join(__binDir, "taglines.txt"),
|
|
43
102
|
"utf8",
|
|
44
103
|
);
|
|
45
104
|
|
|
46
105
|
const taglines = taglinesFile.split("\n").slice(0, -1);
|
|
47
106
|
|
|
107
|
+
// Probe whether the current runtime can natively execute TypeScript with
|
|
108
|
+
// erasable type annotations AND resolve .js imports to .ts files (tsx-style).
|
|
109
|
+
async function runtimeCanExecuteErasableTs() {
|
|
110
|
+
const dir = fs.mkdtempSync(nodePath.join(tmpdir(), "ts-probe-"));
|
|
111
|
+
// helper.ts is imported via .js extension — the TypeScript convention used
|
|
112
|
+
// throughout this codebase. If the runtime resolves helper.js → helper.ts,
|
|
113
|
+
// it is fully capable of running the TypeScript source tree.
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
nodePath.join(dir, "helper.ts"),
|
|
116
|
+
'export const value: string = "ok";\n',
|
|
117
|
+
"utf8",
|
|
118
|
+
);
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
nodePath.join(dir, "main.ts"),
|
|
121
|
+
'import { value } from "./helper.js"; export default value;\n',
|
|
122
|
+
"utf8",
|
|
123
|
+
);
|
|
124
|
+
try {
|
|
125
|
+
const mod = await import(pathToFileURL(nodePath.join(dir, "main.ts")).href);
|
|
126
|
+
return mod?.default === "ok";
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
135
|
+
|
|
136
|
+
const resolve = (rel) => pathToFileURL(nodePath.join(__binDir, rel)).href;
|
|
137
|
+
|
|
138
|
+
const { counterfact } = await import(
|
|
139
|
+
resolve(nativeTs ? "../src/app.ts" : "../dist/app.js")
|
|
140
|
+
);
|
|
141
|
+
const { pathsToRoutes } = await import(
|
|
142
|
+
resolve(
|
|
143
|
+
nativeTs
|
|
144
|
+
? "../src/migrate/paths-to-routes.js"
|
|
145
|
+
: "../dist/migrate/paths-to-routes.js",
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
const { updateRouteTypes } = await import(
|
|
149
|
+
resolve(
|
|
150
|
+
nativeTs
|
|
151
|
+
? "../src/migrate/update-route-types.js"
|
|
152
|
+
: "../dist/migrate/update-route-types.js",
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
|
|
48
156
|
const DEFAULT_PORT = 3100;
|
|
49
157
|
|
|
50
158
|
const debug = createDebug("counterfact:bin:counterfact");
|
|
@@ -234,6 +342,7 @@ async function main(source, destination) {
|
|
|
234
342
|
startRepl: options.repl,
|
|
235
343
|
startServer: options.serve,
|
|
236
344
|
buildCache: options.buildCache || false,
|
|
345
|
+
validateRequests: options.validateRequest !== false,
|
|
237
346
|
|
|
238
347
|
watch: {
|
|
239
348
|
routes: options.watch || options.watchRoutes,
|
|
@@ -248,6 +357,14 @@ async function main(source, destination) {
|
|
|
248
357
|
|
|
249
358
|
debug("loading counterfact (%o)", configForLogging);
|
|
250
359
|
|
|
360
|
+
if (config.startAdminApi && !config.adminApiToken) {
|
|
361
|
+
process.stderr.write(
|
|
362
|
+
"⚠️ WARNING: The admin API is enabled without an authentication token.\n" +
|
|
363
|
+
" Any process on this machine can read and modify server state via /_counterfact/api/*.\n" +
|
|
364
|
+
" Set --admin-api-token or COUNTERFACT_ADMIN_API_TOKEN to restrict access.\n\n",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
251
368
|
let didMigrate = false;
|
|
252
369
|
let didMigrateRouteTypes;
|
|
253
370
|
|
|
@@ -266,7 +383,16 @@ async function main(source, destination) {
|
|
|
266
383
|
didMigrate = true;
|
|
267
384
|
}
|
|
268
385
|
|
|
269
|
-
|
|
386
|
+
let start;
|
|
387
|
+
let startRepl;
|
|
388
|
+
try {
|
|
389
|
+
({ start, startRepl } = await counterfact(config));
|
|
390
|
+
} catch (error) {
|
|
391
|
+
process.stderr.write(
|
|
392
|
+
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
393
|
+
);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
270
396
|
|
|
271
397
|
debug("loaded counterfact", configForLogging);
|
|
272
398
|
|
|
@@ -280,6 +406,14 @@ async function main(source, destination) {
|
|
|
280
406
|
|
|
281
407
|
const watchMessage = createWatchMessage(config);
|
|
282
408
|
|
|
409
|
+
const telemetryWarning = isTelemetryDisabled
|
|
410
|
+
? []
|
|
411
|
+
: [
|
|
412
|
+
"⚠️ Telemetry will be enabled by default starting May 1, 2026.",
|
|
413
|
+
" Learn more and how to disable: https://counterfact.dev/telemetry-discussion",
|
|
414
|
+
"",
|
|
415
|
+
];
|
|
416
|
+
|
|
283
417
|
const introduction = [
|
|
284
418
|
" ____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___",
|
|
285
419
|
String.raw` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
@@ -292,11 +426,7 @@ async function main(source, destination) {
|
|
|
292
426
|
" Instructions https://counterfact.dev/docs/usage.html",
|
|
293
427
|
" Help/feedback https://github.com/pmcelhaney/counterfact/issues",
|
|
294
428
|
"",
|
|
295
|
-
|
|
296
|
-
"🔔 PLEASE READ: Feedback, Telemetry, and Privacy Discussion (10 March 2026)",
|
|
297
|
-
" https://counterfact.dev/telemetry-discussion",
|
|
298
|
-
"",
|
|
299
|
-
"",
|
|
429
|
+
...telemetryWarning,
|
|
300
430
|
watchMessage,
|
|
301
431
|
config.startServer ? " Starting server" : undefined,
|
|
302
432
|
config.startRepl
|
|
@@ -311,7 +441,14 @@ async function main(source, destination) {
|
|
|
311
441
|
process.stdout.write("\n\n");
|
|
312
442
|
|
|
313
443
|
debug("starting server");
|
|
314
|
-
|
|
444
|
+
try {
|
|
445
|
+
await start(config);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
process.stderr.write(
|
|
448
|
+
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
449
|
+
);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
315
452
|
debug("started server");
|
|
316
453
|
|
|
317
454
|
await updateCheckPromise;
|
|
@@ -409,5 +546,9 @@ program
|
|
|
409
546
|
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
410
547
|
)
|
|
411
548
|
.option("--no-update-check", "disable the npm update check on startup")
|
|
549
|
+
.option(
|
|
550
|
+
"--no-validate-request",
|
|
551
|
+
"disable request validation against the OpenAPI spec",
|
|
552
|
+
)
|
|
412
553
|
.action(main)
|
|
413
554
|
.parse(process.argv);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the ts-loader hook using Node's modern module.register() API.
|
|
3
|
+
*
|
|
4
|
+
* Usage (replaces deprecated --loader flag):
|
|
5
|
+
* node --experimental-strip-types --import ./bin/register-ts-loader.mjs bin/counterfact.js ...
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// module.register() was added in Node 20.6 / 22; this file is only used when
|
|
9
|
+
// running counterfact under a TypeScript-capable Node runtime (22.6+).
|
|
10
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
11
|
+
import { register } from "node:module";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
register("./ts-loader.mjs", pathToFileURL(join(__dirname, "/")));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js custom loader that remaps .js import specifiers to .ts when a
|
|
3
|
+
* corresponding .ts file exists alongside the importer.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node --experimental-strip-types --loader ./bin/ts-loader.mjs bin/counterfact.js ...
|
|
7
|
+
*
|
|
8
|
+
* Why: Node's built-in --experimental-strip-types handles type annotation
|
|
9
|
+
* removal, but it does not remap .js → .ts import specifiers. This codebase
|
|
10
|
+
* uses the TypeScript convention of writing .js extensions in import paths
|
|
11
|
+
* (which resolve to .ts files at authoring time). This loader bridges that gap.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
export function resolve(specifier, context, nextResolve) {
|
|
18
|
+
if (specifier.endsWith(".js") && context.parentURL) {
|
|
19
|
+
const tsSpecifier = specifier.slice(0, -3) + ".ts";
|
|
20
|
+
try {
|
|
21
|
+
const resolved = new URL(tsSpecifier, context.parentURL);
|
|
22
|
+
if (existsSync(fileURLToPath(resolved))) {
|
|
23
|
+
return nextResolve(tsSpecifier, context);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// If URL construction fails, fall through to default resolution
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return nextResolve(specifier, context);
|
|
31
|
+
}
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs, { rm } from "node:fs/promises";
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import { dereference } from "@apidevtools/json-schema-ref-parser";
|
|
4
|
+
import createDebug from "debug";
|
|
4
5
|
import { createHttpTerminator } from "http-terminator";
|
|
5
6
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
6
7
|
import { ContextRegistry } from "./server/context-registry.js";
|
|
@@ -11,6 +12,8 @@ import { ModuleLoader } from "./server/module-loader.js";
|
|
|
11
12
|
import { Registry } from "./server/registry.js";
|
|
12
13
|
import { Transpiler } from "./server/transpiler.js";
|
|
13
14
|
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
15
|
+
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
|
|
16
|
+
const debug = createDebug("counterfact:app");
|
|
14
17
|
const allowedMethods = [
|
|
15
18
|
"all",
|
|
16
19
|
"head",
|
|
@@ -25,8 +28,10 @@ export async function loadOpenApiDocument(source) {
|
|
|
25
28
|
try {
|
|
26
29
|
return (await dereference(source));
|
|
27
30
|
}
|
|
28
|
-
catch {
|
|
29
|
-
|
|
31
|
+
catch (error) {
|
|
32
|
+
debug("could not load OpenAPI document from %s: %o", source, error);
|
|
33
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(`Could not load the OpenAPI spec from "${source}".\n${details}`, { cause: error });
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
const mswHandlers = {};
|
|
@@ -44,9 +49,6 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
44
49
|
// If we "pre-read" the file here it works. This is a workaround to avoid the issue.
|
|
45
50
|
await fs.readFile(config.openApiPath);
|
|
46
51
|
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
|
|
47
|
-
if (openApiDocument === undefined) {
|
|
48
|
-
throw new Error(`Could not load OpenAPI document from ${config.openApiPath}`);
|
|
49
|
-
}
|
|
50
52
|
const modulesPath = config.basePath;
|
|
51
53
|
const compiledPathsDirectory = nodePath
|
|
52
54
|
.join(modulesPath, ".cache")
|
|
@@ -75,29 +77,37 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
75
77
|
}
|
|
76
78
|
export async function counterfact(config) {
|
|
77
79
|
const modulesPath = config.basePath;
|
|
80
|
+
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
78
81
|
const compiledPathsDirectory = nodePath
|
|
79
|
-
.join(modulesPath, ".cache")
|
|
82
|
+
.join(modulesPath, nativeTs ? "routes" : ".cache")
|
|
80
83
|
.replaceAll("\\", "/");
|
|
81
|
-
|
|
84
|
+
if (!nativeTs) {
|
|
85
|
+
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
86
|
+
}
|
|
82
87
|
const registry = new Registry();
|
|
83
88
|
const contextRegistry = new ContextRegistry();
|
|
84
89
|
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
|
|
85
|
-
const
|
|
90
|
+
const openApiDocument = config.openApiPath === "_"
|
|
91
|
+
? undefined
|
|
92
|
+
: await loadOpenApiDocument(config.openApiPath);
|
|
93
|
+
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
|
|
86
94
|
const transpiler = new Transpiler(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
|
|
87
95
|
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
|
|
88
96
|
const middleware = koaMiddleware(dispatcher, config);
|
|
89
97
|
const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
|
|
90
98
|
async function start(options) {
|
|
91
99
|
const { generate, startServer, watch, buildCache } = options;
|
|
92
|
-
if (generate.routes || generate.types) {
|
|
100
|
+
if (config.openApiPath !== "_" && (generate.routes || generate.types)) {
|
|
93
101
|
await codeGenerator.generate();
|
|
94
102
|
}
|
|
95
|
-
if (watch.routes || watch.types) {
|
|
103
|
+
if (config.openApiPath !== "_" && (watch.routes || watch.types)) {
|
|
96
104
|
await codeGenerator.watch();
|
|
97
105
|
}
|
|
98
106
|
let httpTerminator;
|
|
99
107
|
if (startServer) {
|
|
100
|
-
|
|
108
|
+
if (!nativeTs) {
|
|
109
|
+
await transpiler.watch();
|
|
110
|
+
}
|
|
101
111
|
await moduleLoader.load();
|
|
102
112
|
await moduleLoader.watch();
|
|
103
113
|
const server = koaApp.listen({
|
|
@@ -127,6 +137,7 @@ export async function counterfact(config) {
|
|
|
127
137
|
koaMiddleware: middleware,
|
|
128
138
|
registry,
|
|
129
139
|
start,
|
|
130
|
-
startRepl: () => startReplServer(contextRegistry, registry, config)
|
|
140
|
+
startRepl: () => startReplServer(contextRegistry, registry, config, undefined, // use the default print function (stdout)
|
|
141
|
+
openApiDocument),
|
|
131
142
|
};
|
|
132
143
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import createDebug from "debug";
|
|
4
|
-
import { OperationTypeCoder } from "../typescript-generator/operation-type-coder.js";
|
|
4
|
+
import { OperationTypeCoder, } from "../typescript-generator/operation-type-coder.js";
|
|
5
5
|
import { Specification } from "../typescript-generator/specification.js";
|
|
6
6
|
const debug = createDebug("counterfact:migrate:update-route-types");
|
|
7
7
|
const HTTP_METHODS = [
|
|
@@ -13,6 +13,23 @@ const HTTP_METHODS = [
|
|
|
13
13
|
"HEAD",
|
|
14
14
|
"OPTIONS",
|
|
15
15
|
];
|
|
16
|
+
// Pre-compile regex patterns derived from HTTP_METHODS
|
|
17
|
+
const HTTP_METHOD_ALTERNATION = HTTP_METHODS.join("|");
|
|
18
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
19
|
+
const NEEDS_MIGRATION_REGEX = new RegExp(`import\\s+type\\s+\\{[^}]*HTTP_(?:${HTTP_METHOD_ALTERNATION})[^}]*\\}`, "iu");
|
|
20
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
21
|
+
const HTTP_TYPE_NAME_REGEX = new RegExp(`^HTTP_(?<method>${HTTP_METHOD_ALTERNATION})$`, "u");
|
|
22
|
+
// Pre-build import/export replacement patterns for each HTTP method type name
|
|
23
|
+
const IMPORT_REPLACE_PATTERNS = new Map(HTTP_METHODS.map((method) => [
|
|
24
|
+
`HTTP_${method}`,
|
|
25
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
26
|
+
new RegExp(`(import\\s+type\\s+\\{[^}]*\\b)HTTP_${method}(\\b[^}]*\\}\\s+from)`, "g"),
|
|
27
|
+
]));
|
|
28
|
+
const EXPORT_REPLACE_PATTERNS = new Map(HTTP_METHODS.map((method) => [
|
|
29
|
+
`HTTP_${method}`,
|
|
30
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
31
|
+
new RegExp(`(export\\s+const\\s+${method}\\s*:\\s*)HTTP_${method}(\\b)`, "g"),
|
|
32
|
+
]));
|
|
16
33
|
/**
|
|
17
34
|
* Converts an OpenAPI path to a file system path
|
|
18
35
|
* e.g., "/hello/{name}" -> "hello/{name}"
|
|
@@ -25,8 +42,8 @@ function openApiPathToFilePath(openApiPath) {
|
|
|
25
42
|
}
|
|
26
43
|
/**
|
|
27
44
|
* Builds a mapping of route file paths to their operation type names per method
|
|
28
|
-
* @param
|
|
29
|
-
* @returns
|
|
45
|
+
* @param specification - The OpenAPI specification
|
|
46
|
+
* @returns Map of filePath -> Map of method -> typeName
|
|
30
47
|
*/
|
|
31
48
|
async function buildTypeNameMapping(specification) {
|
|
32
49
|
debug("building type name mapping from specification");
|
|
@@ -68,19 +85,16 @@ async function buildTypeNameMapping(specification) {
|
|
|
68
85
|
}
|
|
69
86
|
/**
|
|
70
87
|
* Checks if a route file needs migration by looking for old-style HTTP_ imports
|
|
71
|
-
* @param
|
|
72
|
-
* @returns {boolean}
|
|
88
|
+
* @param content - The file content
|
|
73
89
|
*/
|
|
74
90
|
function needsMigration(content) {
|
|
75
|
-
|
|
76
|
-
const pattern = new RegExp(`import\\s+type\\s+\\{[^}]*HTTP_(?:${methodAlternation})[^}]*\\}`, "iu");
|
|
77
|
-
return pattern.test(content);
|
|
91
|
+
return NEEDS_MIGRATION_REGEX.test(content);
|
|
78
92
|
}
|
|
79
93
|
/**
|
|
80
94
|
* Updates a single route file with the correct type names
|
|
81
|
-
* @param
|
|
82
|
-
* @param
|
|
83
|
-
* @returns
|
|
95
|
+
* @param filePath - Absolute path to the route file
|
|
96
|
+
* @param methodToTypeName - Map of HTTP method to type name
|
|
97
|
+
* @returns True if file was updated
|
|
84
98
|
*/
|
|
85
99
|
async function updateRouteFile(filePath, methodToTypeName) {
|
|
86
100
|
debug("processing route file: %s", filePath);
|
|
@@ -97,15 +111,15 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
97
111
|
const importRegex = /import\s+type\s+\{(?<types>[^}]+)\}\s+from\s+["'][^"']+["'];?/gu;
|
|
98
112
|
let importMatch;
|
|
99
113
|
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
100
|
-
const importedTypes = importMatch.groups
|
|
114
|
+
const importedTypes = (importMatch.groups?.["types"] ?? "")
|
|
101
115
|
.split(",")
|
|
102
116
|
.map((t) => t.trim())
|
|
103
117
|
.filter((t) => t.length > 0);
|
|
104
118
|
for (const importedType of importedTypes) {
|
|
105
119
|
// Check if this is an HTTP_ type
|
|
106
|
-
const httpMethodMatch = importedType.match(
|
|
120
|
+
const httpMethodMatch = importedType.match(HTTP_TYPE_NAME_REGEX);
|
|
107
121
|
if (httpMethodMatch) {
|
|
108
|
-
const method = httpMethodMatch.groups
|
|
122
|
+
const method = httpMethodMatch.groups?.["method"] ?? "";
|
|
109
123
|
const newTypeName = methodToTypeName.get(method);
|
|
110
124
|
if (newTypeName && newTypeName !== importedType) {
|
|
111
125
|
replacements.set(importedType, newTypeName);
|
|
@@ -121,15 +135,20 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
121
135
|
// Apply replacements
|
|
122
136
|
for (const [oldName, newName] of replacements.entries()) {
|
|
123
137
|
// Replace in import statement
|
|
124
|
-
const importPattern =
|
|
125
|
-
|
|
138
|
+
const importPattern = IMPORT_REPLACE_PATTERNS.get(oldName);
|
|
139
|
+
if (importPattern) {
|
|
140
|
+
importPattern.lastIndex = 0;
|
|
141
|
+
content = content.replace(importPattern, `$1${newName}$2`);
|
|
142
|
+
}
|
|
126
143
|
// Replace in export statement (e.g., "export const GET: HTTP_GET")
|
|
127
144
|
// Match the method from the old type name
|
|
128
|
-
const methodMatch = oldName.match(
|
|
145
|
+
const methodMatch = oldName.match(HTTP_TYPE_NAME_REGEX);
|
|
129
146
|
if (methodMatch) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
const exportPattern = EXPORT_REPLACE_PATTERNS.get(oldName);
|
|
148
|
+
if (exportPattern) {
|
|
149
|
+
exportPattern.lastIndex = 0;
|
|
150
|
+
content = content.replace(exportPattern, `$1${newName}$2`);
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
modified = true;
|
|
135
154
|
}
|
|
@@ -141,10 +160,10 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
141
160
|
}
|
|
142
161
|
/**
|
|
143
162
|
* Recursively processes route files in a directory
|
|
144
|
-
* @param
|
|
145
|
-
* @param
|
|
146
|
-
* @param
|
|
147
|
-
* @returns
|
|
163
|
+
* @param routesDir - Path to routes directory
|
|
164
|
+
* @param currentPath - Current subdirectory being processed
|
|
165
|
+
* @param mapping - Type name mapping
|
|
166
|
+
* @returns Number of files updated
|
|
148
167
|
*/
|
|
149
168
|
async function processRouteDirectory(routesDir, currentPath, mapping) {
|
|
150
169
|
let updatedCount = 0;
|
|
@@ -183,8 +202,7 @@ async function processRouteDirectory(routesDir, currentPath, mapping) {
|
|
|
183
202
|
}
|
|
184
203
|
/**
|
|
185
204
|
* Checks if any route files need migration
|
|
186
|
-
* @param
|
|
187
|
-
* @returns {Promise<boolean>}
|
|
205
|
+
* @param routesDir - Path to routes directory
|
|
188
206
|
*/
|
|
189
207
|
async function checkIfMigrationNeeded(routesDir) {
|
|
190
208
|
try {
|
|
@@ -213,9 +231,9 @@ async function checkIfMigrationNeeded(routesDir) {
|
|
|
213
231
|
}
|
|
214
232
|
/**
|
|
215
233
|
* Main migration function - updates route type imports to use new naming convention
|
|
216
|
-
* @param
|
|
217
|
-
* @param
|
|
218
|
-
* @returns
|
|
234
|
+
* @param basePath - Base path where routes and types are located
|
|
235
|
+
* @param openApiPath - Path or URL to OpenAPI specification
|
|
236
|
+
* @returns True if migration was performed
|
|
219
237
|
*/
|
|
220
238
|
export async function updateRouteTypes(basePath, openApiPath) {
|
|
221
239
|
debug("starting route type migration for base path: %s", basePath);
|
|
@@ -11,8 +11,8 @@ const colors = {
|
|
|
11
11
|
blue: "\x1b[34m",
|
|
12
12
|
};
|
|
13
13
|
function isLikelyJson(headersBlock, body) {
|
|
14
|
-
const m = headersBlock.match(/^content-type:\s*([^\r\n;]+)/im);
|
|
15
|
-
const ct = (m?.[
|
|
14
|
+
const m = headersBlock.match(/^content-type:\s*(?<contentType>[^\r\n;]+)/im);
|
|
15
|
+
const ct = (m?.groups?.["contentType"] ?? "").toLowerCase();
|
|
16
16
|
if (ct.includes("application/json") || ct.includes("+json"))
|
|
17
17
|
return true;
|
|
18
18
|
const s = body.trim();
|
|
@@ -30,7 +30,7 @@ function highlightJson(text) {
|
|
|
30
30
|
return text;
|
|
31
31
|
}
|
|
32
32
|
const pretty = JSON.stringify(obj, null, 2);
|
|
33
|
-
return pretty.replace(/("(?:\\.|[^"\\])*")(
|
|
33
|
+
return pretty.replace(/(?<str>"(?:\\.|[^"\\])*")(?<colon>\s*:)?|\b(?<boolOrNull>true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (match, str, colon, boolOrNull) => {
|
|
34
34
|
if (str) {
|
|
35
35
|
if (colon)
|
|
36
36
|
return `${colors.blue}${str}${colors.reset}${colon}`;
|
|
@@ -60,31 +60,31 @@ export class RawHttpClient {
|
|
|
60
60
|
this.port = port;
|
|
61
61
|
}
|
|
62
62
|
get(path, headers = {}) {
|
|
63
|
-
this.#send("GET", path, "", headers);
|
|
63
|
+
return this.#send("GET", path, "", headers);
|
|
64
64
|
}
|
|
65
65
|
head(path, headers = {}) {
|
|
66
|
-
this.#send("HEAD", path, "", headers);
|
|
66
|
+
return this.#send("HEAD", path, "", headers);
|
|
67
67
|
}
|
|
68
68
|
post(path, body = "", headers = {}) {
|
|
69
|
-
this.#send("POST", path, body, headers);
|
|
69
|
+
return this.#send("POST", path, body, headers);
|
|
70
70
|
}
|
|
71
71
|
put(path, body = "", headers = {}) {
|
|
72
|
-
this.#send("PUT", path, body, headers);
|
|
72
|
+
return this.#send("PUT", path, body, headers);
|
|
73
73
|
}
|
|
74
74
|
delete(path, headers = {}) {
|
|
75
|
-
this.#send("DELETE", path, "", headers);
|
|
75
|
+
return this.#send("DELETE", path, "", headers);
|
|
76
76
|
}
|
|
77
77
|
connect(path, headers = {}) {
|
|
78
|
-
this.#send("CONNECT", path, "", headers);
|
|
78
|
+
return this.#send("CONNECT", path, "", headers);
|
|
79
79
|
}
|
|
80
80
|
options(path, headers = {}) {
|
|
81
|
-
this.#send("OPTIONS", path, "", headers);
|
|
81
|
+
return this.#send("OPTIONS", path, "", headers);
|
|
82
82
|
}
|
|
83
83
|
trace(path, headers = {}) {
|
|
84
|
-
this.#send("TRACE", path, "", headers);
|
|
84
|
+
return this.#send("TRACE", path, "", headers);
|
|
85
85
|
}
|
|
86
86
|
patch(path, body = "", headers = {}) {
|
|
87
|
-
this.#send("PATCH", path, body, headers);
|
|
87
|
+
return this.#send("PATCH", path, body, headers);
|
|
88
88
|
}
|
|
89
89
|
#send(method, path, bodyAsStringOrObject, headers) {
|
|
90
90
|
const requestNumber = ++this.requestNumber;
|
|
@@ -146,9 +146,9 @@ export class RawHttpClient {
|
|
|
146
146
|
const lines = head.split("\r\n");
|
|
147
147
|
const statusLine = lines[0] ?? "";
|
|
148
148
|
let statusColor = colors.green;
|
|
149
|
-
const match = statusLine.match(/HTTP\/\d+\.\d+\s+(
|
|
149
|
+
const match = statusLine.match(/HTTP\/\d+\.\d+\s+(?<statusCode>\d+)/);
|
|
150
150
|
if (match) {
|
|
151
|
-
const code = Number(match[
|
|
151
|
+
const code = Number(match.groups?.["statusCode"]);
|
|
152
152
|
if (code >= 400)
|
|
153
153
|
statusColor = colors.red;
|
|
154
154
|
else if (code >= 300)
|