counterfact 2.3.0 → 2.5.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 +3 -2
- package/bin/counterfact.js +70 -6
- package/dist/app.js +5 -10
- package/dist/counterfact-types/index.js +0 -3
- package/dist/migrate/update-route-types.js +1 -1
- package/dist/repl/{RawHttpClient.js → raw-http-client.js} +7 -1
- package/dist/repl/repl.js +27 -3
- package/dist/server/admin-api-middleware.js +5 -4
- package/dist/server/counterfact-types/index.ts +43 -20
- package/dist/server/determine-module-kind.js +0 -1
- package/dist/server/dispatcher.js +21 -0
- package/dist/server/koa-middleware.js +13 -4
- package/dist/server/module-loader.js +33 -3
- package/dist/server/module-tree.js +47 -6
- package/dist/server/openapi-middleware.js +2 -2
- package/dist/server/registry.js +9 -0
- package/dist/server/response-builder.js +85 -19
- package/dist/server/tools.js +2 -4
- package/dist/server/uncached-require.cjs +7 -3
- package/dist/typescript-generator/generate.js +0 -1
- package/dist/typescript-generator/operation-type-coder.js +58 -2
- package/dist/typescript-generator/repository.js +1 -1
- package/dist/typescript-generator/requirement.js +1 -1
- package/dist/typescript-generator/response-type-coder.js +1 -1
- package/dist/typescript-generator/schema-type-coder.js +4 -1
- package/dist/typescript-generator/script.js +5 -4
- package/dist/typescript-generator/specification.js +0 -1
- package/dist/util/ensure-directory-exists.js +0 -1
- package/package.json +23 -19
- /package/dist/counterfact-types/{OpenApiHeader.js → open-api-header.js} +0 -0
- /package/dist/server/counterfact-types/{OpenApiHeader.ts → open-api-header.ts} +0 -0
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ npx counterfact@latest [openapi.yaml] [destination] [options]
|
|
|
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
208
|
|
|
209
|
-
Run `npx counterfact --help` for the full list of options.
|
|
209
|
+
Run `npx counterfact@latest --help` for the full list of options.
|
|
210
210
|
|
|
211
211
|
---
|
|
212
212
|
|
package/bin/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This directory contains the executable script that is run when a developer invok
|
|
|
11
11
|
## How It Works
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
npx counterfact openapi.yaml ./api [options]
|
|
14
|
+
npx counterfact@latest openapi.yaml ./api [options]
|
|
15
15
|
│
|
|
16
16
|
▼
|
|
17
17
|
┌────────────────────────────┐
|
|
@@ -40,5 +40,6 @@ npx counterfact openapi.yaml ./api [options]
|
|
|
40
40
|
| `--spec <path>` | Path or URL to the OpenAPI document (alternative to positional argument) |
|
|
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
|
+
| `--no-update-check` | Disable the npm update check on startup |
|
|
43
44
|
|
|
44
|
-
Run `npx counterfact --help` to see the full option list.
|
|
45
|
+
Run `npx counterfact@latest --help` to see the full option list.
|
package/bin/counterfact.js
CHANGED
|
@@ -23,6 +23,18 @@ if (Number.parseInt(process.versions.node.split("."), 10) < MIN_NODE_VERSION) {
|
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const packageJson = JSON.parse(
|
|
27
|
+
await readFile(
|
|
28
|
+
nodePath.join(
|
|
29
|
+
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
30
|
+
"../package.json",
|
|
31
|
+
),
|
|
32
|
+
"utf8",
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const CURRENT_VERSION = packageJson.version;
|
|
37
|
+
|
|
26
38
|
const taglinesFile = await readFile(
|
|
27
39
|
nodePath.join(
|
|
28
40
|
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -37,6 +49,48 @@ const DEFAULT_PORT = 3100;
|
|
|
37
49
|
|
|
38
50
|
const debug = createDebug("counterfact:bin:counterfact");
|
|
39
51
|
|
|
52
|
+
function isOutdated(current, latest) {
|
|
53
|
+
const [cMajor, cMinor, cPatch] = current.split(".").map(Number);
|
|
54
|
+
const [lMajor, lMinor, lPatch] = latest.split(".").map(Number);
|
|
55
|
+
|
|
56
|
+
if (lMajor > cMajor) return true;
|
|
57
|
+
if (lMajor === cMajor && lMinor > cMinor) return true;
|
|
58
|
+
if (lMajor === cMajor && lMinor === cMinor && lPatch > cPatch) return true;
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function checkForUpdates(currentVersion) {
|
|
64
|
+
if (process.env.CI) {
|
|
65
|
+
debug("skipping update check in CI environment");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
"https://registry.npmjs.org/counterfact/latest",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
debug("update check failed with status %d", response.status);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
const latestVersion = data.version;
|
|
81
|
+
|
|
82
|
+
if (isOutdated(currentVersion, latestVersion)) {
|
|
83
|
+
process.stdout.write(
|
|
84
|
+
`\n⚠️ You're running counterfact ${currentVersion}\n`,
|
|
85
|
+
);
|
|
86
|
+
process.stdout.write(` Latest version is ${latestVersion}\n`);
|
|
87
|
+
process.stdout.write(` Run: npx counterfact@latest\n`);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
debug("update check error: %o", error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
40
94
|
debug("running ./bin/counterfact.js");
|
|
41
95
|
|
|
42
96
|
function padTagLine(tagLine) {
|
|
@@ -47,7 +101,7 @@ function padTagLine(tagLine) {
|
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
function createWatchMessage(config) {
|
|
50
|
-
let watchMessage
|
|
104
|
+
let watchMessage;
|
|
51
105
|
|
|
52
106
|
switch (true) {
|
|
53
107
|
case config.watch.routes && config.watch.types: {
|
|
@@ -102,6 +156,11 @@ async function main(source, destination) {
|
|
|
102
156
|
|
|
103
157
|
const options = program.opts();
|
|
104
158
|
|
|
159
|
+
const updateCheckPromise =
|
|
160
|
+
options.updateCheck === false
|
|
161
|
+
? Promise.resolve()
|
|
162
|
+
: checkForUpdates(CURRENT_VERSION);
|
|
163
|
+
|
|
105
164
|
// --spec takes precedence over the positional [openapi.yaml] argument.
|
|
106
165
|
// When --spec is provided, the [openapi.yaml] positional slot shifts to
|
|
107
166
|
// become the [destination] argument (so `counterfact --spec api.yaml ./api`
|
|
@@ -113,8 +172,6 @@ async function main(source, destination) {
|
|
|
113
172
|
source = options.spec;
|
|
114
173
|
}
|
|
115
174
|
|
|
116
|
-
const args = process.argv;
|
|
117
|
-
|
|
118
175
|
const destinationPath = nodePath.resolve(destination).replaceAll("\\", "/");
|
|
119
176
|
|
|
120
177
|
const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
|
|
@@ -192,9 +249,8 @@ async function main(source, destination) {
|
|
|
192
249
|
debug("loading counterfact (%o)", configForLogging);
|
|
193
250
|
|
|
194
251
|
let didMigrate = false;
|
|
195
|
-
let didMigrateRouteTypes
|
|
252
|
+
let didMigrateRouteTypes;
|
|
196
253
|
|
|
197
|
-
// eslint-disable-next-line n/no-sync
|
|
198
254
|
if (fs.existsSync(nodePath.join(config.basePath, "paths"))) {
|
|
199
255
|
await pathsToRoutes(config.basePath);
|
|
200
256
|
await fs.promises.rmdir(nodePath.join(config.basePath, "paths"), {
|
|
@@ -210,7 +266,7 @@ async function main(source, destination) {
|
|
|
210
266
|
didMigrate = true;
|
|
211
267
|
}
|
|
212
268
|
|
|
213
|
-
const { start } = await counterfact(config);
|
|
269
|
+
const { start, startRepl } = await counterfact(config);
|
|
214
270
|
|
|
215
271
|
debug("loaded counterfact", configForLogging);
|
|
216
272
|
|
|
@@ -229,6 +285,7 @@ async function main(source, destination) {
|
|
|
229
285
|
String.raw` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
230
286
|
" " + padTagLine(taglines[Math.floor(Math.random() * taglines.length)]),
|
|
231
287
|
"",
|
|
288
|
+
` Version ${CURRENT_VERSION}`,
|
|
232
289
|
` API Base URL ${url}`,
|
|
233
290
|
source === "_" ? undefined : ` Swagger UI ${swaggerUrl}`,
|
|
234
291
|
"",
|
|
@@ -257,6 +314,12 @@ async function main(source, destination) {
|
|
|
257
314
|
await start(config);
|
|
258
315
|
debug("started server");
|
|
259
316
|
|
|
317
|
+
await updateCheckPromise;
|
|
318
|
+
|
|
319
|
+
if (config.startRepl) {
|
|
320
|
+
startRepl();
|
|
321
|
+
}
|
|
322
|
+
|
|
260
323
|
if (openBrowser) {
|
|
261
324
|
debug("opening browser");
|
|
262
325
|
await open(guiUrl);
|
|
@@ -345,5 +408,6 @@ program
|
|
|
345
408
|
"--spec <string>",
|
|
346
409
|
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
347
410
|
)
|
|
411
|
+
.option("--no-update-check", "disable the npm update check on startup")
|
|
348
412
|
.action(main)
|
|
349
413
|
.parse(process.argv);
|
package/dist/app.js
CHANGED
|
@@ -2,8 +2,7 @@ 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
4
|
import { createHttpTerminator } from "http-terminator";
|
|
5
|
-
import
|
|
6
|
-
import { startRepl } from "./repl/repl.js";
|
|
5
|
+
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
7
6
|
import { ContextRegistry } from "./server/context-registry.js";
|
|
8
7
|
import { createKoaApp } from "./server/create-koa-app.js";
|
|
9
8
|
import { Dispatcher, } from "./server/dispatcher.js";
|
|
@@ -12,7 +11,6 @@ import { ModuleLoader } from "./server/module-loader.js";
|
|
|
12
11
|
import { Registry } from "./server/registry.js";
|
|
13
12
|
import { Transpiler } from "./server/transpiler.js";
|
|
14
13
|
import { CodeGenerator } from "./typescript-generator/code-generator.js";
|
|
15
|
-
import { readFile } from "./util/read-file.js";
|
|
16
14
|
const allowedMethods = [
|
|
17
15
|
"all",
|
|
18
16
|
"head",
|
|
@@ -25,9 +23,7 @@ const allowedMethods = [
|
|
|
25
23
|
];
|
|
26
24
|
export async function loadOpenApiDocument(source) {
|
|
27
25
|
try {
|
|
28
|
-
|
|
29
|
-
const openApiDocument = await yaml.load(text);
|
|
30
|
-
return (await dereference(openApiDocument));
|
|
26
|
+
return (await dereference(source));
|
|
31
27
|
}
|
|
32
28
|
catch {
|
|
33
29
|
return undefined;
|
|
@@ -46,7 +42,7 @@ export async function handleMswRequest(request) {
|
|
|
46
42
|
export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader) {
|
|
47
43
|
// TODO: For some reason the Vitest Custom Commands needed by Vitest Browser mode fail on fs.readFile when they are called from the nested loadOpenApiDocument function.
|
|
48
44
|
// If we "pre-read" the file here it works. This is a workaround to avoid the issue.
|
|
49
|
-
|
|
45
|
+
await fs.readFile(config.openApiPath);
|
|
50
46
|
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
|
|
51
47
|
if (openApiDocument === undefined) {
|
|
52
48
|
throw new Error(`Could not load OpenAPI document from ${config.openApiPath}`);
|
|
@@ -92,7 +88,7 @@ export async function counterfact(config) {
|
|
|
92
88
|
const middleware = koaMiddleware(dispatcher, config);
|
|
93
89
|
const koaApp = createKoaApp(registry, middleware, config, contextRegistry);
|
|
94
90
|
async function start(options) {
|
|
95
|
-
const { generate,
|
|
91
|
+
const { generate, startServer, watch, buildCache } = options;
|
|
96
92
|
if (generate.routes || generate.types) {
|
|
97
93
|
await codeGenerator.generate();
|
|
98
94
|
}
|
|
@@ -116,9 +112,7 @@ export async function counterfact(config) {
|
|
|
116
112
|
await transpiler.watch();
|
|
117
113
|
await transpiler.stopWatching();
|
|
118
114
|
}
|
|
119
|
-
const replServer = shouldStartRepl && startRepl(contextRegistry, config);
|
|
120
115
|
return {
|
|
121
|
-
replServer,
|
|
122
116
|
async stop() {
|
|
123
117
|
await codeGenerator.stopWatching();
|
|
124
118
|
await transpiler.stopWatching();
|
|
@@ -133,5 +127,6 @@ export async function counterfact(config) {
|
|
|
133
127
|
koaMiddleware: middleware,
|
|
134
128
|
registry,
|
|
135
129
|
start,
|
|
130
|
+
startRepl: () => startReplServer(contextRegistry, registry, config),
|
|
136
131
|
};
|
|
137
132
|
}
|
|
@@ -94,7 +94,7 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
94
94
|
// Build a map of old type names to new type names found in this file
|
|
95
95
|
const replacements = new Map();
|
|
96
96
|
// Find all import statements with HTTP_ patterns
|
|
97
|
-
const importRegex = /import\s+type\s+\{(?<types>[^}]+)\}\s+from\s+["']
|
|
97
|
+
const importRegex = /import\s+type\s+\{(?<types>[^}]+)\}\s+from\s+["'][^"']+["'];?/gu;
|
|
98
98
|
let importMatch;
|
|
99
99
|
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
100
100
|
const importedTypes = importMatch.groups.types
|
|
@@ -89,6 +89,12 @@ export class RawHttpClient {
|
|
|
89
89
|
#send(method, path, bodyAsStringOrObject, headers) {
|
|
90
90
|
const requestNumber = ++this.requestNumber;
|
|
91
91
|
const body = stringifyBody(bodyAsStringOrObject);
|
|
92
|
+
const effectiveHeaders = { ...headers };
|
|
93
|
+
if (typeof bodyAsStringOrObject === "object" &&
|
|
94
|
+
bodyAsStringOrObject !== null &&
|
|
95
|
+
!Object.keys(effectiveHeaders).some((k) => k.toLowerCase() === "content-type")) {
|
|
96
|
+
effectiveHeaders["Content-Type"] = "application/json";
|
|
97
|
+
}
|
|
92
98
|
return new Promise((resolve, reject) => {
|
|
93
99
|
const socket = net.createConnection({ host: this.host, port: this.port }, () => {
|
|
94
100
|
let request = `${method} ${path} HTTP/1.1\r\n`;
|
|
@@ -97,7 +103,7 @@ export class RawHttpClient {
|
|
|
97
103
|
if (body != null) {
|
|
98
104
|
request += `Content-Length: ${Buffer.byteLength(body)}\r\n`;
|
|
99
105
|
}
|
|
100
|
-
for (const [key, value] of Object.entries(
|
|
106
|
+
for (const [key, value] of Object.entries(effectiveHeaders)) {
|
|
101
107
|
request += `${key}: ${value}\r\n`;
|
|
102
108
|
}
|
|
103
109
|
request += `\r\n`;
|
package/dist/repl/repl.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import repl from "node:repl";
|
|
2
|
-
import { RawHttpClient } from "./
|
|
2
|
+
import { RawHttpClient } from "./raw-http-client.js";
|
|
3
3
|
function printToStdout(line) {
|
|
4
4
|
process.stdout.write(`${line}\n`);
|
|
5
5
|
}
|
|
6
|
-
export function
|
|
6
|
+
export function createCompleter(registry, fallback) {
|
|
7
|
+
return (line, callback) => {
|
|
8
|
+
const match = line.match(/client\.(?:get|post|put|patch|delete)\("(?<partial>[^"]*)$/u);
|
|
9
|
+
if (!match) {
|
|
10
|
+
if (fallback) {
|
|
11
|
+
fallback(line, callback);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
callback(null, [[], line]);
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const partial = match.groups?.["partial"] ?? "";
|
|
19
|
+
const routes = registry.routes.map((route) => route.path);
|
|
20
|
+
const matches = routes.filter((route) => route.startsWith(partial));
|
|
21
|
+
callback(null, [matches, partial]);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function startRepl(contextRegistry, registry, config, print = printToStdout) {
|
|
7
25
|
function printProxyStatus() {
|
|
8
26
|
if (config.proxyUrl === "") {
|
|
9
27
|
print("The proxy URL is not set.");
|
|
@@ -41,7 +59,13 @@ export function startRepl(contextRegistry, config, print = printToStdout) {
|
|
|
41
59
|
print(`Requests to ${printEndpoint} will be handled by local code`);
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
|
-
const replServer = repl.start({
|
|
62
|
+
const replServer = repl.start({
|
|
63
|
+
prompt: "⬣> ",
|
|
64
|
+
});
|
|
65
|
+
const builtinCompleter = replServer.completer;
|
|
66
|
+
// completer is typed as readonly in @types/node but is writable at runtime
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
replServer.completer = createCompleter(registry, builtinCompleter);
|
|
45
69
|
replServer.defineCommand("counterfact", {
|
|
46
70
|
action() {
|
|
47
71
|
print("This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.");
|
|
@@ -118,10 +118,10 @@ export function adminApiMiddleware(registry, contextRegistry, config) {
|
|
|
118
118
|
// ===== Update Context =====
|
|
119
119
|
if (resource === "contexts" && rest.length > 0 && ctx.method === "POST") {
|
|
120
120
|
const path = "/" + rest.join("/");
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
typeof
|
|
124
|
-
Array.isArray(
|
|
121
|
+
const newContextCandidate = ctx.request.body;
|
|
122
|
+
if (!newContextCandidate ||
|
|
123
|
+
typeof newContextCandidate !== "object" ||
|
|
124
|
+
Array.isArray(newContextCandidate)) {
|
|
125
125
|
ctx.status = 400;
|
|
126
126
|
ctx.body = {
|
|
127
127
|
success: false,
|
|
@@ -130,6 +130,7 @@ export function adminApiMiddleware(registry, contextRegistry, config) {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
// Update the context using the registry's smart diffing
|
|
133
|
+
const newContext = newContextCandidate;
|
|
133
134
|
contextRegistry.update(path, newContext);
|
|
134
135
|
ctx.body = {
|
|
135
136
|
success: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpenApiHeader } from "./
|
|
1
|
+
import { OpenApiHeader } from "./open-api-header";
|
|
2
2
|
|
|
3
3
|
interface OpenApiContent {
|
|
4
4
|
schema: unknown;
|
|
@@ -10,14 +10,22 @@ interface Example {
|
|
|
10
10
|
value: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface CookieOptions {
|
|
14
|
+
domain?: string;
|
|
15
|
+
expires?: Date;
|
|
16
|
+
httpOnly?: boolean;
|
|
17
|
+
maxAge?: number;
|
|
18
|
+
path?: string;
|
|
19
|
+
sameSite?: "lax" | "none" | "strict";
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
const counterfactResponse = Symbol("Counterfact Response");
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
[counterfactResponse]: counterfactResponse
|
|
25
|
+
type COUNTERFACT_RESPONSE = {
|
|
26
|
+
[counterfactResponse]: typeof counterfactResponse;
|
|
17
27
|
};
|
|
18
28
|
|
|
19
|
-
type COUNTERFACT_RESPONSE = typeof counterfactResponseObject;
|
|
20
|
-
|
|
21
29
|
type MediaType = `${string}/${string}`;
|
|
22
30
|
|
|
23
31
|
type MaybePromise<T> = T | Promise<T>;
|
|
@@ -55,12 +63,12 @@ type IfHasKey<
|
|
|
55
63
|
infer FirstKey extends string,
|
|
56
64
|
...infer RestKeys extends string[],
|
|
57
65
|
]
|
|
58
|
-
? keyof SomeObject extends
|
|
59
|
-
? Yes
|
|
60
|
-
:
|
|
66
|
+
? Extract<keyof SomeObject, `${string}${FirstKey}${string}`> extends never
|
|
67
|
+
? IfHasKey<SomeObject, RestKeys, Yes, No>
|
|
68
|
+
: Yes
|
|
61
69
|
: No;
|
|
62
70
|
|
|
63
|
-
type SchemasOf<T extends { [key: string]: { schema:
|
|
71
|
+
type SchemasOf<T extends { [key: string]: { schema: unknown } }> = {
|
|
64
72
|
[K in keyof T]: T[K]["schema"];
|
|
65
73
|
}[keyof T];
|
|
66
74
|
|
|
@@ -78,7 +86,7 @@ type MaybeShortcut<
|
|
|
78
86
|
never
|
|
79
87
|
>;
|
|
80
88
|
|
|
81
|
-
type NeverIfEmpty<Record> =
|
|
89
|
+
type NeverIfEmpty<Record> = object extends Record ? never : Record;
|
|
82
90
|
|
|
83
91
|
type MatchFunction<Response extends OpenApiResponse> = <
|
|
84
92
|
ContentType extends MediaType & keyof Response["content"],
|
|
@@ -102,9 +110,7 @@ type HeaderFunction<Response extends OpenApiResponse> = <
|
|
|
102
110
|
requiredHeaders: Exclude<Response["requiredHeaders"], Header>;
|
|
103
111
|
}>;
|
|
104
112
|
|
|
105
|
-
type RandomFunction
|
|
106
|
-
Header extends string & keyof Response["headers"],
|
|
107
|
-
>() => COUNTERFACT_RESPONSE;
|
|
113
|
+
type RandomFunction = () => MaybePromise<COUNTERFACT_RESPONSE>;
|
|
108
114
|
|
|
109
115
|
type ExampleNames<Response extends OpenApiResponse> = Response extends {
|
|
110
116
|
examples: infer E;
|
|
@@ -114,15 +120,21 @@ type ExampleNames<Response extends OpenApiResponse> = Response extends {
|
|
|
114
120
|
|
|
115
121
|
interface ResponseBuilder {
|
|
116
122
|
[status: number | `${number} ${string}`]: ResponseBuilder;
|
|
123
|
+
binary: (body: Uint8Array | string) => ResponseBuilder;
|
|
117
124
|
content?: { body: unknown; type: string }[];
|
|
125
|
+
cookie: (
|
|
126
|
+
name: string,
|
|
127
|
+
value: string,
|
|
128
|
+
options?: CookieOptions,
|
|
129
|
+
) => ResponseBuilder;
|
|
118
130
|
example: (name: string) => ResponseBuilder;
|
|
119
131
|
header: (name: string, value: string) => ResponseBuilder;
|
|
120
|
-
headers: { [name: string]: string };
|
|
132
|
+
headers: { [name: string]: string | string[] };
|
|
121
133
|
html: (body: unknown) => ResponseBuilder;
|
|
122
134
|
json: (body: unknown) => ResponseBuilder;
|
|
123
135
|
match: (contentType: string, body: unknown) => ResponseBuilder;
|
|
124
|
-
random: () => ResponseBuilder
|
|
125
|
-
randomLegacy: () => ResponseBuilder
|
|
136
|
+
random: () => MaybePromise<ResponseBuilder>;
|
|
137
|
+
randomLegacy: () => MaybePromise<ResponseBuilder>;
|
|
126
138
|
status?: number;
|
|
127
139
|
text: (body: unknown) => ResponseBuilder;
|
|
128
140
|
xml: (body: unknown) => ResponseBuilder;
|
|
@@ -131,6 +143,12 @@ interface ResponseBuilder {
|
|
|
131
143
|
export type GenericResponseBuilderInner<
|
|
132
144
|
Response extends OpenApiResponse = OpenApiResponse,
|
|
133
145
|
> = OmitValueWhenNever<{
|
|
146
|
+
binary: MaybeShortcut<["application/octet-stream"], Response>;
|
|
147
|
+
cookie: (
|
|
148
|
+
name: string,
|
|
149
|
+
value: string,
|
|
150
|
+
options?: CookieOptions,
|
|
151
|
+
) => GenericResponseBuilder<Response>;
|
|
134
152
|
header: [keyof Response["headers"]] extends [never]
|
|
135
153
|
? never
|
|
136
154
|
: HeaderFunction<Response>;
|
|
@@ -148,9 +166,7 @@ export type GenericResponseBuilderInner<
|
|
|
148
166
|
match: [keyof Response["content"]] extends [never]
|
|
149
167
|
? never
|
|
150
168
|
: MatchFunction<Response>;
|
|
151
|
-
random: [keyof Response["content"]] extends [never]
|
|
152
|
-
? never
|
|
153
|
-
: RandomFunction<Response>;
|
|
169
|
+
random: [keyof Response["content"]] extends [never] ? never : RandomFunction;
|
|
154
170
|
example: [ExampleNames<Response>] extends [never]
|
|
155
171
|
? never
|
|
156
172
|
: (name: ExampleNames<Response>) => COUNTERFACT_RESPONSE;
|
|
@@ -266,12 +282,18 @@ interface OpenApiOperation {
|
|
|
266
282
|
}
|
|
267
283
|
|
|
268
284
|
interface WideResponseBuilder {
|
|
285
|
+
binary: (body: Uint8Array | string) => WideResponseBuilder;
|
|
269
286
|
example: (name: string) => WideResponseBuilder;
|
|
287
|
+
cookie: (
|
|
288
|
+
name: string,
|
|
289
|
+
value: string,
|
|
290
|
+
options?: CookieOptions,
|
|
291
|
+
) => WideResponseBuilder;
|
|
270
292
|
header: (body: unknown) => WideResponseBuilder;
|
|
271
293
|
html: (body: unknown) => WideResponseBuilder;
|
|
272
294
|
json: (body: unknown) => WideResponseBuilder;
|
|
273
295
|
match: (contentType: string, body: unknown) => WideResponseBuilder;
|
|
274
|
-
random: () => WideResponseBuilder
|
|
296
|
+
random: () => MaybePromise<WideResponseBuilder>;
|
|
275
297
|
text: (body: unknown) => WideResponseBuilder;
|
|
276
298
|
xml: (body: unknown) => WideResponseBuilder;
|
|
277
299
|
}
|
|
@@ -289,6 +311,7 @@ interface WideOperationArgument {
|
|
|
289
311
|
export type { COUNTERFACT_RESPONSE };
|
|
290
312
|
|
|
291
313
|
export type {
|
|
314
|
+
CookieOptions,
|
|
292
315
|
ExampleNames,
|
|
293
316
|
HttpStatusCode,
|
|
294
317
|
MaybePromise,
|
|
@@ -4,6 +4,26 @@ import fetch, { Headers } from "node-fetch";
|
|
|
4
4
|
import { createResponseBuilder } from "./response-builder.js";
|
|
5
5
|
import { Tools } from "./tools.js";
|
|
6
6
|
const debug = createDebugger("counterfact:server:dispatcher");
|
|
7
|
+
function parseCookies(cookieHeader) {
|
|
8
|
+
const cookies = {};
|
|
9
|
+
for (const part of cookieHeader.split(";")) {
|
|
10
|
+
const eqIndex = part.indexOf("=");
|
|
11
|
+
if (eqIndex === -1) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const key = part.slice(0, eqIndex).trim();
|
|
15
|
+
const value = part.slice(eqIndex + 1).trim();
|
|
16
|
+
if (key && !(key in cookies)) {
|
|
17
|
+
try {
|
|
18
|
+
cookies[key] = decodeURIComponent(value);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
cookies[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return cookies;
|
|
26
|
+
}
|
|
7
27
|
export class Dispatcher {
|
|
8
28
|
registry;
|
|
9
29
|
contextRegistry;
|
|
@@ -143,6 +163,7 @@ export class Dispatcher {
|
|
|
143
163
|
: continuousDistribution(milliseconds, maxMilliseconds);
|
|
144
164
|
return new Promise((resolve) => setTimeout(resolve, delayInMs));
|
|
145
165
|
},
|
|
166
|
+
cookie: parseCookies(headers.cookie ?? headers.Cookie ?? ""),
|
|
146
167
|
headers,
|
|
147
168
|
proxy: async (url) => {
|
|
148
169
|
if (body !== undefined && headers.contentType !== "application/json") {
|
|
@@ -19,10 +19,10 @@ const HEADERS_TO_DROP = new Set([
|
|
|
19
19
|
"trailer",
|
|
20
20
|
"trailers",
|
|
21
21
|
]);
|
|
22
|
-
function addCors(ctx, headers) {
|
|
22
|
+
function addCors(ctx, allowedMethods, headers) {
|
|
23
23
|
// Always append CORS headers, reflecting back the headers requested if any
|
|
24
24
|
ctx.set("Access-Control-Allow-Origin", headers?.origin ?? "*");
|
|
25
|
-
ctx.set("Access-Control-Allow-Methods",
|
|
25
|
+
ctx.set("Access-Control-Allow-Methods", allowedMethods);
|
|
26
26
|
ctx.set("Access-Control-Allow-Headers", headers?.["access-control-request-headers"] ?? []);
|
|
27
27
|
ctx.set("Access-Control-Expose-Headers", headers?.["access-control-request-headers"] ?? []);
|
|
28
28
|
ctx.set("Access-Control-Allow-Credentials", "true");
|
|
@@ -55,7 +55,7 @@ export function koaMiddleware(dispatcher, config, proxy = koaProxy) {
|
|
|
55
55
|
if (isProxyEnabledForPath(path, config) && proxyUrl) {
|
|
56
56
|
return proxy("/", { changeOrigin: true, target: proxyUrl })(ctx, next);
|
|
57
57
|
}
|
|
58
|
-
addCors(ctx, headers);
|
|
58
|
+
addCors(ctx, dispatcher.registry.allowedMethods(path), headers);
|
|
59
59
|
if (method === "OPTIONS") {
|
|
60
60
|
ctx.status = HTTP_STATUS_CODE_OK;
|
|
61
61
|
return undefined;
|
|
@@ -72,10 +72,19 @@ export function koaMiddleware(dispatcher, config, proxy = koaProxy) {
|
|
|
72
72
|
req: { path: "", ...ctx.req },
|
|
73
73
|
});
|
|
74
74
|
ctx.body = response.body;
|
|
75
|
+
if (response.contentType !== undefined &&
|
|
76
|
+
response.contentType !== "unknown/unknown") {
|
|
77
|
+
ctx.type = response.contentType;
|
|
78
|
+
}
|
|
75
79
|
if (response.headers) {
|
|
76
80
|
for (const [key, value] of Object.entries(response.headers)) {
|
|
77
81
|
if (!HEADERS_TO_DROP.has(key.toLowerCase())) {
|
|
78
|
-
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
ctx.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
ctx.set(key, value.toString());
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable n/no-sync */
|
|
2
1
|
import { once } from "node:events";
|
|
3
2
|
import { existsSync } from "node:fs";
|
|
4
3
|
import fs from "node:fs/promises";
|
|
@@ -56,6 +55,7 @@ export class ModuleLoader extends EventTarget {
|
|
|
56
55
|
if (eventName === "unlink") {
|
|
57
56
|
this.registry.remove(url);
|
|
58
57
|
this.dispatchEvent(new Event("remove"));
|
|
58
|
+
return;
|
|
59
59
|
}
|
|
60
60
|
const dependencies = this.dependencyGraph.dependentsOf(pathName);
|
|
61
61
|
void this.loadEndpoint(pathName);
|
|
@@ -103,9 +103,39 @@ export class ModuleLoader extends EventTarget {
|
|
|
103
103
|
const doImport = (await determineModuleKind(pathName)) === "commonjs"
|
|
104
104
|
? uncachedRequire
|
|
105
105
|
: uncachedImport;
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
let importError;
|
|
107
|
+
const endpoint = (await doImport(pathName).catch((error) => {
|
|
108
|
+
importError = error;
|
|
108
109
|
}));
|
|
110
|
+
if (importError !== undefined) {
|
|
111
|
+
const isSyntaxError = importError instanceof SyntaxError ||
|
|
112
|
+
String(importError).startsWith("SyntaxError:");
|
|
113
|
+
const displayPath = nodePath
|
|
114
|
+
.relative(process.cwd(), unescapePathForWindows(pathName))
|
|
115
|
+
.replaceAll("\\", "/");
|
|
116
|
+
const message = isSyntaxError
|
|
117
|
+
? `There is a syntax error in the route file: ${displayPath}`
|
|
118
|
+
: `There was an error loading the route file: ${displayPath}`;
|
|
119
|
+
const errorResponse = () => ({
|
|
120
|
+
body: message,
|
|
121
|
+
status: 500,
|
|
122
|
+
});
|
|
123
|
+
this.registry.add(url, {
|
|
124
|
+
DELETE: errorResponse,
|
|
125
|
+
GET: errorResponse,
|
|
126
|
+
HEAD: errorResponse,
|
|
127
|
+
OPTIONS: errorResponse,
|
|
128
|
+
PATCH: errorResponse,
|
|
129
|
+
POST: errorResponse,
|
|
130
|
+
PUT: errorResponse,
|
|
131
|
+
TRACE: errorResponse,
|
|
132
|
+
});
|
|
133
|
+
this.dispatchEvent(new Event("add"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!endpoint) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
109
139
|
this.dispatchEvent(new Event("add"));
|
|
110
140
|
if (basename(pathName).startsWith("_.context.") &&
|
|
111
141
|
isContextModule(endpoint)) {
|
|
@@ -17,6 +17,7 @@ export class ModuleTree {
|
|
|
17
17
|
if (remainingSegments.length === 0) {
|
|
18
18
|
return directory;
|
|
19
19
|
}
|
|
20
|
+
const isNewDirectory = directory.directories[segment.toLowerCase()] === undefined;
|
|
20
21
|
const nextDirectory = (directory.directories[segment.toLowerCase()] ??= {
|
|
21
22
|
directories: {},
|
|
22
23
|
files: {},
|
|
@@ -24,6 +25,12 @@ export class ModuleTree {
|
|
|
24
25
|
name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
|
|
25
26
|
rawName: segment,
|
|
26
27
|
});
|
|
28
|
+
if (isNewDirectory && segment.startsWith("{")) {
|
|
29
|
+
const ambiguousWildcardDirectories = Object.values(directory.directories).filter((subdirectory) => subdirectory.isWildcard);
|
|
30
|
+
if (ambiguousWildcardDirectories.length > 1) {
|
|
31
|
+
process.stderr.write(`[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard directories exist at the same level: ${ambiguousWildcardDirectories.map((d) => d.rawName).join(", ")}. Requests may be routed unpredictably.\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
27
34
|
return this.putDirectory(nextDirectory, remainingSegments);
|
|
28
35
|
}
|
|
29
36
|
addModuleToDirectory(directory, segments, module) {
|
|
@@ -41,6 +48,12 @@ export class ModuleTree {
|
|
|
41
48
|
name: filename.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
|
|
42
49
|
rawName: filename,
|
|
43
50
|
};
|
|
51
|
+
if (filename.startsWith("{")) {
|
|
52
|
+
const ambiguousWildcardFiles = Object.values(targetDirectory.files).filter((file) => file.isWildcard);
|
|
53
|
+
if (ambiguousWildcardFiles.length > 1) {
|
|
54
|
+
process.stderr.write(`[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard files exist at the same path level: ${ambiguousWildcardFiles.map((f) => f.rawName).join(", ")}. Requests may be routed unpredictably.\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
add(url, module) {
|
|
46
59
|
this.addModuleToDirectory(this.root, url.split("/").slice(1), module);
|
|
@@ -75,8 +88,31 @@ export class ModuleTree {
|
|
|
75
88
|
}
|
|
76
89
|
return "";
|
|
77
90
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
91
|
+
const exactMatchFile = directory.files[normalizedSegment(segment, directory)];
|
|
92
|
+
// If the URL segment literally matches a file key (e.g., requesting "/{x}"
|
|
93
|
+
// as a literal URL value), exactMatchFile may be a wildcard file. In that
|
|
94
|
+
// case, fall through to wildcard matching below.
|
|
95
|
+
if (exactMatchFile !== undefined && !exactMatchFile.isWildcard) {
|
|
96
|
+
return {
|
|
97
|
+
...exactMatchFile,
|
|
98
|
+
matchedPath: `${matchedPath}/${exactMatchFile.rawName}`,
|
|
99
|
+
pathVariables,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const wildcardFiles = Object.values(directory.files).filter((file) => file.isWildcard && this.fileModuleDefined(file, method));
|
|
103
|
+
if (wildcardFiles.length > 1) {
|
|
104
|
+
const firstWildcard = wildcardFiles[0];
|
|
105
|
+
return {
|
|
106
|
+
...firstWildcard,
|
|
107
|
+
ambiguous: true,
|
|
108
|
+
matchedPath: `${matchedPath}/${firstWildcard.rawName}`,
|
|
109
|
+
pathVariables: {
|
|
110
|
+
...pathVariables,
|
|
111
|
+
[firstWildcard.name]: segment,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const match = exactMatchFile ?? wildcardFiles[0];
|
|
80
116
|
if (match === undefined) {
|
|
81
117
|
return undefined;
|
|
82
118
|
}
|
|
@@ -113,16 +149,21 @@ export class ModuleTree {
|
|
|
113
149
|
return this.matchWithinDirectory(exactMatch, remainingSegments, pathVariables, `${matchedPath}/${segment}`, method);
|
|
114
150
|
}
|
|
115
151
|
const wildcardDirectories = Object.values(directory.directories).filter((subdirectory) => subdirectory.isWildcard);
|
|
152
|
+
const wildcardMatches = [];
|
|
116
153
|
for (const wildcardDirectory of wildcardDirectories) {
|
|
117
|
-
const
|
|
154
|
+
const wildcardMatch = this.matchWithinDirectory(wildcardDirectory, remainingSegments, {
|
|
118
155
|
...pathVariables,
|
|
119
156
|
[wildcardDirectory.name]: segment,
|
|
120
157
|
}, `${matchedPath}/${wildcardDirectory.rawName}`, method);
|
|
121
|
-
if (
|
|
122
|
-
|
|
158
|
+
if (wildcardMatch !== undefined) {
|
|
159
|
+
wildcardMatches.push(wildcardMatch);
|
|
123
160
|
}
|
|
124
161
|
}
|
|
125
|
-
|
|
162
|
+
if (wildcardMatches.length > 1) {
|
|
163
|
+
const firstMatch = wildcardMatches[0];
|
|
164
|
+
return { ...firstMatch, ambiguous: true };
|
|
165
|
+
}
|
|
166
|
+
return wildcardMatches[0];
|
|
126
167
|
}
|
|
127
168
|
match(url, method) {
|
|
128
169
|
return this.matchWithinDirectory(this.root, url.split("/").slice(1), {}, "", method);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { bundle } from "@apidevtools/json-schema-ref-parser";
|
|
1
2
|
import yaml from "js-yaml";
|
|
2
|
-
import { readFile } from "../util/read-file.js";
|
|
3
3
|
export function openapiMiddleware(openApiPath, url) {
|
|
4
4
|
return async (ctx, next) => {
|
|
5
5
|
if (ctx.URL.pathname === "/counterfact/openapi") {
|
|
6
|
-
const openApiDocument = (await
|
|
6
|
+
const openApiDocument = (await bundle(openApiPath));
|
|
7
7
|
openApiDocument.servers ??= [];
|
|
8
8
|
openApiDocument.servers.unshift({
|
|
9
9
|
description: "Counterfact",
|
package/dist/server/registry.js
CHANGED
|
@@ -57,6 +57,7 @@ export class Registry {
|
|
|
57
57
|
handler(url, method) {
|
|
58
58
|
const match = this.moduleTree.match(url, method);
|
|
59
59
|
return {
|
|
60
|
+
ambiguous: match?.ambiguous ?? false,
|
|
60
61
|
matchedPath: match?.matchedPath ?? "",
|
|
61
62
|
module: match?.module,
|
|
62
63
|
path: match?.pathVariables ?? {},
|
|
@@ -71,6 +72,14 @@ export class Registry {
|
|
|
71
72
|
endpoint(httpRequestMethod, url, parameterTypes = {}) {
|
|
72
73
|
const handler = this.handler(url, httpRequestMethod);
|
|
73
74
|
debug("handler for %s: %o", url, handler);
|
|
75
|
+
if (handler.ambiguous) {
|
|
76
|
+
return () => ({
|
|
77
|
+
body: `Ambiguous wildcard paths: the request to ${url} matches multiple routes. Please resolve the ambiguity in your API spec or route handlers.`,
|
|
78
|
+
contentType: "text/plain",
|
|
79
|
+
headers: {},
|
|
80
|
+
status: 500,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
74
83
|
const execute = handler.module?.[httpRequestMethod];
|
|
75
84
|
if (!execute) {
|
|
76
85
|
debug(`Could not find a ${httpRequestMethod} method matching ${url}\n`);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generate } from "json-schema-faker";
|
|
2
2
|
import { jsonToXml } from "./json-to-xml.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const DEFAULT_GENERATE_OPTIONS = {
|
|
4
|
+
useExamplesValue: true,
|
|
5
|
+
minItems: 0,
|
|
6
|
+
maxItems: 20,
|
|
7
|
+
failOnInvalidTypes: false,
|
|
8
|
+
fillProperties: false,
|
|
9
|
+
};
|
|
9
10
|
function convertToXmlIfNecessary(type, body, schema) {
|
|
10
11
|
if (type.endsWith("/xml")) {
|
|
11
12
|
return jsonToXml(body, schema, "root");
|
|
@@ -18,6 +19,32 @@ function oneOf(items) {
|
|
|
18
19
|
}
|
|
19
20
|
return oneOf(Object.values(items));
|
|
20
21
|
}
|
|
22
|
+
function serializeCookie(name, value, options = {}) {
|
|
23
|
+
const parts = [`${name}=${value}`];
|
|
24
|
+
if (options.path !== undefined) {
|
|
25
|
+
parts.push(`Path=${options.path}`);
|
|
26
|
+
}
|
|
27
|
+
if (options.domain !== undefined) {
|
|
28
|
+
parts.push(`Domain=${options.domain}`);
|
|
29
|
+
}
|
|
30
|
+
if (options.maxAge !== undefined) {
|
|
31
|
+
parts.push(`Max-Age=${options.maxAge}`);
|
|
32
|
+
}
|
|
33
|
+
if (options.expires !== undefined) {
|
|
34
|
+
parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
35
|
+
}
|
|
36
|
+
if (options.httpOnly) {
|
|
37
|
+
parts.push("HttpOnly");
|
|
38
|
+
}
|
|
39
|
+
if (options.secure) {
|
|
40
|
+
parts.push("Secure");
|
|
41
|
+
}
|
|
42
|
+
if (options.sameSite !== undefined) {
|
|
43
|
+
const sameSiteMap = { lax: "Lax", none: "None", strict: "Strict" };
|
|
44
|
+
parts.push(`SameSite=${sameSiteMap[options.sameSite]}`);
|
|
45
|
+
}
|
|
46
|
+
return parts.join("; ");
|
|
47
|
+
}
|
|
21
48
|
function unknownStatusCodeResponse(statusCode) {
|
|
22
49
|
return {
|
|
23
50
|
content: [
|
|
@@ -41,6 +68,28 @@ export function createResponseBuilder(operation, config) {
|
|
|
41
68
|
},
|
|
42
69
|
};
|
|
43
70
|
},
|
|
71
|
+
binary(body) {
|
|
72
|
+
const buffer = typeof body === "string"
|
|
73
|
+
? Buffer.from(body, "base64")
|
|
74
|
+
: Buffer.from(body);
|
|
75
|
+
return this.match("application/octet-stream", buffer);
|
|
76
|
+
},
|
|
77
|
+
cookie(name, value, options = {}) {
|
|
78
|
+
const cookieString = serializeCookie(name, value, options);
|
|
79
|
+
const existing = this.headers?.["set-cookie"];
|
|
80
|
+
const existingArray = Array.isArray(existing)
|
|
81
|
+
? existing
|
|
82
|
+
: existing !== undefined
|
|
83
|
+
? [existing]
|
|
84
|
+
: [];
|
|
85
|
+
return {
|
|
86
|
+
...this,
|
|
87
|
+
headers: {
|
|
88
|
+
...this.headers,
|
|
89
|
+
"set-cookie": [...existingArray, cookieString],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
44
93
|
html(body) {
|
|
45
94
|
return this.match("text/html", body);
|
|
46
95
|
},
|
|
@@ -73,6 +122,18 @@ export function createResponseBuilder(operation, config) {
|
|
|
73
122
|
return unknownStatusCodeResponse(this.status);
|
|
74
123
|
}
|
|
75
124
|
const { content } = response;
|
|
125
|
+
const exampleExists = Object.values(content).some((contentType) => contentType?.examples?.[name] !== undefined);
|
|
126
|
+
if (!exampleExists) {
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
body: `The OpenAPI document does not define an example named "${name}" for status code ${this.status ?? "unknown"}`,
|
|
131
|
+
type: "text/plain",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
status: 500,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
76
137
|
return {
|
|
77
138
|
...this,
|
|
78
139
|
content: Object.keys(content).map((type) => ({
|
|
@@ -81,12 +142,15 @@ export function createResponseBuilder(operation, config) {
|
|
|
81
142
|
})),
|
|
82
143
|
};
|
|
83
144
|
},
|
|
84
|
-
random() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
async random() {
|
|
146
|
+
const generateOptions = config?.alwaysFakeOptionals
|
|
147
|
+
? {
|
|
148
|
+
...DEFAULT_GENERATE_OPTIONS,
|
|
149
|
+
alwaysFakeOptionals: true,
|
|
150
|
+
fixedProbabilities: true,
|
|
151
|
+
optionalsProbability: 1.0,
|
|
152
|
+
}
|
|
153
|
+
: DEFAULT_GENERATE_OPTIONS;
|
|
90
154
|
if (operation.produces) {
|
|
91
155
|
return this.randomLegacy();
|
|
92
156
|
}
|
|
@@ -99,24 +163,26 @@ export function createResponseBuilder(operation, config) {
|
|
|
99
163
|
const generatedHeaders = {};
|
|
100
164
|
for (const [name, header] of Object.entries(response.headers ?? {})) {
|
|
101
165
|
if (header.required && !(name in (this.headers ?? {}))) {
|
|
102
|
-
generatedHeaders[name] =
|
|
166
|
+
generatedHeaders[name] = (await generate((header.schema ?? { type: "string" }), generateOptions));
|
|
103
167
|
}
|
|
104
168
|
}
|
|
105
169
|
return {
|
|
106
170
|
...this,
|
|
107
|
-
content: Object.keys(content).map((type) => ({
|
|
171
|
+
content: await Promise.all(Object.keys(content).map(async (type) => ({
|
|
108
172
|
body: convertToXmlIfNecessary(type, content[type]?.examples
|
|
109
173
|
? oneOf(Object.values(content[type]?.examples ?? []).map((example) => example.value))
|
|
110
|
-
:
|
|
174
|
+
: await generate((content[type]?.schema ?? {
|
|
175
|
+
type: "object",
|
|
176
|
+
}), generateOptions), content[type]?.schema),
|
|
111
177
|
type,
|
|
112
|
-
})),
|
|
178
|
+
}))),
|
|
113
179
|
headers: {
|
|
114
180
|
...generatedHeaders,
|
|
115
181
|
...this.headers,
|
|
116
182
|
},
|
|
117
183
|
};
|
|
118
184
|
},
|
|
119
|
-
randomLegacy() {
|
|
185
|
+
async randomLegacy() {
|
|
120
186
|
const response = operation.responses[this.status ?? "default"] ??
|
|
121
187
|
operation.responses.default;
|
|
122
188
|
if (response === undefined) {
|
|
@@ -124,7 +190,7 @@ export function createResponseBuilder(operation, config) {
|
|
|
124
190
|
}
|
|
125
191
|
const body = response.examples
|
|
126
192
|
? oneOf(response.examples)
|
|
127
|
-
:
|
|
193
|
+
: await generate((response.schema ?? { type: "object" }), DEFAULT_GENERATE_OPTIONS);
|
|
128
194
|
return {
|
|
129
195
|
...this,
|
|
130
196
|
content: operation.produces?.map((type) => ({
|
package/dist/server/tools.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
JSONSchemaFaker.option("useExamplesValue", true);
|
|
3
|
-
JSONSchemaFaker.option("fillProperties", false);
|
|
1
|
+
import { generate } from "json-schema-faker";
|
|
4
2
|
export class Tools {
|
|
5
3
|
headers;
|
|
6
4
|
constructor({ headers = {}, } = {}) {
|
|
@@ -22,6 +20,6 @@ export class Tools {
|
|
|
22
20
|
});
|
|
23
21
|
}
|
|
24
22
|
randomFromSchema(schema) {
|
|
25
|
-
return
|
|
23
|
+
return generate(schema, { useExamplesValue: true, fillProperties: false });
|
|
26
24
|
}
|
|
27
25
|
}
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
uncachedRequire: function uncachedRequire(moduleName) {
|
|
5
|
-
|
|
5
|
+
try {
|
|
6
|
+
delete require.cache[require.resolve(moduleName)];
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
// eslint-disable-next-line security/detect-non-literal-require
|
|
9
|
+
return Promise.resolve(require(moduleName));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return Promise.reject(error);
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
};
|
|
@@ -6,6 +6,56 @@ import { ResponsesTypeCoder } from "./responses-type-coder.js";
|
|
|
6
6
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
7
7
|
import { TypeCoder } from "./type-coder.js";
|
|
8
8
|
import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
|
|
9
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
|
|
10
|
+
const RESERVED_WORDS = new Set([
|
|
11
|
+
"break",
|
|
12
|
+
"case",
|
|
13
|
+
"catch",
|
|
14
|
+
"class",
|
|
15
|
+
"const",
|
|
16
|
+
"continue",
|
|
17
|
+
"debugger",
|
|
18
|
+
"default",
|
|
19
|
+
"delete",
|
|
20
|
+
"do",
|
|
21
|
+
"else",
|
|
22
|
+
"export",
|
|
23
|
+
"extends",
|
|
24
|
+
"false",
|
|
25
|
+
"finally",
|
|
26
|
+
"for",
|
|
27
|
+
"function",
|
|
28
|
+
"if",
|
|
29
|
+
"import",
|
|
30
|
+
"in",
|
|
31
|
+
"instanceof",
|
|
32
|
+
"new",
|
|
33
|
+
"null",
|
|
34
|
+
"return",
|
|
35
|
+
"static",
|
|
36
|
+
"super",
|
|
37
|
+
"switch",
|
|
38
|
+
"this",
|
|
39
|
+
"throw",
|
|
40
|
+
"true",
|
|
41
|
+
"try",
|
|
42
|
+
"typeof",
|
|
43
|
+
"var",
|
|
44
|
+
"void",
|
|
45
|
+
"while",
|
|
46
|
+
"with",
|
|
47
|
+
"yield",
|
|
48
|
+
"await",
|
|
49
|
+
"enum",
|
|
50
|
+
"implements",
|
|
51
|
+
"interface",
|
|
52
|
+
"let",
|
|
53
|
+
"package",
|
|
54
|
+
"private",
|
|
55
|
+
"protected",
|
|
56
|
+
"public",
|
|
57
|
+
"type",
|
|
58
|
+
]);
|
|
9
59
|
function sanitizeIdentifier(value) {
|
|
10
60
|
// Treat any run of non-identifier characters as a camelCase separator
|
|
11
61
|
let result = value.replaceAll(/[^\w$]+(?<next>.)/gu, (_, char) => char.toUpperCase());
|
|
@@ -15,6 +65,10 @@ function sanitizeIdentifier(value) {
|
|
|
15
65
|
if (/^\d/u.test(result)) {
|
|
16
66
|
result = `_${result}`;
|
|
17
67
|
}
|
|
68
|
+
// If the identifier is a reserved word, append an underscore
|
|
69
|
+
if (RESERVED_WORDS.has(result)) {
|
|
70
|
+
result = `${result}_`;
|
|
71
|
+
}
|
|
18
72
|
return result || "_";
|
|
19
73
|
}
|
|
20
74
|
export class OperationTypeCoder extends TypeCoder {
|
|
@@ -56,7 +110,7 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
56
110
|
return response.get("content").map((content, contentType) => `{
|
|
57
111
|
status: ${status},
|
|
58
112
|
contentType?: "${contentType}",
|
|
59
|
-
body?: ${new SchemaTypeCoder(content.get("schema")).write(script)}
|
|
113
|
+
body?: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
|
|
60
114
|
}`);
|
|
61
115
|
}
|
|
62
116
|
if (response.has("schema")) {
|
|
@@ -102,6 +156,7 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
102
156
|
const queryType = new ParametersTypeCoder(parameters, "query").write(script);
|
|
103
157
|
const pathType = new ParametersTypeCoder(parameters, "path").write(script);
|
|
104
158
|
const headersType = new ParametersTypeCoder(parameters, "header").write(script);
|
|
159
|
+
const cookieType = new ParametersTypeCoder(parameters, "cookie").write(script);
|
|
105
160
|
const bodyRequirement = this.requirement.get("consumes") ||
|
|
106
161
|
this.requirement.specification?.rootRequirement?.get("consumes")
|
|
107
162
|
? parameters
|
|
@@ -121,6 +176,7 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
121
176
|
const queryTypeName = this.exportParameterType(script, "query", queryType, baseName, modulePath);
|
|
122
177
|
const pathTypeName = this.exportParameterType(script, "path", pathType, baseName, modulePath);
|
|
123
178
|
const headersTypeName = this.exportParameterType(script, "headers", headersType, baseName, modulePath);
|
|
124
|
-
|
|
179
|
+
const cookieTypeName = this.exportParameterType(script, "cookie", cookieType, baseName, modulePath);
|
|
180
|
+
return `($: OmitValueWhenNever<{ query: ${queryTypeName}, path: ${pathTypeName}, headers: ${headersTypeName}, cookie: ${cookieTypeName}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, x: ${xType}, proxy: ${proxyType}, user: ${this.userType()}, delay: ${delayType} }>) => MaybePromise<${this.responseTypes(script)} | { status: 415, contentType: "text/plain", body: string } | COUNTERFACT_RESPONSE | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE }>`;
|
|
125
181
|
}
|
|
126
182
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable n/no-sync */
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import nodePath, { dirname } from "node:path";
|
|
@@ -38,6 +37,7 @@ export class Repository {
|
|
|
38
37
|
if (!existsSync(sourcePath)) {
|
|
39
38
|
return false;
|
|
40
39
|
}
|
|
40
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
41
41
|
return fs.cp(sourcePath, destinationPath, { recursive: true });
|
|
42
42
|
}
|
|
43
43
|
async writeFiles(destination, { routes, types }) {
|
|
@@ -25,7 +25,7 @@ export class Requirement {
|
|
|
25
25
|
}
|
|
26
26
|
return new Requirement(this.data[item], `${this.url}/${this.escapeJsonPointer(item)}`, this.specification);
|
|
27
27
|
}
|
|
28
|
-
select(path
|
|
28
|
+
select(path) {
|
|
29
29
|
const parts = path
|
|
30
30
|
.split("/")
|
|
31
31
|
.map(this.unescapeJsonPointer)
|
|
@@ -14,7 +14,7 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
14
14
|
return response.get("content").map((content, mediaType) => [
|
|
15
15
|
mediaType,
|
|
16
16
|
`{
|
|
17
|
-
schema: ${new SchemaTypeCoder(content.get("schema")).write(script)}
|
|
17
|
+
schema: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
|
|
18
18
|
}`,
|
|
19
19
|
]);
|
|
20
20
|
}
|
|
@@ -80,13 +80,16 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
80
80
|
}
|
|
81
81
|
writeCode(script) {
|
|
82
82
|
// script.comments = READ_ONLY_COMMENTS;
|
|
83
|
-
const { allOf, anyOf, oneOf, type } = this.requirement.data;
|
|
83
|
+
const { allOf, anyOf, oneOf, type, format } = this.requirement.data;
|
|
84
84
|
if (allOf ?? anyOf ?? oneOf) {
|
|
85
85
|
return this.writeGroup(script, { allOf, anyOf, oneOf });
|
|
86
86
|
}
|
|
87
87
|
if (this.requirement.has("enum")) {
|
|
88
88
|
return this.writeEnum(script, this.requirement.get("enum"));
|
|
89
89
|
}
|
|
90
|
+
if ((type === "string" && format === "binary") || type === "file") {
|
|
91
|
+
return "Uint8Array | string";
|
|
92
|
+
}
|
|
90
93
|
return this.writeType(script, type);
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import nodePath from "node:path";
|
|
2
2
|
import createDebugger from "debug";
|
|
3
|
-
import
|
|
3
|
+
import { format } from "prettier";
|
|
4
|
+
import { escapePathForWindows } from "../util/windows-escape.js";
|
|
4
5
|
const debug = createDebugger("counterfact:typescript-generator:script");
|
|
5
6
|
export class Script {
|
|
6
7
|
constructor(repository, path) {
|
|
@@ -117,9 +118,9 @@ export class Script {
|
|
|
117
118
|
}
|
|
118
119
|
importStatements() {
|
|
119
120
|
return Array.from(this.imports, ([name, { isDefault, isType, script }]) => {
|
|
120
|
-
const resolvedPath = nodePath
|
|
121
|
+
const resolvedPath = escapePathForWindows(nodePath
|
|
121
122
|
.relative(nodePath.dirname(this.path).replaceAll("\\", "/"), script.path.replace(/\.ts$/u, ".js"))
|
|
122
|
-
.replaceAll("\\", "/");
|
|
123
|
+
.replaceAll("\\", "/"));
|
|
123
124
|
return `import${isType ? " type" : ""} ${isDefault ? name : `{ ${name} }`} from "${resolvedPath.includes("../") ? "" : "./"}${resolvedPath}";`;
|
|
124
125
|
});
|
|
125
126
|
}
|
|
@@ -137,7 +138,7 @@ export class Script {
|
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
contents() {
|
|
140
|
-
return
|
|
141
|
+
return format([
|
|
141
142
|
this.comments.map((comment) => `// ${comment}`).join("\n"),
|
|
142
143
|
this.comments.length > 0 ? "\n\n" : "",
|
|
143
144
|
this.externalImportStatements().join("\n"),
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/app.js",
|
|
7
|
-
"exports":
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/app.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
8
12
|
"types": "./dist/server/types.d.ts",
|
|
9
13
|
"typesVersions": {
|
|
10
14
|
"*": {
|
|
@@ -49,7 +53,7 @@
|
|
|
49
53
|
"swagger-tools"
|
|
50
54
|
],
|
|
51
55
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
56
|
+
"node": ">=22"
|
|
53
57
|
},
|
|
54
58
|
"bin": {
|
|
55
59
|
"counterfact": "./bin/counterfact.js"
|
|
@@ -61,14 +65,14 @@
|
|
|
61
65
|
"sideEffects": false,
|
|
62
66
|
"scripts": {
|
|
63
67
|
"test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box",
|
|
64
|
-
"test:black-box": "rimraf dist &&
|
|
65
|
-
"test:mutants": "stryker run stryker.config.json",
|
|
68
|
+
"test:black-box": "rimraf dist && yarn build && python3 -m pytest test-black-box/ -v",
|
|
66
69
|
"test:tsd": "tsd --typings ./dist/server/counterfact-types/index.ts --files ./test/**/*.test-d.ts",
|
|
67
70
|
"build": "rm -rf dist && tsc && copyfiles -f \"src/client/**\" dist/client && copyfiles -f \"src/counterfact-types/*.ts\" dist/server/counterfact-types && copyfiles -f \"src/server/*.cjs\" dist/server",
|
|
68
71
|
"prepack": "yarn build",
|
|
69
72
|
"release": "npx changeset publish",
|
|
70
73
|
"prepare": "husky install",
|
|
71
74
|
"lint": "eslint . --plugin file-progress --rule 'file-progress/activate: 1' --ignore-pattern dist --ignore-pattern out --ignore-pattern coverage",
|
|
75
|
+
"lint:fix": "eslint --fix . --ignore-pattern dist --ignore-pattern out --ignore-pattern coverage",
|
|
72
76
|
"lint:quickfix": "eslint --fix . eslint --fix demo-ts --rule=\"import/namespace: 0,etc/no-deprecated:0,import/no-cycle:0,no-explicit-type-exports/no-explicit-type-exports:0,import/no-deprecated:0,import/no-self-import:0,import/default:0,import/no-named-as-default:0\" --ignore-pattern dist --ignore-pattern out",
|
|
73
77
|
"go:petstore": "yarn build && yarn counterfact https://petstore3.swagger.io/api/v3/openapi.json out",
|
|
74
78
|
"go:petstore2": "yarn build && yarn counterfact https://petstore.swagger.io/v2/swagger.json out",
|
|
@@ -78,10 +82,9 @@
|
|
|
78
82
|
},
|
|
79
83
|
"devDependencies": {
|
|
80
84
|
"@changesets/cli": "2.30.0",
|
|
81
|
-
"@
|
|
82
|
-
"@
|
|
83
|
-
"@
|
|
84
|
-
"@swc/core": "1.15.18",
|
|
85
|
+
"@eslint/js": "10.0.1",
|
|
86
|
+
"@jest/globals": "^30.3.0",
|
|
87
|
+
"@swc/core": "1.15.21",
|
|
85
88
|
"@swc/jest": "0.2.39",
|
|
86
89
|
"@testing-library/dom": "10.4.1",
|
|
87
90
|
"@types/debug": "^4.1.12",
|
|
@@ -92,16 +95,17 @@
|
|
|
92
95
|
"@types/koa-proxy": "1.0.8",
|
|
93
96
|
"@types/koa-static": "4.0.4",
|
|
94
97
|
"@types/lodash": "4.17.24",
|
|
98
|
+
"@types/node": "22",
|
|
95
99
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
96
100
|
"@typescript-eslint/parser": "^8.53.0",
|
|
97
101
|
"copyfiles": "2.4.1",
|
|
98
|
-
"eslint": "
|
|
102
|
+
"eslint": "10.1.0",
|
|
99
103
|
"eslint-formatter-github-annotations": "0.1.0",
|
|
100
104
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
101
105
|
"eslint-plugin-etc": "2.0.3",
|
|
102
|
-
"eslint-plugin-file-progress": "
|
|
106
|
+
"eslint-plugin-file-progress": "4.0.0",
|
|
103
107
|
"eslint-plugin-import": "2.32.0",
|
|
104
|
-
"eslint-plugin-jest": "29.15.
|
|
108
|
+
"eslint-plugin-jest": "29.15.1",
|
|
105
109
|
"eslint-plugin-jest-dom": "5.5.0",
|
|
106
110
|
"eslint-plugin-n": "^17.24.0",
|
|
107
111
|
"eslint-plugin-no-explicit-type-exports": "0.12.1",
|
|
@@ -115,7 +119,6 @@
|
|
|
115
119
|
"jest-retries": "1.0.1",
|
|
116
120
|
"node-mocks-http": "1.17.2",
|
|
117
121
|
"rimraf": "6.1.3",
|
|
118
|
-
"stryker-cli": "1.1.0",
|
|
119
122
|
"supertest": "7.2.2",
|
|
120
123
|
"tsd": "0.33.0",
|
|
121
124
|
"using-temporary-files": "2.2.1"
|
|
@@ -129,25 +132,26 @@
|
|
|
129
132
|
"debug": "4.4.3",
|
|
130
133
|
"fetch": "1.1.0",
|
|
131
134
|
"fs-extra": "11.3.4",
|
|
132
|
-
"handlebars": "4.7.
|
|
135
|
+
"handlebars": "4.7.9",
|
|
133
136
|
"http-terminator": "3.2.0",
|
|
134
137
|
"js-yaml": "4.1.1",
|
|
135
|
-
"json-schema-faker": "0.
|
|
138
|
+
"json-schema-faker": "0.6.0",
|
|
136
139
|
"jsonwebtoken": "9.0.3",
|
|
137
|
-
"koa": "3.
|
|
140
|
+
"koa": "3.2.0",
|
|
138
141
|
"koa-bodyparser": "4.4.1",
|
|
139
142
|
"koa-proxies": "0.12.4",
|
|
140
143
|
"koa2-swagger-ui": "5.12.0",
|
|
141
|
-
"lodash": "4.
|
|
144
|
+
"lodash": "4.18.1",
|
|
142
145
|
"node-fetch": "3.3.2",
|
|
143
146
|
"open": "11.0.0",
|
|
144
147
|
"patch-package": "8.0.1",
|
|
145
148
|
"precinct": "12.2.0",
|
|
146
149
|
"prettier": "3.8.1",
|
|
147
|
-
"typescript": "
|
|
150
|
+
"typescript": "6.0.2"
|
|
148
151
|
},
|
|
149
152
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
|
150
153
|
"resolutions": {
|
|
151
|
-
"js-yaml": "4.1.1"
|
|
154
|
+
"js-yaml": "4.1.1",
|
|
155
|
+
"@typescript-eslint/utils": "^8.58.0"
|
|
152
156
|
}
|
|
153
157
|
}
|
|
File without changes
|
|
File without changes
|