@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,53 @@
1
+ import createDebug from "debug";
2
+ import { OperationTypeCoder, } from "./operation-type-coder.js";
3
+ import { Specification } from "./specification.js";
4
+ const debug = createDebug("counterfact:typescript-generator:operation-type-name-mapping");
5
+ const HTTP_METHODS = new Set([
6
+ "GET",
7
+ "POST",
8
+ "PUT",
9
+ "DELETE",
10
+ "PATCH",
11
+ "HEAD",
12
+ "OPTIONS",
13
+ "QUERY",
14
+ ]);
15
+ function openApiPathToFilePath(openApiPath) {
16
+ if (openApiPath === "/") {
17
+ return "index";
18
+ }
19
+ return openApiPath.startsWith("/") ? openApiPath.slice(1) : openApiPath;
20
+ }
21
+ /**
22
+ * Resolves the generated operation type name for each route and HTTP method.
23
+ * This is the migration-facing API; concrete coder and specification classes
24
+ * remain internal implementation details.
25
+ */
26
+ export async function buildOperationTypeNameMapping(source, overlays = []) {
27
+ debug("building operation type name mapping from %s", source);
28
+ const specification = await Specification.fromFile(source, overlays);
29
+ const mapping = new Map();
30
+ const paths = specification.getRequirement("#/paths");
31
+ if (!paths) {
32
+ return mapping;
33
+ }
34
+ const securityRequirement = specification.getRequirement("#/components/securitySchemes");
35
+ const securitySchemes = Object.values(securityRequirement?.data ?? {});
36
+ paths.forEach((pathDefinition, openApiPath) => {
37
+ const methodMap = new Map();
38
+ pathDefinition.forEach((operation, requestMethod) => {
39
+ const method = requestMethod.toUpperCase();
40
+ if (!HTTP_METHODS.has(method)) {
41
+ return;
42
+ }
43
+ const typeName = new OperationTypeCoder(operation, "", requestMethod, securitySchemes)
44
+ .names()
45
+ .next().value;
46
+ methodMap.set(method, typeName);
47
+ });
48
+ if (methodMap.size > 0) {
49
+ mapping.set(openApiPathToFilePath(openApiPath), methodMap);
50
+ }
51
+ });
52
+ return mapping;
53
+ }
@@ -0,0 +1,13 @@
1
+ import { TypeCoder } from "./type-coder.js";
2
+ import type { Requirement } from "./requirement.js";
3
+ export declare class ParameterExportTypeCoder extends TypeCoder {
4
+ _typeName: string;
5
+ _typeCode: string;
6
+ _parameterKind: string;
7
+ _modulePath: string;
8
+ constructor(requirement: Requirement, version: string, typeName: string, typeCode: string, parameterKind: string);
9
+ get id(): string;
10
+ names(): Generator<string>;
11
+ writeCode(): string;
12
+ modulePath(): string;
13
+ }
@@ -0,0 +1,27 @@
1
+ import { TypeCoder } from "./type-coder.js";
2
+ export class ParameterExportTypeCoder extends TypeCoder {
3
+ _typeName;
4
+ _typeCode;
5
+ _parameterKind;
6
+ _modulePath;
7
+ constructor(requirement, version, typeName, typeCode, parameterKind) {
8
+ super(requirement, version);
9
+ this._typeName = typeName;
10
+ this._typeCode = typeCode;
11
+ this._parameterKind = parameterKind;
12
+ }
13
+ get id() {
14
+ // Make the id unique by including the parameter kind
15
+ return `${super.id}:${this._parameterKind}`;
16
+ }
17
+ *names() {
18
+ yield this._typeName;
19
+ }
20
+ writeCode() {
21
+ return this._typeCode;
22
+ }
23
+ modulePath() {
24
+ // Use the same module path as the parent operation
25
+ return this._modulePath;
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ import { TypeCoder } from "./type-coder.js";
2
+ import type { Requirement } from "./requirement.js";
3
+ import type { Script } from "./script.js";
4
+ export declare class ParametersTypeCoder extends TypeCoder {
5
+ placement: string;
6
+ constructor(requirement: Requirement, version?: string, placement?: string);
7
+ names(): Generator<string>;
8
+ writeCode(script: Script): string;
9
+ modulePath(): string;
10
+ }
@@ -0,0 +1,44 @@
1
+ import { pathJoin } from "./forward-slash-path.js";
2
+ import { buildJsDoc } from "./jsdoc.js";
3
+ import { SchemaTypeCoder } from "./schema-type-coder.js";
4
+ import { TypeCoder } from "./type-coder.js";
5
+ export class ParametersTypeCoder extends TypeCoder {
6
+ placement;
7
+ constructor(requirement, version = "", placement = "") {
8
+ super(requirement, version);
9
+ this.placement = placement;
10
+ }
11
+ names() {
12
+ return super.names("parameters");
13
+ }
14
+ writeCode(script) {
15
+ const typeDefinitions = (this.requirement?.data ?? [])
16
+ .map((_, index) => {
17
+ return this.requirement.get(index);
18
+ })
19
+ .filter((parameter) => parameter.get("in").data === this.placement)
20
+ .map((parameter) => {
21
+ const name = parameter.get("name")?.data;
22
+ const required = parameter.get("required")?.data;
23
+ const optionalFlag = required ? "" : "?";
24
+ const schema = parameter.has("schema")
25
+ ? parameter.get("schema")
26
+ : parameter;
27
+ const comment = buildJsDoc(parameter.data);
28
+ const commentPrefix = comment ? `\n${comment}` : "";
29
+ const typeString = new SchemaTypeCoder(schema, this.version).write(script);
30
+ return `${commentPrefix}"${name}"${optionalFlag}: ${typeString}`;
31
+ });
32
+ if (typeDefinitions.length === 0) {
33
+ return "never";
34
+ }
35
+ return `{${typeDefinitions.join(", ")}}`;
36
+ }
37
+ modulePath() {
38
+ const pathString = this.requirement.url
39
+ .split("/")
40
+ .at(-2)
41
+ .replaceAll("~1", "/");
42
+ return `${pathJoin("parameters", pathString)}.types.ts`;
43
+ }
44
+ }
@@ -0,0 +1,2 @@
1
+ export declare function printObjectWithoutQuotes(entries: [string, string][]): string;
2
+ export declare function printObject(entries: [string, string][]): string;
@@ -0,0 +1,10 @@
1
+ export function printObjectWithoutQuotes(entries) {
2
+ return `{\n${entries
3
+ .map(([key, value]) => `${key}: ${value}`)
4
+ .join(",\n")}\n}`;
5
+ }
6
+ export function printObject(entries) {
7
+ return `{\n${entries
8
+ .map(([key, value]) => `"${key}": ${value}`)
9
+ .join(",\n")}\n}`;
10
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Prunes route files that no longer correspond to any path in the OpenAPI spec.
3
+ * Context files (_.context.ts) are never pruned.
4
+ * @param destination - Base destination directory (contains the routes/ sub-directory)
5
+ * @param openApiPaths - Iterable of OpenAPI path strings (e.g. "/pet/{id}")
6
+ * @returns Number of files removed
7
+ */
8
+ export declare function pruneRoutes(destination: string, openApiPaths: Iterable<string>): Promise<number>;
9
+ /**
10
+ * Prunes obsolete files from the OpenAPI generator-owned namespaces below
11
+ * `types/`. Hidden files are included by the recursive directory walk.
12
+ *
13
+ * `types/_.context.ts` and paths outside the known generated namespaces are
14
+ * preserved because separate generators or users own them.
15
+ *
16
+ * @param destination - Base destination directory (contains `types/`).
17
+ * @param expectedPaths - Repository-relative generated paths to retain.
18
+ * @returns Number of files removed.
19
+ */
20
+ export declare function pruneTypes(destination: string, expectedPaths: Iterable<string>): Promise<number>;
package/dist/prune.js ADDED
@@ -0,0 +1,146 @@
1
+ import fs from "node:fs/promises";
2
+ import nodePath from "node:path";
3
+ /* eslint-disable security/detect-non-literal-fs-filename -- pruning only traverses and removes files under generator-owned destination subdirectories. */
4
+ import createDebug from "debug";
5
+ import { toForwardSlashPath } from "./forward-slash-path.js";
6
+ import { normalizeOpenApiPath } from "./openapi-path.js";
7
+ const debug = createDebug("counterfact:typescript-generator:prune");
8
+ /**
9
+ * Collects all .ts route files in a directory recursively.
10
+ * Context files (_.context.ts) are excluded.
11
+ * @param routesDir - Path to routes directory
12
+ * @param currentPath - Current subdirectory being processed (relative to routesDir)
13
+ * @returns Array of relative paths (using forward slashes)
14
+ */
15
+ async function collectTypeScriptFiles(rootDir, currentPath = "") {
16
+ const files = [];
17
+ try {
18
+ const fullDir = currentPath ? nodePath.join(rootDir, currentPath) : rootDir;
19
+ const entries = await fs.readdir(fullDir, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ const relativePath = currentPath
22
+ ? `${currentPath}/${entry.name}`
23
+ : entry.name;
24
+ if (entry.isDirectory()) {
25
+ files.push(...(await collectTypeScriptFiles(rootDir, relativePath)));
26
+ }
27
+ else if (entry.name.endsWith(".ts")) {
28
+ files.push(relativePath);
29
+ }
30
+ }
31
+ }
32
+ catch (error) {
33
+ if (error.code !== "ENOENT") {
34
+ throw error;
35
+ }
36
+ }
37
+ return files;
38
+ }
39
+ /**
40
+ * Recursively removes empty directories under rootDir, but not rootDir itself.
41
+ * @param dir - Directory to check
42
+ * @param rootDir - Root directory that should never be removed
43
+ */
44
+ async function removeEmptyDirectories(dir, rootDir) {
45
+ let entries;
46
+ try {
47
+ entries = await fs.readdir(dir, { withFileTypes: true });
48
+ }
49
+ catch (error) {
50
+ debug("could not read directory %s: %o", dir, error);
51
+ return;
52
+ }
53
+ for (const entry of entries) {
54
+ if (entry.isDirectory()) {
55
+ await removeEmptyDirectories(nodePath.join(dir, entry.name), rootDir);
56
+ }
57
+ }
58
+ if (nodePath.resolve(dir) === nodePath.resolve(rootDir)) {
59
+ return;
60
+ }
61
+ const remaining = await fs.readdir(dir);
62
+ if (remaining.length === 0) {
63
+ await fs.rmdir(dir);
64
+ debug("removed empty directory: %s", dir);
65
+ }
66
+ }
67
+ /**
68
+ * Converts an OpenAPI path to the expected route file path (relative to routesDir).
69
+ * e.g. "/pet/{id}" -> "pet/{id}.ts", "/" -> "index.ts"
70
+ * @param openApiPath - The OpenAPI path string
71
+ */
72
+ function openApiPathToRouteFile(openApiPath) {
73
+ const normalizedPath = normalizeOpenApiPath(openApiPath);
74
+ const filePath = normalizedPath === "/" ? "index" : normalizedPath.slice(1);
75
+ return `${filePath}.ts`;
76
+ }
77
+ /**
78
+ * Prunes route files that no longer correspond to any path in the OpenAPI spec.
79
+ * Context files (_.context.ts) are never pruned.
80
+ * @param destination - Base destination directory (contains the routes/ sub-directory)
81
+ * @param openApiPaths - Iterable of OpenAPI path strings (e.g. "/pet/{id}")
82
+ * @returns Number of files removed
83
+ */
84
+ export async function pruneRoutes(destination, openApiPaths) {
85
+ const routesDir = nodePath.join(destination, "routes");
86
+ const expectedFiles = new Set(Array.from(openApiPaths).map(openApiPathToRouteFile));
87
+ debug("expected route files: %o", Array.from(expectedFiles));
88
+ const actualFiles = (await collectTypeScriptFiles(routesDir)).filter((file) => nodePath.basename(file) !== "_.context.ts");
89
+ debug("actual route files: %o", actualFiles);
90
+ let prunedCount = 0;
91
+ for (const file of actualFiles) {
92
+ const normalizedFile = toForwardSlashPath(file);
93
+ if (!expectedFiles.has(normalizedFile)) {
94
+ const fullPath = nodePath.join(routesDir, file);
95
+ debug("pruning %s", fullPath);
96
+ await fs.rm(fullPath);
97
+ prunedCount++;
98
+ }
99
+ }
100
+ await removeEmptyDirectories(routesDir, routesDir);
101
+ debug("pruned %d files", prunedCount);
102
+ return prunedCount;
103
+ }
104
+ /**
105
+ * Returns whether a path below `types/` belongs to a namespace populated by
106
+ * the OpenAPI generator. Context support and arbitrary user-authored type
107
+ * modules are deliberately outside this ownership policy.
108
+ */
109
+ function isGeneratorOwnedTypePath(path) {
110
+ const [first, second] = toForwardSlashPath(path).split("/");
111
+ const generatedCategory = (segment) => segment === "paths" || segment === "components" || segment === "#";
112
+ return (path === "versions.ts" ||
113
+ generatedCategory(first) ||
114
+ generatedCategory(second));
115
+ }
116
+ /**
117
+ * Prunes obsolete files from the OpenAPI generator-owned namespaces below
118
+ * `types/`. Hidden files are included by the recursive directory walk.
119
+ *
120
+ * `types/_.context.ts` and paths outside the known generated namespaces are
121
+ * preserved because separate generators or users own them.
122
+ *
123
+ * @param destination - Base destination directory (contains `types/`).
124
+ * @param expectedPaths - Repository-relative generated paths to retain.
125
+ * @returns Number of files removed.
126
+ */
127
+ export async function pruneTypes(destination, expectedPaths) {
128
+ const typesDir = nodePath.join(destination, "types");
129
+ const expectedFiles = new Set(Array.from(expectedPaths, (path) => toForwardSlashPath(path).replace(/^types\//u, "")));
130
+ const actualFiles = (await collectTypeScriptFiles(typesDir)).filter(isGeneratorOwnedTypePath);
131
+ let prunedCount = 0;
132
+ debug("expected type files: %o", Array.from(expectedFiles));
133
+ debug("actual generated type files: %o", actualFiles);
134
+ for (const file of actualFiles) {
135
+ const normalizedFile = toForwardSlashPath(file);
136
+ if (!expectedFiles.has(normalizedFile)) {
137
+ const fullPath = nodePath.join(typesDir, file);
138
+ debug("pruning %s", fullPath);
139
+ await fs.rm(fullPath);
140
+ prunedCount++;
141
+ }
142
+ }
143
+ await removeEmptyDirectories(typesDir, typesDir);
144
+ debug("pruned %d type files", prunedCount);
145
+ return prunedCount;
146
+ }
@@ -0,0 +1 @@
1
+ export declare const READ_ONLY_COMMENTS: string[];
@@ -0,0 +1,5 @@
1
+ export const READ_ONLY_COMMENTS = [
2
+ "This code was automatically generated from an OpenAPI description.",
3
+ "Do not edit this file. Edit the OpenAPI file instead.",
4
+ "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md",
5
+ ];
@@ -0,0 +1,87 @@
1
+ import { Script } from "./script.js";
2
+ interface WriteFilesOptions {
3
+ routes?: boolean;
4
+ types?: boolean;
5
+ }
6
+ /**
7
+ * Collection of {@link Script} objects keyed by their repository-relative
8
+ * path.
9
+ *
10
+ * Coders call {@link get} to obtain (or create) the script where they should
11
+ * export their generated TypeScript. After all coders have been registered,
12
+ * {@link writeFiles} waits for every script to finish and writes the output to
13
+ * disk.
14
+ */
15
+ export declare class Repository {
16
+ scripts: Map<string, Script>;
17
+ constructor();
18
+ /**
19
+ * Returns the {@link Script} for `path`, creating it if it does not yet
20
+ * exist.
21
+ *
22
+ * @param path - Repository-relative path (e.g. `"routes/pets.ts"`).
23
+ */
24
+ get(path: string): Script;
25
+ /** Waits until all scripts have resolved all of their pending export promises. */
26
+ finished(): Promise<void>;
27
+ /**
28
+ * Copies the compiled `counterfact-types` directory from the Counterfact
29
+ * distribution into the generated output tree.
30
+ *
31
+ * Returns `false` when the source directory does not exist (e.g. running
32
+ * from source without a prior build).
33
+ *
34
+ * @param destination - The root of the generated output tree.
35
+ */
36
+ copyCoreFiles(destination: string): Promise<boolean | void>;
37
+ /**
38
+ * Waits for all scripts to finish, then writes each one to disk.
39
+ *
40
+ * Route files (`routes/…`) are never fully overwritten if they already exist
41
+ * on disk, preserving user edits. However, if the generated script contains
42
+ * HTTP-method handler exports that are absent from the existing file, those
43
+ * new exports (and their `import type` statements) are appended to the file.
44
+ * Type files (`types/…`) are always overwritten.
45
+ *
46
+ * @param destination - Absolute path to the output root directory.
47
+ * @param options - Controls which artefacts are written.
48
+ */
49
+ writeFiles(destination: string, { routes, types }: WriteFilesOptions): Promise<void>;
50
+ /**
51
+ * Creates the default `routes/_.context.ts` file if it does not already
52
+ * exist.
53
+ *
54
+ * @param destination - Absolute path to the output root directory.
55
+ */
56
+ createDefaultContextFile(destination: string): Promise<void>;
57
+ /**
58
+ * Appends any HTTP-method handler exports that appear in `generatedContent`
59
+ * but are absent from the existing file at `fullPath`.
60
+ *
61
+ * For each new export the corresponding `import type` statement is inserted
62
+ * after the last existing import line (or prepended when no imports exist),
63
+ * and the export block is appended at the end of the file.
64
+ *
65
+ * @param fullPath - Absolute path of the route file to update.
66
+ * @param generatedContent - The fully-generated file content (used as the
67
+ * source of new import and export statements).
68
+ */
69
+ private appendNewHandlers;
70
+ /**
71
+ * Returns the path of the `_.context.ts` file that is nearest to `path` in
72
+ * the directory hierarchy, relative to the script's output directory.
73
+ *
74
+ * @param destination - Output root directory.
75
+ * @param path - Repository-relative path of the script being generated.
76
+ */
77
+ findContextPath(destination: string, path: string): string;
78
+ /**
79
+ * Walks up the directory tree from `path` to find the nearest
80
+ * `_.context.ts` file, falling back to `routes/_.context.ts` at the root.
81
+ *
82
+ * @param destination - Output root directory.
83
+ * @param path - Repository-relative path to start from.
84
+ */
85
+ nearestContextFile(destination: string, path: string): string;
86
+ }
87
+ export {};