@counterfact/generator 0.1.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/License.md +21 -0
- package/README.md +18 -0
- package/dist/code-generator.d.ts +60 -0
- package/dist/code-generator.js +182 -0
- package/dist/coder.d.ts +107 -0
- package/dist/coder.js +132 -0
- package/dist/context-file-token.d.ts +1 -0
- package/dist/context-file-token.js +1 -0
- package/dist/ensure-directory-exists.d.ts +1 -0
- package/dist/ensure-directory-exists.js +12 -0
- package/dist/forward-slash-path.d.ts +9 -0
- package/dist/forward-slash-path.js +13 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jsdoc.d.ts +5 -0
- package/dist/jsdoc.js +49 -0
- package/dist/openapi-path.d.ts +11 -0
- package/dist/openapi-path.js +23 -0
- package/dist/operation-coder.d.ts +23 -0
- package/dist/operation-coder.js +73 -0
- package/dist/operation-type-coder.d.ts +140 -0
- package/dist/operation-type-coder.js +387 -0
- package/dist/operation-type-name-mapping.d.ts +7 -0
- package/dist/operation-type-name-mapping.js +53 -0
- package/dist/parameter-export-type-coder.d.ts +13 -0
- package/dist/parameter-export-type-coder.js +27 -0
- package/dist/parameters-type-coder.d.ts +10 -0
- package/dist/parameters-type-coder.js +44 -0
- package/dist/printers.d.ts +2 -0
- package/dist/printers.js +10 -0
- package/dist/prune.d.ts +20 -0
- package/dist/prune.js +146 -0
- package/dist/read-only-comments.d.ts +1 -0
- package/dist/read-only-comments.js +5 -0
- package/dist/repository.d.ts +87 -0
- package/dist/repository.js +264 -0
- package/dist/requirement.d.ts +104 -0
- package/dist/requirement.js +191 -0
- package/dist/reserved-words.d.ts +1 -0
- package/dist/reserved-words.js +50 -0
- package/dist/response-type-coder.d.ts +16 -0
- package/dist/response-type-coder.js +104 -0
- package/dist/responses-type-coder.d.ts +11 -0
- package/dist/responses-type-coder.js +43 -0
- package/dist/scenario-file-generator.d.ts +31 -0
- package/dist/scenario-file-generator.js +368 -0
- package/dist/schema-coder.d.ts +10 -0
- package/dist/schema-coder.js +49 -0
- package/dist/schema-type-coder.d.ts +20 -0
- package/dist/schema-type-coder.js +121 -0
- package/dist/script.d.ts +130 -0
- package/dist/script.js +322 -0
- package/dist/specification.d.ts +40 -0
- package/dist/specification.js +58 -0
- package/dist/templates/counterfact-types/cookie-options.ts +14 -0
- package/dist/templates/counterfact-types/counterfact-response.ts +15 -0
- package/dist/templates/counterfact-types/example-names.ts +13 -0
- package/dist/templates/counterfact-types/example.ts +14 -0
- package/dist/templates/counterfact-types/generic-response-builder.ts +167 -0
- package/dist/templates/counterfact-types/http-status-code.ts +62 -0
- package/dist/templates/counterfact-types/if-has-key.ts +19 -0
- package/dist/templates/counterfact-types/index.ts +30 -0
- package/dist/templates/counterfact-types/maybe-promise.ts +6 -0
- package/dist/templates/counterfact-types/media-type.ts +6 -0
- package/dist/templates/counterfact-types/middleware.ts +87 -0
- package/dist/templates/counterfact-types/omit-all.ts +11 -0
- package/dist/templates/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/templates/counterfact-types/open-api-content.ts +8 -0
- package/dist/templates/counterfact-types/open-api-header.ts +4 -0
- package/dist/templates/counterfact-types/open-api-operation.ts +36 -0
- package/dist/templates/counterfact-types/open-api-parameters.ts +26 -0
- package/dist/templates/counterfact-types/open-api-response.ts +22 -0
- package/dist/templates/counterfact-types/random-function.ts +9 -0
- package/dist/templates/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/templates/counterfact-types/response-builder.ts +36 -0
- package/dist/templates/counterfact-types/wide-operation-argument.ts +20 -0
- package/dist/templates/counterfact-types/wide-response-builder.ts +28 -0
- package/dist/type-coder.d.ts +5 -0
- package/dist/type-coder.js +9 -0
- package/dist/versions-ts-generator.d.ts +10 -0
- package/dist/versions-ts-generator.js +82 -0
- package/dist/wait-for-event.d.ts +2 -0
- package/dist/wait-for-event.js +17 -0
- package/dist/watch-options.d.ts +5 -0
- package/dist/watch-options.js +5 -0
- package/dist/windows-escape.d.ts +1 -0
- package/dist/windows-escape.js +9 -0
- package/examples/generate-routes.mjs +40 -0
- package/package.json +61 -0
package/License.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Patrick McElhaney
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@counterfact/generator`
|
|
2
|
+
|
|
3
|
+
Generate Counterfact route scaffolds, OpenAPI-derived TypeScript contracts,
|
|
4
|
+
scenario types, and compatible local `counterfact-types` templates.
|
|
5
|
+
|
|
6
|
+
The public package boundary is intentionally small:
|
|
7
|
+
`CodeGenerator`, `ScenarioFileGenerator`, and `Repository` support the
|
|
8
|
+
`counterfact` facade, `generateVersionsTsContent` supports multi-version
|
|
9
|
+
projects, and `buildOperationTypeNameMapping` supports legacy route migration.
|
|
10
|
+
Concrete coders remain private implementation details.
|
|
11
|
+
|
|
12
|
+
The build carries the generated-handler-compatible portion of
|
|
13
|
+
`@counterfact/types` under `dist/templates/counterfact-types`. Runtime-only
|
|
14
|
+
shared values are excluded so generated projects retain their existing file
|
|
15
|
+
contract.
|
|
16
|
+
|
|
17
|
+
See [`examples/generate-routes.mjs`](./examples/generate-routes.mjs) for a
|
|
18
|
+
complete public-import example.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Repository } from "./repository.js";
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrates the code-generation pipeline and optional file-system watching.
|
|
4
|
+
*
|
|
5
|
+
* When {@link watch} is called, Counterfact watches the source OpenAPI document
|
|
6
|
+
* for changes and re-runs code generation automatically. `"generate"` and
|
|
7
|
+
* `"failed"` events are emitted after each attempt.
|
|
8
|
+
*/
|
|
9
|
+
export declare class CodeGenerator extends EventTarget {
|
|
10
|
+
private readonly openapiPath;
|
|
11
|
+
private readonly destination;
|
|
12
|
+
private readonly version;
|
|
13
|
+
private readonly overlays;
|
|
14
|
+
private readonly generateOptions;
|
|
15
|
+
private watcher;
|
|
16
|
+
constructor(openApiPath: string, destination: string, generateOptions: {
|
|
17
|
+
prune?: boolean;
|
|
18
|
+
routes?: boolean;
|
|
19
|
+
types?: boolean;
|
|
20
|
+
}, version?: string, overlays?: readonly string[]);
|
|
21
|
+
/**
|
|
22
|
+
* Initialises the `.cache` directory that holds compiled JS output.
|
|
23
|
+
*
|
|
24
|
+
* Creates a `.gitignore` file that excludes the `.cache` sub-directory and a
|
|
25
|
+
* `README.md` inside `.cache` that explains its purpose.
|
|
26
|
+
*
|
|
27
|
+
* @param destination - The root output directory.
|
|
28
|
+
*/
|
|
29
|
+
private buildCacheDirectory;
|
|
30
|
+
/**
|
|
31
|
+
* Reads and returns the `#/paths` requirement from `specification`.
|
|
32
|
+
*
|
|
33
|
+
* Writes a diagnostic message to stderr and returns an empty set when the
|
|
34
|
+
* `paths` key is missing or cannot be read.
|
|
35
|
+
*
|
|
36
|
+
* @param specification - The loaded OpenAPI specification.
|
|
37
|
+
*/
|
|
38
|
+
private getPathsFromSpecification;
|
|
39
|
+
/**
|
|
40
|
+
* Runs the main code-generation pipeline once and resolves when complete.
|
|
41
|
+
*
|
|
42
|
+
* Loads the OpenAPI spec from `openapiPath`, optionally prunes defunct route
|
|
43
|
+
* files, registers all path operations as {@link OperationCoder} exports, and
|
|
44
|
+
* writes the resulting TypeScript files to `destination`.
|
|
45
|
+
*
|
|
46
|
+
* @param repository - Injectable repository instance; defaults to a fresh one
|
|
47
|
+
* (primarily useful in tests).
|
|
48
|
+
*/
|
|
49
|
+
generate(repository?: Repository): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Starts watching the OpenAPI document and any overlay files for changes.
|
|
52
|
+
*
|
|
53
|
+
* Has no effect when neither source is watchable (for example, an HTTP
|
|
54
|
+
* OpenAPI source with no local overlays).
|
|
55
|
+
* Resolves once the watcher is ready.
|
|
56
|
+
*/
|
|
57
|
+
watch(): Promise<void>;
|
|
58
|
+
/** Closes the file-system watcher. */
|
|
59
|
+
stopWatching(): Promise<void>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import nodePath from "node:path";
|
|
4
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- generated files are written under the caller-provided destination tree. */
|
|
5
|
+
import { watch } from "chokidar";
|
|
6
|
+
import createDebug from "debug";
|
|
7
|
+
import { ensureDirectoryExists } from "./ensure-directory-exists.js";
|
|
8
|
+
import { OperationCoder } from "./operation-coder.js";
|
|
9
|
+
import { assertNoNormalizedPathCollisions, normalizeOpenApiPath, } from "./openapi-path.js";
|
|
10
|
+
import { pruneRoutes, pruneTypes } from "./prune.js";
|
|
11
|
+
import { Repository } from "./repository.js";
|
|
12
|
+
import { Specification } from "./specification.js";
|
|
13
|
+
import { waitForEvent } from "./wait-for-event.js";
|
|
14
|
+
import { CHOKIDAR_OPTIONS } from "./watch-options.js";
|
|
15
|
+
const debug = createDebug("counterfact:typescript-generator:generate");
|
|
16
|
+
/**
|
|
17
|
+
* Orchestrates the code-generation pipeline and optional file-system watching.
|
|
18
|
+
*
|
|
19
|
+
* When {@link watch} is called, Counterfact watches the source OpenAPI document
|
|
20
|
+
* for changes and re-runs code generation automatically. `"generate"` and
|
|
21
|
+
* `"failed"` events are emitted after each attempt.
|
|
22
|
+
*/
|
|
23
|
+
export class CodeGenerator extends EventTarget {
|
|
24
|
+
openapiPath;
|
|
25
|
+
destination;
|
|
26
|
+
version;
|
|
27
|
+
overlays;
|
|
28
|
+
generateOptions;
|
|
29
|
+
watcher;
|
|
30
|
+
constructor(openApiPath, destination, generateOptions, version = "", overlays = []) {
|
|
31
|
+
super();
|
|
32
|
+
this.openapiPath = openApiPath;
|
|
33
|
+
this.destination = destination;
|
|
34
|
+
this.version = version;
|
|
35
|
+
this.overlays = overlays;
|
|
36
|
+
this.generateOptions = generateOptions;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialises the `.cache` directory that holds compiled JS output.
|
|
40
|
+
*
|
|
41
|
+
* Creates a `.gitignore` file that excludes the `.cache` sub-directory and a
|
|
42
|
+
* `README.md` inside `.cache` that explains its purpose.
|
|
43
|
+
*
|
|
44
|
+
* @param destination - The root output directory.
|
|
45
|
+
*/
|
|
46
|
+
async buildCacheDirectory(destination) {
|
|
47
|
+
const gitignorePath = nodePath.join(destination, ".gitignore");
|
|
48
|
+
const cacheReadmePath = nodePath.join(destination, ".cache", "README.md");
|
|
49
|
+
debug("ensuring the directory containing .gitgnore exists");
|
|
50
|
+
await ensureDirectoryExists(gitignorePath);
|
|
51
|
+
debug("creating the .gitignore file if it doesn't already exist");
|
|
52
|
+
if (!existsSync(gitignorePath)) {
|
|
53
|
+
await fs.writeFile(gitignorePath, ".cache\n", "utf8");
|
|
54
|
+
}
|
|
55
|
+
debug("creating the .cache/README.md file");
|
|
56
|
+
ensureDirectoryExists(cacheReadmePath);
|
|
57
|
+
await fs.writeFile(cacheReadmePath, "This directory contains compiled JS files from the paths directory. Do not edit these files directly.\n", "utf8");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Reads and returns the `#/paths` requirement from `specification`.
|
|
61
|
+
*
|
|
62
|
+
* Writes a diagnostic message to stderr and returns an empty set when the
|
|
63
|
+
* `paths` key is missing or cannot be read.
|
|
64
|
+
*
|
|
65
|
+
* @param specification - The loaded OpenAPI specification.
|
|
66
|
+
*/
|
|
67
|
+
async getPathsFromSpecification(specification) {
|
|
68
|
+
try {
|
|
69
|
+
return specification.getRequirement("#/paths") ?? new Set();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
process.stderr.write(`Could not find #/paths in the specification.\n${error}\n`);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Runs the main code-generation pipeline once and resolves when complete.
|
|
78
|
+
*
|
|
79
|
+
* Loads the OpenAPI spec from `openapiPath`, optionally prunes defunct route
|
|
80
|
+
* files, registers all path operations as {@link OperationCoder} exports, and
|
|
81
|
+
* writes the resulting TypeScript files to `destination`.
|
|
82
|
+
*
|
|
83
|
+
* @param repository - Injectable repository instance; defaults to a fresh one
|
|
84
|
+
* (primarily useful in tests).
|
|
85
|
+
*/
|
|
86
|
+
async generate(repository = new Repository()) {
|
|
87
|
+
const { destination } = this;
|
|
88
|
+
debug("generating code from %s to %s", this.openapiPath, destination);
|
|
89
|
+
debug("initializing the .cache directory");
|
|
90
|
+
await this.buildCacheDirectory(destination);
|
|
91
|
+
debug("done initializing the .cache directory");
|
|
92
|
+
debug("creating specification from %s", this.openapiPath);
|
|
93
|
+
const specification = await Specification.fromFile(this.openapiPath, this.overlays);
|
|
94
|
+
debug("created specification: $o", specification);
|
|
95
|
+
debug("reading the #/paths from the specification");
|
|
96
|
+
const paths = await this.getPathsFromSpecification(specification);
|
|
97
|
+
debug("got %i paths", paths?.map?.length ?? 0);
|
|
98
|
+
const openApiPaths = paths.map((_pathDefinition, key) => key);
|
|
99
|
+
assertNoNormalizedPathCollisions(openApiPaths);
|
|
100
|
+
if (this.generateOptions.prune && this.generateOptions.routes) {
|
|
101
|
+
debug("pruning defunct route files");
|
|
102
|
+
await pruneRoutes(destination, openApiPaths);
|
|
103
|
+
debug("done pruning");
|
|
104
|
+
}
|
|
105
|
+
const securityRequirement = specification.getRequirement("#/components/securitySchemes");
|
|
106
|
+
const securitySchemes = Object.values(securityRequirement?.data ?? {});
|
|
107
|
+
const HTTP_VERBS = new Set([
|
|
108
|
+
"get",
|
|
109
|
+
"put",
|
|
110
|
+
"post",
|
|
111
|
+
"delete",
|
|
112
|
+
"options",
|
|
113
|
+
"head",
|
|
114
|
+
"patch",
|
|
115
|
+
"trace",
|
|
116
|
+
]);
|
|
117
|
+
const operationMethodsForPath = (pathDefinition) => pathDefinition.flatMap((operation, requestMethod) => {
|
|
118
|
+
if (requestMethod === "additionalOperations") {
|
|
119
|
+
return operation.map((additionalOperation, additionalMethod) => [
|
|
120
|
+
additionalOperation,
|
|
121
|
+
additionalMethod.toLowerCase(),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
if (!HTTP_VERBS.has(requestMethod)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return [[operation, requestMethod]];
|
|
128
|
+
});
|
|
129
|
+
paths.forEach((pathDefinition, key) => {
|
|
130
|
+
debug("processing path %s", key);
|
|
131
|
+
const normalizedPath = normalizeOpenApiPath(key);
|
|
132
|
+
const path = normalizedPath === "/" ? "/index" : normalizedPath;
|
|
133
|
+
operationMethodsForPath(pathDefinition).forEach(([operation, requestMethod]) => {
|
|
134
|
+
repository
|
|
135
|
+
.get(`routes${path}.ts`)
|
|
136
|
+
.export(new OperationCoder(operation, this.version, requestMethod, securitySchemes));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
if (this.generateOptions.prune && this.generateOptions.types) {
|
|
140
|
+
debug("resolving the expected generated type files");
|
|
141
|
+
await repository.finished();
|
|
142
|
+
debug("pruning defunct generated type files");
|
|
143
|
+
await pruneTypes(destination, [
|
|
144
|
+
...repository.scripts.keys(),
|
|
145
|
+
...(this.version === "" ? [] : ["types/versions.ts"]),
|
|
146
|
+
]);
|
|
147
|
+
debug("done pruning generated type files");
|
|
148
|
+
}
|
|
149
|
+
debug("telling the repository to write the files to %s", destination);
|
|
150
|
+
await repository.writeFiles(destination, this.generateOptions);
|
|
151
|
+
debug("finished writing the files");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts watching the OpenAPI document and any overlay files for changes.
|
|
155
|
+
*
|
|
156
|
+
* Has no effect when neither source is watchable (for example, an HTTP
|
|
157
|
+
* OpenAPI source with no local overlays).
|
|
158
|
+
* Resolves once the watcher is ready.
|
|
159
|
+
*/
|
|
160
|
+
async watch() {
|
|
161
|
+
const watchablePaths = this.openapiPath.startsWith("http")
|
|
162
|
+
? [...this.overlays]
|
|
163
|
+
: [this.openapiPath, ...this.overlays];
|
|
164
|
+
if (watchablePaths.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.watcher = watch(watchablePaths, CHOKIDAR_OPTIONS).on("change", () => {
|
|
168
|
+
void this.generate().then(() => {
|
|
169
|
+
this.dispatchEvent(new Event("generate"));
|
|
170
|
+
return true;
|
|
171
|
+
}, () => {
|
|
172
|
+
this.dispatchEvent(new Event("failed"));
|
|
173
|
+
return false;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
await waitForEvent(this.watcher, "ready");
|
|
177
|
+
}
|
|
178
|
+
/** Closes the file-system watcher. */
|
|
179
|
+
async stopWatching() {
|
|
180
|
+
await this.watcher?.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/coder.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Requirement } from "./requirement.js";
|
|
2
|
+
import type { Script } from "./script.js";
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all code-generation helpers in the TypeScript generator.
|
|
5
|
+
*
|
|
6
|
+
* A `Coder` wraps a single {@link Requirement} node from the OpenAPI spec and
|
|
7
|
+
* knows how to emit TypeScript code for it. Subclasses override
|
|
8
|
+
* {@link writeCode} to produce the actual source text.
|
|
9
|
+
*
|
|
10
|
+
* Coders are used by {@link Script} and {@link Repository} to lazily generate
|
|
11
|
+
* exports and imports, resolving `$ref` references through the
|
|
12
|
+
* {@link Specification} before writing.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Coder {
|
|
15
|
+
requirement: Requirement;
|
|
16
|
+
version: string;
|
|
17
|
+
constructor(requirement: Requirement, version?: string);
|
|
18
|
+
/**
|
|
19
|
+
* A stable cache key for this coder, composed of the constructor name and
|
|
20
|
+
* either the `$ref` value (for references) or the requirement URL.
|
|
21
|
+
*/
|
|
22
|
+
get id(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional preamble emitted before the `export` keyword.
|
|
25
|
+
*
|
|
26
|
+
* Subclasses can return a string (e.g. a type alias) that must appear in the
|
|
27
|
+
* output before this coder's export statement.
|
|
28
|
+
*
|
|
29
|
+
* @param _path - The path of the script being written (unused in base class).
|
|
30
|
+
*/
|
|
31
|
+
beforeExport(_path?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Returns a JSDoc comment block to be placed immediately before the export.
|
|
34
|
+
*
|
|
35
|
+
* Returns `""` by default; subclasses override this to surface OpenAPI
|
|
36
|
+
* metadata (description, summary, examples, etc.).
|
|
37
|
+
*/
|
|
38
|
+
jsdoc(): string;
|
|
39
|
+
/**
|
|
40
|
+
* Writes this coder's contribution to `script`.
|
|
41
|
+
*
|
|
42
|
+
* When the requirement is a `$ref`, delegates to {@link Script.import} so
|
|
43
|
+
* the reference target is exported from its own module. Otherwise calls
|
|
44
|
+
* {@link writeCode}.
|
|
45
|
+
*
|
|
46
|
+
* @param script - The script being assembled.
|
|
47
|
+
* @returns The TypeScript source text for this coder's export value.
|
|
48
|
+
*/
|
|
49
|
+
write(script: Script): string;
|
|
50
|
+
/**
|
|
51
|
+
* Generates the TypeScript source text for this coder's value.
|
|
52
|
+
*
|
|
53
|
+
* This method is abstract — subclasses **must** override it.
|
|
54
|
+
*
|
|
55
|
+
* @param _script - The script being assembled.
|
|
56
|
+
* @throws Always — callers should never reach the base implementation.
|
|
57
|
+
*/
|
|
58
|
+
writeCode(_script: Script): string;
|
|
59
|
+
/**
|
|
60
|
+
* Resolves `$ref` references by returning the target coder.
|
|
61
|
+
*
|
|
62
|
+
* When this coder's requirement is not a reference, returns `this`.
|
|
63
|
+
* Otherwise loads the referenced requirement and wraps it in an instance of
|
|
64
|
+
* the same concrete coder class.
|
|
65
|
+
*/
|
|
66
|
+
delegate(): Promise<Coder>;
|
|
67
|
+
/**
|
|
68
|
+
* Generator that yields candidate export names for this coder.
|
|
69
|
+
*
|
|
70
|
+
* The first name is derived from the last path segment of the requirement
|
|
71
|
+
* URL, sanitised to be a valid TypeScript identifier. Subsequent names have
|
|
72
|
+
* an incrementing numeric suffix to resolve collisions.
|
|
73
|
+
*
|
|
74
|
+
* @param rawName - Override the starting name (used by subclasses).
|
|
75
|
+
*/
|
|
76
|
+
names(rawName?: string): Generator<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Returns an optional TypeScript type annotation string to be placed between
|
|
79
|
+
* the export name and its value (`export const name: <type> = value`).
|
|
80
|
+
*
|
|
81
|
+
* Returns `""` by default.
|
|
82
|
+
*/
|
|
83
|
+
typeDeclaration(_namespace?: Map<string, ExportStatement>, _script?: Script): string;
|
|
84
|
+
/**
|
|
85
|
+
* Returns the repository-relative path of the script where this coder's
|
|
86
|
+
* export should live when imported by another script.
|
|
87
|
+
*
|
|
88
|
+
* Subclasses override this to place type exports in `types/paths/…` and
|
|
89
|
+
* route exports in `routes/…`.
|
|
90
|
+
*/
|
|
91
|
+
modulePath(): string;
|
|
92
|
+
}
|
|
93
|
+
export interface ExportStatement {
|
|
94
|
+
beforeExport: string;
|
|
95
|
+
done: boolean;
|
|
96
|
+
id: string;
|
|
97
|
+
isDefault: boolean;
|
|
98
|
+
isType: boolean;
|
|
99
|
+
jsdoc: string;
|
|
100
|
+
typeDeclaration: string;
|
|
101
|
+
name?: string;
|
|
102
|
+
code?: string | {
|
|
103
|
+
raw: string;
|
|
104
|
+
};
|
|
105
|
+
error?: Error;
|
|
106
|
+
promise?: Promise<Coder | undefined>;
|
|
107
|
+
}
|
package/dist/coder.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { RESERVED_WORDS } from "./reserved-words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for all code-generation helpers in the TypeScript generator.
|
|
4
|
+
*
|
|
5
|
+
* A `Coder` wraps a single {@link Requirement} node from the OpenAPI spec and
|
|
6
|
+
* knows how to emit TypeScript code for it. Subclasses override
|
|
7
|
+
* {@link writeCode} to produce the actual source text.
|
|
8
|
+
*
|
|
9
|
+
* Coders are used by {@link Script} and {@link Repository} to lazily generate
|
|
10
|
+
* exports and imports, resolving `$ref` references through the
|
|
11
|
+
* {@link Specification} before writing.
|
|
12
|
+
*/
|
|
13
|
+
export class Coder {
|
|
14
|
+
requirement;
|
|
15
|
+
version;
|
|
16
|
+
constructor(requirement, version = "") {
|
|
17
|
+
this.requirement = requirement;
|
|
18
|
+
this.version = version;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A stable cache key for this coder, composed of the constructor name and
|
|
22
|
+
* either the `$ref` value (for references) or the requirement URL.
|
|
23
|
+
*/
|
|
24
|
+
get id() {
|
|
25
|
+
if (this.requirement.isReference) {
|
|
26
|
+
return `${this.constructor.name}@${this.requirement.refUrl}`;
|
|
27
|
+
}
|
|
28
|
+
return `${this.constructor.name}@${this.requirement.url}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Optional preamble emitted before the `export` keyword.
|
|
32
|
+
*
|
|
33
|
+
* Subclasses can return a string (e.g. a type alias) that must appear in the
|
|
34
|
+
* output before this coder's export statement.
|
|
35
|
+
*
|
|
36
|
+
* @param _path - The path of the script being written (unused in base class).
|
|
37
|
+
*/
|
|
38
|
+
beforeExport(_path) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns a JSDoc comment block to be placed immediately before the export.
|
|
43
|
+
*
|
|
44
|
+
* Returns `""` by default; subclasses override this to surface OpenAPI
|
|
45
|
+
* metadata (description, summary, examples, etc.).
|
|
46
|
+
*/
|
|
47
|
+
jsdoc() {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Writes this coder's contribution to `script`.
|
|
52
|
+
*
|
|
53
|
+
* When the requirement is a `$ref`, delegates to {@link Script.import} so
|
|
54
|
+
* the reference target is exported from its own module. Otherwise calls
|
|
55
|
+
* {@link writeCode}.
|
|
56
|
+
*
|
|
57
|
+
* @param script - The script being assembled.
|
|
58
|
+
* @returns The TypeScript source text for this coder's export value.
|
|
59
|
+
*/
|
|
60
|
+
write(script) {
|
|
61
|
+
if (this.requirement.isReference) {
|
|
62
|
+
return script.import(this);
|
|
63
|
+
}
|
|
64
|
+
return this.writeCode(script);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generates the TypeScript source text for this coder's value.
|
|
68
|
+
*
|
|
69
|
+
* This method is abstract — subclasses **must** override it.
|
|
70
|
+
*
|
|
71
|
+
* @param _script - The script being assembled.
|
|
72
|
+
* @throws Always — callers should never reach the base implementation.
|
|
73
|
+
*/
|
|
74
|
+
writeCode(_script) {
|
|
75
|
+
throw new Error("write() is abstract and should be overwritten by a subclass");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolves `$ref` references by returning the target coder.
|
|
79
|
+
*
|
|
80
|
+
* When this coder's requirement is not a reference, returns `this`.
|
|
81
|
+
* Otherwise loads the referenced requirement and wraps it in an instance of
|
|
82
|
+
* the same concrete coder class.
|
|
83
|
+
*/
|
|
84
|
+
async delegate() {
|
|
85
|
+
if (!this.requirement.isReference) {
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
const requirement = await this.requirement.reference();
|
|
89
|
+
return new this.constructor(requirement, this.version);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generator that yields candidate export names for this coder.
|
|
93
|
+
*
|
|
94
|
+
* The first name is derived from the last path segment of the requirement
|
|
95
|
+
* URL, sanitised to be a valid TypeScript identifier. Subsequent names have
|
|
96
|
+
* an incrementing numeric suffix to resolve collisions.
|
|
97
|
+
*
|
|
98
|
+
* @param rawName - Override the starting name (used by subclasses).
|
|
99
|
+
*/
|
|
100
|
+
*names(rawName = this.requirement.url.split("/").at(-1)) {
|
|
101
|
+
const name = rawName
|
|
102
|
+
.replace(/^\d/u, (digit) => `_${digit}`)
|
|
103
|
+
.replaceAll(/[^\w$]/gu, "_");
|
|
104
|
+
const baseName = RESERVED_WORDS.has(name) ? `${name}_` : name;
|
|
105
|
+
yield baseName;
|
|
106
|
+
let index = 1;
|
|
107
|
+
const MAX_NAMES_TO_GENERATE_BEFORE_GIVING_UP = 100;
|
|
108
|
+
while (index < MAX_NAMES_TO_GENERATE_BEFORE_GIVING_UP) {
|
|
109
|
+
index += 1;
|
|
110
|
+
yield baseName + index;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns an optional TypeScript type annotation string to be placed between
|
|
115
|
+
* the export name and its value (`export const name: <type> = value`).
|
|
116
|
+
*
|
|
117
|
+
* Returns `""` by default.
|
|
118
|
+
*/
|
|
119
|
+
typeDeclaration(_namespace, _script) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns the repository-relative path of the script where this coder's
|
|
124
|
+
* export should live when imported by another script.
|
|
125
|
+
*
|
|
126
|
+
* Subclasses override this to place type exports in `types/paths/…` and
|
|
127
|
+
* route exports in `routes/…`.
|
|
128
|
+
*/
|
|
129
|
+
modulePath() {
|
|
130
|
+
return "did-not-override-coder-modulePath.ts";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CONTEXT_FILE_TOKEN = "@@CONTEXT_FILE_TOKEN@@";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONTEXT_FILE_TOKEN = "@@CONTEXT_FILE_TOKEN@@";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureDirectoryExists(filePath: string): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import nodePath from "node:path";
|
|
3
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- helper creates parent directories for caller-provided generator output paths. */
|
|
4
|
+
export function ensureDirectoryExists(filePath) {
|
|
5
|
+
const directory = nodePath.dirname(filePath);
|
|
6
|
+
try {
|
|
7
|
+
fs.accessSync(directory, fs.constants.W_OK);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const __forwardSlashPath: unique symbol;
|
|
2
|
+
type ForwardSlashPath = string & {
|
|
3
|
+
readonly [__forwardSlashPath]: never;
|
|
4
|
+
};
|
|
5
|
+
export declare function toForwardSlashPath(path: string): ForwardSlashPath;
|
|
6
|
+
export declare function pathJoin(...paths: string[]): ForwardSlashPath;
|
|
7
|
+
export declare function pathRelative(from: string, to: string): ForwardSlashPath;
|
|
8
|
+
export declare function pathDirname(path: string): ForwardSlashPath;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import nodePath from "node:path";
|
|
2
|
+
export function toForwardSlashPath(path) {
|
|
3
|
+
return path.replaceAll("\\", "/");
|
|
4
|
+
}
|
|
5
|
+
export function pathJoin(...paths) {
|
|
6
|
+
return toForwardSlashPath(nodePath.join(...paths));
|
|
7
|
+
}
|
|
8
|
+
export function pathRelative(from, to) {
|
|
9
|
+
return toForwardSlashPath(nodePath.relative(from, to));
|
|
10
|
+
}
|
|
11
|
+
export function pathDirname(path) {
|
|
12
|
+
return toForwardSlashPath(nodePath.dirname(path));
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CodeGenerator } from "./code-generator.js";
|
|
2
|
+
export { buildOperationTypeNameMapping, type OperationTypeNameMapping, } from "./operation-type-name-mapping.js";
|
|
3
|
+
export { Repository } from "./repository.js";
|
|
4
|
+
export { ScenarioFileGenerator } from "./scenario-file-generator.js";
|
|
5
|
+
export { generateVersionsTsContent } from "./versions-ts-generator.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CodeGenerator } from "./code-generator.js";
|
|
2
|
+
export { buildOperationTypeNameMapping, } from "./operation-type-name-mapping.js";
|
|
3
|
+
export { Repository } from "./repository.js";
|
|
4
|
+
export { ScenarioFileGenerator } from "./scenario-file-generator.js";
|
|
5
|
+
export { generateVersionsTsContent } from "./versions-ts-generator.js";
|
package/dist/jsdoc.d.ts
ADDED
package/dist/jsdoc.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a JSDoc comment string from OpenAPI schema metadata.
|
|
3
|
+
* Returns an empty string if there is no relevant metadata.
|
|
4
|
+
*/
|
|
5
|
+
export function buildJsDoc(data) {
|
|
6
|
+
if (typeof data !== "object" || data === null) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
const record = data;
|
|
10
|
+
const lines = [];
|
|
11
|
+
const description = record["description"];
|
|
12
|
+
const summary = record["summary"];
|
|
13
|
+
const example = record["example"];
|
|
14
|
+
const examples = record["examples"];
|
|
15
|
+
const defaultValue = record["default"];
|
|
16
|
+
const format = record["format"];
|
|
17
|
+
const deprecated = record["deprecated"];
|
|
18
|
+
const mainText = description ?? summary;
|
|
19
|
+
if (mainText) {
|
|
20
|
+
// Escape */ to prevent prematurely closing the JSDoc block
|
|
21
|
+
const escaped = String(mainText).replace(/\*\//gu, "* /");
|
|
22
|
+
const textLines = escaped.split("\n");
|
|
23
|
+
for (const line of textLines) {
|
|
24
|
+
lines.push(` * ${line}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (format !== undefined) {
|
|
28
|
+
lines.push(` * @format ${format}`);
|
|
29
|
+
}
|
|
30
|
+
if (defaultValue !== undefined) {
|
|
31
|
+
lines.push(` * @default ${JSON.stringify(defaultValue)}`);
|
|
32
|
+
}
|
|
33
|
+
// Use scalar `example`, or fall back to the first value from `examples`
|
|
34
|
+
const exampleValue = example !== undefined
|
|
35
|
+
? example
|
|
36
|
+
: examples !== undefined
|
|
37
|
+
? Object.values(examples)[0]?.value
|
|
38
|
+
: undefined;
|
|
39
|
+
if (exampleValue !== undefined) {
|
|
40
|
+
lines.push(` * @example ${JSON.stringify(exampleValue)}`);
|
|
41
|
+
}
|
|
42
|
+
if (deprecated === true) {
|
|
43
|
+
lines.push(` * @deprecated`);
|
|
44
|
+
}
|
|
45
|
+
if (lines.length === 0) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
return `/**\n${lines.join("\n")}\n */\n`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an OpenAPI path for Counterfact's route and type module trees.
|
|
3
|
+
*
|
|
4
|
+
* Incoming requests already match route modules with or without a terminal
|
|
5
|
+
* slash, so terminal slashes must not create an extra empty file segment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeOpenApiPath(openApiPath: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Rejects path keys that would target the same generated route module.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assertNoNormalizedPathCollisions(openApiPaths: Iterable<string>): void;
|