zod-codegen 1.6.3 → 1.7.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 (120) hide show
  1. package/.github/workflows/ci.yml +50 -48
  2. package/.github/workflows/release.yml +13 -3
  3. package/.husky/commit-msg +1 -1
  4. package/.husky/pre-commit +1 -1
  5. package/.lintstagedrc.json +5 -1
  6. package/.nvmrc +1 -1
  7. package/.prettierrc.json +12 -5
  8. package/CHANGELOG.md +17 -0
  9. package/CONTRIBUTING.md +12 -12
  10. package/EXAMPLES.md +135 -57
  11. package/PERFORMANCE.md +4 -4
  12. package/README.md +87 -64
  13. package/SECURITY.md +1 -1
  14. package/dist/src/cli.js +11 -18
  15. package/dist/src/generator.d.ts +2 -2
  16. package/dist/src/generator.d.ts.map +1 -1
  17. package/dist/src/generator.js +5 -3
  18. package/dist/src/interfaces/code-generator.d.ts.map +1 -1
  19. package/dist/src/services/code-generator.service.d.ts +3 -1
  20. package/dist/src/services/code-generator.service.d.ts.map +1 -1
  21. package/dist/src/services/code-generator.service.js +236 -219
  22. package/dist/src/services/file-reader.service.d.ts +2 -0
  23. package/dist/src/services/file-reader.service.d.ts.map +1 -1
  24. package/dist/src/services/file-reader.service.js +25 -11
  25. package/dist/src/services/file-writer.service.d.ts.map +1 -1
  26. package/dist/src/services/file-writer.service.js +2 -2
  27. package/dist/src/services/import-builder.service.d.ts.map +1 -1
  28. package/dist/src/services/import-builder.service.js +3 -3
  29. package/dist/src/services/type-builder.service.d.ts.map +1 -1
  30. package/dist/src/types/generator-options.d.ts.map +1 -1
  31. package/dist/src/types/openapi.d.ts.map +1 -1
  32. package/dist/src/types/openapi.js +20 -20
  33. package/dist/src/utils/error-handler.d.ts.map +1 -1
  34. package/dist/src/utils/naming-convention.d.ts.map +1 -1
  35. package/dist/src/utils/naming-convention.js +6 -3
  36. package/dist/src/utils/signal-handler.d.ts.map +1 -1
  37. package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
  38. package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
  39. package/dist/tests/integration/cli-comprehensive.test.js +123 -0
  40. package/dist/tests/integration/cli.test.d.ts +2 -0
  41. package/dist/tests/integration/cli.test.d.ts.map +1 -0
  42. package/dist/tests/integration/cli.test.js +25 -0
  43. package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
  44. package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
  45. package/dist/tests/integration/error-scenarios.test.js +169 -0
  46. package/dist/tests/integration/snapshots.test.d.ts +2 -0
  47. package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
  48. package/dist/tests/integration/snapshots.test.js +100 -0
  49. package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
  50. package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
  51. package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
  52. package/dist/tests/unit/code-generator.test.d.ts +2 -0
  53. package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
  54. package/dist/tests/unit/code-generator.test.js +1364 -0
  55. package/dist/tests/unit/file-reader.test.d.ts +2 -0
  56. package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
  57. package/dist/tests/unit/file-reader.test.js +153 -0
  58. package/dist/tests/unit/generator.test.d.ts +2 -0
  59. package/dist/tests/unit/generator.test.d.ts.map +1 -0
  60. package/dist/tests/unit/generator.test.js +119 -0
  61. package/dist/tests/unit/naming-convention.test.d.ts +2 -0
  62. package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
  63. package/dist/tests/unit/naming-convention.test.js +256 -0
  64. package/dist/tests/unit/reporter.test.d.ts +2 -0
  65. package/dist/tests/unit/reporter.test.d.ts.map +1 -0
  66. package/dist/tests/unit/reporter.test.js +44 -0
  67. package/dist/tests/unit/type-builder.test.d.ts +2 -0
  68. package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
  69. package/dist/tests/unit/type-builder.test.js +108 -0
  70. package/dist/vitest.config.d.ts.map +1 -1
  71. package/dist/vitest.config.js +10 -20
  72. package/eslint.config.mjs +38 -28
  73. package/examples/.gitkeep +1 -1
  74. package/examples/README.md +4 -2
  75. package/examples/petstore/README.md +18 -17
  76. package/examples/petstore/{type.ts → api.ts} +158 -74
  77. package/examples/petstore/authenticated-usage.ts +6 -4
  78. package/examples/petstore/basic-usage.ts +4 -3
  79. package/examples/petstore/error-handling-usage.ts +84 -0
  80. package/examples/petstore/retry-handler-usage.ts +11 -18
  81. package/examples/petstore/server-variables-usage.ts +10 -10
  82. package/examples/pokeapi/README.md +8 -8
  83. package/examples/pokeapi/api.ts +218 -0
  84. package/examples/pokeapi/basic-usage.ts +3 -2
  85. package/examples/pokeapi/custom-client.ts +5 -4
  86. package/package.json +17 -21
  87. package/src/cli.ts +20 -25
  88. package/src/generator.ts +13 -11
  89. package/src/interfaces/code-generator.ts +1 -1
  90. package/src/services/code-generator.service.ts +799 -1120
  91. package/src/services/file-reader.service.ts +35 -15
  92. package/src/services/file-writer.service.ts +7 -7
  93. package/src/services/import-builder.service.ts +9 -13
  94. package/src/services/type-builder.service.ts +8 -19
  95. package/src/types/generator-options.ts +1 -1
  96. package/src/types/openapi.ts +22 -22
  97. package/src/utils/error-handler.ts +2 -2
  98. package/src/utils/naming-convention.ts +13 -10
  99. package/src/utils/reporter.ts +2 -2
  100. package/src/utils/signal-handler.ts +7 -8
  101. package/tests/integration/cli-comprehensive.test.ts +53 -31
  102. package/tests/integration/cli.test.ts +5 -5
  103. package/tests/integration/error-scenarios.test.ts +20 -26
  104. package/tests/integration/snapshots.test.ts +19 -23
  105. package/tests/unit/code-generator-edge-cases.test.ts +133 -133
  106. package/tests/unit/code-generator.test.ts +431 -330
  107. package/tests/unit/file-reader.test.ts +58 -18
  108. package/tests/unit/generator.test.ts +30 -18
  109. package/tests/unit/naming-convention.test.ts +27 -27
  110. package/tests/unit/type-builder.test.ts +2 -2
  111. package/tsconfig.json +5 -3
  112. package/vitest.config.ts +11 -21
  113. package/dist/scripts/update-manifest.d.ts +0 -14
  114. package/dist/scripts/update-manifest.d.ts.map +0 -1
  115. package/dist/scripts/update-manifest.js +0 -33
  116. package/dist/src/assets/manifest.json +0 -5
  117. package/examples/pokeapi/type.ts +0 -109
  118. package/generated/type.ts +0 -371
  119. package/scripts/update-manifest.ts +0 -49
  120. package/src/assets/manifest.json +0 -5
@@ -0,0 +1,169 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { Generator } from '../../src/generator';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const testOutputDir = join(__dirname, '../../test-output-errors');
8
+ describe('Error Scenarios', () => {
9
+ let generator;
10
+ let mockReporter;
11
+ const logSpy = vi.fn();
12
+ const errorSpy = vi.fn();
13
+ beforeEach(() => {
14
+ if (existsSync(testOutputDir)) {
15
+ rmSync(testOutputDir, { recursive: true, force: true });
16
+ }
17
+ mkdirSync(testOutputDir, { recursive: true });
18
+ mockReporter = {
19
+ log: logSpy,
20
+ error: errorSpy
21
+ };
22
+ logSpy.mockClear();
23
+ errorSpy.mockClear();
24
+ });
25
+ afterEach(() => {
26
+ if (existsSync(testOutputDir)) {
27
+ rmSync(testOutputDir, { recursive: true, force: true });
28
+ }
29
+ });
30
+ describe('File Reading Errors', () => {
31
+ it('should handle non-existent local file', async () => {
32
+ generator = new Generator('test-app', '1.0.0', mockReporter, './non-existent-file.yaml', testOutputDir);
33
+ const exitCode = await generator.run();
34
+ expect(exitCode).toBe(1);
35
+ expect(errorSpy).toHaveBeenCalled();
36
+ expect(String(errorSpy.mock.calls[0]?.[0] ?? '')).toContain('Error');
37
+ });
38
+ it('should handle network errors when fetching URL', async () => {
39
+ const originalFetch = global.fetch;
40
+ const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
41
+ global.fetch = mockFetch;
42
+ generator = new Generator('test-app', '1.0.0', mockReporter, 'https://example.com/nonexistent.json', testOutputDir);
43
+ const exitCode = await generator.run();
44
+ expect(exitCode).toBe(1);
45
+ expect(errorSpy).toHaveBeenCalled();
46
+ expect(mockFetch).toHaveBeenCalled();
47
+ global.fetch = originalFetch;
48
+ });
49
+ it('should handle HTTP errors when fetching URL', async () => {
50
+ const originalFetch = global.fetch;
51
+ const mockFetch = vi.fn().mockResolvedValue({
52
+ ok: false,
53
+ status: 500,
54
+ statusText: 'Internal Server Error',
55
+ text: async () => 'Error response'
56
+ });
57
+ global.fetch = mockFetch;
58
+ generator = new Generator('test-app', '1.0.0', mockReporter, 'https://example.com/error.json', testOutputDir);
59
+ const exitCode = await generator.run();
60
+ expect(exitCode).toBe(1);
61
+ expect(errorSpy).toHaveBeenCalled();
62
+ // Error should mention the HTTP error or parsing failure
63
+ const errorMessage = (errorSpy.mock.calls[0]?.[0] ?? '');
64
+ expect(errorMessage).toMatch(/500|Error|Failed/);
65
+ global.fetch = originalFetch;
66
+ });
67
+ });
68
+ describe('Parsing Errors', () => {
69
+ it('should handle invalid JSON', async () => {
70
+ const invalidFile = join(testOutputDir, 'invalid.json');
71
+ writeFileSync(invalidFile, '{ invalid json }');
72
+ generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
73
+ const exitCode = await generator.run();
74
+ expect(exitCode).toBe(1);
75
+ expect(errorSpy).toHaveBeenCalled();
76
+ });
77
+ it('should handle invalid YAML', async () => {
78
+ const invalidFile = join(testOutputDir, 'invalid.yaml');
79
+ writeFileSync(invalidFile, 'invalid: yaml: [unclosed');
80
+ generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
81
+ const exitCode = await generator.run();
82
+ expect(exitCode).toBe(1);
83
+ expect(errorSpy).toHaveBeenCalled();
84
+ });
85
+ it('should handle invalid OpenAPI version', async () => {
86
+ const invalidFile = join(testOutputDir, 'invalid-version.yaml');
87
+ writeFileSync(invalidFile, `
88
+ openapi: 2.0.0
89
+ info:
90
+ title: Test API
91
+ version: 1.0.0
92
+ paths: {}
93
+ `);
94
+ generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
95
+ const exitCode = await generator.run();
96
+ expect(exitCode).toBe(1);
97
+ expect(errorSpy).toHaveBeenCalled();
98
+ });
99
+ it('should handle missing required fields', async () => {
100
+ const invalidFile = join(testOutputDir, 'missing-fields.yaml');
101
+ writeFileSync(invalidFile, `
102
+ openapi: 3.0.0
103
+ paths: {}
104
+ `);
105
+ generator = new Generator('test-app', '1.0.0', mockReporter, invalidFile, testOutputDir);
106
+ const exitCode = await generator.run();
107
+ expect(exitCode).toBe(1);
108
+ expect(errorSpy).toHaveBeenCalled();
109
+ });
110
+ });
111
+ describe('File Writing Errors', () => {
112
+ it('should handle write permission errors gracefully', async () => {
113
+ // Create a read-only directory (on Unix systems)
114
+ const readOnlyDir = join(testOutputDir, 'readonly');
115
+ mkdirSync(readOnlyDir, { recursive: true });
116
+ // Note: chmod doesn't work the same way on all systems, so this test
117
+ // might need to be adjusted based on the environment
118
+ generator = new Generator('test-app', '1.0.0', mockReporter, './samples/swagger-petstore.yaml', readOnlyDir);
119
+ const exitCode = await generator.run();
120
+ // Should either succeed or fail gracefully
121
+ expect([0, 1]).toContain(exitCode);
122
+ });
123
+ });
124
+ describe('Malformed OpenAPI Specs', () => {
125
+ it('should handle empty spec', async () => {
126
+ const emptyFile = join(testOutputDir, 'empty.yaml');
127
+ writeFileSync(emptyFile, '');
128
+ generator = new Generator('test-app', '1.0.0', mockReporter, emptyFile, testOutputDir);
129
+ const exitCode = await generator.run();
130
+ expect(exitCode).toBe(1);
131
+ expect(errorSpy).toHaveBeenCalled();
132
+ });
133
+ it('should handle spec with no paths', async () => {
134
+ const noPathsFile = join(testOutputDir, 'no-paths.yaml');
135
+ writeFileSync(noPathsFile, `
136
+ openapi: 3.0.0
137
+ info:
138
+ title: Test API
139
+ version: 1.0.0
140
+ paths: {}
141
+ `);
142
+ generator = new Generator('test-app', '1.0.0', mockReporter, noPathsFile, testOutputDir);
143
+ const exitCode = await generator.run();
144
+ // Should succeed even with no paths
145
+ expect(exitCode).toBe(0);
146
+ expect(logSpy).toHaveBeenCalled();
147
+ });
148
+ it('should handle spec with no components', async () => {
149
+ const noComponentsFile = join(testOutputDir, 'no-components.yaml');
150
+ writeFileSync(noComponentsFile, `
151
+ openapi: 3.0.0
152
+ info:
153
+ title: Test API
154
+ version: 1.0.0
155
+ paths:
156
+ /test:
157
+ get:
158
+ operationId: test
159
+ responses:
160
+ '200':
161
+ description: Success
162
+ `);
163
+ generator = new Generator('test-app', '1.0.0', mockReporter, noComponentsFile, testOutputDir);
164
+ const exitCode = await generator.run();
165
+ // Should succeed even with no components
166
+ expect(exitCode).toBe(0);
167
+ });
168
+ });
169
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=snapshots.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.test.d.ts","sourceRoot":"","sources":["../../../tests/integration/snapshots.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { Generator } from '../../src/generator';
6
+ import { Reporter } from '../../src/utils/reporter';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const testOutputDir = join(__dirname, '../../test-output-snapshots');
9
+ describe('Generated Code Snapshots', () => {
10
+ let generator;
11
+ const reporter = new Reporter(process.stdout, process.stderr);
12
+ beforeEach(() => {
13
+ if (existsSync(testOutputDir)) {
14
+ rmSync(testOutputDir, { recursive: true, force: true });
15
+ }
16
+ mkdirSync(testOutputDir, { recursive: true });
17
+ });
18
+ afterEach(() => {
19
+ if (existsSync(testOutputDir)) {
20
+ rmSync(testOutputDir, { recursive: true, force: true });
21
+ }
22
+ });
23
+ describe('swagger-petstore.yaml', () => {
24
+ it('should generate valid TypeScript code', async () => {
25
+ generator = new Generator('test-app', '1.0.0', reporter, './samples/swagger-petstore.yaml', testOutputDir);
26
+ const exitCode = await generator.run();
27
+ expect(exitCode).toBe(0);
28
+ const outputFile = join(testOutputDir, 'api.ts');
29
+ expect(existsSync(outputFile)).toBe(true);
30
+ const content = readFileSync(outputFile, 'utf-8');
31
+ // Verify key components exist
32
+ expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
33
+ expect(content).toContain('SwaggerPetstoreOpenAPI30');
34
+ expect(content).toContain('export const Pet');
35
+ expect(content).toContain('export const Order');
36
+ expect(content).toContain('export const User');
37
+ expect(content).toContain('async findPetsByStatus');
38
+ expect(content).toContain('async addPet');
39
+ expect(content).toContain('protected getBaseRequestOptions');
40
+ expect(content).toContain('protected async handleResponse');
41
+ expect(content).toContain('class ResponseValidationError<T> extends Error');
42
+ expect(content).toContain('Pet.safeParse(response)');
43
+ expect(content).toContain('new ResponseValidationError<Pet>');
44
+ });
45
+ it('should generate syntactically valid TypeScript code', async () => {
46
+ generator = new Generator('test-app', '1.0.0', reporter, './samples/swagger-petstore.yaml', testOutputDir);
47
+ await generator.run();
48
+ const outputFile = join(testOutputDir, 'api.ts');
49
+ const content = readFileSync(outputFile, 'utf-8');
50
+ // Verify basic TypeScript syntax
51
+ expect(content).toContain('import');
52
+ expect(content).toContain('export');
53
+ expect(content).toContain('class');
54
+ expect(content).toContain('async');
55
+ expect(content).toContain('protected');
56
+ // Verify code structure
57
+ expect(content.length).toBeGreaterThan(1000); // Should be substantial
58
+ expect(content.split('{').length).toBeGreaterThan(content.split('}').length - 10); // Rough bracket balance check
59
+ });
60
+ });
61
+ describe('test-logical.yaml', () => {
62
+ it('should generate correct logical operators', async () => {
63
+ generator = new Generator('test-app', '1.0.0', reporter, './samples/test-logical.yaml', testOutputDir);
64
+ await generator.run();
65
+ const outputFile = join(testOutputDir, 'api.ts');
66
+ const content = readFileSync(outputFile, 'utf-8');
67
+ // Verify logical operators are generated correctly
68
+ expect(content).toContain('TestAnyOf');
69
+ expect(content).toContain('z.union');
70
+ expect(content).toContain('TestOneOf');
71
+ expect(content).toContain('TestAllOf');
72
+ expect(content).toContain('z.intersection');
73
+ expect(content).toContain('TestNot');
74
+ });
75
+ });
76
+ describe('server-variables-example.yaml', () => {
77
+ it('should generate server configuration with variables', async () => {
78
+ generator = new Generator('test-app', '1.0.0', reporter, './samples/server-variables-example.yaml', testOutputDir);
79
+ await generator.run();
80
+ const outputFile = join(testOutputDir, 'api.ts');
81
+ const content = readFileSync(outputFile, 'utf-8');
82
+ // Verify server variables are handled
83
+ expect(content).toContain('serverConfigurations');
84
+ expect(content).toContain('serverVariables');
85
+ expect(content).toContain('resolveServerUrl');
86
+ expect(content).toContain('ClientOptions');
87
+ });
88
+ });
89
+ describe('pokeapi-openapi.json', () => {
90
+ it('should generate code for PokéAPI spec', async () => {
91
+ generator = new Generator('test-app', '1.0.0', reporter, './samples/pokeapi-openapi.json', testOutputDir);
92
+ await generator.run();
93
+ const outputFile = join(testOutputDir, 'api.ts');
94
+ const content = readFileSync(outputFile, 'utf-8');
95
+ expect(content).toMatch(/export (default )?class/);
96
+ expect(content).toContain('export const Pokemon');
97
+ expect(content).toContain('async getPokemonById');
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=code-generator-edge-cases.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-generator-edge-cases.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/code-generator-edge-cases.test.ts"],"names":[],"mappings":""}