@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.
Files changed (89) hide show
  1. package/License.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/code-generator.d.ts +60 -0
  4. package/dist/code-generator.js +182 -0
  5. package/dist/coder.d.ts +107 -0
  6. package/dist/coder.js +132 -0
  7. package/dist/context-file-token.d.ts +1 -0
  8. package/dist/context-file-token.js +1 -0
  9. package/dist/ensure-directory-exists.d.ts +1 -0
  10. package/dist/ensure-directory-exists.js +12 -0
  11. package/dist/forward-slash-path.d.ts +9 -0
  12. package/dist/forward-slash-path.js +13 -0
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.js +5 -0
  15. package/dist/jsdoc.d.ts +5 -0
  16. package/dist/jsdoc.js +49 -0
  17. package/dist/openapi-path.d.ts +11 -0
  18. package/dist/openapi-path.js +23 -0
  19. package/dist/operation-coder.d.ts +23 -0
  20. package/dist/operation-coder.js +73 -0
  21. package/dist/operation-type-coder.d.ts +140 -0
  22. package/dist/operation-type-coder.js +387 -0
  23. package/dist/operation-type-name-mapping.d.ts +7 -0
  24. package/dist/operation-type-name-mapping.js +53 -0
  25. package/dist/parameter-export-type-coder.d.ts +13 -0
  26. package/dist/parameter-export-type-coder.js +27 -0
  27. package/dist/parameters-type-coder.d.ts +10 -0
  28. package/dist/parameters-type-coder.js +44 -0
  29. package/dist/printers.d.ts +2 -0
  30. package/dist/printers.js +10 -0
  31. package/dist/prune.d.ts +20 -0
  32. package/dist/prune.js +146 -0
  33. package/dist/read-only-comments.d.ts +1 -0
  34. package/dist/read-only-comments.js +5 -0
  35. package/dist/repository.d.ts +87 -0
  36. package/dist/repository.js +264 -0
  37. package/dist/requirement.d.ts +104 -0
  38. package/dist/requirement.js +191 -0
  39. package/dist/reserved-words.d.ts +1 -0
  40. package/dist/reserved-words.js +50 -0
  41. package/dist/response-type-coder.d.ts +16 -0
  42. package/dist/response-type-coder.js +104 -0
  43. package/dist/responses-type-coder.d.ts +11 -0
  44. package/dist/responses-type-coder.js +43 -0
  45. package/dist/scenario-file-generator.d.ts +31 -0
  46. package/dist/scenario-file-generator.js +368 -0
  47. package/dist/schema-coder.d.ts +10 -0
  48. package/dist/schema-coder.js +49 -0
  49. package/dist/schema-type-coder.d.ts +20 -0
  50. package/dist/schema-type-coder.js +121 -0
  51. package/dist/script.d.ts +130 -0
  52. package/dist/script.js +322 -0
  53. package/dist/specification.d.ts +40 -0
  54. package/dist/specification.js +58 -0
  55. package/dist/templates/counterfact-types/cookie-options.ts +14 -0
  56. package/dist/templates/counterfact-types/counterfact-response.ts +15 -0
  57. package/dist/templates/counterfact-types/example-names.ts +13 -0
  58. package/dist/templates/counterfact-types/example.ts +14 -0
  59. package/dist/templates/counterfact-types/generic-response-builder.ts +167 -0
  60. package/dist/templates/counterfact-types/http-status-code.ts +62 -0
  61. package/dist/templates/counterfact-types/if-has-key.ts +19 -0
  62. package/dist/templates/counterfact-types/index.ts +30 -0
  63. package/dist/templates/counterfact-types/maybe-promise.ts +6 -0
  64. package/dist/templates/counterfact-types/media-type.ts +6 -0
  65. package/dist/templates/counterfact-types/middleware.ts +87 -0
  66. package/dist/templates/counterfact-types/omit-all.ts +11 -0
  67. package/dist/templates/counterfact-types/omit-value-when-never.ts +11 -0
  68. package/dist/templates/counterfact-types/open-api-content.ts +8 -0
  69. package/dist/templates/counterfact-types/open-api-header.ts +4 -0
  70. package/dist/templates/counterfact-types/open-api-operation.ts +36 -0
  71. package/dist/templates/counterfact-types/open-api-parameters.ts +26 -0
  72. package/dist/templates/counterfact-types/open-api-response.ts +22 -0
  73. package/dist/templates/counterfact-types/random-function.ts +9 -0
  74. package/dist/templates/counterfact-types/response-builder-factory.ts +16 -0
  75. package/dist/templates/counterfact-types/response-builder.ts +36 -0
  76. package/dist/templates/counterfact-types/wide-operation-argument.ts +20 -0
  77. package/dist/templates/counterfact-types/wide-response-builder.ts +28 -0
  78. package/dist/type-coder.d.ts +5 -0
  79. package/dist/type-coder.js +9 -0
  80. package/dist/versions-ts-generator.d.ts +10 -0
  81. package/dist/versions-ts-generator.js +82 -0
  82. package/dist/wait-for-event.d.ts +2 -0
  83. package/dist/wait-for-event.js +17 -0
  84. package/dist/watch-options.d.ts +5 -0
  85. package/dist/watch-options.js +5 -0
  86. package/dist/windows-escape.d.ts +1 -0
  87. package/dist/windows-escape.js +9 -0
  88. package/examples/generate-routes.mjs +40 -0
  89. 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
+ }
@@ -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
+ }
@@ -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";
@@ -0,0 +1,5 @@
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 declare function buildJsDoc(data: unknown): string;
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;