counterfact 2.5.1 → 2.7.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 +103 -140
- package/bin/README.md +25 -4
- package/bin/counterfact.js +208 -24
- package/bin/register-ts-loader.mjs +17 -0
- package/bin/ts-loader.mjs +31 -0
- package/dist/app.js +31 -21
- package/dist/counterfact-types/cookie-options.js +1 -0
- package/dist/counterfact-types/counterfact-response.js +7 -0
- package/dist/counterfact-types/example-names.js +1 -0
- package/dist/counterfact-types/example.js +1 -0
- package/dist/counterfact-types/generic-response-builder.js +1 -0
- package/dist/counterfact-types/http-status-code.js +1 -0
- package/dist/counterfact-types/if-has-key.js +1 -0
- package/dist/counterfact-types/index.js +0 -1
- package/dist/counterfact-types/maybe-promise.js +1 -0
- package/dist/counterfact-types/media-type.js +1 -0
- package/dist/counterfact-types/omit-all.js +1 -0
- package/dist/counterfact-types/omit-value-when-never.js +1 -0
- package/dist/counterfact-types/open-api-content.js +1 -0
- package/dist/counterfact-types/open-api-operation.js +1 -0
- package/dist/counterfact-types/open-api-parameters.js +1 -0
- package/dist/counterfact-types/open-api-response.js +1 -0
- package/dist/counterfact-types/random-function.js +1 -0
- package/dist/counterfact-types/response-builder-factory.js +1 -0
- package/dist/counterfact-types/response-builder.js +1 -0
- package/dist/counterfact-types/wide-operation-argument.js +1 -0
- package/dist/counterfact-types/wide-response-builder.js +1 -0
- package/dist/migrate/update-route-types.js +30 -10
- package/dist/repl/raw-http-client.js +14 -14
- package/dist/repl/repl.js +119 -4
- package/dist/repl/route-builder.js +270 -0
- package/dist/server/config.js +1 -1
- package/dist/server/context-registry.js +44 -4
- package/dist/server/counterfact-types/cookie-options.ts +14 -0
- package/dist/server/counterfact-types/counterfact-response.ts +15 -0
- package/dist/server/counterfact-types/example-names.ts +13 -0
- package/dist/server/counterfact-types/example.ts +10 -0
- package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
- package/dist/server/counterfact-types/http-status-code.ts +62 -0
- package/dist/server/counterfact-types/if-has-key.ts +19 -0
- package/dist/server/counterfact-types/index.ts +20 -328
- package/dist/server/counterfact-types/maybe-promise.ts +6 -0
- package/dist/server/counterfact-types/media-type.ts +6 -0
- package/dist/server/counterfact-types/omit-all.ts +11 -0
- package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/server/counterfact-types/open-api-content.ts +8 -0
- package/dist/server/counterfact-types/open-api-operation.ts +36 -0
- package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
- package/dist/server/counterfact-types/open-api-response.ts +22 -0
- package/dist/server/counterfact-types/random-function.ts +9 -0
- package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/server/counterfact-types/response-builder.ts +31 -0
- package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
- package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
- package/dist/server/create-koa-app.js +1 -20
- package/dist/server/determine-module-kind.js +1 -1
- package/dist/server/dispatcher.js +39 -15
- package/dist/server/file-discovery.js +34 -0
- package/dist/server/json-to-xml.js +1 -1
- package/dist/server/koa-middleware.js +7 -1
- package/dist/server/load-openapi-document.js +13 -0
- package/dist/server/middleware-detector.js +8 -0
- package/dist/server/module-dependency-graph.js +4 -1
- package/dist/server/module-loader.js +81 -33
- package/dist/server/module-tree.js +26 -23
- package/dist/server/openapi-middleware.js +2 -2
- package/dist/server/openapi-watcher.js +35 -0
- package/dist/server/registry.js +2 -2
- package/dist/server/request-validator.js +57 -0
- package/dist/server/response-builder.js +3 -0
- package/dist/server/response-validator.js +58 -0
- package/dist/server/scenario-registry.js +29 -0
- package/dist/server/tools.js +2 -2
- package/dist/server/transpiler.js +13 -5
- package/dist/typescript-generator/coder.js +7 -2
- package/dist/typescript-generator/generate.js +155 -0
- package/dist/typescript-generator/jsdoc.js +45 -0
- package/dist/typescript-generator/operation-coder.js +1 -1
- package/dist/typescript-generator/operation-type-coder.js +5 -49
- package/dist/typescript-generator/parameters-type-coder.js +5 -1
- package/dist/typescript-generator/prune.js +2 -1
- package/dist/typescript-generator/read-only-comments.js +1 -1
- package/dist/typescript-generator/requirement.js +8 -1
- package/dist/typescript-generator/reserved-words.js +50 -0
- package/dist/typescript-generator/schema-type-coder.js +7 -1
- package/dist/typescript-generator/script.js +5 -3
- package/dist/typescript-generator/specification.js +7 -1
- package/dist/util/load-config-file.js +44 -0
- package/dist/util/runtime-can-execute-erasable-ts.js +22 -0
- package/package.json +12 -12
- package/dist/client/README.md +0 -14
- package/dist/client/index.html.hbs +0 -244
- package/dist/client/rapi-doc.html.hbs +0 -36
- package/dist/server/page-middleware.js +0 -23
package/bin/counterfact.js
CHANGED
|
@@ -1,21 +1,49 @@
|
|
|
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
|
|
|
18
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
Number.parseInt(process.versions.node.split(".")[0], 10) < MIN_NODE_VERSION
|
|
46
|
+
) {
|
|
19
47
|
process.stdout.write(
|
|
20
48
|
`Counterfact works with Node version ${MIN_NODE_VERSION}+. You are running version ${process.version}`,
|
|
21
49
|
);
|
|
@@ -23,28 +51,117 @@ if (Number.parseInt(process.versions.node.split("."), 10) < MIN_NODE_VERSION) {
|
|
|
23
51
|
process.exit(1);
|
|
24
52
|
}
|
|
25
53
|
|
|
54
|
+
const __binDir = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
|
|
26
56
|
const packageJson = JSON.parse(
|
|
27
|
-
await readFile(
|
|
28
|
-
nodePath.join(
|
|
29
|
-
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
30
|
-
"../package.json",
|
|
31
|
-
),
|
|
32
|
-
"utf8",
|
|
33
|
-
),
|
|
57
|
+
await readFile(nodePath.join(__binDir, "../package.json"), "utf8"),
|
|
34
58
|
);
|
|
35
59
|
|
|
36
60
|
const CURRENT_VERSION = packageJson.version;
|
|
37
61
|
|
|
62
|
+
// Telemetry — fire-and-forget, never blocks startup
|
|
63
|
+
const POSTHOG_API_KEY = "phc_msXmBxiL8FVugNMLCx9bnPQGqfEMqmyBjnVkKhHkN3m7";
|
|
64
|
+
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
65
|
+
|
|
66
|
+
const telemetryKey = process.env.POSTHOG_API_KEY ?? POSTHOG_API_KEY;
|
|
67
|
+
const telemetryHost = process.env.POSTHOG_HOST ?? POSTHOG_HOST;
|
|
68
|
+
|
|
69
|
+
const isCI = Boolean(process.env.CI);
|
|
70
|
+
const isBeforeRollout = new Date() < new Date("2026-05-01");
|
|
71
|
+
const telemetryDisabledEnv = process.env.COUNTERFACT_TELEMETRY_DISABLED;
|
|
72
|
+
|
|
73
|
+
const isTelemetryDisabled =
|
|
74
|
+
isCI ||
|
|
75
|
+
telemetryDisabledEnv === "true" ||
|
|
76
|
+
(isBeforeRollout && telemetryDisabledEnv !== "false");
|
|
77
|
+
|
|
78
|
+
if (!isTelemetryDisabled) {
|
|
79
|
+
try {
|
|
80
|
+
const posthog = new PostHog(telemetryKey, { host: telemetryHost });
|
|
81
|
+
|
|
82
|
+
posthog.capture({
|
|
83
|
+
distinctId: randomUUID(),
|
|
84
|
+
event: "counterfact_started",
|
|
85
|
+
properties: {
|
|
86
|
+
version: CURRENT_VERSION,
|
|
87
|
+
nodeVersion: process.version,
|
|
88
|
+
platform: process.platform,
|
|
89
|
+
arch: process.arch,
|
|
90
|
+
source: "counterfact-cli",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
posthog.shutdownAsync().catch(() => {
|
|
95
|
+
// ignore errors — telemetry is best-effort
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore errors — telemetry must never surface to the user
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
38
102
|
const taglinesFile = await readFile(
|
|
39
|
-
nodePath.join(
|
|
40
|
-
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
41
|
-
"taglines.txt",
|
|
42
|
-
),
|
|
103
|
+
nodePath.join(__binDir, "taglines.txt"),
|
|
43
104
|
"utf8",
|
|
44
105
|
);
|
|
45
106
|
|
|
46
107
|
const taglines = taglinesFile.split("\n").slice(0, -1);
|
|
47
108
|
|
|
109
|
+
// Probe whether the current runtime can natively execute TypeScript with
|
|
110
|
+
// erasable type annotations AND resolve .js imports to .ts files (tsx-style).
|
|
111
|
+
async function runtimeCanExecuteErasableTs() {
|
|
112
|
+
const dir = fs.mkdtempSync(nodePath.join(tmpdir(), "ts-probe-"));
|
|
113
|
+
// helper.ts is imported via .js extension — the TypeScript convention used
|
|
114
|
+
// throughout this codebase. If the runtime resolves helper.js → helper.ts,
|
|
115
|
+
// it is fully capable of running the TypeScript source tree.
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
nodePath.join(dir, "helper.ts"),
|
|
118
|
+
'export const value: string = "ok";\n',
|
|
119
|
+
"utf8",
|
|
120
|
+
);
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
nodePath.join(dir, "main.ts"),
|
|
123
|
+
'import { value } from "./helper.js"; export default value;\n',
|
|
124
|
+
"utf8",
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
const mod = await import(pathToFileURL(nodePath.join(dir, "main.ts")).href);
|
|
128
|
+
return mod?.default === "ok";
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
137
|
+
|
|
138
|
+
const resolve = (rel) => pathToFileURL(nodePath.join(__binDir, rel)).href;
|
|
139
|
+
|
|
140
|
+
const { counterfact } = await import(
|
|
141
|
+
resolve(nativeTs ? "../src/app.ts" : "../dist/app.js")
|
|
142
|
+
);
|
|
143
|
+
const { pathsToRoutes } = await import(
|
|
144
|
+
resolve(
|
|
145
|
+
nativeTs
|
|
146
|
+
? "../src/migrate/paths-to-routes.js"
|
|
147
|
+
: "../dist/migrate/paths-to-routes.js",
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
const { updateRouteTypes } = await import(
|
|
151
|
+
resolve(
|
|
152
|
+
nativeTs
|
|
153
|
+
? "../src/migrate/update-route-types.js"
|
|
154
|
+
: "../dist/migrate/update-route-types.js",
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
const { loadConfigFile } = await import(
|
|
158
|
+
resolve(
|
|
159
|
+
nativeTs
|
|
160
|
+
? "../src/util/load-config-file.js"
|
|
161
|
+
: "../dist/util/load-config-file.js",
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
|
|
48
165
|
const DEFAULT_PORT = 3100;
|
|
49
166
|
|
|
50
167
|
const debug = createDebug("counterfact:bin:counterfact");
|
|
@@ -161,6 +278,31 @@ async function main(source, destination) {
|
|
|
161
278
|
? Promise.resolve()
|
|
162
279
|
: checkForUpdates(CURRENT_VERSION);
|
|
163
280
|
|
|
281
|
+
// Load the config file (counterfact.yaml by default, or --config <path>).
|
|
282
|
+
// CLI options always take precedence over config file settings.
|
|
283
|
+
const configFilePath = nodePath.resolve(options.config ?? "counterfact.yaml");
|
|
284
|
+
const fileConfig = await loadConfigFile(
|
|
285
|
+
configFilePath,
|
|
286
|
+
options.config !== undefined,
|
|
287
|
+
);
|
|
288
|
+
debug("fileConfig: %o", fileConfig);
|
|
289
|
+
|
|
290
|
+
// Apply config file values for any option that was not explicitly set on the
|
|
291
|
+
// command line (i.e. its source is "default" or it was never defined).
|
|
292
|
+
for (const [key, value] of Object.entries(fileConfig)) {
|
|
293
|
+
const optionSource = program.getOptionValueSource(key);
|
|
294
|
+
|
|
295
|
+
if (optionSource !== "cli") {
|
|
296
|
+
options[key] = value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// If the config file specifies a destination and none was given on the CLI,
|
|
301
|
+
// use it (destination has no Commander option — it's a positional argument).
|
|
302
|
+
if (fileConfig.destination !== undefined && destination === ".") {
|
|
303
|
+
destination = String(fileConfig.destination);
|
|
304
|
+
}
|
|
305
|
+
|
|
164
306
|
// --spec takes precedence over the positional [openapi.yaml] argument.
|
|
165
307
|
// When --spec is provided, the [openapi.yaml] positional slot shifts to
|
|
166
308
|
// become the [destination] argument (so `counterfact --spec api.yaml ./api`
|
|
@@ -234,6 +376,8 @@ async function main(source, destination) {
|
|
|
234
376
|
startRepl: options.repl,
|
|
235
377
|
startServer: options.serve,
|
|
236
378
|
buildCache: options.buildCache || false,
|
|
379
|
+
validateRequests: options.validateRequest !== false,
|
|
380
|
+
validateResponses: options.validateResponse !== false,
|
|
237
381
|
|
|
238
382
|
watch: {
|
|
239
383
|
routes: options.watch || options.watchRoutes,
|
|
@@ -248,6 +392,14 @@ async function main(source, destination) {
|
|
|
248
392
|
|
|
249
393
|
debug("loading counterfact (%o)", configForLogging);
|
|
250
394
|
|
|
395
|
+
if (config.startAdminApi && !config.adminApiToken) {
|
|
396
|
+
process.stderr.write(
|
|
397
|
+
"⚠️ WARNING: The admin API is enabled without an authentication token.\n" +
|
|
398
|
+
" Any process on this machine can read and modify server state via /_counterfact/api/*.\n" +
|
|
399
|
+
" Set --admin-api-token or COUNTERFACT_ADMIN_API_TOKEN to restrict access.\n\n",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
251
403
|
let didMigrate = false;
|
|
252
404
|
let didMigrateRouteTypes;
|
|
253
405
|
|
|
@@ -266,7 +418,16 @@ async function main(source, destination) {
|
|
|
266
418
|
didMigrate = true;
|
|
267
419
|
}
|
|
268
420
|
|
|
269
|
-
|
|
421
|
+
let start;
|
|
422
|
+
let startRepl;
|
|
423
|
+
try {
|
|
424
|
+
({ start, startRepl } = await counterfact(config));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
process.stderr.write(
|
|
427
|
+
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
428
|
+
);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
270
431
|
|
|
271
432
|
debug("loaded counterfact", configForLogging);
|
|
272
433
|
|
|
@@ -280,6 +441,14 @@ async function main(source, destination) {
|
|
|
280
441
|
|
|
281
442
|
const watchMessage = createWatchMessage(config);
|
|
282
443
|
|
|
444
|
+
const telemetryWarning = isTelemetryDisabled
|
|
445
|
+
? []
|
|
446
|
+
: [
|
|
447
|
+
"⚠️ Telemetry will be enabled by default starting May 1, 2026.",
|
|
448
|
+
" Learn more and how to disable: https://counterfact.dev/telemetry-discussion",
|
|
449
|
+
"",
|
|
450
|
+
];
|
|
451
|
+
|
|
283
452
|
const introduction = [
|
|
284
453
|
" ____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___",
|
|
285
454
|
String.raw` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
@@ -292,11 +461,7 @@ async function main(source, destination) {
|
|
|
292
461
|
" Instructions https://counterfact.dev/docs/usage.html",
|
|
293
462
|
" Help/feedback https://github.com/pmcelhaney/counterfact/issues",
|
|
294
463
|
"",
|
|
295
|
-
|
|
296
|
-
"🔔 PLEASE READ: Feedback, Telemetry, and Privacy Discussion (10 March 2026)",
|
|
297
|
-
" https://counterfact.dev/telemetry-discussion",
|
|
298
|
-
"",
|
|
299
|
-
"",
|
|
464
|
+
...telemetryWarning,
|
|
300
465
|
watchMessage,
|
|
301
466
|
config.startServer ? " Starting server" : undefined,
|
|
302
467
|
config.startRepl
|
|
@@ -311,7 +476,14 @@ async function main(source, destination) {
|
|
|
311
476
|
process.stdout.write("\n\n");
|
|
312
477
|
|
|
313
478
|
debug("starting server");
|
|
314
|
-
|
|
479
|
+
try {
|
|
480
|
+
await start(config);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
process.stderr.write(
|
|
483
|
+
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
484
|
+
);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
315
487
|
debug("started server");
|
|
316
488
|
|
|
317
489
|
await updateCheckPromise;
|
|
@@ -409,5 +581,17 @@ program
|
|
|
409
581
|
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
410
582
|
)
|
|
411
583
|
.option("--no-update-check", "disable the npm update check on startup")
|
|
584
|
+
.option(
|
|
585
|
+
"--no-validate-request",
|
|
586
|
+
"disable request validation against the OpenAPI spec",
|
|
587
|
+
)
|
|
588
|
+
.option(
|
|
589
|
+
"--no-validate-response",
|
|
590
|
+
"disable response validation against the OpenAPI spec",
|
|
591
|
+
)
|
|
592
|
+
.option(
|
|
593
|
+
"--config <path>",
|
|
594
|
+
"path to a counterfact.yaml config file (default: counterfact.yaml in the current directory)",
|
|
595
|
+
)
|
|
412
596
|
.action(main)
|
|
413
597
|
.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,16 +1,21 @@
|
|
|
1
1
|
import fs, { rm } from "node:fs/promises";
|
|
2
2
|
import nodePath from "node:path";
|
|
3
|
-
import { dereference } from "@apidevtools/json-schema-ref-parser";
|
|
4
3
|
import { createHttpTerminator } from "http-terminator";
|
|
5
4
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
6
5
|
import { ContextRegistry } from "./server/context-registry.js";
|
|
7
6
|
import { createKoaApp } from "./server/create-koa-app.js";
|
|
8
|
-
import { Dispatcher
|
|
7
|
+
import { Dispatcher } from "./server/dispatcher.js";
|
|
9
8
|
import { koaMiddleware } from "./server/koa-middleware.js";
|
|
9
|
+
import { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
10
10
|
import { ModuleLoader } from "./server/module-loader.js";
|
|
11
|
+
import { OpenApiWatcher } from "./server/openapi-watcher.js";
|
|
11
12
|
import { Registry } from "./server/registry.js";
|
|
13
|
+
import { ScenarioRegistry } from "./server/scenario-registry.js";
|
|
12
14
|
import { Transpiler } from "./server/transpiler.js";
|
|
13
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";
|
|
18
|
+
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
14
19
|
const allowedMethods = [
|
|
15
20
|
"all",
|
|
16
21
|
"head",
|
|
@@ -21,14 +26,6 @@ const allowedMethods = [
|
|
|
21
26
|
"patch",
|
|
22
27
|
"options",
|
|
23
28
|
];
|
|
24
|
-
export async function loadOpenApiDocument(source) {
|
|
25
|
-
try {
|
|
26
|
-
return (await dereference(source));
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
29
|
const mswHandlers = {};
|
|
33
30
|
export async function handleMswRequest(request) {
|
|
34
31
|
const { method, rawPath } = request;
|
|
@@ -44,9 +41,6 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
44
41
|
// If we "pre-read" the file here it works. This is a workaround to avoid the issue.
|
|
45
42
|
await fs.readFile(config.openApiPath);
|
|
46
43
|
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
|
|
47
|
-
if (openApiDocument === undefined) {
|
|
48
|
-
throw new Error(`Could not load OpenAPI document from ${config.openApiPath}`);
|
|
49
|
-
}
|
|
50
44
|
const modulesPath = config.basePath;
|
|
51
45
|
const compiledPathsDirectory = nodePath
|
|
52
46
|
.join(modulesPath, ".cache")
|
|
@@ -75,29 +69,43 @@ export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader
|
|
|
75
69
|
}
|
|
76
70
|
export async function counterfact(config) {
|
|
77
71
|
const modulesPath = config.basePath;
|
|
72
|
+
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
78
73
|
const compiledPathsDirectory = nodePath
|
|
79
|
-
.join(modulesPath, ".cache")
|
|
74
|
+
.join(modulesPath, nativeTs ? "routes" : ".cache")
|
|
80
75
|
.replaceAll("\\", "/");
|
|
81
|
-
|
|
76
|
+
if (!nativeTs) {
|
|
77
|
+
await rm(compiledPathsDirectory, { force: true, recursive: true });
|
|
78
|
+
}
|
|
82
79
|
const registry = new Registry();
|
|
83
80
|
const contextRegistry = new ContextRegistry();
|
|
81
|
+
const scenarioRegistry = new ScenarioRegistry();
|
|
84
82
|
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
|
|
85
|
-
const
|
|
83
|
+
const openApiDocument = config.openApiPath === "_"
|
|
84
|
+
? undefined
|
|
85
|
+
: await loadOpenApiDocument(config.openApiPath);
|
|
86
|
+
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
|
|
86
87
|
const transpiler = new Transpiler(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
|
|
87
|
-
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
|
|
88
|
+
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry, nodePath.join(modulesPath, "scenarios").replaceAll("\\", "/"), scenarioRegistry);
|
|
89
|
+
contextRegistry.addEventListener("context-changed", () => {
|
|
90
|
+
void writeApplyContextType(modulesPath);
|
|
91
|
+
});
|
|
88
92
|
const middleware = koaMiddleware(dispatcher, config);
|
|
89
93
|
const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
|
|
94
|
+
const openApiWatcher = new OpenApiWatcher(config.openApiPath, dispatcher);
|
|
90
95
|
async function start(options) {
|
|
91
96
|
const { generate, startServer, watch, buildCache } = options;
|
|
92
|
-
if (generate.routes || generate.types) {
|
|
97
|
+
if (config.openApiPath !== "_" && (generate.routes || generate.types)) {
|
|
93
98
|
await codeGenerator.generate();
|
|
94
99
|
}
|
|
95
|
-
if (watch.routes || watch.types) {
|
|
100
|
+
if (config.openApiPath !== "_" && (watch.routes || watch.types)) {
|
|
96
101
|
await codeGenerator.watch();
|
|
97
102
|
}
|
|
98
103
|
let httpTerminator;
|
|
99
104
|
if (startServer) {
|
|
100
|
-
await
|
|
105
|
+
await openApiWatcher.watch();
|
|
106
|
+
if (!nativeTs) {
|
|
107
|
+
await transpiler.watch();
|
|
108
|
+
}
|
|
101
109
|
await moduleLoader.load();
|
|
102
110
|
await moduleLoader.watch();
|
|
103
111
|
const server = koaApp.listen({
|
|
@@ -117,6 +125,7 @@ export async function counterfact(config) {
|
|
|
117
125
|
await codeGenerator.stopWatching();
|
|
118
126
|
await transpiler.stopWatching();
|
|
119
127
|
await moduleLoader.stopWatching();
|
|
128
|
+
await openApiWatcher.stopWatching();
|
|
120
129
|
await httpTerminator?.terminate();
|
|
121
130
|
},
|
|
122
131
|
};
|
|
@@ -127,6 +136,7 @@ export async function counterfact(config) {
|
|
|
127
136
|
koaMiddleware: middleware,
|
|
128
137
|
registry,
|
|
129
138
|
start,
|
|
130
|
-
startRepl: () => startReplServer(contextRegistry, registry, config)
|
|
139
|
+
startRepl: () => startReplServer(contextRegistry, registry, config, undefined, // use the default print function (stdout)
|
|
140
|
+
openApiDocument, scenarioRegistry),
|
|
131
141
|
};
|
|
132
142
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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}"
|
|
@@ -71,9 +88,7 @@ async function buildTypeNameMapping(specification) {
|
|
|
71
88
|
* @param content - The file content
|
|
72
89
|
*/
|
|
73
90
|
function needsMigration(content) {
|
|
74
|
-
|
|
75
|
-
const pattern = new RegExp(`import\\s+type\\s+\\{[^}]*HTTP_(?:${methodAlternation})[^}]*\\}`, "iu");
|
|
76
|
-
return pattern.test(content);
|
|
91
|
+
return NEEDS_MIGRATION_REGEX.test(content);
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
94
|
* Updates a single route file with the correct type names
|
|
@@ -102,7 +117,7 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
102
117
|
.filter((t) => t.length > 0);
|
|
103
118
|
for (const importedType of importedTypes) {
|
|
104
119
|
// Check if this is an HTTP_ type
|
|
105
|
-
const httpMethodMatch = importedType.match(
|
|
120
|
+
const httpMethodMatch = importedType.match(HTTP_TYPE_NAME_REGEX);
|
|
106
121
|
if (httpMethodMatch) {
|
|
107
122
|
const method = httpMethodMatch.groups?.["method"] ?? "";
|
|
108
123
|
const newTypeName = methodToTypeName.get(method);
|
|
@@ -120,15 +135,20 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
120
135
|
// Apply replacements
|
|
121
136
|
for (const [oldName, newName] of replacements.entries()) {
|
|
122
137
|
// Replace in import statement
|
|
123
|
-
const importPattern =
|
|
124
|
-
|
|
138
|
+
const importPattern = IMPORT_REPLACE_PATTERNS.get(oldName);
|
|
139
|
+
if (importPattern) {
|
|
140
|
+
importPattern.lastIndex = 0;
|
|
141
|
+
content = content.replace(importPattern, `$1${newName}$2`);
|
|
142
|
+
}
|
|
125
143
|
// Replace in export statement (e.g., "export const GET: HTTP_GET")
|
|
126
144
|
// Match the method from the old type name
|
|
127
|
-
const methodMatch = oldName.match(
|
|
145
|
+
const methodMatch = oldName.match(HTTP_TYPE_NAME_REGEX);
|
|
128
146
|
if (methodMatch) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
const exportPattern = EXPORT_REPLACE_PATTERNS.get(oldName);
|
|
148
|
+
if (exportPattern) {
|
|
149
|
+
exportPattern.lastIndex = 0;
|
|
150
|
+
content = content.replace(exportPattern, `$1${newName}$2`);
|
|
151
|
+
}
|
|
132
152
|
}
|
|
133
153
|
modified = true;
|
|
134
154
|
}
|