zod-codegen 1.6.3 → 1.7.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 (119) 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 +11 -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.map +1 -1
  23. package/dist/src/services/file-reader.service.js +1 -1
  24. package/dist/src/services/file-writer.service.d.ts.map +1 -1
  25. package/dist/src/services/file-writer.service.js +2 -2
  26. package/dist/src/services/import-builder.service.d.ts.map +1 -1
  27. package/dist/src/services/import-builder.service.js +3 -3
  28. package/dist/src/services/type-builder.service.d.ts.map +1 -1
  29. package/dist/src/types/generator-options.d.ts.map +1 -1
  30. package/dist/src/types/openapi.d.ts.map +1 -1
  31. package/dist/src/types/openapi.js +20 -20
  32. package/dist/src/utils/error-handler.d.ts.map +1 -1
  33. package/dist/src/utils/naming-convention.d.ts.map +1 -1
  34. package/dist/src/utils/naming-convention.js +6 -3
  35. package/dist/src/utils/signal-handler.d.ts.map +1 -1
  36. package/dist/tests/integration/cli-comprehensive.test.d.ts +2 -0
  37. package/dist/tests/integration/cli-comprehensive.test.d.ts.map +1 -0
  38. package/dist/tests/integration/cli-comprehensive.test.js +110 -0
  39. package/dist/tests/integration/cli.test.d.ts +2 -0
  40. package/dist/tests/integration/cli.test.d.ts.map +1 -0
  41. package/dist/tests/integration/cli.test.js +25 -0
  42. package/dist/tests/integration/error-scenarios.test.d.ts +2 -0
  43. package/dist/tests/integration/error-scenarios.test.d.ts.map +1 -0
  44. package/dist/tests/integration/error-scenarios.test.js +169 -0
  45. package/dist/tests/integration/snapshots.test.d.ts +2 -0
  46. package/dist/tests/integration/snapshots.test.d.ts.map +1 -0
  47. package/dist/tests/integration/snapshots.test.js +100 -0
  48. package/dist/tests/unit/code-generator-edge-cases.test.d.ts +2 -0
  49. package/dist/tests/unit/code-generator-edge-cases.test.d.ts.map +1 -0
  50. package/dist/tests/unit/code-generator-edge-cases.test.js +506 -0
  51. package/dist/tests/unit/code-generator.test.d.ts +2 -0
  52. package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
  53. package/dist/tests/unit/code-generator.test.js +1364 -0
  54. package/dist/tests/unit/file-reader.test.d.ts +2 -0
  55. package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
  56. package/dist/tests/unit/file-reader.test.js +125 -0
  57. package/dist/tests/unit/generator.test.d.ts +2 -0
  58. package/dist/tests/unit/generator.test.d.ts.map +1 -0
  59. package/dist/tests/unit/generator.test.js +119 -0
  60. package/dist/tests/unit/naming-convention.test.d.ts +2 -0
  61. package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
  62. package/dist/tests/unit/naming-convention.test.js +256 -0
  63. package/dist/tests/unit/reporter.test.d.ts +2 -0
  64. package/dist/tests/unit/reporter.test.d.ts.map +1 -0
  65. package/dist/tests/unit/reporter.test.js +44 -0
  66. package/dist/tests/unit/type-builder.test.d.ts +2 -0
  67. package/dist/tests/unit/type-builder.test.d.ts.map +1 -0
  68. package/dist/tests/unit/type-builder.test.js +108 -0
  69. package/dist/vitest.config.d.ts.map +1 -1
  70. package/dist/vitest.config.js +10 -20
  71. package/eslint.config.mjs +38 -28
  72. package/examples/.gitkeep +1 -1
  73. package/examples/README.md +4 -2
  74. package/examples/petstore/README.md +18 -17
  75. package/examples/petstore/{type.ts → api.ts} +158 -74
  76. package/examples/petstore/authenticated-usage.ts +6 -4
  77. package/examples/petstore/basic-usage.ts +4 -3
  78. package/examples/petstore/error-handling-usage.ts +84 -0
  79. package/examples/petstore/retry-handler-usage.ts +11 -18
  80. package/examples/petstore/server-variables-usage.ts +10 -10
  81. package/examples/pokeapi/README.md +8 -8
  82. package/examples/pokeapi/api.ts +218 -0
  83. package/examples/pokeapi/basic-usage.ts +3 -2
  84. package/examples/pokeapi/custom-client.ts +5 -4
  85. package/package.json +17 -21
  86. package/src/cli.ts +20 -25
  87. package/src/generator.ts +13 -11
  88. package/src/interfaces/code-generator.ts +1 -1
  89. package/src/services/code-generator.service.ts +799 -1120
  90. package/src/services/file-reader.service.ts +6 -5
  91. package/src/services/file-writer.service.ts +7 -7
  92. package/src/services/import-builder.service.ts +9 -13
  93. package/src/services/type-builder.service.ts +8 -19
  94. package/src/types/generator-options.ts +1 -1
  95. package/src/types/openapi.ts +22 -22
  96. package/src/utils/error-handler.ts +2 -2
  97. package/src/utils/naming-convention.ts +13 -10
  98. package/src/utils/reporter.ts +2 -2
  99. package/src/utils/signal-handler.ts +7 -8
  100. package/tests/integration/cli-comprehensive.test.ts +38 -32
  101. package/tests/integration/cli.test.ts +5 -5
  102. package/tests/integration/error-scenarios.test.ts +20 -26
  103. package/tests/integration/snapshots.test.ts +19 -23
  104. package/tests/unit/code-generator-edge-cases.test.ts +133 -133
  105. package/tests/unit/code-generator.test.ts +431 -330
  106. package/tests/unit/file-reader.test.ts +14 -14
  107. package/tests/unit/generator.test.ts +30 -18
  108. package/tests/unit/naming-convention.test.ts +27 -27
  109. package/tests/unit/type-builder.test.ts +2 -2
  110. package/tsconfig.json +5 -3
  111. package/vitest.config.ts +11 -21
  112. package/dist/scripts/update-manifest.d.ts +0 -14
  113. package/dist/scripts/update-manifest.d.ts.map +0 -1
  114. package/dist/scripts/update-manifest.js +0 -33
  115. package/dist/src/assets/manifest.json +0 -5
  116. package/examples/pokeapi/type.ts +0 -109
  117. package/generated/type.ts +0 -371
  118. package/scripts/update-manifest.ts +0 -49
  119. package/src/assets/manifest.json +0 -5
@@ -1,8 +1,8 @@
1
- import {readFileSync} from 'node:fs';
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';
1
+ import { load } from 'js-yaml';
2
+ import { readFileSync } from 'node:fs';
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> {
@@ -14,6 +14,7 @@ export class SyncFileReaderService implements OpenApiFileReader {
14
14
  if (!response.ok) {
15
15
  throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
16
16
  }
17
+
17
18
  return await response.text();
18
19
  } catch {
19
20
  // If URL parsing fails, treat it as a local file path
@@ -1,12 +1,12 @@
1
- import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
2
- import {dirname, resolve} from 'node:path';
3
- import type {FileWriter} from '../interfaces/code-generator';
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import type { FileWriter } from '../interfaces/code-generator';
4
4
 
5
5
  export class SyncFileWriterService implements FileWriter {
6
6
  constructor(
7
7
  private readonly name: string,
8
8
  private readonly version: string,
9
- private readonly inputPath: string,
9
+ private readonly inputPath: string
10
10
  ) {}
11
11
 
12
12
  writeFile(filePath: string, content: string): void {
@@ -18,19 +18,19 @@ export class SyncFileWriterService implements FileWriter {
18
18
  '/* eslint-disable */',
19
19
  '// @ts-nocheck',
20
20
  '',
21
- content,
21
+ content
22
22
  ].join('\n');
23
23
 
24
24
  const dirPath = dirname(filePath);
25
25
 
26
26
  if (!existsSync(dirPath)) {
27
- mkdirSync(dirPath, {recursive: true});
27
+ mkdirSync(dirPath, { recursive: true });
28
28
  }
29
29
 
30
30
  writeFileSync(filePath, generatedContent);
31
31
  }
32
32
 
33
- resolveOutputPath(outputDir: string, fileName = 'type.ts'): string {
33
+ resolveOutputPath(outputDir: string, fileName = 'api.ts'): string {
34
34
  return resolve(outputDir, fileName);
35
35
  }
36
36
  }
@@ -1,13 +1,13 @@
1
1
  import * as ts from 'typescript';
2
- import {z} from 'zod';
3
- import type {ImportBuilder} from '../interfaces/code-generator';
2
+ import { z } from 'zod';
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);
7
7
 
8
8
  const ImportOptions = z.object({
9
9
  defaultImport: ImportedElement.optional(),
10
- namedImports: ImportedElement.optional(),
10
+ namedImports: ImportedElement.optional()
11
11
  });
12
12
 
13
13
  type ImportOptionsType = z.infer<typeof ImportOptions>;
@@ -16,15 +16,15 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
16
16
  buildImports(): ts.ImportDeclaration[] {
17
17
  return [
18
18
  this.createImport('zod', {
19
- namedImports: {z: false},
20
- }),
19
+ namedImports: { z: false }
20
+ })
21
21
  ];
22
22
  }
23
23
 
24
24
  createImport(target: string, options: ImportOptionsType): ts.ImportDeclaration {
25
25
  const safeOptions = ImportOptions.parse(options);
26
26
  const [defaultImport] = Object.entries(safeOptions.defaultImport ?? {})[0] ?? [undefined, false];
27
- const {success: hasDefaultImport} = z.string().safeParse(defaultImport);
27
+ const { success: hasDefaultImport } = z.string().safeParse(defaultImport);
28
28
 
29
29
  const safeNameImports = ImportedElement.safeParse(safeOptions.namedImports);
30
30
  const namedImportList = safeNameImports.success ? Object.entries(safeNameImports.data) : [];
@@ -35,7 +35,7 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
35
35
  ? ts.factory.createNamedImports(
36
36
  namedImportList.map(([name, isTypeImport]) => {
37
37
  return ts.factory.createImportSpecifier(isTypeImport, undefined, ts.factory.createIdentifier(name));
38
- }),
38
+ })
39
39
  )
40
40
  : undefined;
41
41
 
@@ -48,14 +48,10 @@ export class TypeScriptImportBuilderService implements ImportBuilder {
48
48
  undefined,
49
49
  hasAnyImports
50
50
  ? // eslint-disable-next-line @typescript-eslint/no-deprecated
51
- ts.factory.createImportClause(
52
- false,
53
- hasDefaultImport && defaultImport ? ts.factory.createIdentifier(defaultImport) : undefined,
54
- namedImports ?? undefined,
55
- )
51
+ ts.factory.createImportClause(false, hasDefaultImport && defaultImport ? ts.factory.createIdentifier(defaultImport) : undefined, namedImports ?? undefined)
56
52
  : undefined,
57
53
  ts.factory.createStringLiteral(target, true),
58
- undefined,
54
+ undefined
59
55
  );
60
56
  }
61
57
  }
@@ -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 {
@@ -18,16 +18,15 @@ export class TypeScriptTypeBuilderService implements TypeBuilder {
18
18
  const itemType = type.slice(0, -2);
19
19
  return ts.factory.createArrayTypeNode(this.buildType(itemType));
20
20
  }
21
+
21
22
  // Handle Record types
22
23
  if (type.startsWith('Record<')) {
23
24
  // For now, return unknown for complex Record types
24
25
  return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
25
26
  }
27
+
26
28
  // Custom type name - create a type reference
27
- return ts.factory.createTypeReferenceNode(
28
- ts.factory.createIdentifier(this.sanitizeIdentifier(type)),
29
- undefined,
30
- );
29
+ return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(this.sanitizeIdentifier(type)), undefined);
31
30
  }
32
31
  }
33
32
 
@@ -39,33 +38,23 @@ export class TypeScriptTypeBuilderService implements TypeBuilder {
39
38
  ts.factory[createIdentifier](name),
40
39
  undefined,
41
40
  this.buildType(type),
42
- undefined,
41
+ undefined
43
42
  );
44
43
  }
45
44
 
46
- createParameter(
47
- name: string,
48
- type?: string | ts.TypeNode,
49
- defaultValue?: ts.Expression,
50
- isOptional = false,
51
- ): ts.ParameterDeclaration {
45
+ createParameter(name: string, type?: string | ts.TypeNode, defaultValue?: ts.Expression, isOptional = false): ts.ParameterDeclaration {
52
46
  return ts.factory.createParameterDeclaration(
53
47
  undefined,
54
48
  undefined,
55
49
  ts.factory.createIdentifier(this.sanitizeIdentifier(name)),
56
50
  isOptional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
57
51
  typeof type === 'string' ? this.buildType(type) : type,
58
- defaultValue,
52
+ defaultValue
59
53
  );
60
54
  }
61
55
 
62
56
  createGenericType(name: string): ts.TypeParameterDeclaration {
63
- return ts.factory.createTypeParameterDeclaration(
64
- undefined,
65
- ts.factory.createIdentifier(name),
66
- undefined,
67
- undefined,
68
- );
57
+ return ts.factory.createTypeParameterDeclaration(undefined, ts.factory.createIdentifier(name), undefined, undefined);
69
58
  }
70
59
 
71
60
  sanitizeIdentifier(name: string): string {
@@ -1,4 +1,4 @@
1
- import type {NamingConvention, OperationNameTransformer} from '../utils/naming-convention';
1
+ import type { NamingConvention, OperationNameTransformer } from '../utils/naming-convention';
2
2
 
3
3
  /**
4
4
  * Configuration options for the Generator class.
@@ -1,7 +1,7 @@
1
- import {z} from 'zod';
1
+ import { z } from 'zod';
2
2
 
3
3
  export const Reference = z.object({
4
- $ref: z.string().optional(),
4
+ $ref: z.string().optional()
5
5
  });
6
6
 
7
7
  const BaseSchemaProperties = z.object({
@@ -38,32 +38,32 @@ const BaseSchemaProperties = z.object({
38
38
  xml: z
39
39
  .object({
40
40
  name: z.string().optional(),
41
- wrapped: z.boolean().optional(),
41
+ wrapped: z.boolean().optional()
42
42
  })
43
43
  .optional(),
44
44
  externalDocs: Reference.optional(),
45
45
  example: z.unknown().optional(),
46
- deprecated: z.boolean().optional(),
46
+ deprecated: z.boolean().optional()
47
47
  });
48
48
 
49
49
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
50
  export const SchemaProperties: z.ZodLazy<z.ZodObject<any>> = z.lazy(() =>
51
51
  BaseSchemaProperties.extend({
52
52
  properties: z.record(z.string(), SchemaProperties).optional(),
53
- items: SchemaProperties.optional(),
54
- }),
53
+ items: SchemaProperties.optional()
54
+ })
55
55
  );
56
56
 
57
57
  const ServerVariable = z.object({
58
58
  default: z.string(),
59
59
  description: z.string().optional(),
60
- enum: z.array(z.string()).optional(),
60
+ enum: z.array(z.string()).optional()
61
61
  });
62
62
 
63
63
  const Server = z.object({
64
64
  url: z.string(), // Allow templated URLs with {variables}
65
65
  description: z.string().optional(),
66
- variables: z.record(z.string(), ServerVariable).optional(),
66
+ variables: z.record(z.string(), ServerVariable).optional()
67
67
  });
68
68
 
69
69
  export const Parameter = z.object({
@@ -77,7 +77,7 @@ export const Parameter = z.object({
77
77
  style: z.string().optional(),
78
78
  explode: z.boolean().optional(),
79
79
  allowReserved: z.boolean().optional(),
80
- schema: SchemaProperties.optional(),
80
+ schema: SchemaProperties.optional()
81
81
  });
82
82
 
83
83
  const ResponseHeader = z.object({
@@ -89,25 +89,25 @@ const ResponseHeader = z.object({
89
89
  style: z.string().optional(),
90
90
  explode: z.boolean().optional(),
91
91
  allowReserved: z.boolean().optional(),
92
- schema: Reference.optional(),
92
+ schema: Reference.optional()
93
93
  });
94
94
 
95
95
  const MediaType = z.object({
96
- schema: z.unknown().optional(),
96
+ schema: z.unknown().optional()
97
97
  });
98
98
 
99
99
  export const Response = z.object({
100
100
  $ref: z.string().optional(),
101
101
  description: z.string(),
102
102
  headers: z.record(z.string(), ResponseHeader).optional(),
103
- content: z.record(z.string(), MediaType).optional(),
103
+ content: z.record(z.string(), MediaType).optional()
104
104
  });
105
105
 
106
106
  export const RequestBody = z.object({
107
107
  $ref: z.string().optional(),
108
108
  description: z.string().optional(),
109
109
  required: z.boolean().optional(),
110
- content: z.record(z.string(), MediaType).optional(),
110
+ content: z.record(z.string(), MediaType).optional()
111
111
  });
112
112
 
113
113
  export const MethodSchema = z.object({
@@ -118,7 +118,7 @@ export const MethodSchema = z.object({
118
118
  requestBody: RequestBody.optional(),
119
119
  responses: z.record(z.string(), Response).optional(),
120
120
  tags: z.array(z.string()).optional(),
121
- deprecated: z.boolean().optional(),
121
+ deprecated: z.boolean().optional()
122
122
  });
123
123
 
124
124
  export const PathItem = z.object({
@@ -133,7 +133,7 @@ export const PathItem = z.object({
133
133
  head: MethodSchema.optional(),
134
134
  options: MethodSchema.optional(),
135
135
  trace: MethodSchema.optional(),
136
- parameters: z.array(Parameter).optional(),
136
+ parameters: z.array(Parameter).optional()
137
137
  });
138
138
 
139
139
  const Info = z.object({
@@ -145,15 +145,15 @@ const Info = z.object({
145
145
  .object({
146
146
  name: z.string().optional(),
147
147
  email: z.email().optional(),
148
- url: z.url().optional(),
148
+ url: z.url().optional()
149
149
  })
150
150
  .optional(),
151
151
  license: z
152
152
  .object({
153
153
  name: z.string().min(1),
154
- url: z.url().optional(),
154
+ url: z.url().optional()
155
155
  })
156
- .optional(),
156
+ .optional()
157
157
  });
158
158
 
159
159
  const SecurityRequirement = z.record(z.string(), z.array(z.string()));
@@ -161,12 +161,12 @@ const SecurityRequirement = z.record(z.string(), z.array(z.string()));
161
161
  const Tag = z.object({
162
162
  name: z.string().min(1),
163
163
  description: z.string().optional(),
164
- externalDocs: Reference.optional(),
164
+ externalDocs: Reference.optional()
165
165
  });
166
166
 
167
167
  const ExternalDocumentation = z.object({
168
168
  description: z.string().optional(),
169
- url: z.url(),
169
+ url: z.url()
170
170
  });
171
171
 
172
172
  const Components = z.object({
@@ -178,7 +178,7 @@ const Components = z.object({
178
178
  headers: z.record(z.string(), ResponseHeader).optional(),
179
179
  securitySchemes: z.record(z.string(), Reference).optional(),
180
180
  links: z.record(z.string(), Reference).optional(),
181
- callbacks: z.record(z.string(), Reference).optional(),
181
+ callbacks: z.record(z.string(), Reference).optional()
182
182
  });
183
183
 
184
184
  export const OpenApiSpec = z.object({
@@ -189,7 +189,7 @@ export const OpenApiSpec = z.object({
189
189
  components: Components.optional(),
190
190
  security: z.array(SecurityRequirement).optional(),
191
191
  tags: z.array(Tag).optional(),
192
- externalDocs: ExternalDocumentation.optional(),
192
+ externalDocs: ExternalDocumentation.optional()
193
193
  });
194
194
 
195
195
  export type OpenApiSpecType = z.infer<typeof OpenApiSpec>;
@@ -1,5 +1,5 @@
1
- import {getExecutionTime} from './execution-time';
2
- import type {Reporter} from './reporter';
1
+ import { getExecutionTime } from './execution-time';
2
+ import type { Reporter } from './reporter';
3
3
 
4
4
  export const errorReceived = (process: NodeJS.Process, startTime: bigint, reporter: Reporter) => (): void => {
5
5
  reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
@@ -8,13 +8,7 @@
8
8
  * transformNamingConvention('getUserById', 'snake_case') // 'get_user_by_id'
9
9
  * ```
10
10
  */
11
- export type NamingConvention =
12
- | 'camelCase'
13
- | 'PascalCase'
14
- | 'snake_case'
15
- | 'kebab-case'
16
- | 'SCREAMING_SNAKE_CASE'
17
- | 'SCREAMING-KEBAB-CASE';
11
+ export type NamingConvention = 'camelCase' | 'PascalCase' | 'snake_case' | 'kebab-case' | 'SCREAMING_SNAKE_CASE' | 'SCREAMING-KEBAB-CASE';
18
12
 
19
13
  /**
20
14
  * Operation details provided to custom transformers.
@@ -66,7 +60,10 @@ export type OperationNameTransformer = (details: OperationDetails) => string;
66
60
  * Capitalizes the first letter of a word
67
61
  */
68
62
  function capitalize(word: string): string {
69
- if (word.length === 0) return word;
63
+ if (word.length === 0) {
64
+ return word;
65
+ }
66
+
70
67
  return word.charAt(0).toUpperCase() + word.slice(1);
71
68
  }
72
69
 
@@ -126,9 +123,15 @@ function normalizeToWords(input: string): string[] {
126
123
  * Converts words array to camelCase
127
124
  */
128
125
  function toCamelCase(words: string[]): string {
129
- if (words.length === 0) return '';
126
+ if (words.length === 0) {
127
+ return '';
128
+ }
129
+
130
130
  const [first, ...rest] = words;
131
- if (!first) return '';
131
+ if (!first) {
132
+ return '';
133
+ }
134
+
132
135
  return first + rest.map((w) => capitalize(w)).join('');
133
136
  }
134
137
 
@@ -1,9 +1,9 @@
1
- import {format} from 'node:util';
1
+ import { format } from 'node:util';
2
2
 
3
3
  export class Reporter {
4
4
  constructor(
5
5
  private readonly stdout: NodeJS.WriteStream,
6
- private readonly stderr: NodeJS.WriteStream = stdout,
6
+ private readonly stderr: NodeJS.WriteStream = stdout
7
7
  ) {
8
8
  this.log = this.log.bind(this);
9
9
  this.error = this.error.bind(this);
@@ -1,12 +1,11 @@
1
- import {getExecutionTime} from './execution-time';
2
- import type {Reporter} from './reporter';
1
+ import { getExecutionTime } from './execution-time';
2
+ import type { Reporter } from './reporter';
3
3
 
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
- };
4
+ export const signalReceived = (process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals, reporter: Reporter) => (): void => {
5
+ reporter.log(`Done after ${String(getExecutionTime(startTime))}s`);
6
+ process.kill(process.pid, event);
7
+ process.exit(1);
8
+ };
10
9
 
11
10
  export const handleSignals = (process: NodeJS.Process, startTime: bigint, reporter: Reporter): void => {
12
11
  const catchSignals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
@@ -1,7 +1,7 @@
1
- import {describe, expect, it} from 'vitest';
2
- import {execSync} from 'node:child_process';
3
- import {resolve} from 'node:path';
4
- import {existsSync, rmSync, readFileSync} from 'node:fs';
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
5
 
6
6
  describe('CLI Comprehensive Integration', () => {
7
7
  const cwd = resolve(__dirname, '../..');
@@ -9,13 +9,13 @@ describe('CLI Comprehensive Integration', () => {
9
9
 
10
10
  beforeEach(() => {
11
11
  if (existsSync(testOutputDir)) {
12
- rmSync(testOutputDir, {recursive: true, force: true});
12
+ rmSync(testOutputDir, { recursive: true, force: true });
13
13
  }
14
14
  });
15
15
 
16
16
  afterEach(() => {
17
17
  if (existsSync(testOutputDir)) {
18
- rmSync(testOutputDir, {recursive: true, force: true});
18
+ rmSync(testOutputDir, { recursive: true, force: true });
19
19
  }
20
20
  });
21
21
 
@@ -23,10 +23,10 @@ describe('CLI Comprehensive Integration', () => {
23
23
  it('should generate code with default output directory', () => {
24
24
  execSync('node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output generated', {
25
25
  encoding: 'utf-8',
26
- cwd,
26
+ cwd
27
27
  });
28
28
 
29
- const outputFile = resolve(cwd, 'generated/type.ts');
29
+ const outputFile = resolve(cwd, 'generated/api.ts');
30
30
  expect(existsSync(outputFile)).toBe(true);
31
31
 
32
32
  const content = readFileSync(outputFile, 'utf-8');
@@ -36,23 +36,20 @@ describe('CLI Comprehensive Integration', () => {
36
36
  it('should generate code with custom output directory', () => {
37
37
  execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir}`, {
38
38
  encoding: 'utf-8',
39
- cwd,
39
+ cwd
40
40
  });
41
41
 
42
- const outputFile = resolve(testOutputDir, 'type.ts');
42
+ const outputFile = resolve(testOutputDir, 'api.ts');
43
43
  expect(existsSync(outputFile)).toBe(true);
44
44
  });
45
45
 
46
46
  it('should accept naming convention option', () => {
47
- execSync(
48
- `node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention camelCase`,
49
- {
50
- encoding: 'utf-8',
51
- cwd,
52
- },
53
- );
47
+ execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention camelCase`, {
48
+ encoding: 'utf-8',
49
+ cwd
50
+ });
54
51
 
55
- const outputFile = resolve(testOutputDir, 'type.ts');
52
+ const outputFile = resolve(testOutputDir, 'api.ts');
56
53
  const content = readFileSync(outputFile, 'utf-8');
57
54
 
58
55
  // Verify camelCase is applied (operation IDs should be camelCase)
@@ -61,16 +58,25 @@ describe('CLI Comprehensive Integration', () => {
61
58
 
62
59
  it('should reject invalid naming convention', () => {
63
60
  expect(() => {
64
- execSync(
65
- `node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention invalid`,
66
- {
67
- encoding: 'utf-8',
68
- cwd,
69
- stdio: 'pipe',
70
- },
71
- );
61
+ execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir} --naming-convention invalid`, {
62
+ encoding: 'utf-8',
63
+ cwd,
64
+ stdio: 'pipe'
65
+ });
72
66
  }).toThrow();
73
67
  });
68
+
69
+ it('should write to custom file when output is a .ts path', () => {
70
+ const outputFile = resolve(testOutputDir, 'client.ts');
71
+ execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output "${outputFile}"`, {
72
+ encoding: 'utf-8',
73
+ cwd
74
+ });
75
+
76
+ expect(existsSync(outputFile)).toBe(true);
77
+ const content = readFileSync(outputFile, 'utf-8');
78
+ expect(content).toContain('SwaggerPetstoreOpenAPI30');
79
+ });
74
80
  });
75
81
 
76
82
  describe('CLI Error Handling', () => {
@@ -79,7 +85,7 @@ describe('CLI Comprehensive Integration', () => {
79
85
  execSync('node ./dist/src/cli.js --input ./nonexistent.yaml --output generated', {
80
86
  encoding: 'utf-8',
81
87
  cwd,
82
- stdio: 'pipe',
88
+ stdio: 'pipe'
83
89
  });
84
90
  expect.fail('Should have thrown an error');
85
91
  } catch (error: unknown) {
@@ -93,7 +99,7 @@ describe('CLI Comprehensive Integration', () => {
93
99
  execSync('node ./dist/src/cli.js --output generated', {
94
100
  encoding: 'utf-8',
95
101
  cwd,
96
- stdio: 'pipe',
102
+ stdio: 'pipe'
97
103
  });
98
104
  }).toThrow();
99
105
  });
@@ -103,20 +109,20 @@ describe('CLI Comprehensive Integration', () => {
103
109
  it('should handle JSON format', () => {
104
110
  execSync(`node ./dist/src/cli.js --input ./samples/pokeapi-openapi.json --output ${testOutputDir}`, {
105
111
  encoding: 'utf-8',
106
- cwd,
112
+ cwd
107
113
  });
108
114
 
109
- const outputFile = resolve(testOutputDir, 'type.ts');
115
+ const outputFile = resolve(testOutputDir, 'api.ts');
110
116
  expect(existsSync(outputFile)).toBe(true);
111
117
  });
112
118
 
113
119
  it('should handle YAML format', () => {
114
120
  execSync(`node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ${testOutputDir}`, {
115
121
  encoding: 'utf-8',
116
- cwd,
122
+ cwd
117
123
  });
118
124
 
119
- const outputFile = resolve(testOutputDir, 'type.ts');
125
+ const outputFile = resolve(testOutputDir, 'api.ts');
120
126
  expect(existsSync(outputFile)).toBe(true);
121
127
  });
122
128
  });
@@ -1,13 +1,13 @@
1
- import {describe, expect, it} from 'vitest';
2
- import {execSync} from 'node:child_process';
3
- import {resolve} from 'node:path';
1
+ import { execSync } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
4
 
5
5
  describe('CLI Integration', () => {
6
6
  describe('--help', () => {
7
7
  it('should display help information', () => {
8
8
  const result = execSync('node ./dist/src/cli.js --help', {
9
9
  encoding: 'utf-8',
10
- cwd: resolve(__dirname, '../..'),
10
+ cwd: resolve(__dirname, '../..')
11
11
  });
12
12
 
13
13
  expect(result).toContain('Usage:');
@@ -20,7 +20,7 @@ describe('CLI Integration', () => {
20
20
  it('should display version information', () => {
21
21
  const result = execSync('node ./dist/src/cli.js --version', {
22
22
  encoding: 'utf-8',
23
- cwd: resolve(__dirname, '../..'),
23
+ cwd: resolve(__dirname, '../..')
24
24
  });
25
25
 
26
26
  expect(result).toMatch(/\d+\.\d+\.\d+/);