zod-codegen 1.5.0 → 1.6.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/.github/workflows/ci.yml +6 -0
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +37 -0
- package/CONTRIBUTING.md +1 -1
- package/EXAMPLES.md +91 -12
- package/README.md +11 -4
- package/dist/scripts/add-js-extensions.d.ts +2 -0
- package/dist/scripts/add-js-extensions.d.ts.map +1 -0
- package/dist/scripts/add-js-extensions.js +66 -0
- package/dist/scripts/update-manifest.d.ts +14 -0
- package/dist/scripts/update-manifest.d.ts.map +1 -0
- package/dist/scripts/update-manifest.js +33 -0
- package/dist/src/assets/manifest.json +1 -1
- package/dist/src/cli.js +3 -3
- package/dist/src/generator.d.ts +46 -4
- package/dist/src/generator.d.ts.map +1 -1
- package/dist/src/generator.js +43 -1
- package/dist/src/interfaces/code-generator.d.ts +1 -1
- package/dist/src/interfaces/code-generator.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.d.ts +5 -3
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +69 -1
- package/dist/src/services/file-reader.service.d.ts +2 -2
- package/dist/src/services/file-reader.service.d.ts.map +1 -1
- package/dist/src/services/file-writer.service.d.ts +1 -1
- package/dist/src/services/file-writer.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.d.ts +1 -1
- package/dist/src/services/import-builder.service.d.ts.map +1 -1
- package/dist/src/services/import-builder.service.js +1 -1
- package/dist/src/services/type-builder.service.d.ts +1 -1
- package/dist/src/services/type-builder.service.d.ts.map +1 -1
- package/dist/src/types/generator-options.d.ts +1 -1
- package/dist/src/types/generator-options.d.ts.map +1 -1
- package/dist/src/utils/error-handler.d.ts +3 -2
- package/dist/src/utils/error-handler.d.ts.map +1 -1
- package/dist/src/utils/error-handler.js +4 -4
- package/dist/src/utils/reporter.d.ts +3 -2
- package/dist/src/utils/reporter.d.ts.map +1 -1
- package/dist/src/utils/reporter.js +7 -5
- package/dist/src/utils/signal-handler.d.ts +3 -2
- package/dist/src/utils/signal-handler.d.ts.map +1 -1
- package/dist/src/utils/signal-handler.js +4 -4
- package/examples/README.md +10 -1
- package/examples/petstore/README.md +6 -6
- package/examples/petstore/authenticated-usage.ts +1 -1
- package/examples/petstore/basic-usage.ts +1 -1
- package/examples/petstore/retry-handler-usage.ts +173 -0
- package/examples/petstore/server-variables-usage.ts +1 -1
- package/examples/petstore/type.ts +68 -47
- package/examples/pokeapi/README.md +3 -3
- package/examples/pokeapi/basic-usage.ts +1 -1
- package/examples/pokeapi/custom-client.ts +1 -1
- package/generated/type.ts +323 -0
- package/package.json +10 -13
- package/scripts/add-js-extensions.ts +79 -0
- package/scripts/update-manifest.ts +4 -2
- package/src/assets/manifest.json +1 -1
- package/src/cli.ts +7 -7
- package/src/generator.ts +51 -9
- package/src/interfaces/code-generator.ts +1 -1
- package/src/services/code-generator.service.ts +114 -8
- package/src/services/file-reader.service.ts +3 -3
- package/src/services/file-writer.service.ts +1 -1
- package/src/services/import-builder.service.ts +2 -2
- package/src/services/type-builder.service.ts +1 -1
- package/src/types/generator-options.ts +1 -1
- package/src/utils/error-handler.ts +6 -5
- package/src/utils/reporter.ts +6 -3
- package/src/utils/signal-handler.ts +10 -8
- package/tests/integration/cli-comprehensive.test.ts +123 -0
- package/tests/integration/cli.test.ts +2 -2
- package/tests/integration/error-scenarios.test.ts +240 -0
- package/tests/integration/snapshots.test.ts +131 -0
- package/tests/unit/code-generator-edge-cases.test.ts +551 -0
- package/tests/unit/code-generator.test.ts +385 -2
- package/tests/unit/file-reader.test.ts +16 -1
- package/tests/unit/generator.test.ts +19 -2
- package/tests/unit/naming-convention.test.ts +30 -1
- package/tests/unit/reporter.test.ts +63 -0
- package/tests/unit/type-builder.test.ts +131 -0
- package/tsconfig.json +3 -3
- package/dist/src/http/fetch-client.d.ts +0 -15
- package/dist/src/http/fetch-client.d.ts.map +0 -1
- package/dist/src/http/fetch-client.js +0 -140
- package/dist/src/polyfills/fetch.d.ts +0 -5
- package/dist/src/polyfills/fetch.d.ts.map +0 -1
- package/dist/src/polyfills/fetch.js +0 -18
- package/dist/src/types/http.d.ts +0 -25
- package/dist/src/types/http.d.ts.map +0 -1
- package/dist/src/types/http.js +0 -10
- package/dist/src/utils/manifest.d.ts +0 -8
- package/dist/src/utils/manifest.d.ts.map +0 -1
- package/dist/src/utils/manifest.js +0 -9
- package/dist/src/utils/tty.d.ts +0 -2
- package/dist/src/utils/tty.d.ts.map +0 -1
- package/dist/src/utils/tty.js +0 -3
- package/src/http/fetch-client.ts +0 -181
- package/src/polyfills/fetch.ts +0 -26
- package/src/types/http.ts +0 -35
- package/src/utils/manifest.ts +0 -17
- package/src/utils/tty.ts +0 -3
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typescript": "^5.9.3",
|
|
18
18
|
"url-pattern": "^1.0.3",
|
|
19
19
|
"yargs": "^18.0.0",
|
|
20
|
-
"zod": "^4.
|
|
20
|
+
"zod": "^4.3.5"
|
|
21
21
|
},
|
|
22
22
|
"description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications",
|
|
23
23
|
"keywords": [
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"validation"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@commitlint/cli": "^20.
|
|
36
|
-
"@commitlint/config-conventional": "^20.
|
|
35
|
+
"@commitlint/cli": "^20.3.0",
|
|
36
|
+
"@commitlint/config-conventional": "^20.3.0",
|
|
37
37
|
"@eslint/js": "^9.39.2",
|
|
38
38
|
"@semantic-release/changelog": "^6.0.3",
|
|
39
39
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"@types/jest": "^30.0.0",
|
|
42
42
|
"@types/js-yaml": "^4.0.9",
|
|
43
43
|
"@types/jsonpath": "^0.2.4",
|
|
44
|
-
"@types/node": "^25.0.
|
|
44
|
+
"@types/node": "^25.0.3",
|
|
45
45
|
"@types/yargs": "^17.0.35",
|
|
46
|
-
"@vitest/coverage-v8": "^4.0.
|
|
46
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
47
47
|
"cross-env": "^10.1.0",
|
|
48
48
|
"eslint": "^9.39.2",
|
|
49
49
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -51,13 +51,10 @@
|
|
|
51
51
|
"lint-staged": "^16.2.7",
|
|
52
52
|
"semantic-release": "^25.0.2",
|
|
53
53
|
"ts-node": "^10.9.2",
|
|
54
|
-
"typescript-eslint": "^8.
|
|
54
|
+
"typescript-eslint": "^8.51.0",
|
|
55
55
|
"undici": "^7.16.0",
|
|
56
56
|
"vitest": "^4.0.14"
|
|
57
57
|
},
|
|
58
|
-
"optionalDependencies": {
|
|
59
|
-
"undici": "^7.16.0"
|
|
60
|
-
},
|
|
61
58
|
"homepage": "https://github.com/julienandreu/zod-codegen",
|
|
62
59
|
"license": "Apache-2.0",
|
|
63
60
|
"name": "zod-codegen",
|
|
@@ -75,7 +72,7 @@
|
|
|
75
72
|
"url": "git+ssh://git@github.com/julienandreu/zod-codegen.git"
|
|
76
73
|
},
|
|
77
74
|
"engines": {
|
|
78
|
-
"node": ">=
|
|
75
|
+
"node": ">=18.0.0"
|
|
79
76
|
},
|
|
80
77
|
"overrides": {
|
|
81
78
|
"npm": {
|
|
@@ -88,8 +85,8 @@
|
|
|
88
85
|
},
|
|
89
86
|
"scripts": {
|
|
90
87
|
"audit:fix": "npm audit fix",
|
|
91
|
-
"build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js'",
|
|
92
|
-
"build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
|
|
88
|
+
"build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && ts-node scripts/add-js-extensions.ts && mkdir -p dist/src/assets && cp -r src/assets/. dist/src/assets/ && chmod +x ./dist/src/cli.js'",
|
|
89
|
+
"build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && ts-node scripts/add-js-extensions.ts && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
|
|
93
90
|
"build:watch": "tsc --project tsconfig.json --watch",
|
|
94
91
|
"dev": "npm run build && node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ./examples/petstore && npm run format && npm run lint",
|
|
95
92
|
"lint": "eslint src --fix",
|
|
@@ -110,5 +107,5 @@
|
|
|
110
107
|
"release": "semantic-release",
|
|
111
108
|
"release:dry": "semantic-release --dry-run"
|
|
112
109
|
},
|
|
113
|
-
"version": "1.
|
|
110
|
+
"version": "1.6.1"
|
|
114
111
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {readdirSync, readFileSync, statSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {extname, join} from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively finds all .js files in a directory
|
|
6
|
+
*/
|
|
7
|
+
function findJsFiles(dir: string): string[] {
|
|
8
|
+
const files: string[] = [];
|
|
9
|
+
const entries = readdirSync(dir);
|
|
10
|
+
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = join(dir, entry);
|
|
13
|
+
const stat = statSync(fullPath);
|
|
14
|
+
|
|
15
|
+
if (stat.isDirectory()) {
|
|
16
|
+
files.push(...findJsFiles(fullPath));
|
|
17
|
+
} else if (extname(entry) === '.js') {
|
|
18
|
+
files.push(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Adds .js extensions to relative imports in a JavaScript file
|
|
27
|
+
*/
|
|
28
|
+
function addJsExtensions(content: string): string {
|
|
29
|
+
// Match relative imports: './something' or '../something' but not './something.js' or node: imports
|
|
30
|
+
// Handles both single and double quotes
|
|
31
|
+
// Also handles type imports: import type { ... } from './something'
|
|
32
|
+
const importRegex = /from\s+(['"])(\.\.?\/[^'"]+?)(['"])/g;
|
|
33
|
+
|
|
34
|
+
return content.replace(importRegex, (match, quote: string, importPath: string, endQuote: string) => {
|
|
35
|
+
// Skip if already has an extension
|
|
36
|
+
if (importPath.endsWith('.js') || importPath.endsWith('.json')) {
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add .js extension
|
|
41
|
+
return `from ${quote}${importPath}.js${endQuote}`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Main function to process all JavaScript files in dist/src
|
|
47
|
+
*/
|
|
48
|
+
function main(): void {
|
|
49
|
+
const distDir = join(process.cwd(), 'dist', 'src');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const jsFiles = findJsFiles(distDir);
|
|
53
|
+
|
|
54
|
+
if (jsFiles.length === 0) {
|
|
55
|
+
console.warn('No .js files found in dist/src');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let processedCount = 0;
|
|
60
|
+
|
|
61
|
+
for (const filePath of jsFiles) {
|
|
62
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
63
|
+
const updatedContent = addJsExtensions(content);
|
|
64
|
+
|
|
65
|
+
if (content !== updatedContent) {
|
|
66
|
+
writeFileSync(filePath, updatedContent, 'utf-8');
|
|
67
|
+
processedCount++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`✅ Added .js extensions to ${String(processedCount)} file(s)`);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('❌ Error processing files:', error);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {readFileSync, writeFileSync} from 'fs';
|
|
2
|
-
import {resolve} from 'path';
|
|
1
|
+
import {readFileSync, writeFileSync} from 'node:fs';
|
|
2
|
+
import {dirname, resolve} from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
3
4
|
import {z} from 'zod';
|
|
4
5
|
|
|
5
6
|
interface PackageJson {
|
|
@@ -30,6 +31,7 @@ export function isPackageJson(input: unknown): input is PackageJson {
|
|
|
30
31
|
return success;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
35
|
const sourcePath = resolve(__dirname, '..', 'package.json');
|
|
34
36
|
|
|
35
37
|
const data: unknown = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
package/src/assets/manifest.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import yargs from 'yargs';
|
|
4
4
|
import {hideBin} from 'yargs/helpers';
|
|
5
|
-
import {Generator, type GeneratorOptions, type NamingConvention} from './generator
|
|
5
|
+
import {Generator, type GeneratorOptions, type NamingConvention} from './generator';
|
|
6
6
|
import {readFileSync} from 'node:fs';
|
|
7
7
|
import {fileURLToPath} from 'node:url';
|
|
8
8
|
import {dirname, join} from 'node:path';
|
|
9
9
|
|
|
10
10
|
import loudRejection from 'loud-rejection';
|
|
11
|
-
import {handleErrors} from './utils/error-handler
|
|
12
|
-
import {handleSignals} from './utils/signal-handler
|
|
11
|
+
import {handleErrors} from './utils/error-handler';
|
|
12
|
+
import {handleSignals} from './utils/signal-handler';
|
|
13
13
|
import debug from 'debug';
|
|
14
|
-
import {Reporter} from './utils/reporter
|
|
14
|
+
import {Reporter} from './utils/reporter';
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
// Read package.json from the project root
|
|
@@ -47,14 +47,14 @@ const packageData = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const {name, description, version} = packageData;
|
|
50
|
-
const reporter = new Reporter(process.stdout);
|
|
50
|
+
const reporter = new Reporter(process.stdout, process.stderr);
|
|
51
51
|
const startTime = process.hrtime.bigint();
|
|
52
52
|
|
|
53
53
|
debug(`${name}:${String(process.pid)}`);
|
|
54
54
|
|
|
55
55
|
loudRejection();
|
|
56
|
-
handleSignals(process, startTime);
|
|
57
|
-
handleErrors(process, startTime);
|
|
56
|
+
handleSignals(process, startTime, reporter);
|
|
57
|
+
handleErrors(process, startTime, reporter);
|
|
58
58
|
|
|
59
59
|
const argv = yargs(hideBin(process.argv))
|
|
60
60
|
.scriptName(name)
|
package/src/generator.ts
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
|
-
import type {Reporter} from './utils/reporter
|
|
2
|
-
import type {OpenApiSpecType} from './types/openapi
|
|
3
|
-
import type {GeneratorOptions} from './types/generator-options
|
|
4
|
-
import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service
|
|
5
|
-
import {TypeScriptCodeGeneratorService} from './services/code-generator.service
|
|
6
|
-
import {SyncFileWriterService} from './services/file-writer.service
|
|
1
|
+
import type {Reporter} from './utils/reporter';
|
|
2
|
+
import type {OpenApiSpecType} from './types/openapi';
|
|
3
|
+
import type {GeneratorOptions} from './types/generator-options';
|
|
4
|
+
import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service';
|
|
5
|
+
import {TypeScriptCodeGeneratorService} from './services/code-generator.service';
|
|
6
|
+
import {SyncFileWriterService} from './services/file-writer.service';
|
|
7
7
|
|
|
8
8
|
// Re-export types for library users
|
|
9
|
-
export type {GeneratorOptions} from './types/generator-options
|
|
10
|
-
export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention
|
|
9
|
+
export type {GeneratorOptions} from './types/generator-options';
|
|
10
|
+
export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Main generator class for creating TypeScript code from OpenAPI specifications.
|
|
14
|
+
*
|
|
15
|
+
* This class orchestrates the code generation process:
|
|
16
|
+
* 1. Reads the OpenAPI specification file (local or remote)
|
|
17
|
+
* 2. Parses and validates the specification
|
|
18
|
+
* 3. Generates TypeScript code with Zod schemas and type-safe API client
|
|
19
|
+
* 4. Writes the generated code to the output directory
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import {Generator} from 'zod-codegen';
|
|
24
|
+
* import {Reporter} from './utils/reporter';
|
|
25
|
+
*
|
|
26
|
+
* const reporter = new Reporter(process.stdout, process.stderr);
|
|
27
|
+
* const generator = new Generator(
|
|
28
|
+
* 'my-app',
|
|
29
|
+
* '1.0.0',
|
|
30
|
+
* reporter,
|
|
31
|
+
* './openapi.yaml',
|
|
32
|
+
* './generated',
|
|
33
|
+
* {namingConvention: 'camelCase'}
|
|
34
|
+
* );
|
|
35
|
+
*
|
|
36
|
+
* const exitCode = await generator.run();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
12
39
|
export class Generator {
|
|
13
40
|
private readonly fileReader = new SyncFileReaderService();
|
|
14
41
|
private readonly fileParser = new OpenApiFileParserService();
|
|
@@ -16,6 +43,16 @@ export class Generator {
|
|
|
16
43
|
private readonly fileWriter: SyncFileWriterService;
|
|
17
44
|
private readonly outputPath: string;
|
|
18
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new Generator instance.
|
|
48
|
+
*
|
|
49
|
+
* @param _name - The name of the application/library (used in generated file headers)
|
|
50
|
+
* @param _version - The version of the application/library (used in generated file headers)
|
|
51
|
+
* @param reporter - Reporter instance for logging messages and errors
|
|
52
|
+
* @param inputPath - Path or URL to the OpenAPI specification file
|
|
53
|
+
* @param _outputDir - Directory where generated files will be written
|
|
54
|
+
* @param options - Optional configuration for code generation
|
|
55
|
+
*/
|
|
19
56
|
constructor(
|
|
20
57
|
private readonly _name: string,
|
|
21
58
|
private readonly _version: string,
|
|
@@ -29,6 +66,11 @@ export class Generator {
|
|
|
29
66
|
this.codeGenerator = new TypeScriptCodeGeneratorService(options);
|
|
30
67
|
}
|
|
31
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Executes the code generation process.
|
|
71
|
+
*
|
|
72
|
+
* @returns Promise that resolves to an exit code (0 for success, 1 for failure)
|
|
73
|
+
*/
|
|
32
74
|
async run(): Promise<number> {
|
|
33
75
|
try {
|
|
34
76
|
const rawSource = await this.readFile();
|
|
@@ -46,7 +88,7 @@ export class Generator {
|
|
|
46
88
|
this.reporter.error('❌ An unknown error occurred');
|
|
47
89
|
}
|
|
48
90
|
|
|
49
|
-
return
|
|
91
|
+
return 1;
|
|
50
92
|
}
|
|
51
93
|
}
|
|
52
94
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import jp from 'jsonpath';
|
|
2
2
|
import * as ts from 'typescript';
|
|
3
3
|
import {z} from 'zod';
|
|
4
|
-
import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator
|
|
5
|
-
import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi
|
|
6
|
-
import type {GeneratorOptions} from '../types/generator-options
|
|
7
|
-
import {MethodSchema, Reference, SchemaProperties} from '../types/openapi
|
|
8
|
-
import {TypeScriptImportBuilderService} from './import-builder.service
|
|
9
|
-
import {TypeScriptTypeBuilderService} from './type-builder.service
|
|
4
|
+
import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator';
|
|
5
|
+
import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi';
|
|
6
|
+
import type {GeneratorOptions} from '../types/generator-options';
|
|
7
|
+
import {MethodSchema, Reference, SchemaProperties} from '../types/openapi';
|
|
8
|
+
import {TypeScriptImportBuilderService} from './import-builder.service';
|
|
9
|
+
import {TypeScriptTypeBuilderService} from './type-builder.service';
|
|
10
10
|
import {
|
|
11
11
|
type NamingConvention,
|
|
12
12
|
type OperationDetails,
|
|
13
13
|
type OperationNameTransformer,
|
|
14
14
|
transformNamingConvention,
|
|
15
|
-
} from '../utils/naming-convention
|
|
15
|
+
} from '../utils/naming-convention';
|
|
16
16
|
|
|
17
17
|
export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuilder {
|
|
18
18
|
private readonly typeBuilder = new TypeScriptTypeBuilderService();
|
|
@@ -161,6 +161,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
161
161
|
this.typeBuilder.createProperty('#baseUrl', 'string', true),
|
|
162
162
|
this.buildConstructor(openapi),
|
|
163
163
|
this.buildGetBaseRequestOptionsMethod(),
|
|
164
|
+
this.buildHandleResponseMethod(),
|
|
164
165
|
this.buildHttpRequestMethod(),
|
|
165
166
|
...methods,
|
|
166
167
|
],
|
|
@@ -290,6 +291,29 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
290
291
|
);
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
private buildHandleResponseMethod(): ts.MethodDeclaration {
|
|
295
|
+
return ts.factory.createMethodDeclaration(
|
|
296
|
+
[ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
|
|
297
|
+
undefined,
|
|
298
|
+
ts.factory.createIdentifier('handleResponse'),
|
|
299
|
+
undefined,
|
|
300
|
+
[this.typeBuilder.createGenericType('T')],
|
|
301
|
+
[
|
|
302
|
+
this.typeBuilder.createParameter('response', 'Response'),
|
|
303
|
+
this.typeBuilder.createParameter('method', 'string'),
|
|
304
|
+
this.typeBuilder.createParameter('path', 'string'),
|
|
305
|
+
this.typeBuilder.createParameter(
|
|
306
|
+
'options',
|
|
307
|
+
'{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string; headers?: Record<string, string>}',
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Promise'), [
|
|
311
|
+
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Response'), undefined),
|
|
312
|
+
]),
|
|
313
|
+
ts.factory.createBlock([ts.factory.createReturnStatement(ts.factory.createIdentifier('response'))], true),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
293
317
|
private buildHttpRequestMethod(): ts.MethodDeclaration {
|
|
294
318
|
return ts.factory.createMethodDeclaration(
|
|
295
319
|
[ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
|
|
@@ -812,7 +836,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
812
836
|
ts.factory.createVariableDeclarationList(
|
|
813
837
|
[
|
|
814
838
|
ts.factory.createVariableDeclaration(
|
|
815
|
-
ts.factory.createIdentifier('
|
|
839
|
+
ts.factory.createIdentifier('rawResponse'),
|
|
816
840
|
undefined,
|
|
817
841
|
undefined,
|
|
818
842
|
ts.factory.createAwaitExpression(
|
|
@@ -853,6 +877,35 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
853
877
|
ts.NodeFlags.Const,
|
|
854
878
|
),
|
|
855
879
|
),
|
|
880
|
+
// Handle response through hook (allows subclasses to intercept and modify response)
|
|
881
|
+
ts.factory.createVariableStatement(
|
|
882
|
+
undefined,
|
|
883
|
+
ts.factory.createVariableDeclarationList(
|
|
884
|
+
[
|
|
885
|
+
ts.factory.createVariableDeclaration(
|
|
886
|
+
ts.factory.createIdentifier('response'),
|
|
887
|
+
undefined,
|
|
888
|
+
undefined,
|
|
889
|
+
ts.factory.createAwaitExpression(
|
|
890
|
+
ts.factory.createCallExpression(
|
|
891
|
+
ts.factory.createPropertyAccessExpression(
|
|
892
|
+
ts.factory.createThis(),
|
|
893
|
+
ts.factory.createIdentifier('handleResponse'),
|
|
894
|
+
),
|
|
895
|
+
[ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('T'), undefined)],
|
|
896
|
+
[
|
|
897
|
+
ts.factory.createIdentifier('rawResponse'),
|
|
898
|
+
ts.factory.createIdentifier('method'),
|
|
899
|
+
ts.factory.createIdentifier('path'),
|
|
900
|
+
ts.factory.createIdentifier('options'),
|
|
901
|
+
],
|
|
902
|
+
),
|
|
903
|
+
),
|
|
904
|
+
),
|
|
905
|
+
],
|
|
906
|
+
ts.NodeFlags.Const,
|
|
907
|
+
),
|
|
908
|
+
),
|
|
856
909
|
// Check response status
|
|
857
910
|
ts.factory.createIfStatement(
|
|
858
911
|
ts.factory.createPrefixUnaryExpression(
|
|
@@ -907,6 +960,28 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
907
960
|
openapi: OpenApiSpecType,
|
|
908
961
|
schemas: Record<string, ts.VariableStatement>,
|
|
909
962
|
): ts.MethodDeclaration[] {
|
|
963
|
+
// Track operation IDs to detect duplicates
|
|
964
|
+
const operationIdMap = new Map<string, {method: string; path: string}[]>();
|
|
965
|
+
|
|
966
|
+
// First pass: collect all operation IDs and their methods/paths
|
|
967
|
+
Object.entries(openapi.paths).forEach(([path, pathItem]) => {
|
|
968
|
+
Object.entries(pathItem)
|
|
969
|
+
.filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
|
|
970
|
+
.forEach(([method, methodSchema]) => {
|
|
971
|
+
const safeMethodSchema = MethodSchema.parse(methodSchema);
|
|
972
|
+
if (safeMethodSchema.operationId) {
|
|
973
|
+
const operationId = safeMethodSchema.operationId;
|
|
974
|
+
const existing = operationIdMap.get(operationId);
|
|
975
|
+
if (existing) {
|
|
976
|
+
existing.push({method, path});
|
|
977
|
+
} else {
|
|
978
|
+
operationIdMap.set(operationId, [{method, path}]);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
// Second pass: build methods, appending method name for HEAD/OPTIONS or when duplicates exist
|
|
910
985
|
return Object.entries(openapi.paths).reduce<ts.MethodDeclaration[]>((endpoints, [path, pathItem]) => {
|
|
911
986
|
const methods = Object.entries(pathItem)
|
|
912
987
|
.filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method))
|
|
@@ -917,6 +992,26 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
917
992
|
return null;
|
|
918
993
|
}
|
|
919
994
|
|
|
995
|
+
const operationId = safeMethodSchema.operationId;
|
|
996
|
+
const methodLower = method.toLowerCase();
|
|
997
|
+
|
|
998
|
+
// Check if this operationId is used by multiple methods
|
|
999
|
+
const operations = operationIdMap.get(operationId);
|
|
1000
|
+
const hasDuplicates = operations !== undefined && operations.length > 1;
|
|
1001
|
+
|
|
1002
|
+
// For HEAD/OPTIONS or when duplicates exist, we need to ensure uniqueness
|
|
1003
|
+
// We'll handle this in transformOperationName by appending the method
|
|
1004
|
+
// But we need to mark it here so transformOperationName knows to append
|
|
1005
|
+
if (hasDuplicates || methodLower === 'head' || methodLower === 'options') {
|
|
1006
|
+
// Temporarily modify the operationId to include method for uniqueness
|
|
1007
|
+
// This will be handled in transformOperationName
|
|
1008
|
+
const modifiedSchema = {
|
|
1009
|
+
...safeMethodSchema,
|
|
1010
|
+
operationId: `${operationId}_${methodLower}`,
|
|
1011
|
+
};
|
|
1012
|
+
return this.buildEndpointMethod(method, path, modifiedSchema, schemas);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
920
1015
|
return this.buildEndpointMethod(method, path, safeMethodSchema, schemas);
|
|
921
1016
|
})
|
|
922
1017
|
.filter((method): method is ts.MethodDeclaration => method !== null);
|
|
@@ -928,6 +1023,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
928
1023
|
/**
|
|
929
1024
|
* Transforms operation ID according to the configured naming convention or transformer
|
|
930
1025
|
* Ensures the result is a valid TypeScript identifier
|
|
1026
|
+
* For HEAD and OPTIONS methods, appends the method name to ensure uniqueness when same operationId is used
|
|
931
1027
|
*/
|
|
932
1028
|
private transformOperationName(operationId: string, method: string, path: string, schema: MethodSchemaType): string {
|
|
933
1029
|
let transformed: string;
|
|
@@ -951,6 +1047,16 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
951
1047
|
transformed = operationId;
|
|
952
1048
|
}
|
|
953
1049
|
|
|
1050
|
+
// For HEAD and OPTIONS methods, append method name to ensure uniqueness
|
|
1051
|
+
// This prevents duplicate method names when GET and HEAD share the same operationId
|
|
1052
|
+
const methodLower = method.toLowerCase();
|
|
1053
|
+
if (methodLower === 'head' || methodLower === 'options') {
|
|
1054
|
+
// Only append if not already present to avoid double-appending
|
|
1055
|
+
if (!transformed.toLowerCase().endsWith(`_${methodLower}`)) {
|
|
1056
|
+
transformed = `${transformed}_${methodLower}`;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
954
1060
|
// Sanitize to ensure valid TypeScript identifier (handles edge cases from custom transformers)
|
|
955
1061
|
return this.typeBuilder.sanitizeIdentifier(transformed);
|
|
956
1062
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {readFileSync} from 'node:fs';
|
|
2
2
|
import {load} from 'js-yaml';
|
|
3
|
-
import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader
|
|
4
|
-
import type {OpenApiSpecType} from '../types/openapi
|
|
5
|
-
import {OpenApiSpec} from '../types/openapi
|
|
3
|
+
import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader';
|
|
4
|
+
import type {OpenApiSpecType} from '../types/openapi';
|
|
5
|
+
import {OpenApiSpec} from '../types/openapi';
|
|
6
6
|
|
|
7
7
|
export class SyncFileReaderService implements OpenApiFileReader {
|
|
8
8
|
async readFile(path: string): Promise<string> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
|
|
2
2
|
import {dirname, resolve} from 'node:path';
|
|
3
|
-
import type {FileWriter} from '../interfaces/code-generator
|
|
3
|
+
import type {FileWriter} from '../interfaces/code-generator';
|
|
4
4
|
|
|
5
5
|
export class SyncFileWriterService implements FileWriter {
|
|
6
6
|
constructor(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import {z} from 'zod';
|
|
3
|
-
import type {ImportBuilder} from '../interfaces/code-generator
|
|
3
|
+
import type {ImportBuilder} from '../interfaces/code-generator';
|
|
4
4
|
|
|
5
5
|
const IsTypeImport = z.boolean();
|
|
6
6
|
const ImportedElement = z.record(z.string(), IsTypeImport);
|
|
@@ -33,7 +33,7 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
|
|
|
33
33
|
const namedImports =
|
|
34
34
|
namedImportList.length > 0
|
|
35
35
|
? ts.factory.createNamedImports(
|
|
36
|
-
namedImportList.map(([name, isTypeImport
|
|
36
|
+
namedImportList.map(([name, isTypeImport]) => {
|
|
37
37
|
return ts.factory.createImportSpecifier(isTypeImport, undefined, ts.factory.createIdentifier(name));
|
|
38
38
|
}),
|
|
39
39
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import type {TypeBuilder} from '../interfaces/code-generator
|
|
2
|
+
import type {TypeBuilder} from '../interfaces/code-generator';
|
|
3
3
|
|
|
4
4
|
export class TypeScriptTypeBuilderService implements TypeBuilder {
|
|
5
5
|
buildType(type: string): ts.TypeNode {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {getExecutionTime} from './execution-time
|
|
1
|
+
import {getExecutionTime} from './execution-time';
|
|
2
|
+
import type {Reporter} from './reporter';
|
|
2
3
|
|
|
3
|
-
export const errorReceived = (process: NodeJS.Process, startTime: bigint) => (): void => {
|
|
4
|
-
|
|
4
|
+
export const errorReceived = (process: NodeJS.Process, startTime: bigint, reporter: Reporter) => (): void => {
|
|
5
|
+
reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
|
|
5
6
|
process.exit(1);
|
|
6
7
|
};
|
|
7
8
|
|
|
8
|
-
export const handleErrors = (process: NodeJS.Process, startTime: bigint): void => {
|
|
9
|
+
export const handleErrors = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
|
|
9
10
|
const catchErrors: string[] = ['unhandledRejection', 'uncaughtException'];
|
|
10
11
|
catchErrors.forEach((event) => {
|
|
11
|
-
process.on(event, errorReceived(process, startTime));
|
|
12
|
+
process.on(event, errorReceived(process, startTime, reporter));
|
|
12
13
|
});
|
|
13
14
|
};
|
package/src/utils/reporter.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import {format} from 'node:util';
|
|
2
2
|
|
|
3
3
|
export class Reporter {
|
|
4
|
-
constructor(
|
|
4
|
+
constructor(
|
|
5
|
+
private readonly stdout: NodeJS.WriteStream,
|
|
6
|
+
private readonly stderr: NodeJS.WriteStream = stdout,
|
|
7
|
+
) {
|
|
5
8
|
this.log = this.log.bind(this);
|
|
6
9
|
this.error = this.error.bind(this);
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
log(...args: readonly unknown[]): void {
|
|
10
|
-
this.
|
|
13
|
+
this.stdout.write(format(...args) + '\n');
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
error(...args: readonly unknown[]): void {
|
|
14
|
-
this.
|
|
17
|
+
this.stderr.write(format(...args) + '\n');
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {getExecutionTime} from './execution-time
|
|
1
|
+
import {getExecutionTime} from './execution-time';
|
|
2
|
+
import type {Reporter} from './reporter';
|
|
2
3
|
|
|
3
|
-
export const signalReceived =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export const signalReceived =
|
|
5
|
+
(process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals, reporter: Reporter) => (): void => {
|
|
6
|
+
reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
|
|
7
|
+
process.kill(process.pid, event);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
};
|
|
8
10
|
|
|
9
|
-
export const handleSignals = (process: NodeJS.Process, startTime: bigint): void => {
|
|
11
|
+
export const handleSignals = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
|
|
10
12
|
const catchSignals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
|
11
13
|
catchSignals.forEach((event) => {
|
|
12
|
-
process.once(event, signalReceived(process, startTime, event));
|
|
14
|
+
process.once(event, signalReceived(process, startTime, event, reporter));
|
|
13
15
|
});
|
|
14
16
|
};
|