zod-codegen 1.0.0

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 (76) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +93 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +70 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +87 -0
  4. package/.github/dependabot.yml +76 -0
  5. package/.github/workflows/ci.yml +143 -0
  6. package/.github/workflows/release.yml +65 -0
  7. package/.husky/commit-msg +2 -0
  8. package/.husky/pre-commit +5 -0
  9. package/.lintstagedrc.json +4 -0
  10. package/.nvmrc +1 -0
  11. package/.prettierrc.json +7 -0
  12. package/.releaserc.json +159 -0
  13. package/CHANGELOG.md +24 -0
  14. package/CONTRIBUTING.md +274 -0
  15. package/LICENCE +201 -0
  16. package/README.md +263 -0
  17. package/SECURITY.md +108 -0
  18. package/codecov.yml +29 -0
  19. package/commitlint.config.mjs +28 -0
  20. package/dist/scripts/update-manifest.js +31 -0
  21. package/dist/src/assets/manifest.json +5 -0
  22. package/dist/src/cli.js +60 -0
  23. package/dist/src/generator.js +55 -0
  24. package/dist/src/http/fetch-client.js +141 -0
  25. package/dist/src/interfaces/code-generator.js +1 -0
  26. package/dist/src/interfaces/file-reader.js +1 -0
  27. package/dist/src/polyfills/fetch.js +18 -0
  28. package/dist/src/services/code-generator.service.js +419 -0
  29. package/dist/src/services/file-reader.service.js +25 -0
  30. package/dist/src/services/file-writer.service.js +32 -0
  31. package/dist/src/services/import-builder.service.js +45 -0
  32. package/dist/src/services/type-builder.service.js +42 -0
  33. package/dist/src/types/http.js +10 -0
  34. package/dist/src/types/openapi.js +173 -0
  35. package/dist/src/utils/error-handler.js +11 -0
  36. package/dist/src/utils/execution-time.js +3 -0
  37. package/dist/src/utils/manifest.js +9 -0
  38. package/dist/src/utils/reporter.js +15 -0
  39. package/dist/src/utils/signal-handler.js +12 -0
  40. package/dist/src/utils/tty.js +3 -0
  41. package/dist/tests/integration/cli.test.js +25 -0
  42. package/dist/tests/unit/generator.test.js +29 -0
  43. package/dist/vitest.config.js +38 -0
  44. package/eslint.config.mjs +33 -0
  45. package/package.json +102 -0
  46. package/samples/openapi.json +1 -0
  47. package/samples/saris-openapi.json +7122 -0
  48. package/samples/swagger-petstore.yaml +802 -0
  49. package/samples/swagger-saris.yaml +3585 -0
  50. package/samples/test-logical.yaml +50 -0
  51. package/scripts/update-manifest.js +31 -0
  52. package/scripts/update-manifest.ts +47 -0
  53. package/src/assets/manifest.json +5 -0
  54. package/src/cli.ts +68 -0
  55. package/src/generator.ts +61 -0
  56. package/src/http/fetch-client.ts +181 -0
  57. package/src/interfaces/code-generator.ts +25 -0
  58. package/src/interfaces/file-reader.ts +15 -0
  59. package/src/polyfills/fetch.ts +26 -0
  60. package/src/services/code-generator.service.ts +775 -0
  61. package/src/services/file-reader.service.ts +29 -0
  62. package/src/services/file-writer.service.ts +36 -0
  63. package/src/services/import-builder.service.ts +64 -0
  64. package/src/services/type-builder.service.ts +77 -0
  65. package/src/types/http.ts +35 -0
  66. package/src/types/openapi.ts +202 -0
  67. package/src/utils/error-handler.ts +13 -0
  68. package/src/utils/execution-time.ts +3 -0
  69. package/src/utils/manifest.ts +17 -0
  70. package/src/utils/reporter.ts +16 -0
  71. package/src/utils/signal-handler.ts +14 -0
  72. package/src/utils/tty.ts +3 -0
  73. package/tests/integration/cli.test.ts +29 -0
  74. package/tests/unit/generator.test.ts +36 -0
  75. package/tsconfig.json +44 -0
  76. package/vitest.config.ts +39 -0
@@ -0,0 +1,50 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test Logical Operators
4
+ version: 1.0.0
5
+ paths: {}
6
+ components:
7
+ schemas:
8
+ TestAnyOf:
9
+ anyOf:
10
+ - type: string
11
+ - type: number
12
+ - type: boolean
13
+
14
+ TestOneOf:
15
+ oneOf:
16
+ - type: string
17
+ - type: number
18
+
19
+ TestAllOf:
20
+ allOf:
21
+ - type: object
22
+ properties:
23
+ name:
24
+ type: string
25
+ - type: object
26
+ properties:
27
+ age:
28
+ type: number
29
+
30
+ TestNot:
31
+ not:
32
+ type: string
33
+
34
+ TestComplex:
35
+ type: object
36
+ properties:
37
+ unionField:
38
+ anyOf:
39
+ - type: string
40
+ - type: number
41
+ intersectionField:
42
+ allOf:
43
+ - type: object
44
+ properties:
45
+ id:
46
+ type: string
47
+ - type: object
48
+ properties:
49
+ timestamp:
50
+ type: number
@@ -0,0 +1,31 @@
1
+ import {readFileSync, writeFileSync} from 'fs';
2
+ import {resolve} from 'path';
3
+ import {z} from 'zod';
4
+ /**
5
+ * Type guard for the package.json object
6
+ *
7
+ * @param input Unknown input
8
+ * @returns true if the input is an event object
9
+ */
10
+ export function isPackageJson(input) {
11
+ const event = z
12
+ .object({
13
+ name: z.string(),
14
+ version: z.string(),
15
+ description: z.string(),
16
+ })
17
+ .strict()
18
+ .catchall(z.any())
19
+ .required();
20
+ const {success} = event.safeParse(input);
21
+ return success;
22
+ }
23
+ const sourcePath = resolve(__dirname, '..', 'package.json');
24
+ const data = JSON.parse(readFileSync(sourcePath, 'utf8'));
25
+ if (!isPackageJson(data)) {
26
+ process.exit(1);
27
+ }
28
+ const {name, version, description} = data;
29
+ const targetPath = resolve(__dirname, '..', 'src', 'assets', 'manifest.json');
30
+ writeFileSync(targetPath, JSON.stringify({name, version, description}, null, 2));
31
+ process.exit(0);
@@ -0,0 +1,47 @@
1
+ import {readFileSync, writeFileSync} from 'fs';
2
+ import {resolve} from 'path';
3
+ import {z} from 'zod';
4
+
5
+ interface PackageJson {
6
+ name: string;
7
+ version: string;
8
+ description: string;
9
+ }
10
+
11
+ /**
12
+ * Type guard for the package.json object
13
+ *
14
+ * @param input Unknown input
15
+ * @returns true if the input is an event object
16
+ */
17
+ export function isPackageJson(input: unknown): input is PackageJson {
18
+ const event = z
19
+ .object({
20
+ name: z.string(),
21
+ version: z.string(),
22
+ description: z.string(),
23
+ })
24
+ .strict()
25
+ .catchall(z.any())
26
+ .required();
27
+
28
+ const {success} = event.safeParse(input);
29
+
30
+ return success;
31
+ }
32
+
33
+ const sourcePath = resolve(__dirname, '..', 'package.json');
34
+
35
+ const data: unknown = JSON.parse(readFileSync(sourcePath, 'utf8'));
36
+
37
+ if (!isPackageJson(data)) {
38
+ process.exit(1);
39
+ }
40
+
41
+ const {name, version, description} = data;
42
+
43
+ const targetPath = resolve(__dirname, '..', 'src', 'assets', 'manifest.json');
44
+
45
+ writeFileSync(targetPath, JSON.stringify({name, version, description}, null, 2));
46
+
47
+ process.exit(0);
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "zod-codegen",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Zod code generator"
5
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs';
4
+ import {hideBin} from 'yargs/helpers';
5
+ import {Generator} from './generator.js';
6
+ import {readFileSync} from 'node:fs';
7
+ import {fileURLToPath} from 'node:url';
8
+ import {dirname, join} from 'node:path';
9
+
10
+ import loudRejection from 'loud-rejection';
11
+ import {handleErrors} from './utils/error-handler.js';
12
+ import {handleSignals} from './utils/signal-handler.js';
13
+ import debug from 'debug';
14
+ import {isManifest} from './utils/manifest.js';
15
+ import {Reporter} from './utils/reporter.js';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const manifestData: unknown = JSON.parse(readFileSync(join(__dirname, 'assets', 'manifest.json'), 'utf-8'));
19
+
20
+ if (!isManifest(manifestData)) {
21
+ process.exit(1);
22
+ }
23
+
24
+ const {name, description, version} = manifestData;
25
+ const reporter = new Reporter(process.stdout);
26
+ const startTime = process.hrtime.bigint();
27
+
28
+ debug(`${name}:${String(process.pid)}`);
29
+
30
+ loudRejection();
31
+ handleSignals(process, startTime);
32
+ handleErrors(process, startTime);
33
+
34
+ const argv = yargs(hideBin(process.argv))
35
+ .scriptName(name)
36
+ .usage(`${description}\n\nUsage: $0 [options]`)
37
+ .version(version)
38
+ .option('input', {
39
+ alias: 'i',
40
+ type: 'string',
41
+ description: 'Path or URL to OpenAPI file',
42
+ demandOption: true,
43
+ })
44
+ .option('output', {
45
+ alias: 'o',
46
+ type: 'string',
47
+ description: 'Directory to output the generated files',
48
+ default: 'generated',
49
+ })
50
+ .help()
51
+ .parseSync();
52
+
53
+ const {input, output} = argv;
54
+
55
+ void (async () => {
56
+ try {
57
+ const generator = new Generator(name, version, reporter, input, output);
58
+ const exitCode = await generator.run();
59
+ process.exit(exitCode);
60
+ } catch (error) {
61
+ if (error instanceof Error) {
62
+ reporter.error(`Fatal error: ${error.message}`);
63
+ } else {
64
+ reporter.error('An unknown fatal error occurred');
65
+ }
66
+ process.exit(1);
67
+ }
68
+ })();
@@ -0,0 +1,61 @@
1
+ import type {Reporter} from './utils/reporter.js';
2
+ import type {OpenApiSpecType} from './types/openapi.js';
3
+ import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service.js';
4
+ import {TypeScriptCodeGeneratorService} from './services/code-generator.service.js';
5
+ import {SyncFileWriterService} from './services/file-writer.service.js';
6
+
7
+ export class Generator {
8
+ private readonly fileReader = new SyncFileReaderService();
9
+ private readonly fileParser = new OpenApiFileParserService();
10
+ private readonly codeGenerator = new TypeScriptCodeGeneratorService();
11
+ private readonly fileWriter: SyncFileWriterService;
12
+ private readonly outputPath: string;
13
+
14
+ constructor(
15
+ private readonly _name: string,
16
+ private readonly _version: string,
17
+ private readonly reporter: Reporter,
18
+ private readonly inputPath: string,
19
+ private readonly _outputDir: string,
20
+ ) {
21
+ this.fileWriter = new SyncFileWriterService(this._name, this._version, inputPath);
22
+ this.outputPath = this.fileWriter.resolveOutputPath(this._outputDir);
23
+ }
24
+
25
+ async run(): Promise<number> {
26
+ try {
27
+ const rawSource = this.readFile();
28
+ const openApiSpec = this.parseFile(rawSource);
29
+ const generatedCode = this.generateCode(openApiSpec);
30
+
31
+ this.writeFile(generatedCode);
32
+ this.reporter.log(`✅ Generated types successfully at: ${this.outputPath}`);
33
+
34
+ return 0;
35
+ } catch (error) {
36
+ if (error instanceof Error) {
37
+ this.reporter.error(`❌ Error: ${error.message}`);
38
+ } else {
39
+ this.reporter.error('❌ An unknown error occurred');
40
+ }
41
+
42
+ return Promise.resolve(1);
43
+ }
44
+ }
45
+
46
+ private readFile(): string {
47
+ return this.fileReader.readFile(this.inputPath);
48
+ }
49
+
50
+ private parseFile(source: string): OpenApiSpecType {
51
+ return this.fileParser.parse(source);
52
+ }
53
+
54
+ private generateCode(spec: OpenApiSpecType): string {
55
+ return this.codeGenerator.generate(spec);
56
+ }
57
+
58
+ private writeFile(content: string): void {
59
+ this.fileWriter.writeFile(this.outputPath, content);
60
+ }
61
+ }
@@ -0,0 +1,181 @@
1
+ import {HttpClient, HttpError, HttpRequestConfig, HttpResponse} from '../types/http.js';
2
+
3
+ declare const globalThis: typeof global & {
4
+ fetch?: typeof fetch;
5
+ Headers?: typeof Headers;
6
+ Request?: typeof Request;
7
+ Response?: typeof Response;
8
+ };
9
+
10
+ type FetchFunction = (input: string | Request, init?: RequestInit) => Promise<Response>;
11
+
12
+ type HeadersConstructor = new (init?: HeadersInit) => Headers;
13
+
14
+ export class FetchHttpClient implements HttpClient {
15
+ private readonly fetch: FetchFunction;
16
+ private readonly Headers: HeadersConstructor;
17
+
18
+ constructor(
19
+ private readonly baseUrl = '',
20
+ private readonly defaultHeaders: Record<string, string> = {},
21
+ ) {
22
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
23
+ if (typeof globalThis.fetch === 'function' && globalThis.Headers) {
24
+ this.fetch = globalThis.fetch.bind(globalThis);
25
+ this.Headers = globalThis.Headers;
26
+ return;
27
+ }
28
+
29
+ if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
30
+ this.fetch = window.fetch.bind(window);
31
+ this.Headers = window.Headers;
32
+ return;
33
+ }
34
+
35
+ throw new Error(
36
+ "Fetch API is not available. Please ensure you're running in a compatible environment or polyfill fetch.",
37
+ );
38
+ }
39
+
40
+ async request<TResponse = unknown, TRequest = unknown>(
41
+ config: HttpRequestConfig<TRequest>,
42
+ ): Promise<HttpResponse<TResponse>> {
43
+ const url = this.buildUrl(config.url, config.params);
44
+ const headers = this.buildHeaders(config.headers);
45
+ const body = this.buildBody(config.data, headers);
46
+
47
+ const controller = new AbortController();
48
+ const timeoutId = config.timeout
49
+ ? setTimeout(() => {
50
+ controller.abort();
51
+ }, config.timeout)
52
+ : undefined;
53
+
54
+ try {
55
+ const response = await this.fetch(url, {
56
+ method: config.method,
57
+ headers,
58
+ body,
59
+ signal: controller.signal,
60
+ });
61
+
62
+ const responseHeaders = this.extractHeaders(response.headers);
63
+ const data = await this.parseResponse<TResponse>(response);
64
+
65
+ if (!response.ok) {
66
+ throw new HttpError(`HTTP ${String(response.status)}: ${response.statusText}`, response.status, {
67
+ data,
68
+ status: response.status,
69
+ statusText: response.statusText,
70
+ headers: responseHeaders,
71
+ url: response.url,
72
+ });
73
+ }
74
+
75
+ return {
76
+ data,
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ headers: responseHeaders,
80
+ url: response.url,
81
+ };
82
+ } catch (error) {
83
+ if (error instanceof HttpError) {
84
+ throw error;
85
+ }
86
+
87
+ if (error instanceof Error) {
88
+ if (error.name === 'AbortError') {
89
+ throw new HttpError('Request timeout', 408);
90
+ }
91
+ throw new HttpError(`Network error: ${error.message}`, 0);
92
+ }
93
+
94
+ throw new HttpError('Unknown network error', 0);
95
+ } finally {
96
+ if (timeoutId) {
97
+ clearTimeout(timeoutId);
98
+ }
99
+ }
100
+ }
101
+
102
+ private buildUrl(path: string, params?: Record<string, string | number | boolean>): string {
103
+ const url = new URL(path, this.baseUrl || 'http://localhost');
104
+
105
+ if (params) {
106
+ Object.entries(params).forEach(([key, value]) => {
107
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
108
+ if (value !== undefined && value !== null) {
109
+ url.searchParams.set(key, String(value));
110
+ }
111
+ });
112
+ }
113
+
114
+ return url.toString();
115
+ }
116
+
117
+ private buildHeaders(customHeaders?: Record<string, string>): Headers {
118
+ const headers = new this.Headers();
119
+
120
+ Object.entries(this.defaultHeaders).forEach(([key, value]) => {
121
+ headers.set(key, value);
122
+ });
123
+
124
+ if (customHeaders) {
125
+ Object.entries(customHeaders).forEach(([key, value]) => {
126
+ headers.set(key, value);
127
+ });
128
+ }
129
+
130
+ return headers;
131
+ }
132
+
133
+ private buildBody(data?: unknown, headers?: Headers): string | null {
134
+ if (!data) {
135
+ return null;
136
+ }
137
+
138
+ const contentType = headers?.get('content-type') ?? 'application/json';
139
+
140
+ if (contentType.includes('application/json')) {
141
+ if (!headers?.has('content-type')) {
142
+ headers?.set('content-type', 'application/json');
143
+ }
144
+ return JSON.stringify(data);
145
+ }
146
+
147
+ if (typeof data === 'string') {
148
+ return data;
149
+ }
150
+
151
+ return JSON.stringify(data);
152
+ }
153
+
154
+ private extractHeaders(headers: Headers): Record<string, string> {
155
+ const result: Record<string, string> = {};
156
+ headers.forEach((value, key) => {
157
+ result[key.toLowerCase()] = value;
158
+ });
159
+ return result;
160
+ }
161
+
162
+ private async parseResponse<TResponse>(response: Response): Promise<TResponse> {
163
+ const contentType = response.headers.get('content-type') ?? '';
164
+
165
+ if (contentType.includes('application/json')) {
166
+ return (await response.json()) as TResponse;
167
+ }
168
+
169
+ if (contentType.includes('text/')) {
170
+ return (await response.text()) as unknown as TResponse;
171
+ }
172
+
173
+ try {
174
+ const text = await response.text();
175
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
176
+ return text ? JSON.parse(text) : ({} as TResponse);
177
+ } catch {
178
+ return {} as TResponse;
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,25 @@
1
+ import type {OpenApiSpecType} from '../types/openapi.js';
2
+
3
+ export interface CodeGenerator {
4
+ generate(spec: OpenApiSpecType): string;
5
+ }
6
+
7
+ export interface SchemaBuilder {
8
+ buildSchema(schema: unknown, required?: boolean): unknown;
9
+ }
10
+
11
+ export interface TypeBuilder {
12
+ buildType(type: string): unknown;
13
+ }
14
+
15
+ export interface ImportBuilder {
16
+ buildImports(): unknown[];
17
+ }
18
+
19
+ export interface ClassBuilder {
20
+ buildClass(spec: OpenApiSpecType): unknown;
21
+ }
22
+
23
+ export interface FileWriter {
24
+ writeFile(filePath: string, content: string): Promise<void> | void;
25
+ }
@@ -0,0 +1,15 @@
1
+ export interface FileReader {
2
+ readFile(path: string): Promise<string> | string;
3
+ }
4
+
5
+ export interface FileParser<TInput, TOutput> {
6
+ parse(input: TInput): TOutput;
7
+ }
8
+
9
+ export interface OpenApiFileReader extends FileReader {
10
+ readFile(path: string): Promise<string> | string;
11
+ }
12
+
13
+ export interface OpenApiFileParser<TOutput> extends FileParser<unknown, TOutput> {
14
+ parse(input: unknown): TOutput;
15
+ }
@@ -0,0 +1,26 @@
1
+ export interface FetchPolyfillOptions {
2
+ enableNodejsPolyfill?: boolean;
3
+ }
4
+
5
+ export async function setupFetchPolyfill(options: FetchPolyfillOptions = {}): Promise<void> {
6
+ if (typeof fetch === 'function') {
7
+ return;
8
+ }
9
+
10
+ if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
11
+ return;
12
+ }
13
+
14
+ if (options.enableNodejsPolyfill && typeof process !== 'undefined' && process.versions.node) {
15
+ try {
16
+ await import('undici');
17
+ return;
18
+ } catch {
19
+ // Fall through to error
20
+ }
21
+ }
22
+
23
+ throw new Error(
24
+ 'Fetch API is not available. ' + 'For Node.js environments, please install undici: npm install undici',
25
+ );
26
+ }