counterfact 2.8.1 → 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 +1 -1
- package/bin/README.md +39 -14
- package/bin/counterfact.js +18 -547
- package/bin/ts-loader.mjs +1 -0
- package/dist/api-runner.js +202 -0
- package/dist/app.js +72 -138
- package/dist/cli/banner.js +81 -0
- package/dist/cli/check-for-updates.js +45 -0
- package/dist/cli/run.js +304 -0
- package/dist/cli/telemetry.js +50 -0
- package/dist/migrate/paths-to-routes.js +1 -0
- package/dist/migrate/update-route-types.js +1 -0
- package/dist/msw.js +78 -0
- package/dist/repl/raw-http-client.js +3 -1
- package/dist/repl/repl.js +228 -60
- package/dist/server/determine-module-kind.js +1 -0
- package/dist/server/file-discovery.js +1 -0
- package/dist/server/module-loader.js +8 -0
- package/dist/server/transpiler.js +1 -0
- package/dist/server/{admin-api-middleware.js → web-server/admin-api-middleware.js} +19 -9
- package/dist/server/web-server/create-koa-app.js +68 -0
- package/dist/server/web-server/openapi-middleware.js +34 -0
- package/dist/server/{koa-middleware.js → web-server/routes-middleware.js} +11 -8
- package/dist/typescript-generator/code-generator.js +1 -0
- package/dist/typescript-generator/prune.js +1 -0
- package/dist/typescript-generator/repository.js +1 -0
- package/dist/typescript-generator/scenario-file-generator.js +1 -0
- package/dist/util/ensure-directory-exists.js +1 -0
- package/dist/util/load-config-file.js +2 -2
- package/dist/util/read-file.js +16 -2
- package/dist/util/runtime-can-execute-erasable-ts.js +1 -0
- package/package.json +3 -2
- package/dist/server/create-koa-app.js +0 -65
- package/dist/server/openapi-middleware.js +0 -48
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br>
|
|
6
6
|
|
|
7
|
-
 [](https://github.com/ellerbrock/typescript-badges/) [ [](https://github.com/ellerbrock/typescript-badges/) [](https://coveralls.io/github/pmcelhaney/counterfact)
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
package/bin/README.md
CHANGED
|
@@ -6,7 +6,19 @@ This directory contains the executable script that is run when a developer invok
|
|
|
6
6
|
|
|
7
7
|
| File | Description |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `counterfact.js` |
|
|
9
|
+
| `counterfact.js` | Thin bootstrap: enforces minimum Node version, probes for native TypeScript execution, then delegates to `src/cli/run.ts` (or `dist/cli/run.js`) |
|
|
10
|
+
| `taglines.txt` | One-per-line list of random taglines shown in the startup banner |
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
Most of the CLI logic lives in **`src/cli/`** as TypeScript:
|
|
15
|
+
|
|
16
|
+
| Module | Description |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `src/cli/run.ts` | Commander program setup, `main()` action handler, and the `runCli()` entry point |
|
|
19
|
+
| `src/cli/banner.ts` | Startup banner utilities: `padTagLine`, `createWatchMessage`, `createIntroduction` |
|
|
20
|
+
| `src/cli/check-for-updates.ts` | npm update check: `isOutdated`, `checkForUpdates` |
|
|
21
|
+
| `src/cli/telemetry.ts` | PostHog telemetry: `isTelemetryEnabled`, `sendTelemetry` |
|
|
10
22
|
|
|
11
23
|
## How It Works
|
|
12
24
|
|
|
@@ -14,19 +26,32 @@ This directory contains the executable script that is run when a developer invok
|
|
|
14
26
|
npx counterfact@latest openapi.yaml ./api [options]
|
|
15
27
|
│
|
|
16
28
|
▼
|
|
17
|
-
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│ 1.
|
|
21
|
-
│ 2.
|
|
22
|
-
│ 3.
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
|
|
26
|
-
│
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
┌────────────────────────────────────────────────┐
|
|
30
|
+
│ bin/counterfact.js (thin bootstrap) │
|
|
31
|
+
│ │
|
|
32
|
+
│ 1. Enforce minimum Node.js version │
|
|
33
|
+
│ 2. Probe native TypeScript execution │
|
|
34
|
+
│ 3. Import runCli() from src/cli/run.ts │
|
|
35
|
+
│ (or dist/cli/run.js when compiled) │
|
|
36
|
+
│ 4. Call runCli(process.argv) │
|
|
37
|
+
└────────────────────────────────────────────────┘
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌────────────────────────────────────────────────┐
|
|
41
|
+
│ src/cli/run.ts (all CLI logic) │
|
|
42
|
+
│ │
|
|
43
|
+
│ 1. Read version from package.json │
|
|
44
|
+
│ 2. Read taglines from bin/taglines.txt │
|
|
45
|
+
│ 3. Fire telemetry (if enabled) │
|
|
46
|
+
│ 4. Parse args (Commander) │
|
|
47
|
+
│ 5. Load counterfact.yaml │
|
|
48
|
+
│ 6. Merge config + args │
|
|
49
|
+
│ 7. Resolve paths │
|
|
50
|
+
│ 8. Build Config object │
|
|
51
|
+
│ 9. Run migrations if old layout detected │
|
|
52
|
+
│ 10. Print startup banner │
|
|
53
|
+
│ 11. Call start(config) from src/app.ts │
|
|
54
|
+
└────────────────────────────────────────────────┘
|
|
30
55
|
```
|
|
31
56
|
|
|
32
57
|
### Key CLI Options
|
package/bin/counterfact.js
CHANGED
|
@@ -1,43 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- this bootstrap writes only fixed probe files under a fresh mkdtemp directory it just created. */
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* bin/counterfact.js —
|
|
5
|
+
* bin/counterfact.js — Minimal bootstrap for the `counterfact` CLI.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
7
|
+
* This file is intentionally kept as thin as possible. Its sole
|
|
8
|
+
* responsibilities are:
|
|
13
9
|
*
|
|
14
|
-
*
|
|
10
|
+
* 1. Enforce the minimum Node.js version (must run before any imports).
|
|
11
|
+
* 2. Detect whether the current runtime can execute TypeScript natively
|
|
12
|
+
* (must run before any .ts imports).
|
|
13
|
+
* 3. Dynamically import the real CLI entry point from `src/cli/run.ts`
|
|
14
|
+
* (native TS) or `dist/cli/run.js` (compiled JS) and delegate to it.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
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
|
-
* └────────────────────────┘
|
|
16
|
+
* All CLI logic — Commander setup, option parsing, migration, telemetry,
|
|
17
|
+
* banner, update check, and server startup — lives in src/cli/ as TypeScript.
|
|
28
18
|
*/
|
|
29
19
|
|
|
30
20
|
import fs from "node:fs";
|
|
31
|
-
import { readFile } from "node:fs/promises";
|
|
32
21
|
import { tmpdir } from "node:os";
|
|
33
22
|
import nodePath from "node:path";
|
|
34
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
|
-
import { randomUUID } from "node:crypto";
|
|
36
|
-
|
|
37
|
-
import { program } from "commander";
|
|
38
|
-
import createDebug from "debug";
|
|
39
|
-
import open from "open";
|
|
40
|
-
import { PostHog } from "posthog-node";
|
|
41
24
|
|
|
42
25
|
const MIN_NODE_VERSION = 17;
|
|
43
26
|
|
|
@@ -53,61 +36,10 @@ if (
|
|
|
53
36
|
|
|
54
37
|
const __binDir = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
55
38
|
|
|
56
|
-
const packageJson = JSON.parse(
|
|
57
|
-
await readFile(nodePath.join(__binDir, "../package.json"), "utf8"),
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const CURRENT_VERSION = packageJson.version;
|
|
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
|
-
|
|
102
|
-
const taglinesFile = await readFile(
|
|
103
|
-
nodePath.join(__binDir, "taglines.txt"),
|
|
104
|
-
"utf8",
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const taglines = taglinesFile.split("\n").slice(0, -1);
|
|
108
|
-
|
|
109
39
|
// Probe whether the current runtime can natively execute TypeScript with
|
|
110
|
-
// erasable type annotations AND resolve .js imports to .ts files
|
|
40
|
+
// erasable type annotations AND resolve .js imports to .ts files.
|
|
41
|
+
// This must be inlined here because we need the result before we can
|
|
42
|
+
// import any .ts files from src/.
|
|
111
43
|
async function runtimeCanExecuteErasableTs() {
|
|
112
44
|
const dir = fs.mkdtempSync(nodePath.join(tmpdir(), "ts-probe-"));
|
|
113
45
|
// helper.ts is imported via .js extension — the TypeScript convention used
|
|
@@ -135,471 +67,10 @@ async function runtimeCanExecuteErasableTs() {
|
|
|
135
67
|
|
|
136
68
|
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
137
69
|
|
|
138
|
-
const
|
|
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
|
-
);
|
|
70
|
+
const toFileUrl = (rel) => pathToFileURL(nodePath.join(__binDir, rel)).href;
|
|
164
71
|
|
|
165
|
-
const {
|
|
166
|
-
|
|
167
|
-
nativeTs
|
|
168
|
-
? "../src/util/forward-slash-path.js"
|
|
169
|
-
: "../dist/util/forward-slash-path.js",
|
|
170
|
-
)
|
|
72
|
+
const { runCli } = await import(
|
|
73
|
+
toFileUrl(nativeTs ? "../src/cli/run.ts" : "../dist/cli/run.js")
|
|
171
74
|
);
|
|
172
75
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const debug = createDebug("counterfact:bin:counterfact");
|
|
176
|
-
|
|
177
|
-
function isOutdated(current, latest) {
|
|
178
|
-
const [cMajor, cMinor, cPatch] = current.split(".").map(Number);
|
|
179
|
-
const [lMajor, lMinor, lPatch] = latest.split(".").map(Number);
|
|
180
|
-
|
|
181
|
-
if (lMajor > cMajor) return true;
|
|
182
|
-
if (lMajor === cMajor && lMinor > cMinor) return true;
|
|
183
|
-
if (lMajor === cMajor && lMinor === cMinor && lPatch > cPatch) return true;
|
|
184
|
-
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function checkForUpdates(currentVersion) {
|
|
189
|
-
if (process.env.CI) {
|
|
190
|
-
debug("skipping update check in CI environment");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const response = await fetch(
|
|
196
|
-
"https://registry.npmjs.org/counterfact/latest",
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
debug("update check failed with status %d", response.status);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const data = await response.json();
|
|
205
|
-
const latestVersion = data.version;
|
|
206
|
-
|
|
207
|
-
if (isOutdated(currentVersion, latestVersion)) {
|
|
208
|
-
process.stdout.write(
|
|
209
|
-
`\n⚠️ You're running counterfact ${currentVersion}\n`,
|
|
210
|
-
);
|
|
211
|
-
process.stdout.write(` Latest version is ${latestVersion}\n`);
|
|
212
|
-
process.stdout.write(` Run: npx counterfact@latest\n`);
|
|
213
|
-
}
|
|
214
|
-
} catch (error) {
|
|
215
|
-
debug("update check error: %o", error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
debug("running ./bin/counterfact.js");
|
|
220
|
-
|
|
221
|
-
function padTagLine(tagLine) {
|
|
222
|
-
const headerLength = 51;
|
|
223
|
-
const padding = " ".repeat((headerLength - tagLine.length) / 2);
|
|
224
|
-
|
|
225
|
-
return `${padding}${tagLine}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function createWatchMessage(config) {
|
|
229
|
-
let watchMessage;
|
|
230
|
-
|
|
231
|
-
switch (true) {
|
|
232
|
-
case config.watch.routes && config.watch.types: {
|
|
233
|
-
watchMessage = " Watching for changes";
|
|
234
|
-
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case config.watch.routes: {
|
|
238
|
-
watchMessage = " Watching routes for changes";
|
|
239
|
-
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
case config.watch.types: {
|
|
243
|
-
watchMessage = " Watching types for changes";
|
|
244
|
-
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
default: {
|
|
249
|
-
watchMessage = undefined;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!watchMessage) {
|
|
254
|
-
switch (true) {
|
|
255
|
-
case config.generate.routes && config.generate.types: {
|
|
256
|
-
watchMessage = "Generating routes and types";
|
|
257
|
-
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
case config.generate.routes: {
|
|
261
|
-
watchMessage = "Generating routes";
|
|
262
|
-
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
case config.generate.types: {
|
|
266
|
-
watchMessage = "Generating types";
|
|
267
|
-
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
default: {
|
|
272
|
-
watchMessage = undefined;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return watchMessage;
|
|
278
|
-
}
|
|
279
|
-
async function main(source, destination) {
|
|
280
|
-
debug("executing the main function");
|
|
281
|
-
|
|
282
|
-
const options = program.opts();
|
|
283
|
-
|
|
284
|
-
const updateCheckPromise =
|
|
285
|
-
options.updateCheck === false
|
|
286
|
-
? Promise.resolve()
|
|
287
|
-
: checkForUpdates(CURRENT_VERSION);
|
|
288
|
-
|
|
289
|
-
// Load the config file (counterfact.yaml by default, or --config <path>).
|
|
290
|
-
// CLI options always take precedence over config file settings.
|
|
291
|
-
const configFilePath = nodePath.resolve(options.config ?? "counterfact.yaml");
|
|
292
|
-
const fileConfig = await loadConfigFile(
|
|
293
|
-
configFilePath,
|
|
294
|
-
options.config !== undefined,
|
|
295
|
-
);
|
|
296
|
-
debug("fileConfig: %o", fileConfig);
|
|
297
|
-
|
|
298
|
-
// Apply config file values for any option that was not explicitly set on the
|
|
299
|
-
// command line (i.e. its source is "default" or it was never defined).
|
|
300
|
-
for (const [key, value] of Object.entries(fileConfig)) {
|
|
301
|
-
const optionSource = program.getOptionValueSource(key);
|
|
302
|
-
|
|
303
|
-
if (optionSource !== "cli") {
|
|
304
|
-
options[key] = value;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// If the config file specifies a destination and none was given on the CLI,
|
|
309
|
-
// use it (destination has no Commander option — it's a positional argument).
|
|
310
|
-
if (fileConfig.destination !== undefined && destination === ".") {
|
|
311
|
-
destination = String(fileConfig.destination);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// --spec takes precedence over the positional [openapi.yaml] argument.
|
|
315
|
-
// When --spec is provided, the [openapi.yaml] positional slot shifts to
|
|
316
|
-
// become the [destination] argument (so `counterfact --spec api.yaml ./api`
|
|
317
|
-
// works the same as `counterfact api.yaml ./api`).
|
|
318
|
-
if (options.spec) {
|
|
319
|
-
if (source !== "_") {
|
|
320
|
-
destination = source;
|
|
321
|
-
}
|
|
322
|
-
source = options.spec;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const destinationPath = pathResolve(destination);
|
|
326
|
-
|
|
327
|
-
const basePath = pathResolve(destinationPath);
|
|
328
|
-
|
|
329
|
-
// If no action-related option is provided, default to all options
|
|
330
|
-
|
|
331
|
-
const actions = ["repl", "serve", "watch", "generate", "buildCache"];
|
|
332
|
-
if (
|
|
333
|
-
!Object.keys(options).some((argument) =>
|
|
334
|
-
actions.some((action) => argument.startsWith(action)),
|
|
335
|
-
)
|
|
336
|
-
) {
|
|
337
|
-
for (const action of actions) {
|
|
338
|
-
options[action] = true;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
debug("options: %o", options);
|
|
343
|
-
debug("source: %s", source);
|
|
344
|
-
debug("destination: %s", destination);
|
|
345
|
-
|
|
346
|
-
const openBrowser = options.open;
|
|
347
|
-
|
|
348
|
-
const url = `http://localhost:${options.port}`;
|
|
349
|
-
|
|
350
|
-
const guiUrl = `${url}/counterfact/`;
|
|
351
|
-
|
|
352
|
-
const swaggerUrl = `${url}/counterfact/swagger/`;
|
|
353
|
-
|
|
354
|
-
const config = {
|
|
355
|
-
adminApiToken:
|
|
356
|
-
options.adminApiToken ?? process.env.COUNTERFACT_ADMIN_API_TOKEN ?? "",
|
|
357
|
-
alwaysFakeOptionals: options.alwaysFakeOptionals,
|
|
358
|
-
basePath,
|
|
359
|
-
|
|
360
|
-
generate: {
|
|
361
|
-
routes:
|
|
362
|
-
options.generate ||
|
|
363
|
-
options.generateRoutes ||
|
|
364
|
-
options.watch ||
|
|
365
|
-
options.watchRoutes ||
|
|
366
|
-
options.buildCache,
|
|
367
|
-
|
|
368
|
-
types:
|
|
369
|
-
options.generate ||
|
|
370
|
-
options.generateTypes ||
|
|
371
|
-
options.watch ||
|
|
372
|
-
options.watchTypes ||
|
|
373
|
-
options.buildCache,
|
|
374
|
-
|
|
375
|
-
prune: Boolean(options.prune),
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
openApiPath: source,
|
|
379
|
-
port: options.port,
|
|
380
|
-
proxyPaths: new Map([["", Boolean(options.proxyUrl)]]),
|
|
381
|
-
proxyUrl: options.proxyUrl ?? "",
|
|
382
|
-
routePrefix: options.prefix,
|
|
383
|
-
startAdminApi: options.adminApi,
|
|
384
|
-
startRepl: options.repl,
|
|
385
|
-
startServer: options.serve,
|
|
386
|
-
buildCache: options.buildCache || false,
|
|
387
|
-
validateRequests: options.validateRequest !== false,
|
|
388
|
-
validateResponses: options.validateResponse !== false,
|
|
389
|
-
|
|
390
|
-
watch: {
|
|
391
|
-
routes: options.watch || options.watchRoutes,
|
|
392
|
-
types: options.watch || options.watchTypes,
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const configForLogging = {
|
|
397
|
-
...config,
|
|
398
|
-
adminApiToken: config.adminApiToken ? "[REDACTED]" : "",
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
debug("loading counterfact (%o)", configForLogging);
|
|
402
|
-
|
|
403
|
-
if (config.startAdminApi && !config.adminApiToken) {
|
|
404
|
-
process.stderr.write(
|
|
405
|
-
"⚠️ WARNING: The admin API is enabled without an authentication token.\n" +
|
|
406
|
-
" Any process on this machine can read and modify server state via /_counterfact/api/*.\n" +
|
|
407
|
-
" Set --admin-api-token or COUNTERFACT_ADMIN_API_TOKEN to restrict access.\n\n",
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
let didMigrate = false;
|
|
412
|
-
let didMigrateRouteTypes;
|
|
413
|
-
|
|
414
|
-
if (fs.existsSync(nodePath.join(config.basePath, "paths"))) {
|
|
415
|
-
await pathsToRoutes(config.basePath);
|
|
416
|
-
await fs.promises.rmdir(nodePath.join(config.basePath, "paths"), {
|
|
417
|
-
recursive: true,
|
|
418
|
-
});
|
|
419
|
-
await fs.promises.rmdir(nodePath.join(config.basePath, "path-types"), {
|
|
420
|
-
recursive: true,
|
|
421
|
-
});
|
|
422
|
-
await fs.promises.rmdir(nodePath.join(config.basePath, "components"), {
|
|
423
|
-
recursive: true,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
didMigrate = true;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
let start;
|
|
430
|
-
let startRepl;
|
|
431
|
-
try {
|
|
432
|
-
({ start, startRepl } = await counterfact(config));
|
|
433
|
-
} catch (error) {
|
|
434
|
-
process.stderr.write(
|
|
435
|
-
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
436
|
-
);
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
debug("loaded counterfact", configForLogging);
|
|
441
|
-
|
|
442
|
-
// Migrate route type imports if needed
|
|
443
|
-
debug("checking if route type migration is needed");
|
|
444
|
-
didMigrateRouteTypes = await updateRouteTypes(
|
|
445
|
-
config.basePath,
|
|
446
|
-
config.openApiPath,
|
|
447
|
-
);
|
|
448
|
-
debug("route type migration check complete: %s", didMigrateRouteTypes);
|
|
449
|
-
|
|
450
|
-
const watchMessage = createWatchMessage(config);
|
|
451
|
-
|
|
452
|
-
const telemetryWarning = isTelemetryDisabled
|
|
453
|
-
? []
|
|
454
|
-
: [
|
|
455
|
-
"⚠️ Telemetry will be enabled by default starting May 1, 2026.",
|
|
456
|
-
" Learn more and how to disable: https://counterfact.dev/telemetry-discussion",
|
|
457
|
-
"",
|
|
458
|
-
];
|
|
459
|
-
|
|
460
|
-
const introduction = [
|
|
461
|
-
" ____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___",
|
|
462
|
-
String.raw` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
463
|
-
" " + padTagLine(taglines[Math.floor(Math.random() * taglines.length)]),
|
|
464
|
-
"",
|
|
465
|
-
` Version ${CURRENT_VERSION}`,
|
|
466
|
-
` API Base URL ${url}`,
|
|
467
|
-
source === "_" ? undefined : ` Swagger UI ${swaggerUrl}`,
|
|
468
|
-
"",
|
|
469
|
-
" Instructions https://counterfact.dev/docs/usage.html",
|
|
470
|
-
" Help/feedback https://github.com/pmcelhaney/counterfact/issues",
|
|
471
|
-
"",
|
|
472
|
-
...telemetryWarning,
|
|
473
|
-
watchMessage,
|
|
474
|
-
config.startServer ? " Starting server" : undefined,
|
|
475
|
-
config.startRepl
|
|
476
|
-
? " Starting REPL (type .help for more info)"
|
|
477
|
-
: undefined,
|
|
478
|
-
];
|
|
479
|
-
|
|
480
|
-
process.stdout.write(
|
|
481
|
-
introduction.filter((line) => line !== undefined).join("\n"),
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
process.stdout.write("\n\n");
|
|
485
|
-
|
|
486
|
-
debug("starting server");
|
|
487
|
-
try {
|
|
488
|
-
await start(config);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
process.stderr.write(
|
|
491
|
-
`\n❌ ${error instanceof Error ? error.message : String(error)}\n\n`,
|
|
492
|
-
);
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
495
|
-
debug("started server");
|
|
496
|
-
|
|
497
|
-
await updateCheckPromise;
|
|
498
|
-
|
|
499
|
-
if (config.startRepl) {
|
|
500
|
-
startRepl();
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (openBrowser) {
|
|
504
|
-
debug("opening browser");
|
|
505
|
-
await open(guiUrl);
|
|
506
|
-
debug("opened browser");
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (didMigrate) {
|
|
510
|
-
process.stdout.write("\n\n\n*******************************\n");
|
|
511
|
-
process.stdout.write("MIGRATING TO NEW FILE STRUCTURE\n\n");
|
|
512
|
-
process.stdout.write(
|
|
513
|
-
"In preparation for version 1.0, Counterfact has migrated to a new file structure.\n",
|
|
514
|
-
);
|
|
515
|
-
process.stdout.write("- The paths directory has been renamed to routes.\n");
|
|
516
|
-
process.stdout.write(
|
|
517
|
-
"- The path-types and components directories are now stored under types.\n",
|
|
518
|
-
);
|
|
519
|
-
process.stdout.write("Your files have automatically been migrated.\n");
|
|
520
|
-
process.stdout.write(
|
|
521
|
-
"Please report any issues to https://github.com/pmcelhaney/counterfact/issues\n",
|
|
522
|
-
);
|
|
523
|
-
process.stdout.write("*******************************\n\n\n");
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (didMigrateRouteTypes) {
|
|
527
|
-
process.stdout.write("\n\n\n*******************************\n");
|
|
528
|
-
process.stdout.write("MIGRATING ROUTE TYPE IMPORTS\n\n");
|
|
529
|
-
process.stdout.write(
|
|
530
|
-
"Operation types now use operationId from your OpenAPI spec when available.\n",
|
|
531
|
-
);
|
|
532
|
-
process.stdout.write(
|
|
533
|
-
"Your route files have been automatically updated to use the new type names.\n",
|
|
534
|
-
);
|
|
535
|
-
process.stdout.write(
|
|
536
|
-
"Example: 'HTTP_GET' may now be 'getPetById' if operationId is defined.\n",
|
|
537
|
-
);
|
|
538
|
-
process.stdout.write(
|
|
539
|
-
"Please review the changes and report any issues to:\n",
|
|
540
|
-
);
|
|
541
|
-
process.stdout.write("https://github.com/pmcelhaney/counterfact/issues\n");
|
|
542
|
-
process.stdout.write("*******************************\n\n\n");
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
program
|
|
547
|
-
.name("counterfact")
|
|
548
|
-
.description(
|
|
549
|
-
"Counterfact is a tool for mocking REST APIs in development. See https://counterfact.dev for more info.",
|
|
550
|
-
)
|
|
551
|
-
.argument(
|
|
552
|
-
"[openapi.yaml]",
|
|
553
|
-
'path or URL to OpenAPI document or "_" to run without OpenAPI',
|
|
554
|
-
"_",
|
|
555
|
-
)
|
|
556
|
-
.argument("[destination]", "path to generated code", ".")
|
|
557
|
-
.option("-p, --port <number>", "server port number", DEFAULT_PORT)
|
|
558
|
-
.option("-o, --open", "open a browser")
|
|
559
|
-
.option("-g, --generate", "generate all code for both routes and types")
|
|
560
|
-
.option("--generate-types", "generate types")
|
|
561
|
-
.option("--generate-routes", "generate routes")
|
|
562
|
-
.option("-w, --watch", "generate + watch all code for changes")
|
|
563
|
-
.option("--watch-types", "generate + watch types for changes")
|
|
564
|
-
.option("--watch-routes", "generate + watch routes for changes")
|
|
565
|
-
.option("-s, --serve", "start the server")
|
|
566
|
-
.option("-b, --build-cache", "builds the cache of compiled routes and types")
|
|
567
|
-
.option("--no-admin-api", "disable the admin API at /_counterfact/api/*")
|
|
568
|
-
.option("-r, --repl", "start the REPL")
|
|
569
|
-
.option("--proxy-url <string>", "proxy URL")
|
|
570
|
-
.option(
|
|
571
|
-
"--admin-api-token <string>",
|
|
572
|
-
"bearer token required for /_counterfact/api/* endpoints (defaults to COUNTERFACT_ADMIN_API_TOKEN)",
|
|
573
|
-
)
|
|
574
|
-
.option(
|
|
575
|
-
"--prefix <string>",
|
|
576
|
-
"base path from which routes will be served (e.g. /api/v1)",
|
|
577
|
-
"",
|
|
578
|
-
)
|
|
579
|
-
.option(
|
|
580
|
-
"--always-fake-optionals",
|
|
581
|
-
"random responses will include optional fields",
|
|
582
|
-
)
|
|
583
|
-
.option(
|
|
584
|
-
"--prune",
|
|
585
|
-
"remove route files that no longer exist in the OpenAPI spec",
|
|
586
|
-
)
|
|
587
|
-
.option(
|
|
588
|
-
"--spec <string>",
|
|
589
|
-
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
590
|
-
)
|
|
591
|
-
.option("--no-update-check", "disable the npm update check on startup")
|
|
592
|
-
.option(
|
|
593
|
-
"--no-validate-request",
|
|
594
|
-
"disable request validation against the OpenAPI spec",
|
|
595
|
-
)
|
|
596
|
-
.option(
|
|
597
|
-
"--no-validate-response",
|
|
598
|
-
"disable response validation against the OpenAPI spec",
|
|
599
|
-
)
|
|
600
|
-
.option(
|
|
601
|
-
"--config <path>",
|
|
602
|
-
"path to a counterfact.yaml config file (default: counterfact.yaml in the current directory)",
|
|
603
|
-
)
|
|
604
|
-
.action(main)
|
|
605
|
-
.parse(process.argv);
|
|
76
|
+
await runCli(process.argv);
|
package/bin/ts-loader.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* uses the TypeScript convention of writing .js extensions in import paths
|
|
11
11
|
* (which resolve to .ts files at authoring time). This loader bridges that gap.
|
|
12
12
|
*/
|
|
13
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- loader checks file existence for Node-resolved internal module URLs only. */
|
|
13
14
|
|
|
14
15
|
import { existsSync } from "node:fs";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|