counterfact 2.4.0 → 2.5.1
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 +63 -0
- package/dist/migrate/update-route-types.js +18 -20
- package/dist/repl/{RawHttpClient.js → raw-http-client.js} +7 -1
- package/dist/repl/repl.js +1 -1
- package/dist/server/counterfact-types/index.ts +1 -1
- package/dist/server/koa-middleware.js +3 -3
- package/dist/server/module-loader.js +33 -2
- package/dist/server/uncached-require.cjs +7 -3
- package/dist/typescript-generator/coder.js +5 -4
- package/dist/typescript-generator/generate.js +3 -3
- package/dist/typescript-generator/operation-coder.js +8 -5
- package/dist/typescript-generator/operation-type-coder.js +72 -12
- package/dist/typescript-generator/parameter-export-type-coder.js +4 -1
- package/dist/typescript-generator/parameters-type-coder.js +1 -0
- package/dist/typescript-generator/prune.js +9 -10
- package/dist/typescript-generator/repository.js +1 -1
- package/dist/typescript-generator/requirement.js +10 -5
- package/dist/typescript-generator/response-type-coder.js +11 -6
- package/dist/typescript-generator/responses-type-coder.js +1 -0
- package/dist/typescript-generator/schema-coder.js +5 -5
- package/dist/typescript-generator/schema-type-coder.js +16 -11
- package/dist/typescript-generator/script.js +17 -5
- package/dist/typescript-generator/specification.js +7 -4
- package/dist/util/ensure-directory-exists.js +1 -1
- package/package.json +4 -8
- /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) {
|
|
@@ -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`
|
|
@@ -226,6 +285,7 @@ async function main(source, destination) {
|
|
|
226
285
|
String.raw` |___ [__] |__| |\| | |=== |--< |--- |--| |___ | `,
|
|
227
286
|
" " + padTagLine(taglines[Math.floor(Math.random() * taglines.length)]),
|
|
228
287
|
"",
|
|
288
|
+
` Version ${CURRENT_VERSION}`,
|
|
229
289
|
` API Base URL ${url}`,
|
|
230
290
|
source === "_" ? undefined : ` Swagger UI ${swaggerUrl}`,
|
|
231
291
|
"",
|
|
@@ -254,6 +314,8 @@ async function main(source, destination) {
|
|
|
254
314
|
await start(config);
|
|
255
315
|
debug("started server");
|
|
256
316
|
|
|
317
|
+
await updateCheckPromise;
|
|
318
|
+
|
|
257
319
|
if (config.startRepl) {
|
|
258
320
|
startRepl();
|
|
259
321
|
}
|
|
@@ -346,5 +408,6 @@ program
|
|
|
346
408
|
"--spec <string>",
|
|
347
409
|
"path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)",
|
|
348
410
|
)
|
|
411
|
+
.option("--no-update-check", "disable the npm update check on startup")
|
|
349
412
|
.action(main)
|
|
350
413
|
.parse(process.argv);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import createDebug from "debug";
|
|
4
|
-
import { OperationTypeCoder } from "../typescript-generator/operation-type-coder.js";
|
|
4
|
+
import { OperationTypeCoder, } from "../typescript-generator/operation-type-coder.js";
|
|
5
5
|
import { Specification } from "../typescript-generator/specification.js";
|
|
6
6
|
const debug = createDebug("counterfact:migrate:update-route-types");
|
|
7
7
|
const HTTP_METHODS = [
|
|
@@ -25,8 +25,8 @@ function openApiPathToFilePath(openApiPath) {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Builds a mapping of route file paths to their operation type names per method
|
|
28
|
-
* @param
|
|
29
|
-
* @returns
|
|
28
|
+
* @param specification - The OpenAPI specification
|
|
29
|
+
* @returns Map of filePath -> Map of method -> typeName
|
|
30
30
|
*/
|
|
31
31
|
async function buildTypeNameMapping(specification) {
|
|
32
32
|
debug("building type name mapping from specification");
|
|
@@ -68,8 +68,7 @@ async function buildTypeNameMapping(specification) {
|
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Checks if a route file needs migration by looking for old-style HTTP_ imports
|
|
71
|
-
* @param
|
|
72
|
-
* @returns {boolean}
|
|
71
|
+
* @param content - The file content
|
|
73
72
|
*/
|
|
74
73
|
function needsMigration(content) {
|
|
75
74
|
const methodAlternation = HTTP_METHODS.map((method) => method.toUpperCase()).join("|");
|
|
@@ -78,9 +77,9 @@ function needsMigration(content) {
|
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
79
|
* Updates a single route file with the correct type names
|
|
81
|
-
* @param
|
|
82
|
-
* @param
|
|
83
|
-
* @returns
|
|
80
|
+
* @param filePath - Absolute path to the route file
|
|
81
|
+
* @param methodToTypeName - Map of HTTP method to type name
|
|
82
|
+
* @returns True if file was updated
|
|
84
83
|
*/
|
|
85
84
|
async function updateRouteFile(filePath, methodToTypeName) {
|
|
86
85
|
debug("processing route file: %s", filePath);
|
|
@@ -97,7 +96,7 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
97
96
|
const importRegex = /import\s+type\s+\{(?<types>[^}]+)\}\s+from\s+["'][^"']+["'];?/gu;
|
|
98
97
|
let importMatch;
|
|
99
98
|
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
100
|
-
const importedTypes = importMatch.groups
|
|
99
|
+
const importedTypes = (importMatch.groups?.["types"] ?? "")
|
|
101
100
|
.split(",")
|
|
102
101
|
.map((t) => t.trim())
|
|
103
102
|
.filter((t) => t.length > 0);
|
|
@@ -105,7 +104,7 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
105
104
|
// Check if this is an HTTP_ type
|
|
106
105
|
const httpMethodMatch = importedType.match(new RegExp(`^HTTP_(?<method>${HTTP_METHODS.join("|")})$`, "u"));
|
|
107
106
|
if (httpMethodMatch) {
|
|
108
|
-
const method = httpMethodMatch.groups
|
|
107
|
+
const method = httpMethodMatch.groups?.["method"] ?? "";
|
|
109
108
|
const newTypeName = methodToTypeName.get(method);
|
|
110
109
|
if (newTypeName && newTypeName !== importedType) {
|
|
111
110
|
replacements.set(importedType, newTypeName);
|
|
@@ -127,7 +126,7 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
127
126
|
// Match the method from the old type name
|
|
128
127
|
const methodMatch = oldName.match(new RegExp(`^HTTP_(?<method>${HTTP_METHODS.join("|")})$`, "u"));
|
|
129
128
|
if (methodMatch) {
|
|
130
|
-
const method = methodMatch.groups
|
|
129
|
+
const method = methodMatch.groups?.["method"] ?? "";
|
|
131
130
|
const exportPattern = new RegExp(`(export\\s+const\\s+${method}\\s*:\\s*)${oldName}(\\b)`, "g");
|
|
132
131
|
content = content.replace(exportPattern, `$1${newName}$2`);
|
|
133
132
|
}
|
|
@@ -141,10 +140,10 @@ async function updateRouteFile(filePath, methodToTypeName) {
|
|
|
141
140
|
}
|
|
142
141
|
/**
|
|
143
142
|
* Recursively processes route files in a directory
|
|
144
|
-
* @param
|
|
145
|
-
* @param
|
|
146
|
-
* @param
|
|
147
|
-
* @returns
|
|
143
|
+
* @param routesDir - Path to routes directory
|
|
144
|
+
* @param currentPath - Current subdirectory being processed
|
|
145
|
+
* @param mapping - Type name mapping
|
|
146
|
+
* @returns Number of files updated
|
|
148
147
|
*/
|
|
149
148
|
async function processRouteDirectory(routesDir, currentPath, mapping) {
|
|
150
149
|
let updatedCount = 0;
|
|
@@ -183,8 +182,7 @@ async function processRouteDirectory(routesDir, currentPath, mapping) {
|
|
|
183
182
|
}
|
|
184
183
|
/**
|
|
185
184
|
* Checks if any route files need migration
|
|
186
|
-
* @param
|
|
187
|
-
* @returns {Promise<boolean>}
|
|
185
|
+
* @param routesDir - Path to routes directory
|
|
188
186
|
*/
|
|
189
187
|
async function checkIfMigrationNeeded(routesDir) {
|
|
190
188
|
try {
|
|
@@ -213,9 +211,9 @@ async function checkIfMigrationNeeded(routesDir) {
|
|
|
213
211
|
}
|
|
214
212
|
/**
|
|
215
213
|
* Main migration function - updates route type imports to use new naming convention
|
|
216
|
-
* @param
|
|
217
|
-
* @param
|
|
218
|
-
* @returns
|
|
214
|
+
* @param basePath - Base path where routes and types are located
|
|
215
|
+
* @param openApiPath - Path or URL to OpenAPI specification
|
|
216
|
+
* @returns True if migration was performed
|
|
219
217
|
*/
|
|
220
218
|
export async function updateRouteTypes(basePath, openApiPath) {
|
|
221
219
|
debug("starting route type migration for base path: %s", basePath);
|
|
@@ -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
|
@@ -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;
|
|
@@ -55,6 +55,7 @@ export class ModuleLoader extends EventTarget {
|
|
|
55
55
|
if (eventName === "unlink") {
|
|
56
56
|
this.registry.remove(url);
|
|
57
57
|
this.dispatchEvent(new Event("remove"));
|
|
58
|
+
return;
|
|
58
59
|
}
|
|
59
60
|
const dependencies = this.dependencyGraph.dependentsOf(pathName);
|
|
60
61
|
void this.loadEndpoint(pathName);
|
|
@@ -102,9 +103,39 @@ export class ModuleLoader extends EventTarget {
|
|
|
102
103
|
const doImport = (await determineModuleKind(pathName)) === "commonjs"
|
|
103
104
|
? uncachedRequire
|
|
104
105
|
: uncachedImport;
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
let importError;
|
|
107
|
+
const endpoint = (await doImport(pathName).catch((error) => {
|
|
108
|
+
importError = error;
|
|
107
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
|
+
}
|
|
108
139
|
this.dispatchEvent(new Event("add"));
|
|
109
140
|
if (basename(pathName).startsWith("_.context.") &&
|
|
110
141
|
isContextModule(endpoint)) {
|
|
@@ -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
|
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export class Coder {
|
|
2
|
+
requirement;
|
|
2
3
|
constructor(requirement) {
|
|
3
4
|
this.requirement = requirement;
|
|
4
5
|
}
|
|
5
6
|
get id() {
|
|
6
7
|
if (this.requirement.isReference) {
|
|
7
|
-
return `${this.constructor.name}@${this.requirement
|
|
8
|
+
return `${this.constructor.name}@${this.requirement.data["$ref"]}`;
|
|
8
9
|
}
|
|
9
10
|
return `${this.constructor.name}@${this.requirement.url}`;
|
|
10
11
|
}
|
|
11
|
-
beforeExport() {
|
|
12
|
+
beforeExport(_path) {
|
|
12
13
|
return "";
|
|
13
14
|
}
|
|
14
15
|
write(script) {
|
|
@@ -17,7 +18,7 @@ export class Coder {
|
|
|
17
18
|
}
|
|
18
19
|
return this.writeCode(script);
|
|
19
20
|
}
|
|
20
|
-
writeCode() {
|
|
21
|
+
writeCode(_script) {
|
|
21
22
|
throw new Error("write() is abstract and should be overwritten by a subclass");
|
|
22
23
|
}
|
|
23
24
|
async delegate() {
|
|
@@ -39,7 +40,7 @@ export class Coder {
|
|
|
39
40
|
yield name + index;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
typeDeclaration() {
|
|
43
|
+
typeDeclaration(_namespace, _script) {
|
|
43
44
|
return "";
|
|
44
45
|
}
|
|
45
46
|
modulePath() {
|
|
@@ -27,7 +27,7 @@ async function getPathsFromSpecification(specification) {
|
|
|
27
27
|
}
|
|
28
28
|
catch (error) {
|
|
29
29
|
process.stderr.write(`Could not find #/paths in the specification.\n${error}\n`);
|
|
30
|
-
return
|
|
30
|
+
return undefined;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
export async function generate(source, destination, generateOptions, repository = new Repository()) {
|
|
@@ -40,10 +40,10 @@ export async function generate(source, destination, generateOptions, repository
|
|
|
40
40
|
debug("created specification: $o", specification);
|
|
41
41
|
debug("reading the #/paths from the specification");
|
|
42
42
|
const paths = await getPathsFromSpecification(specification);
|
|
43
|
-
debug("got %i paths", paths
|
|
43
|
+
debug("got %i paths", paths?.map?.length ?? 0);
|
|
44
44
|
if (generateOptions.prune && generateOptions.routes) {
|
|
45
45
|
debug("pruning defunct route files");
|
|
46
|
-
await pruneRoutes(destination, paths.
|
|
46
|
+
await pruneRoutes(destination, paths.map((_v, key) => key));
|
|
47
47
|
debug("done pruning");
|
|
48
48
|
}
|
|
49
49
|
const securityRequirement = specification.getRequirement("#/components/securitySchemes");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import nodePath from "node:path";
|
|
2
2
|
import { Coder } from "./coder.js";
|
|
3
|
-
import { OperationTypeCoder } from "./operation-type-coder.js";
|
|
3
|
+
import { OperationTypeCoder, } from "./operation-type-coder.js";
|
|
4
4
|
export class OperationCoder extends Coder {
|
|
5
|
-
|
|
5
|
+
requestMethod;
|
|
6
|
+
securitySchemes;
|
|
7
|
+
constructor(requirement, requestMethod, securitySchemes = []) {
|
|
6
8
|
super(requirement);
|
|
7
9
|
if (requestMethod === undefined) {
|
|
8
10
|
throw new Error("requestMethod is required");
|
|
@@ -15,9 +17,10 @@ export class OperationCoder extends Coder {
|
|
|
15
17
|
}
|
|
16
18
|
write() {
|
|
17
19
|
const responses = this.requirement.get("responses");
|
|
18
|
-
const [firstStatusCode] = responses.map((
|
|
20
|
+
const [firstStatusCode] = responses.map((_response, statusCode) => statusCode);
|
|
19
21
|
const [firstResponse] = responses.map((response) => response.data);
|
|
20
|
-
if (
|
|
22
|
+
if (firstResponse === undefined ||
|
|
23
|
+
!("content" in firstResponse || "schema" in firstResponse)) {
|
|
21
24
|
return `async ($) => {
|
|
22
25
|
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}];
|
|
23
26
|
}`;
|
|
@@ -26,7 +29,7 @@ export class OperationCoder extends Coder {
|
|
|
26
29
|
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].random();
|
|
27
30
|
}`;
|
|
28
31
|
}
|
|
29
|
-
typeDeclaration(
|
|
32
|
+
typeDeclaration(_namespace, script) {
|
|
30
33
|
const operationTypeCoder = new OperationTypeCoder(this.requirement, this.requestMethod, this.securitySchemes);
|
|
31
34
|
return script.importType(operationTypeCoder);
|
|
32
35
|
}
|
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
import nodePath from "node:path";
|
|
2
2
|
import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
|
|
3
|
+
import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
|
|
3
4
|
import { ParametersTypeCoder } from "./parameters-type-coder.js";
|
|
4
5
|
import { READ_ONLY_COMMENTS } from "./read-only-comments.js";
|
|
5
6
|
import { ResponsesTypeCoder } from "./responses-type-coder.js";
|
|
6
7
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
7
8
|
import { TypeCoder } from "./type-coder.js";
|
|
8
|
-
|
|
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,9 +65,15 @@ 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 {
|
|
75
|
+
requestMethod;
|
|
76
|
+
securitySchemes;
|
|
21
77
|
constructor(requirement, requestMethod, securitySchemes = []) {
|
|
22
78
|
super(requirement);
|
|
23
79
|
if (requestMethod === undefined) {
|
|
@@ -56,19 +112,22 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
56
112
|
return response.get("content").map((content, contentType) => `{
|
|
57
113
|
status: ${status},
|
|
58
114
|
contentType?: "${contentType}",
|
|
59
|
-
body?: ${new SchemaTypeCoder(content.get("schema")).write(script)}
|
|
115
|
+
body?: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
|
|
60
116
|
}`);
|
|
61
117
|
}
|
|
62
118
|
if (response.has("schema")) {
|
|
63
|
-
const
|
|
64
|
-
this.requirement.specification
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
const producesReq = this.requirement?.get("produces") ??
|
|
120
|
+
this.requirement.specification?.rootRequirement?.get("produces");
|
|
121
|
+
const produces = producesReq?.data;
|
|
122
|
+
if (produces) {
|
|
123
|
+
return produces
|
|
124
|
+
.map((contentType) => `{
|
|
67
125
|
status: ${status},
|
|
68
126
|
contentType?: "${contentType}",
|
|
69
127
|
body?: ${new SchemaTypeCoder(response.get("schema")).write(script)}
|
|
70
128
|
}`)
|
|
71
|
-
|
|
129
|
+
.join(" | ");
|
|
130
|
+
}
|
|
72
131
|
}
|
|
73
132
|
return `{
|
|
74
133
|
status: ${status}
|
|
@@ -103,17 +162,18 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
103
162
|
const pathType = new ParametersTypeCoder(parameters, "path").write(script);
|
|
104
163
|
const headersType = new ParametersTypeCoder(parameters, "header").write(script);
|
|
105
164
|
const cookieType = new ParametersTypeCoder(parameters, "cookie").write(script);
|
|
106
|
-
const bodyRequirement = this.requirement.get("consumes")
|
|
107
|
-
this.requirement.specification?.rootRequirement?.get("consumes")
|
|
165
|
+
const bodyRequirement = (this.requirement.get("consumes") ??
|
|
166
|
+
this.requirement.specification?.rootRequirement?.get("consumes"))
|
|
108
167
|
? parameters
|
|
109
|
-
?.find((parameter) => ["body", "formData"].includes(parameter.get("in")
|
|
168
|
+
?.find((parameter) => ["body", "formData"].includes(parameter.get("in")?.data))
|
|
110
169
|
?.get("schema")
|
|
111
170
|
: this.requirement.select("requestBody/content/application~1json/schema");
|
|
112
171
|
const bodyType = bodyRequirement === undefined
|
|
113
172
|
? "never"
|
|
114
173
|
: new SchemaTypeCoder(bodyRequirement).write(script);
|
|
115
|
-
const responseType = new ResponsesTypeCoder(this.requirement.get("responses"), this.requirement.get("produces")?.data ??
|
|
116
|
-
this.requirement.specification?.rootRequirement?.get("produces")
|
|
174
|
+
const responseType = new ResponsesTypeCoder(this.requirement.get("responses"), (this.requirement.get("produces")?.data ??
|
|
175
|
+
this.requirement.specification?.rootRequirement?.get("produces")
|
|
176
|
+
?.data)).write(script);
|
|
117
177
|
const proxyType = "(url: string) => COUNTERFACT_RESPONSE";
|
|
118
178
|
const delayType = "(milliseconds: number, maxMilliseconds?: number) => Promise<void>";
|
|
119
179
|
// Get the base name for this operation and export parameter types
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { TypeCoder } from "./type-coder.js";
|
|
2
|
-
// Helper class for exporting parameter types
|
|
3
2
|
export class ParameterExportTypeCoder extends TypeCoder {
|
|
3
|
+
_typeName;
|
|
4
|
+
_typeCode;
|
|
5
|
+
_parameterKind;
|
|
6
|
+
_modulePath;
|
|
4
7
|
constructor(requirement, typeName, typeCode, parameterKind) {
|
|
5
8
|
super(requirement);
|
|
6
9
|
this._typeName = typeName;
|
|
@@ -2,6 +2,7 @@ import nodePath from "node:path";
|
|
|
2
2
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
3
3
|
import { TypeCoder } from "./type-coder.js";
|
|
4
4
|
export class ParametersTypeCoder extends TypeCoder {
|
|
5
|
+
placement;
|
|
5
6
|
constructor(requirement, placement) {
|
|
6
7
|
super(requirement);
|
|
7
8
|
this.placement = placement;
|
|
@@ -5,9 +5,9 @@ const debug = createDebug("counterfact:typescript-generator:prune");
|
|
|
5
5
|
/**
|
|
6
6
|
* Collects all .ts route files in a directory recursively.
|
|
7
7
|
* Context files (_.context.ts) are excluded.
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
10
|
-
* @returns
|
|
8
|
+
* @param routesDir - Path to routes directory
|
|
9
|
+
* @param currentPath - Current subdirectory being processed (relative to routesDir)
|
|
10
|
+
* @returns Array of relative paths (using forward slashes)
|
|
11
11
|
*/
|
|
12
12
|
async function collectRouteFiles(routesDir, currentPath = "") {
|
|
13
13
|
const files = [];
|
|
@@ -37,8 +37,8 @@ async function collectRouteFiles(routesDir, currentPath = "") {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Recursively removes empty directories under rootDir, but not rootDir itself.
|
|
40
|
-
* @param
|
|
41
|
-
* @param
|
|
40
|
+
* @param dir - Directory to check
|
|
41
|
+
* @param rootDir - Root directory that should never be removed
|
|
42
42
|
*/
|
|
43
43
|
async function removeEmptyDirectories(dir, rootDir) {
|
|
44
44
|
let entries;
|
|
@@ -65,8 +65,7 @@ async function removeEmptyDirectories(dir, rootDir) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Converts an OpenAPI path to the expected route file path (relative to routesDir).
|
|
67
67
|
* e.g. "/pet/{id}" -> "pet/{id}.ts", "/" -> "index.ts"
|
|
68
|
-
* @param
|
|
69
|
-
* @returns {string}
|
|
68
|
+
* @param openApiPath - The OpenAPI path string
|
|
70
69
|
*/
|
|
71
70
|
function openApiPathToRouteFile(openApiPath) {
|
|
72
71
|
const filePath = openApiPath === "/" ? "index" : openApiPath.slice(1);
|
|
@@ -75,9 +74,9 @@ function openApiPathToRouteFile(openApiPath) {
|
|
|
75
74
|
/**
|
|
76
75
|
* Prunes route files that no longer correspond to any path in the OpenAPI spec.
|
|
77
76
|
* Context files (_.context.ts) are never pruned.
|
|
78
|
-
* @param
|
|
79
|
-
* @param
|
|
80
|
-
* @returns
|
|
77
|
+
* @param destination - Base destination directory (contains the routes/ sub-directory)
|
|
78
|
+
* @param openApiPaths - Iterable of OpenAPI path strings (e.g. "/pet/{id}")
|
|
79
|
+
* @returns Number of files removed
|
|
81
80
|
*/
|
|
82
81
|
export async function pruneRoutes(destination, openApiPaths) {
|
|
83
82
|
const routesDir = nodePath.join(destination, "routes");
|
|
@@ -11,6 +11,7 @@ const debug = createDebug("counterfact:server:repository");
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url)).replaceAll("\\", "/");
|
|
12
12
|
debug("dirname is %s", __dirname);
|
|
13
13
|
export class Repository {
|
|
14
|
+
scripts;
|
|
14
15
|
constructor() {
|
|
15
16
|
this.scripts = new Map();
|
|
16
17
|
}
|
|
@@ -37,7 +38,6 @@ export class Repository {
|
|
|
37
38
|
if (!existsSync(sourcePath)) {
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
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 }) {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export class Requirement {
|
|
2
|
+
data;
|
|
3
|
+
url;
|
|
4
|
+
specification;
|
|
2
5
|
constructor(data, url = "", specification = undefined) {
|
|
3
6
|
this.data = data;
|
|
4
7
|
this.url = url;
|
|
5
8
|
this.specification = specification;
|
|
6
9
|
}
|
|
7
10
|
get isReference() {
|
|
8
|
-
return this.data
|
|
11
|
+
return this.data["$ref"] !== undefined;
|
|
9
12
|
}
|
|
10
13
|
reference() {
|
|
11
|
-
return this.specification.getRequirement(this.data
|
|
14
|
+
return this.specification.getRequirement(this.data["$ref"]);
|
|
12
15
|
}
|
|
13
16
|
has(item) {
|
|
14
17
|
if (this.isReference) {
|
|
@@ -20,19 +23,21 @@ export class Requirement {
|
|
|
20
23
|
if (this.isReference) {
|
|
21
24
|
return this.reference().get(item);
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
const key = String(item);
|
|
27
|
+
if (!this.has(key)) {
|
|
24
28
|
return undefined;
|
|
25
29
|
}
|
|
26
|
-
return new Requirement(this.data[
|
|
30
|
+
return new Requirement(this.data[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification);
|
|
27
31
|
}
|
|
28
32
|
select(path) {
|
|
29
33
|
const parts = path
|
|
30
34
|
.split("/")
|
|
31
|
-
.map(this.unescapeJsonPointer)
|
|
35
|
+
.map((p) => this.unescapeJsonPointer(p))
|
|
32
36
|
// Unescape URL encoded characters (e.g. %20 -> " ")
|
|
33
37
|
// Technically we should not be unescaping, but it came up in https://github.com/pmcelhaney/counterfact/issues/1083
|
|
34
38
|
// and I can't think of a reason anyone would intentionally put a % in a key name.
|
|
35
39
|
.map(unescape);
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
36
41
|
let result = this;
|
|
37
42
|
for (const part of parts) {
|
|
38
43
|
result = result.get(part);
|
|
@@ -2,19 +2,22 @@ import { printObject } from "./printers.js";
|
|
|
2
2
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
3
3
|
import { TypeCoder } from "./type-coder.js";
|
|
4
4
|
export class ResponseTypeCoder extends TypeCoder {
|
|
5
|
+
openApi2MediaTypes;
|
|
5
6
|
constructor(requirement, openApi2MediaTypes = []) {
|
|
6
7
|
super(requirement);
|
|
7
8
|
this.openApi2MediaTypes = openApi2MediaTypes;
|
|
8
9
|
}
|
|
9
10
|
names() {
|
|
10
|
-
return super.names(this.requirement.data
|
|
11
|
+
return super.names(this.requirement.data["$ref"].split("/").at(-1));
|
|
11
12
|
}
|
|
12
13
|
buildContentObjectType(script, response) {
|
|
13
14
|
if (response.has("content")) {
|
|
14
|
-
return response
|
|
15
|
+
return response
|
|
16
|
+
.get("content")
|
|
17
|
+
.map((content, mediaType) => [
|
|
15
18
|
mediaType,
|
|
16
19
|
`{
|
|
17
|
-
schema: ${new SchemaTypeCoder(content.get("schema")).write(script)}
|
|
20
|
+
schema: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
|
|
18
21
|
}`,
|
|
19
22
|
]);
|
|
20
23
|
}
|
|
@@ -46,8 +49,10 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
46
49
|
return printObject(this.buildHeaders(script, response));
|
|
47
50
|
}
|
|
48
51
|
printRequiredHeaders(response) {
|
|
49
|
-
const requiredHeaders = (response.get("headers")
|
|
50
|
-
|
|
52
|
+
const requiredHeaders = (response.get("headers")?.map((value, name) => ({
|
|
53
|
+
name,
|
|
54
|
+
required: value.data.required,
|
|
55
|
+
})) ?? [])
|
|
51
56
|
.filter(({ required }) => required)
|
|
52
57
|
.map(({ name }) => `"${name}"`);
|
|
53
58
|
return requiredHeaders.length === 0 ? "never" : requiredHeaders.join(" | ");
|
|
@@ -72,7 +77,7 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
72
77
|
return printObject(exampleNames.map((name) => [name, "unknown"]));
|
|
73
78
|
}
|
|
74
79
|
modulePath() {
|
|
75
|
-
return `types/${this.requirement.data
|
|
80
|
+
return `types/${this.requirement.data["$ref"]}.ts`;
|
|
76
81
|
}
|
|
77
82
|
writeCode(script) {
|
|
78
83
|
return `{
|
|
@@ -2,6 +2,7 @@ import { printObjectWithoutQuotes } from "./printers.js";
|
|
|
2
2
|
import { ResponseTypeCoder } from "./response-type-coder.js";
|
|
3
3
|
import { TypeCoder } from "./type-coder.js";
|
|
4
4
|
export class ResponsesTypeCoder extends TypeCoder {
|
|
5
|
+
openApi2MediaTypes;
|
|
5
6
|
constructor(requirement, openApi2MediaTypes = []) {
|
|
6
7
|
super(requirement);
|
|
7
8
|
this.openApi2MediaTypes = openApi2MediaTypes;
|
|
@@ -2,13 +2,13 @@ import { Coder } from "./coder.js";
|
|
|
2
2
|
function scrubSchema(schema) {
|
|
3
3
|
// remove properties that are not valid in JSON Schema 6 and not useful anyway
|
|
4
4
|
const cleaned = { ...schema };
|
|
5
|
-
delete cleaned
|
|
6
|
-
delete cleaned
|
|
5
|
+
delete cleaned["example"];
|
|
6
|
+
delete cleaned["xml"];
|
|
7
7
|
return cleaned;
|
|
8
8
|
}
|
|
9
9
|
export class SchemaCoder extends Coder {
|
|
10
10
|
names() {
|
|
11
|
-
return super.names(`${this.requirement.data
|
|
11
|
+
return super.names(`${this.requirement.data["$ref"].split("/").at(-1)}Schema`);
|
|
12
12
|
}
|
|
13
13
|
objectSchema(script) {
|
|
14
14
|
const { properties, required } = this.requirement.data;
|
|
@@ -30,11 +30,11 @@ export class SchemaCoder extends Coder {
|
|
|
30
30
|
items: ${new SchemaCoder(this.requirement.get("items")).write(script)}
|
|
31
31
|
}`;
|
|
32
32
|
}
|
|
33
|
-
typeDeclaration(
|
|
33
|
+
typeDeclaration(_namespace, script) {
|
|
34
34
|
return script.importExternalType("JSONSchema6", "json-schema");
|
|
35
35
|
}
|
|
36
36
|
modulePath() {
|
|
37
|
-
return `types/${this.requirement.data
|
|
37
|
+
return `types/${this.requirement.data["$ref"]}.ts`;
|
|
38
38
|
}
|
|
39
39
|
writeCode(script) {
|
|
40
40
|
const { type } = this.requirement.data;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TypeCoder } from "./type-coder.js";
|
|
2
2
|
export class SchemaTypeCoder extends TypeCoder {
|
|
3
3
|
names() {
|
|
4
|
-
return super.names(this.requirement.data
|
|
4
|
+
return super.names(this.requirement.data["$ref"]?.split("/").at(-1));
|
|
5
5
|
}
|
|
6
6
|
additionalPropertiesType(script) {
|
|
7
7
|
const { additionalProperties, properties } = this.requirement.data;
|
|
@@ -16,13 +16,15 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
16
16
|
}
|
|
17
17
|
objectSchema(script) {
|
|
18
18
|
const { data } = this.requirement;
|
|
19
|
-
const
|
|
19
|
+
const typedData = data;
|
|
20
|
+
const properties = Object.keys(typedData.properties ?? {}).map((name) => {
|
|
20
21
|
const property = this.requirement.get("properties").get(name);
|
|
21
|
-
const
|
|
22
|
+
const propertyData = property.data;
|
|
23
|
+
const isRequired = typedData.required?.includes(name) || propertyData.required === true;
|
|
22
24
|
const optionalFlag = isRequired ? "" : "?";
|
|
23
25
|
return `"${name}"${optionalFlag}: ${new SchemaTypeCoder(property).write(script)}`;
|
|
24
26
|
});
|
|
25
|
-
if (
|
|
27
|
+
if (typedData.additionalProperties) {
|
|
26
28
|
properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
|
|
27
29
|
}
|
|
28
30
|
return `{${properties.join(",")}}`;
|
|
@@ -37,11 +39,13 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
37
39
|
if (value === null) {
|
|
38
40
|
return "null";
|
|
39
41
|
}
|
|
40
|
-
return value;
|
|
42
|
+
return String(value);
|
|
41
43
|
}
|
|
42
44
|
writeType(script, type) {
|
|
43
45
|
if (Array.isArray(type)) {
|
|
44
|
-
return type
|
|
46
|
+
return type
|
|
47
|
+
.map((item) => this.writeType(script, item))
|
|
48
|
+
.join(" | ");
|
|
45
49
|
}
|
|
46
50
|
if (typeof type !== "string") {
|
|
47
51
|
return "unknown";
|
|
@@ -57,7 +61,7 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
57
61
|
}
|
|
58
62
|
return type ?? "unknown";
|
|
59
63
|
}
|
|
60
|
-
writeGroup(script, { allOf, anyOf, oneOf }) {
|
|
64
|
+
writeGroup(script, { allOf, anyOf, oneOf, }) {
|
|
61
65
|
function matchingKey() {
|
|
62
66
|
if (allOf) {
|
|
63
67
|
return "allOf";
|
|
@@ -67,19 +71,20 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
67
71
|
}
|
|
68
72
|
return "oneOf";
|
|
69
73
|
}
|
|
70
|
-
const
|
|
74
|
+
const key = matchingKey();
|
|
75
|
+
const items = (allOf ?? anyOf ?? oneOf);
|
|
76
|
+
const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index)).write(script));
|
|
71
77
|
return types.join(allOf ? " & " : " | ");
|
|
72
78
|
}
|
|
73
|
-
writeEnum(
|
|
79
|
+
writeEnum(_script, requirement) {
|
|
74
80
|
return requirement.data
|
|
75
81
|
.map((item) => this.writePrimitive(item))
|
|
76
82
|
.join(" | ");
|
|
77
83
|
}
|
|
78
84
|
modulePath() {
|
|
79
|
-
return `types/${this.requirement.data
|
|
85
|
+
return `types/${this.requirement.data["$ref"].replace(/^#\//u, "")}.ts`;
|
|
80
86
|
}
|
|
81
87
|
writeCode(script) {
|
|
82
|
-
// script.comments = READ_ONLY_COMMENTS;
|
|
83
88
|
const { allOf, anyOf, oneOf, type, format } = this.requirement.data;
|
|
84
89
|
if (allOf ?? anyOf ?? oneOf) {
|
|
85
90
|
return this.writeGroup(script, { allOf, anyOf, oneOf });
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import nodePath from "node:path";
|
|
2
2
|
import createDebugger from "debug";
|
|
3
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 {
|
|
7
|
+
repository;
|
|
8
|
+
comments;
|
|
9
|
+
exports;
|
|
10
|
+
imports;
|
|
11
|
+
externalImport;
|
|
12
|
+
cache;
|
|
13
|
+
typeCache;
|
|
14
|
+
path;
|
|
6
15
|
constructor(repository, path) {
|
|
7
16
|
this.repository = repository;
|
|
8
17
|
this.comments = [];
|
|
@@ -53,6 +62,7 @@ export class Script {
|
|
|
53
62
|
.catch((error) => {
|
|
54
63
|
exportStatement.code = `{/* error creating export "${name}" for ${this.path}: ${error.stack} */}`;
|
|
55
64
|
exportStatement.error = error;
|
|
65
|
+
return undefined;
|
|
56
66
|
})
|
|
57
67
|
.finally(() => {
|
|
58
68
|
exportStatement.done = true;
|
|
@@ -117,23 +127,25 @@ export class Script {
|
|
|
117
127
|
}
|
|
118
128
|
importStatements() {
|
|
119
129
|
return Array.from(this.imports, ([name, { isDefault, isType, script }]) => {
|
|
120
|
-
const resolvedPath = nodePath
|
|
130
|
+
const resolvedPath = escapePathForWindows(nodePath
|
|
121
131
|
.relative(nodePath.dirname(this.path).replaceAll("\\", "/"), script.path.replace(/\.ts$/u, ".js"))
|
|
122
|
-
.replaceAll("\\", "/");
|
|
132
|
+
.replaceAll("\\", "/"));
|
|
123
133
|
return `import${isType ? " type" : ""} ${isDefault ? name : `{ ${name} }`} from "${resolvedPath.includes("../") ? "" : "./"}${resolvedPath}";`;
|
|
124
134
|
});
|
|
125
135
|
}
|
|
126
136
|
exportStatements() {
|
|
127
137
|
return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, name, typeDeclaration }) => {
|
|
128
|
-
if (code
|
|
138
|
+
if (typeof code === "object" && code !== null && "raw" in code) {
|
|
129
139
|
return code.raw;
|
|
130
140
|
}
|
|
131
141
|
if (isDefault) {
|
|
132
142
|
return `${beforeExport}export default ${code};`;
|
|
133
143
|
}
|
|
134
144
|
const keyword = isType ? "type" : "const";
|
|
135
|
-
const typeAnnotation = typeDeclaration.length === 0
|
|
136
|
-
|
|
145
|
+
const typeAnnotation = (typeDeclaration ?? "").length === 0
|
|
146
|
+
? ""
|
|
147
|
+
: `:${typeDeclaration ?? ""}`;
|
|
148
|
+
return `${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
|
|
137
149
|
});
|
|
138
150
|
}
|
|
139
151
|
contents() {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { bundle } from "@apidevtools/json-schema-ref-parser";
|
|
1
2
|
import createDebug from "debug";
|
|
2
3
|
import { Requirement } from "./requirement.js";
|
|
3
|
-
import { bundle } from "@apidevtools/json-schema-ref-parser";
|
|
4
4
|
const debug = createDebug("counterfact:typescript-generator:specification");
|
|
5
5
|
export class Specification {
|
|
6
|
+
cache;
|
|
7
|
+
rootRequirement;
|
|
6
8
|
constructor(rootRequirement) {
|
|
7
9
|
this.cache = new Map();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
if (rootRequirement) {
|
|
11
|
+
this.rootRequirement = rootRequirement;
|
|
12
|
+
}
|
|
10
13
|
}
|
|
11
14
|
static async fromFile(urlOrPath) {
|
|
12
15
|
const specification = new Specification();
|
|
@@ -18,6 +21,6 @@ export class Specification {
|
|
|
18
21
|
return this.rootRequirement.select(url.slice(2));
|
|
19
22
|
}
|
|
20
23
|
async load(urlOrPath) {
|
|
21
|
-
this.rootRequirement = new Requirement(await bundle(urlOrPath), urlOrPath, this);
|
|
24
|
+
this.rootRequirement = new Requirement((await bundle(urlOrPath)), urlOrPath, this);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -3,7 +3,7 @@ import nodePath from "node:path";
|
|
|
3
3
|
export function ensureDirectoryExists(filePath) {
|
|
4
4
|
const directory = nodePath.dirname(filePath);
|
|
5
5
|
try {
|
|
6
|
-
fs.accessSync(directory, fs.
|
|
6
|
+
fs.accessSync(directory, fs.constants.W_OK);
|
|
7
7
|
}
|
|
8
8
|
catch {
|
|
9
9
|
// with the async option, await doesn't seem to wait for the directory to be created
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
"scripts": {
|
|
67
67
|
"test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box",
|
|
68
68
|
"test:black-box": "rimraf dist && yarn build && python3 -m pytest test-black-box/ -v",
|
|
69
|
-
"test:mutants": "stryker run stryker.config.json",
|
|
70
69
|
"test:tsd": "tsd --typings ./dist/server/counterfact-types/index.ts --files ./test/**/*.test-d.ts",
|
|
71
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",
|
|
72
71
|
"prepack": "yarn build",
|
|
73
72
|
"release": "npx changeset publish",
|
|
74
73
|
"prepare": "husky install",
|
|
75
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",
|
|
76
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",
|
|
77
77
|
"go:petstore": "yarn build && yarn counterfact https://petstore3.swagger.io/api/v3/openapi.json out",
|
|
78
78
|
"go:petstore2": "yarn build && yarn counterfact https://petstore.swagger.io/v2/swagger.json out",
|
|
@@ -84,9 +84,6 @@
|
|
|
84
84
|
"@changesets/cli": "2.30.0",
|
|
85
85
|
"@eslint/js": "10.0.1",
|
|
86
86
|
"@jest/globals": "^30.3.0",
|
|
87
|
-
"@stryker-mutator/core": "9.6.0",
|
|
88
|
-
"@stryker-mutator/jest-runner": "9.6.0",
|
|
89
|
-
"@stryker-mutator/typescript-checker": "9.6.0",
|
|
90
87
|
"@swc/core": "1.15.21",
|
|
91
88
|
"@swc/jest": "0.2.39",
|
|
92
89
|
"@testing-library/dom": "10.4.1",
|
|
@@ -122,7 +119,6 @@
|
|
|
122
119
|
"jest-retries": "1.0.1",
|
|
123
120
|
"node-mocks-http": "1.17.2",
|
|
124
121
|
"rimraf": "6.1.3",
|
|
125
|
-
"stryker-cli": "1.1.0",
|
|
126
122
|
"supertest": "7.2.2",
|
|
127
123
|
"tsd": "0.33.0",
|
|
128
124
|
"using-temporary-files": "2.2.1"
|
|
@@ -141,11 +137,11 @@
|
|
|
141
137
|
"js-yaml": "4.1.1",
|
|
142
138
|
"json-schema-faker": "0.6.0",
|
|
143
139
|
"jsonwebtoken": "9.0.3",
|
|
144
|
-
"koa": "3.
|
|
140
|
+
"koa": "3.2.0",
|
|
145
141
|
"koa-bodyparser": "4.4.1",
|
|
146
142
|
"koa-proxies": "0.12.4",
|
|
147
143
|
"koa2-swagger-ui": "5.12.0",
|
|
148
|
-
"lodash": "4.
|
|
144
|
+
"lodash": "4.18.1",
|
|
149
145
|
"node-fetch": "3.3.2",
|
|
150
146
|
"open": "11.0.0",
|
|
151
147
|
"patch-package": "8.0.1",
|
|
File without changes
|
|
File without changes
|