@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
@@ -0,0 +1,121 @@
1
+ import { pathJoin } from "./forward-slash-path.js";
2
+ import { buildJsDoc } from "./jsdoc.js";
3
+ import { TypeCoder } from "./type-coder.js";
4
+ import { Requirement } from "./requirement.js";
5
+ export class SchemaTypeCoder extends TypeCoder {
6
+ names() {
7
+ return super.names(this.requirement.refUrl?.split("/").at(-1));
8
+ }
9
+ jsdoc() {
10
+ return buildJsDoc(this.requirement.data);
11
+ }
12
+ additionalPropertiesType(script) {
13
+ const { additionalProperties, properties } = this.requirement.data;
14
+ if (!additionalProperties.type) {
15
+ return "unknown";
16
+ }
17
+ if (Object.values(properties ?? {}).some((property) => property.type !== additionalProperties.type)) {
18
+ return "unknown";
19
+ }
20
+ const requirement = this.requirement.get("additionalProperties");
21
+ return new SchemaTypeCoder(requirement, this.version).write(script);
22
+ }
23
+ objectSchema(script) {
24
+ const { data } = this.requirement;
25
+ const typedData = data;
26
+ const properties = Object.keys(typedData.properties ?? {}).map((name) => {
27
+ const property = this.requirement.get("properties").get(name);
28
+ const propertyData = property.data;
29
+ const isRequired = typedData.required?.includes(name) || propertyData.required === true;
30
+ const optionalFlag = isRequired ? "" : "?";
31
+ const comment = buildJsDoc(property.data);
32
+ const commentPrefix = comment ? `\n${comment}` : "";
33
+ const propertyType = new SchemaTypeCoder(property, this.version).write(script);
34
+ return `${commentPrefix}"${name}"${optionalFlag}: ${propertyType}`;
35
+ });
36
+ if (typedData.additionalProperties) {
37
+ properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
38
+ }
39
+ return `{${properties.join(",")}}`;
40
+ }
41
+ arraySchema(script) {
42
+ return `Array<${new SchemaTypeCoder(this.requirement.get("items"), this.version).write(script)}>`;
43
+ }
44
+ writePrimitive(value) {
45
+ if (typeof value === "string") {
46
+ return `"${value}"`;
47
+ }
48
+ if (value === null) {
49
+ return "null";
50
+ }
51
+ return String(value);
52
+ }
53
+ writeType(script, type) {
54
+ if (Array.isArray(type)) {
55
+ return type
56
+ .map((item) => this.writeType(script, item))
57
+ .join(" | ");
58
+ }
59
+ if (typeof type !== "string") {
60
+ return "unknown";
61
+ }
62
+ if (type === "object") {
63
+ return this.objectSchema(script);
64
+ }
65
+ if (type === "array") {
66
+ return this.arraySchema(script);
67
+ }
68
+ if (type === "integer") {
69
+ return "number";
70
+ }
71
+ return type ?? "unknown";
72
+ }
73
+ writeGroup(script, { allOf, anyOf, oneOf, }) {
74
+ function matchingKey() {
75
+ if (allOf) {
76
+ return "allOf";
77
+ }
78
+ if (anyOf) {
79
+ return "anyOf";
80
+ }
81
+ return "oneOf";
82
+ }
83
+ const key = matchingKey();
84
+ const items = (allOf ?? anyOf ?? oneOf);
85
+ const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index), this.version).write(script));
86
+ // Include the default schema from discriminator.defaultMapping (OpenAPI 3.2)
87
+ if (!allOf) {
88
+ const { discriminator } = this.requirement.data;
89
+ if (discriminator?.defaultMapping) {
90
+ const defaultRequirement = new Requirement({ $ref: discriminator.defaultMapping }, "", this.requirement.specification);
91
+ types.push(new SchemaTypeCoder(defaultRequirement, this.version).write(script));
92
+ }
93
+ }
94
+ return types.join(allOf ? " & " : " | ");
95
+ }
96
+ writeEnum(_script, requirement) {
97
+ return requirement.data
98
+ .map((item) => this.writePrimitive(item))
99
+ .join(" | ");
100
+ }
101
+ modulePath() {
102
+ return pathJoin("types", this.version, this.requirement.refUrl.replace(/^#\//u, "") + ".ts");
103
+ }
104
+ writeCode(script) {
105
+ const { allOf, anyOf, oneOf, type, format, itemSchema } = this.requirement
106
+ .data;
107
+ if (itemSchema) {
108
+ return `AsyncIterable<${new SchemaTypeCoder(this.requirement.get("itemSchema"), this.version).write(script)}>`;
109
+ }
110
+ if (allOf ?? anyOf ?? oneOf) {
111
+ return this.writeGroup(script, { allOf, anyOf, oneOf });
112
+ }
113
+ if (this.requirement.has("enum")) {
114
+ return this.writeEnum(script, this.requirement.get("enum"));
115
+ }
116
+ if ((type === "string" && format === "binary") || type === "file") {
117
+ return "Uint8Array | string";
118
+ }
119
+ return this.writeType(script, type);
120
+ }
121
+ }
@@ -0,0 +1,130 @@
1
+ import type { Coder, ExportStatement } from "./coder.js";
2
+ import type { Repository } from "./repository.js";
3
+ interface ImportEntry {
4
+ isDefault: boolean;
5
+ isType: boolean;
6
+ name: string;
7
+ script: Script;
8
+ }
9
+ interface ExternalImportEntry {
10
+ isDefault?: boolean;
11
+ isType: boolean;
12
+ modulePath: string;
13
+ }
14
+ /**
15
+ * Represents a single TypeScript file being assembled by the code generator.
16
+ *
17
+ * A `Script` accumulates exports, imports, and external imports contributed by
18
+ * {@link Coder} instances. Once all coders have resolved, {@link contents}
19
+ * formats the result with Prettier and returns the final source text.
20
+ *
21
+ * Scripts are created and retrieved through a {@link Repository} so that the
22
+ * same module path always maps to the same `Script` instance.
23
+ */
24
+ export declare class Script {
25
+ repository: Repository;
26
+ comments: string[];
27
+ exports: Map<string, ExportStatement>;
28
+ versions: Map<string, Map<string, ExportStatement>>;
29
+ versionFormatters: Map<string, (versionCodes: Map<string, string>) => string>;
30
+ imports: Map<string, ImportEntry>;
31
+ externalImport: Map<string, ExternalImportEntry>;
32
+ cache: Map<string, string>;
33
+ typeCache: Map<string, string>;
34
+ path: string;
35
+ constructor(repository: Repository, path: string);
36
+ /**
37
+ * A `"../"` path fragment that points from this script's directory back to
38
+ * the repository root, used to resolve relative import paths.
39
+ */
40
+ get relativePathToBase(): string;
41
+ /**
42
+ * Picks the first name from `coder.names()` that is not already used as an
43
+ * import in this script, ensuring export/import name uniqueness.
44
+ *
45
+ * @param coder - The coder needing a name.
46
+ * @throws When all 100 candidate names are already taken.
47
+ */
48
+ firstUniqueName(coder: Coder): string;
49
+ /**
50
+ * Registers an export for `coder` in this script and returns the export name.
51
+ *
52
+ * If the same coder has already been exported (cache hit), the previously
53
+ * assigned name is returned without creating a duplicate.
54
+ *
55
+ * @param coder - The coder to export.
56
+ * @param isType - Emit `export type` instead of `export const`.
57
+ * @param isDefault - Emit `export default` instead of a named export.
58
+ * @returns The name under which the coder is exported.
59
+ */
60
+ export(coder: Coder, isType?: boolean, isDefault?: boolean): string;
61
+ exportDefault(coder: Coder, isType?: boolean): void;
62
+ /**
63
+ * Registers an import of `coder` from its owning module and returns the
64
+ * local alias used in this script.
65
+ *
66
+ * The coder is also exported from its home module as a side effect.
67
+ *
68
+ * @param coder - The coder to import.
69
+ * @param isType - Use a `import type` declaration.
70
+ * @param isDefault - Import the default export rather than a named export.
71
+ */
72
+ import(coder: Coder, isType?: boolean, isDefault?: boolean): string;
73
+ importType(coder: Coder): string;
74
+ importDefault(coder: Coder, isType?: boolean): string;
75
+ /**
76
+ * Registers an import from an external npm package or absolute module path
77
+ * (not managed by the repository).
78
+ *
79
+ * @param name - The local binding name.
80
+ * @param modulePath - The module specifier (e.g. `"counterfact-types/index"`).
81
+ * @param isType - Use a `import type` declaration.
82
+ * @returns `name` (for convenience in method chaining).
83
+ */
84
+ importExternal(name: string, modulePath: string, isType?: boolean): string;
85
+ /**
86
+ * Convenience wrapper that calls {@link importExternal} with `isType = true`.
87
+ */
88
+ importExternalType(name: string, modulePath: string): string;
89
+ /**
90
+ * Imports a type from the shared `counterfact-types/index.ts` module,
91
+ * resolving the path relative to this script's location in the repository.
92
+ *
93
+ * @param name - The type name to import (e.g. `"WideOperationArgument"`).
94
+ */
95
+ importSharedType(name: string): string;
96
+ /**
97
+ * Imports a type from the generated `types/versions.ts` module,
98
+ * resolving the path relative to this script's location in the repository.
99
+ *
100
+ * @param name - The type name to import (e.g. `"Versioned"`).
101
+ */
102
+ importVersionsType(name: string): string;
103
+ exportType(coder: Coder): string;
104
+ /**
105
+ * Registers a formatter function for the merged versioned type emitted under
106
+ * `name` by {@link versionsTypeStatements}.
107
+ *
108
+ * When a formatter is present for a name, `versionsTypeStatements` delegates
109
+ * the entire type declaration to it instead of generating the default
110
+ * `Versions` object type. The formatter receives a `Map<version, importAlias>`
111
+ * and must return the complete TypeScript source for that operation type.
112
+ */
113
+ setVersionFormatter(name: string, formatter: (versionCodes: Map<string, string>) => string): void;
114
+ declareVersion(coder: Coder, name: string): void;
115
+ /** `true` while at least one export promise is still pending. */
116
+ isInProgress(): boolean;
117
+ /** Returns a promise that resolves when all pending export promises settle. */
118
+ finished(): Promise<(Coder | undefined)[]>;
119
+ externalImportStatements(): string[];
120
+ importStatements(): string[];
121
+ exportStatements(): string[];
122
+ versionsTypeStatements(): string[];
123
+ /**
124
+ * Formats the fully assembled script source with Prettier and returns it.
125
+ *
126
+ * All pending export promises are awaited before formatting.
127
+ */
128
+ contents(): Promise<string>;
129
+ }
130
+ export {};
package/dist/script.js ADDED
@@ -0,0 +1,322 @@
1
+ import createDebugger from "debug";
2
+ import { format } from "prettier";
3
+ import { escapePathForWindows } from "./windows-escape.js";
4
+ import { pathJoin, pathRelative, pathDirname } from "./forward-slash-path.js";
5
+ const debug = createDebugger("counterfact:typescript-generator:script");
6
+ /**
7
+ * Represents a single TypeScript file being assembled by the code generator.
8
+ *
9
+ * A `Script` accumulates exports, imports, and external imports contributed by
10
+ * {@link Coder} instances. Once all coders have resolved, {@link contents}
11
+ * formats the result with Prettier and returns the final source text.
12
+ *
13
+ * Scripts are created and retrieved through a {@link Repository} so that the
14
+ * same module path always maps to the same `Script` instance.
15
+ */
16
+ export class Script {
17
+ repository;
18
+ comments;
19
+ exports;
20
+ versions;
21
+ versionFormatters;
22
+ imports;
23
+ externalImport;
24
+ cache;
25
+ typeCache;
26
+ path;
27
+ constructor(repository, path) {
28
+ this.repository = repository;
29
+ this.comments = [];
30
+ this.exports = new Map();
31
+ this.versions = new Map();
32
+ this.versionFormatters = new Map();
33
+ this.imports = new Map();
34
+ this.externalImport = new Map();
35
+ this.cache = new Map();
36
+ this.typeCache = new Map();
37
+ this.path = path;
38
+ }
39
+ /**
40
+ * A `"../"` path fragment that points from this script's directory back to
41
+ * the repository root, used to resolve relative import paths.
42
+ */
43
+ get relativePathToBase() {
44
+ return this.path
45
+ .split("/")
46
+ .slice(0, -1)
47
+ .map(() => "..")
48
+ .join("/");
49
+ }
50
+ /**
51
+ * Picks the first name from `coder.names()` that is not already used as an
52
+ * import in this script, ensuring export/import name uniqueness.
53
+ *
54
+ * @param coder - The coder needing a name.
55
+ * @throws When all 100 candidate names are already taken.
56
+ */
57
+ firstUniqueName(coder) {
58
+ for (const name of coder.names()) {
59
+ if (!this.imports.has(name)) {
60
+ return name;
61
+ }
62
+ }
63
+ throw new Error(`could not find a unique name for ${coder.id}`);
64
+ }
65
+ /**
66
+ * Registers an export for `coder` in this script and returns the export name.
67
+ *
68
+ * If the same coder has already been exported (cache hit), the previously
69
+ * assigned name is returned without creating a duplicate.
70
+ *
71
+ * @param coder - The coder to export.
72
+ * @param isType - Emit `export type` instead of `export const`.
73
+ * @param isDefault - Emit `export default` instead of a named export.
74
+ * @returns The name under which the coder is exported.
75
+ */
76
+ export(coder, isType = false, isDefault = false) {
77
+ const cacheKey = isDefault ? "default" : `${coder.id}:${isType}`;
78
+ if (this.cache.has(cacheKey)) {
79
+ return this.cache.get(cacheKey);
80
+ }
81
+ const name = this.firstUniqueName(coder);
82
+ this.cache.set(cacheKey, name);
83
+ const exportStatement = {
84
+ beforeExport: coder.beforeExport(this.path),
85
+ done: false,
86
+ id: coder.id,
87
+ isDefault,
88
+ isType,
89
+ jsdoc: "",
90
+ typeDeclaration: coder.typeDeclaration(this.exports, this),
91
+ };
92
+ exportStatement.promise = coder
93
+ .delegate()
94
+ .then((availableCoder) => {
95
+ exportStatement.name = name;
96
+ exportStatement.code = availableCoder.write(this);
97
+ exportStatement.jsdoc = availableCoder.jsdoc();
98
+ return availableCoder;
99
+ })
100
+ .catch((error) => {
101
+ exportStatement.code = `{/* error creating export "${name}" for ${this.path}: ${error.stack} */}`;
102
+ exportStatement.error = error;
103
+ return undefined;
104
+ })
105
+ .finally(() => {
106
+ exportStatement.done = true;
107
+ });
108
+ this.exports.set(name, exportStatement);
109
+ return name;
110
+ }
111
+ exportDefault(coder, isType = false) {
112
+ this.export(coder, isType, true);
113
+ }
114
+ /**
115
+ * Registers an import of `coder` from its owning module and returns the
116
+ * local alias used in this script.
117
+ *
118
+ * The coder is also exported from its home module as a side effect.
119
+ *
120
+ * @param coder - The coder to import.
121
+ * @param isType - Use a `import type` declaration.
122
+ * @param isDefault - Import the default export rather than a named export.
123
+ */
124
+ import(coder, isType = false, isDefault = false) {
125
+ debug("import coder: %s", coder.id);
126
+ const modulePath = coder.modulePath();
127
+ const cacheKey = `${coder.id}@${modulePath}:${isType}:${isDefault}`;
128
+ debug("cache key: %s", cacheKey);
129
+ if (this.cache.has(cacheKey)) {
130
+ debug("cache hit: %s", cacheKey);
131
+ return this.cache.get(cacheKey);
132
+ }
133
+ debug("cache miss: %s", cacheKey);
134
+ const name = this.firstUniqueName(coder);
135
+ this.cache.set(cacheKey, name);
136
+ const scriptFromWhichToExport = this.repository.get(modulePath);
137
+ const exportedName = scriptFromWhichToExport.export(coder, isType, isDefault);
138
+ this.imports.set(name, {
139
+ isDefault,
140
+ isType,
141
+ name: exportedName,
142
+ script: scriptFromWhichToExport,
143
+ });
144
+ return name;
145
+ }
146
+ importType(coder) {
147
+ return this.import(coder, true);
148
+ }
149
+ importDefault(coder, isType = false) {
150
+ return this.import(coder, isType, true);
151
+ }
152
+ /**
153
+ * Registers an import from an external npm package or absolute module path
154
+ * (not managed by the repository).
155
+ *
156
+ * @param name - The local binding name.
157
+ * @param modulePath - The module specifier (e.g. `"counterfact-types/index"`).
158
+ * @param isType - Use a `import type` declaration.
159
+ * @returns `name` (for convenience in method chaining).
160
+ */
161
+ importExternal(name, modulePath, isType = false) {
162
+ this.externalImport.set(name, { isType, modulePath });
163
+ return name;
164
+ }
165
+ /**
166
+ * Convenience wrapper that calls {@link importExternal} with `isType = true`.
167
+ */
168
+ importExternalType(name, modulePath) {
169
+ return this.importExternal(name, modulePath, true);
170
+ }
171
+ /**
172
+ * Imports a type from the shared `counterfact-types/index.ts` module,
173
+ * resolving the path relative to this script's location in the repository.
174
+ *
175
+ * @param name - The type name to import (e.g. `"WideOperationArgument"`).
176
+ */
177
+ importSharedType(name) {
178
+ return this.importExternal(name, pathJoin(this.relativePathToBase, "counterfact-types/index.ts"), true);
179
+ }
180
+ /**
181
+ * Imports a type from the generated `types/versions.ts` module,
182
+ * resolving the path relative to this script's location in the repository.
183
+ *
184
+ * @param name - The type name to import (e.g. `"Versioned"`).
185
+ */
186
+ importVersionsType(name) {
187
+ return this.importExternal(name, pathJoin(this.relativePathToBase, "types/versions.ts"), true);
188
+ }
189
+ exportType(coder) {
190
+ return this.export(coder, true);
191
+ }
192
+ /**
193
+ * Registers a formatter function for the merged versioned type emitted under
194
+ * `name` by {@link versionsTypeStatements}.
195
+ *
196
+ * When a formatter is present for a name, `versionsTypeStatements` delegates
197
+ * the entire type declaration to it instead of generating the default
198
+ * `Versions` object type. The formatter receives a `Map<version, importAlias>`
199
+ * and must return the complete TypeScript source for that operation type.
200
+ */
201
+ setVersionFormatter(name, formatter) {
202
+ this.versionFormatters.set(name, formatter);
203
+ }
204
+ declareVersion(coder, name) {
205
+ const version = coder.version;
206
+ const versions = this.versions.get(name) ?? new Map();
207
+ this.versions.set(name, versions);
208
+ if (versions.has(version)) {
209
+ return;
210
+ }
211
+ const versionStatement = {
212
+ beforeExport: "",
213
+ done: false,
214
+ id: coder.id,
215
+ isDefault: false,
216
+ isType: true,
217
+ jsdoc: "",
218
+ typeDeclaration: "",
219
+ };
220
+ versionStatement.promise = coder
221
+ .delegate()
222
+ .then((availableCoder) => {
223
+ versionStatement.code = availableCoder.write(this);
224
+ return availableCoder;
225
+ })
226
+ .catch((error) => {
227
+ versionStatement.code = `unknown /* error declaring version "${name}" (${version}) for ${this.path}: ${error.message} */`;
228
+ versionStatement.error = error;
229
+ return undefined;
230
+ })
231
+ .finally(() => {
232
+ versionStatement.done = true;
233
+ });
234
+ versions.set(version, versionStatement);
235
+ }
236
+ /** `true` while at least one export promise is still pending. */
237
+ isInProgress() {
238
+ return (Array.from(this.exports.values()).some((exportStatement) => !exportStatement.done) ||
239
+ Array.from(this.versions.values())
240
+ .flatMap((versions) => Array.from(versions.values()))
241
+ .some((versionStatement) => !versionStatement.done));
242
+ }
243
+ /** Returns a promise that resolves when all pending export promises settle. */
244
+ finished() {
245
+ return Promise.all([
246
+ ...Array.from(this.exports.values(), (value) => value.promise),
247
+ ...Array.from(this.versions.values())
248
+ .flatMap((versions) => Array.from(versions.values()))
249
+ .map((value) => value.promise),
250
+ ]);
251
+ }
252
+ externalImportStatements() {
253
+ return Array.from(this.externalImport, ([name, { isDefault, isType, modulePath }]) => `import${isType ? " type" : ""} ${isDefault ? name : `{ ${name} }`} from "${modulePath}";`);
254
+ }
255
+ importStatements() {
256
+ return Array.from(this.imports, ([name, { isDefault, isType, script }]) => {
257
+ const resolvedPath = escapePathForWindows(pathRelative(pathDirname(this.path), script.path.replace(/\.ts$/u, ".js")));
258
+ return `import${isType ? " type" : ""} ${isDefault ? name : `{ ${name} }`} from "${resolvedPath.includes("../") ? "" : "./"}${resolvedPath}";`;
259
+ });
260
+ }
261
+ exportStatements() {
262
+ return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, jsdoc, name, typeDeclaration, }) => {
263
+ if (typeof code === "object" && code !== null && "raw" in code) {
264
+ return code.raw;
265
+ }
266
+ if (isDefault) {
267
+ return `${jsdoc}${beforeExport}export default ${code};`;
268
+ }
269
+ const keyword = isType ? "type" : "const";
270
+ const typeAnnotation = (typeDeclaration ?? "").length === 0
271
+ ? ""
272
+ : `:${typeDeclaration ?? ""}`;
273
+ return `${jsdoc}${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
274
+ });
275
+ }
276
+ versionsTypeStatements() {
277
+ if (this.versions.size === 0) {
278
+ return [];
279
+ }
280
+ const statements = [];
281
+ const unformatted = [];
282
+ for (const [name, versions] of this.versions) {
283
+ const formatter = this.versionFormatters.get(name);
284
+ if (formatter) {
285
+ const versionCodes = new Map(Array.from(versions, ([version, stmt]) => [
286
+ version,
287
+ stmt.code,
288
+ ]));
289
+ statements.push(formatter(versionCodes));
290
+ }
291
+ else {
292
+ unformatted.push([name, versions]);
293
+ }
294
+ }
295
+ if (unformatted.length > 0) {
296
+ const names = unformatted.map(([name, versions]) => {
297
+ const mappedVersions = Array.from(versions, ([version, versionStatement]) => `"${version}": ${versionStatement.code}`);
298
+ return `"${name}": { ${mappedVersions.join(", ")} }`;
299
+ });
300
+ statements.push(`export type Versions = { ${names.join(", ")} };`);
301
+ }
302
+ return statements;
303
+ }
304
+ /**
305
+ * Formats the fully assembled script source with Prettier and returns it.
306
+ *
307
+ * All pending export promises are awaited before formatting.
308
+ */
309
+ async contents() {
310
+ await this.finished();
311
+ return format([
312
+ this.comments.map((comment) => `// ${comment}`).join("\n"),
313
+ this.comments.length > 0 ? "\n\n" : "",
314
+ this.externalImportStatements().join("\n"),
315
+ this.importStatements().join("\n"),
316
+ "\n\n",
317
+ this.versionsTypeStatements().join("\n"),
318
+ this.versions.size > 0 ? "\n\n" : "",
319
+ this.exportStatements().join("\n\n"),
320
+ ].join(""), { parser: "typescript" });
321
+ }
322
+ }
@@ -0,0 +1,40 @@
1
+ import { Requirement } from "./requirement.js";
2
+ /**
3
+ * Represents a fully dereferenced OpenAPI specification as a navigable tree
4
+ * of {@link Requirement} nodes.
5
+ *
6
+ * Use {@link Specification.fromFile} to load a spec from disk or a URL; the
7
+ * static method bundles external `$ref` references into a single in-memory
8
+ * object before constructing the tree.
9
+ */
10
+ export declare class Specification {
11
+ cache: Map<string, Requirement>;
12
+ rootRequirement: Requirement;
13
+ constructor(rootRequirement?: Requirement);
14
+ /**
15
+ * Loads the OpenAPI document at `urlOrPath`, bundles all external `$ref`
16
+ * references, and returns a fully initialised {@link Specification}.
17
+ *
18
+ * @param urlOrPath - A local file path or HTTP(S) URL.
19
+ * @param overlays - Optional ordered list of overlay file paths/URLs to
20
+ * apply after loading the document.
21
+ * @throws When the document cannot be found or parsed.
22
+ */
23
+ static fromFile(urlOrPath: string, overlays?: readonly string[]): Promise<Specification>;
24
+ /**
25
+ * Returns the {@link Requirement} at `url` (a JSON Pointer such as
26
+ * `"#/paths"`).
27
+ *
28
+ * @param url - A JSON Pointer string (must start with `"#/"`).
29
+ */
30
+ getRequirement(url: string): Requirement;
31
+ /**
32
+ * Loads (or reloads) the specification from `urlOrPath`, then applies any
33
+ * overlay files listed in `overlays` in order.
34
+ *
35
+ * @param urlOrPath - A local file path or HTTP(S) URL.
36
+ * @param overlays - Optional ordered list of overlay file paths/URLs.
37
+ * @throws When the document cannot be found or parsed.
38
+ */
39
+ load(urlOrPath: string, overlays?: readonly string[]): Promise<void>;
40
+ }
@@ -0,0 +1,58 @@
1
+ import { bundleOpenApiDocument } from "@counterfact/openapi";
2
+ import createDebug from "debug";
3
+ import { Requirement } from "./requirement.js";
4
+ const debug = createDebug("counterfact:typescript-generator:specification");
5
+ /**
6
+ * Represents a fully dereferenced OpenAPI specification as a navigable tree
7
+ * of {@link Requirement} nodes.
8
+ *
9
+ * Use {@link Specification.fromFile} to load a spec from disk or a URL; the
10
+ * static method bundles external `$ref` references into a single in-memory
11
+ * object before constructing the tree.
12
+ */
13
+ export class Specification {
14
+ cache;
15
+ rootRequirement;
16
+ constructor(rootRequirement) {
17
+ this.cache = new Map();
18
+ if (rootRequirement) {
19
+ this.rootRequirement = rootRequirement;
20
+ }
21
+ }
22
+ /**
23
+ * Loads the OpenAPI document at `urlOrPath`, bundles all external `$ref`
24
+ * references, and returns a fully initialised {@link Specification}.
25
+ *
26
+ * @param urlOrPath - A local file path or HTTP(S) URL.
27
+ * @param overlays - Optional ordered list of overlay file paths/URLs to
28
+ * apply after loading the document.
29
+ * @throws When the document cannot be found or parsed.
30
+ */
31
+ static async fromFile(urlOrPath, overlays = []) {
32
+ const specification = new Specification();
33
+ await specification.load(urlOrPath, overlays);
34
+ return specification;
35
+ }
36
+ /**
37
+ * Returns the {@link Requirement} at `url` (a JSON Pointer such as
38
+ * `"#/paths"`).
39
+ *
40
+ * @param url - A JSON Pointer string (must start with `"#/"`).
41
+ */
42
+ getRequirement(url) {
43
+ debug("getting requirement at %s", url);
44
+ return this.rootRequirement.select(url.slice(2));
45
+ }
46
+ /**
47
+ * Loads (or reloads) the specification from `urlOrPath`, then applies any
48
+ * overlay files listed in `overlays` in order.
49
+ *
50
+ * @param urlOrPath - A local file path or HTTP(S) URL.
51
+ * @param overlays - Optional ordered list of overlay file paths/URLs.
52
+ * @throws When the document cannot be found or parsed.
53
+ */
54
+ async load(urlOrPath, overlays = []) {
55
+ const document = (await bundleOpenApiDocument(urlOrPath, overlays));
56
+ this.rootRequirement = new Requirement(document, urlOrPath, this);
57
+ }
58
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Options for setting an HTTP cookie on a response.
3
+ * These correspond to standard `Set-Cookie` attributes and are passed to the
4
+ * `.cookie()` method on the response builder.
5
+ */
6
+ export interface CookieOptions {
7
+ domain?: string;
8
+ expires?: Date;
9
+ httpOnly?: boolean;
10
+ maxAge?: number;
11
+ path?: string;
12
+ sameSite?: "lax" | "none" | "strict";
13
+ secure?: boolean;
14
+ }