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,29 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import {load} from 'js-yaml';
3
+ import type {OpenApiFileParser, OpenApiFileReader} from '../interfaces/file-reader.js';
4
+ import type {OpenApiSpecType} from '../types/openapi.js';
5
+ import {OpenApiSpec} from '../types/openapi.js';
6
+
7
+ export class SyncFileReaderService implements OpenApiFileReader {
8
+ readFile(path: string): string {
9
+ return readFileSync(path, 'utf8');
10
+ }
11
+ }
12
+
13
+ export class OpenApiFileParserService implements OpenApiFileParser<OpenApiSpecType> {
14
+ parse(input: unknown): OpenApiSpecType {
15
+ let parsedInput: unknown;
16
+
17
+ if (typeof input === 'string') {
18
+ try {
19
+ parsedInput = JSON.parse(input);
20
+ } catch {
21
+ parsedInput = load(input);
22
+ }
23
+ } else {
24
+ parsedInput = input;
25
+ }
26
+
27
+ return OpenApiSpec.parse(parsedInput);
28
+ }
29
+ }
@@ -0,0 +1,36 @@
1
+ import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
2
+ import {dirname, resolve} from 'node:path';
3
+ import type {FileWriter} from '../interfaces/code-generator.js';
4
+
5
+ export class SyncFileWriterService implements FileWriter {
6
+ constructor(
7
+ private readonly name: string,
8
+ private readonly version: string,
9
+ private readonly inputPath: string,
10
+ ) {}
11
+
12
+ writeFile(filePath: string, content: string): void {
13
+ const generatedContent = [
14
+ '// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
15
+ `// Built with ${this.name}@${this.version}`,
16
+ `// Latest edit: ${new Date().toUTCString()}`,
17
+ `// Source file: ${this.inputPath}`,
18
+ '/* eslint-disable */',
19
+ '// @ts-nocheck',
20
+ '',
21
+ content,
22
+ ].join('\n');
23
+
24
+ const dirPath = dirname(filePath);
25
+
26
+ if (!existsSync(dirPath)) {
27
+ mkdirSync(dirPath, {recursive: true});
28
+ }
29
+
30
+ writeFileSync(filePath, generatedContent);
31
+ }
32
+
33
+ resolveOutputPath(outputDir: string, fileName = 'type.ts'): string {
34
+ return resolve(outputDir, fileName);
35
+ }
36
+ }
@@ -0,0 +1,64 @@
1
+ import * as ts from 'typescript';
2
+ import {z} from 'zod';
3
+ import type {ImportBuilder} from '../interfaces/code-generator.js';
4
+
5
+ const IsTypeImport = z.boolean();
6
+ const ImportedElement = z.record(z.string(), IsTypeImport);
7
+
8
+ const ImportOptions = z.object({
9
+ defaultImport: ImportedElement.optional(),
10
+ namedImports: ImportedElement.optional(),
11
+ });
12
+
13
+ type ImportOptionsType = z.infer<typeof ImportOptions>;
14
+
15
+ export class TypeScriptImportBuilderService implements ImportBuilder {
16
+ buildImports(): ts.ImportDeclaration[] {
17
+ return [
18
+ this.createImport('zod', {
19
+ defaultImport: {z: false},
20
+ }),
21
+ this.createImport('path-to-regexp', {
22
+ namedImports: {compile: false},
23
+ }),
24
+ ];
25
+ }
26
+
27
+ createImport(target: string, options: ImportOptionsType): ts.ImportDeclaration {
28
+ const safeOptions = ImportOptions.parse(options);
29
+ const [defaultImport] = Object.entries(safeOptions.defaultImport ?? {})[0] ?? [undefined, false];
30
+ const {success: hasDefaultImport} = z.string().safeParse(defaultImport);
31
+
32
+ const safeNameImports = ImportedElement.safeParse(safeOptions.namedImports);
33
+ const namedImportList = safeNameImports.success ? Object.entries(safeNameImports.data) : [];
34
+
35
+ // Create import specifiers for named imports
36
+ const namedImports =
37
+ namedImportList.length > 0
38
+ ? ts.factory.createNamedImports(
39
+ namedImportList.map(([name, isTypeImport = false]) => {
40
+ return ts.factory.createImportSpecifier(isTypeImport, undefined, ts.factory.createIdentifier(name));
41
+ }),
42
+ )
43
+ : undefined;
44
+
45
+ // Check if we have any imports at all
46
+ const hasAnyImports = hasDefaultImport || namedImports;
47
+
48
+ // For side effects imports, we can pass undefined as the import clause
49
+ // For imports with bindings, we need to create the clause differently
50
+ return ts.factory.createImportDeclaration(
51
+ undefined,
52
+ hasAnyImports
53
+ ? ({
54
+ kind: ts.SyntaxKind.ImportClause,
55
+ isTypeOnly: false,
56
+ name: hasDefaultImport && defaultImport ? ts.factory.createIdentifier(defaultImport) : undefined,
57
+ namedBindings: namedImports,
58
+ } as ts.ImportClause)
59
+ : undefined,
60
+ ts.factory.createStringLiteral(target, true),
61
+ undefined,
62
+ );
63
+ }
64
+ }
@@ -0,0 +1,77 @@
1
+ import * as ts from 'typescript';
2
+ import type {TypeBuilder} from '../interfaces/code-generator.js';
3
+
4
+ export class TypeScriptTypeBuilderService implements TypeBuilder {
5
+ buildType(type: string): ts.TypeNode {
6
+ switch (type) {
7
+ case 'string':
8
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
9
+ case 'number':
10
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
11
+ case 'boolean':
12
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
13
+ case 'unknown':
14
+ default:
15
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
16
+ }
17
+ }
18
+
19
+ createProperty(name: string, type: string, isReadonly = false): ts.PropertyDeclaration {
20
+ const createIdentifier = name.startsWith('#') ? 'createPrivateIdentifier' : 'createIdentifier';
21
+
22
+ return ts.factory.createPropertyDeclaration(
23
+ isReadonly ? [ts.factory.createToken(ts.SyntaxKind.ReadonlyKeyword)] : undefined,
24
+ ts.factory[createIdentifier](name),
25
+ undefined,
26
+ this.buildType(type),
27
+ undefined,
28
+ );
29
+ }
30
+
31
+ createParameter(
32
+ name: string,
33
+ type?: string | ts.TypeNode,
34
+ defaultValue?: ts.Expression,
35
+ isOptional = false,
36
+ ): ts.ParameterDeclaration {
37
+ return ts.factory.createParameterDeclaration(
38
+ undefined,
39
+ undefined,
40
+ ts.factory.createIdentifier(this.sanitizeIdentifier(name)),
41
+ isOptional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
42
+ typeof type === 'string' ? this.buildType(type) : type,
43
+ defaultValue,
44
+ );
45
+ }
46
+
47
+ createGenericType(name: string): ts.TypeParameterDeclaration {
48
+ return ts.factory.createTypeParameterDeclaration(
49
+ undefined,
50
+ ts.factory.createIdentifier(name),
51
+ undefined,
52
+ undefined,
53
+ );
54
+ }
55
+
56
+ sanitizeIdentifier(name: string): string {
57
+ let sanitized = name.replace(/[^a-zA-Z0-9_]/g, '_');
58
+
59
+ if (/^[0-9]/.test(sanitized)) {
60
+ sanitized = '_' + sanitized;
61
+ }
62
+
63
+ if (sanitized.length === 0) {
64
+ sanitized = '_';
65
+ }
66
+
67
+ return sanitized;
68
+ }
69
+
70
+ toCamelCase(word: string): string {
71
+ return word.charAt(0).toLowerCase() + word.slice(1);
72
+ }
73
+
74
+ toPascalCase(word: string): string {
75
+ return word.charAt(0).toUpperCase() + word.slice(1);
76
+ }
77
+ }
@@ -0,0 +1,35 @@
1
+ export interface HttpRequestConfig<TData = unknown> {
2
+ readonly url: string;
3
+ readonly method: HttpMethod;
4
+ readonly headers?: Record<string, string>;
5
+ readonly params?: Record<string, string | number | boolean>;
6
+ readonly data?: TData;
7
+ readonly timeout?: number;
8
+ }
9
+
10
+ export interface HttpResponse<TData = unknown> {
11
+ readonly data: TData;
12
+ readonly status: number;
13
+ readonly statusText: string;
14
+ readonly headers: Record<string, string>;
15
+ readonly url: string;
16
+ }
17
+
18
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
19
+
20
+ export interface HttpClient {
21
+ request<TResponse = unknown, TRequest = unknown>(
22
+ config: HttpRequestConfig<TRequest>,
23
+ ): Promise<HttpResponse<TResponse>>;
24
+ }
25
+
26
+ export class HttpError extends Error {
27
+ constructor(
28
+ message: string,
29
+ public readonly status: number,
30
+ public readonly response?: HttpResponse,
31
+ ) {
32
+ super(message);
33
+ this.name = 'HttpError';
34
+ }
35
+ }
@@ -0,0 +1,202 @@
1
+ import {z} from 'zod';
2
+
3
+ export const Reference = z.object({
4
+ $ref: z.string().optional(),
5
+ });
6
+
7
+ const BaseSchemaProperties = z.object({
8
+ $ref: z.string().optional(),
9
+ title: z.string().optional(),
10
+ multipleOf: z.number().positive().optional(),
11
+ maximum: z.number().optional(),
12
+ exclusiveMaximum: z.boolean().optional(),
13
+ minimum: z.number().optional(),
14
+ exclusiveMinimum: z.boolean().optional(),
15
+ maxLength: z.number().int().nonnegative().optional(),
16
+ minLength: z.number().int().nonnegative().optional(),
17
+ pattern: z.string().optional(),
18
+ maxItems: z.number().int().nonnegative().optional(),
19
+ minItems: z.number().int().nonnegative().optional(),
20
+ uniqueItems: z.boolean().optional(),
21
+ maxProperties: z.number().int().nonnegative().optional(),
22
+ minProperties: z.number().int().nonnegative().optional(),
23
+ required: z.array(z.string()).optional(),
24
+ enum: z.array(z.unknown()).optional(),
25
+ type: z.string().optional(),
26
+ allOf: z.array(z.unknown()).optional(),
27
+ oneOf: z.array(z.unknown()).optional(),
28
+ anyOf: z.array(z.unknown()).optional(),
29
+ not: z.unknown().optional(),
30
+ additionalProperties: z.unknown().optional(),
31
+ description: z.string().optional(),
32
+ format: z.string().optional(),
33
+ default: z.unknown().optional(),
34
+ nullable: z.boolean().optional(),
35
+ discriminator: Reference.optional(),
36
+ readOnly: z.boolean().optional(),
37
+ writeOnly: z.boolean().optional(),
38
+ xml: z
39
+ .object({
40
+ name: z.string().optional(),
41
+ wrapped: z.boolean().optional(),
42
+ })
43
+ .optional(),
44
+ externalDocs: Reference.optional(),
45
+ example: z.unknown().optional(),
46
+ deprecated: z.boolean().optional(),
47
+ });
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ export const SchemaProperties: z.ZodLazy<z.ZodObject<any>> = z.lazy(() =>
51
+ BaseSchemaProperties.extend({
52
+ properties: z.record(z.string(), SchemaProperties).optional(),
53
+ items: SchemaProperties.optional(),
54
+ }),
55
+ );
56
+
57
+ const ServerVariable = z.object({
58
+ default: z.string(),
59
+ description: z.string().optional(),
60
+ enum: z.array(z.string()).optional(),
61
+ });
62
+
63
+ const Server = z.object({
64
+ url: z.string().url(),
65
+ description: z.string().optional(),
66
+ variables: z.record(z.string(), ServerVariable).optional(),
67
+ });
68
+
69
+ export const Parameter = z.object({
70
+ $ref: z.string().optional(),
71
+ name: z.string(),
72
+ in: z.enum(['query', 'header', 'path', 'cookie']),
73
+ description: z.string().optional(),
74
+ required: z.boolean().optional(),
75
+ deprecated: z.boolean().optional(),
76
+ allowEmptyValue: z.boolean().optional(),
77
+ style: z.string().optional(),
78
+ explode: z.boolean().optional(),
79
+ allowReserved: z.boolean().optional(),
80
+ schema: SchemaProperties.optional(),
81
+ });
82
+
83
+ const ResponseHeader = z.object({
84
+ $ref: z.string().optional(),
85
+ description: z.string().optional(),
86
+ required: z.boolean().optional(),
87
+ deprecated: z.boolean().optional(),
88
+ allowEmptyValue: z.boolean().optional(),
89
+ style: z.string().optional(),
90
+ explode: z.boolean().optional(),
91
+ allowReserved: z.boolean().optional(),
92
+ schema: Reference.optional(),
93
+ });
94
+
95
+ const MediaType = z.object({
96
+ schema: z.unknown().optional(),
97
+ });
98
+
99
+ export const Response = z.object({
100
+ $ref: z.string().optional(),
101
+ description: z.string(),
102
+ headers: z.record(z.string(), ResponseHeader).optional(),
103
+ content: z.record(z.string(), MediaType).optional(),
104
+ });
105
+
106
+ export const RequestBody = z.object({
107
+ $ref: z.string().optional(),
108
+ description: z.string().optional(),
109
+ required: z.boolean().optional(),
110
+ content: z.record(z.string(), MediaType).optional(),
111
+ });
112
+
113
+ export const MethodSchema = z.object({
114
+ summary: z.string().optional(),
115
+ description: z.string().optional(),
116
+ operationId: z.string().optional(),
117
+ parameters: z.array(Parameter).optional(),
118
+ requestBody: RequestBody.optional(),
119
+ responses: z.record(z.string(), Response).optional(),
120
+ tags: z.array(z.string()).optional(),
121
+ deprecated: z.boolean().optional(),
122
+ });
123
+
124
+ export const PathItem = z.object({
125
+ $ref: z.string().optional(),
126
+ summary: z.string().optional(),
127
+ description: z.string().optional(),
128
+ get: MethodSchema.optional(),
129
+ post: MethodSchema.optional(),
130
+ put: MethodSchema.optional(),
131
+ patch: MethodSchema.optional(),
132
+ delete: MethodSchema.optional(),
133
+ head: MethodSchema.optional(),
134
+ options: MethodSchema.optional(),
135
+ trace: MethodSchema.optional(),
136
+ parameters: z.array(Parameter).optional(),
137
+ });
138
+
139
+ const Info = z.object({
140
+ title: z.string().min(1),
141
+ version: z.string().min(1),
142
+ description: z.string().optional(),
143
+ termsOfService: z.string().url().optional(),
144
+ contact: z
145
+ .object({
146
+ name: z.string().optional(),
147
+ email: z.string().email().optional(),
148
+ url: z.string().url().optional(),
149
+ })
150
+ .optional(),
151
+ license: z
152
+ .object({
153
+ name: z.string().min(1),
154
+ url: z.string().url().optional(),
155
+ })
156
+ .optional(),
157
+ });
158
+
159
+ const SecurityRequirement = z.record(z.string(), z.array(z.string()));
160
+
161
+ const Tag = z.object({
162
+ name: z.string().min(1),
163
+ description: z.string().optional(),
164
+ externalDocs: Reference.optional(),
165
+ });
166
+
167
+ const ExternalDocumentation = z.object({
168
+ description: z.string().optional(),
169
+ url: z.string().url(),
170
+ });
171
+
172
+ const Components = z.object({
173
+ schemas: z.record(z.string(), SchemaProperties).optional(),
174
+ responses: z.record(z.string(), Response).optional(),
175
+ parameters: z.record(z.string(), Parameter).optional(),
176
+ examples: z.record(z.string(), Reference).optional(),
177
+ requestBodies: z.record(z.string(), RequestBody).optional(),
178
+ headers: z.record(z.string(), ResponseHeader).optional(),
179
+ securitySchemes: z.record(z.string(), Reference).optional(),
180
+ links: z.record(z.string(), Reference).optional(),
181
+ callbacks: z.record(z.string(), Reference).optional(),
182
+ });
183
+
184
+ export const OpenApiSpec = z.object({
185
+ openapi: z.string().regex(/^3\.\d+\.\d+$/, 'OpenAPI version must be in format 3.x.x'),
186
+ info: Info,
187
+ servers: z.array(Server).optional(),
188
+ paths: z.record(z.string(), PathItem),
189
+ components: Components.optional(),
190
+ security: z.array(SecurityRequirement).optional(),
191
+ tags: z.array(Tag).optional(),
192
+ externalDocs: ExternalDocumentation.optional(),
193
+ });
194
+
195
+ export type OpenApiSpecType = z.infer<typeof OpenApiSpec>;
196
+ export type SchemaPropertiesType = z.infer<typeof SchemaProperties>;
197
+ export type ParameterType = z.infer<typeof Parameter>;
198
+ export type ResponseType = z.infer<typeof Response>;
199
+ export type RequestBodyType = z.infer<typeof RequestBody>;
200
+ export type MethodSchemaType = z.infer<typeof MethodSchema>;
201
+ export type PathItemType = z.infer<typeof PathItem>;
202
+ export type ReferenceType = z.infer<typeof Reference>;
@@ -0,0 +1,13 @@
1
+ import {getExecutionTime} from './execution-time.js';
2
+
3
+ export const errorReceived = (process: NodeJS.Process, startTime: bigint) => (): void => {
4
+ console.log(`Done after ${String(getExecutionTime(startTime))}s`);
5
+ process.exit(1);
6
+ };
7
+
8
+ export const handleErrors = (process: NodeJS.Process, startTime: bigint): void => {
9
+ const catchErrors: string[] = ['unhandledRejection', 'uncaughtException'];
10
+ catchErrors.map((event): NodeJS.EventEmitter => {
11
+ return process.on(event, errorReceived(process, startTime));
12
+ });
13
+ };
@@ -0,0 +1,3 @@
1
+ export function getExecutionTime(startTime: bigint): number {
2
+ return Number(process.hrtime.bigint() - startTime) / 1000000000;
3
+ }
@@ -0,0 +1,17 @@
1
+ import {z} from 'zod';
2
+
3
+ interface Manifest {
4
+ readonly name: string;
5
+ readonly version: string;
6
+ readonly description: string;
7
+ }
8
+
9
+ const ManifestSchema = z.object({
10
+ name: z.string().min(1),
11
+ version: z.string().min(1),
12
+ description: z.string().min(1),
13
+ });
14
+
15
+ export function isManifest(input: unknown): input is Manifest {
16
+ return ManifestSchema.safeParse(input).success;
17
+ }
@@ -0,0 +1,16 @@
1
+ import {format} from 'node:util';
2
+
3
+ export class Reporter {
4
+ constructor(private readonly _stdout: NodeJS.WriteStream) {
5
+ this.log = this.log.bind(this);
6
+ this.error = this.error.bind(this);
7
+ }
8
+
9
+ log(...args: readonly unknown[]): void {
10
+ this._stdout.write(format(...args) + '\n');
11
+ }
12
+
13
+ error(...args: readonly unknown[]): void {
14
+ this._stdout.write(format(...args) + '\n');
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ import {getExecutionTime} from './execution-time.js';
2
+
3
+ export const signalReceived = (process: NodeJS.Process, startTime: bigint, event: NodeJS.Signals) => (): void => {
4
+ console.log(`Done after ${String(getExecutionTime(startTime))}s`);
5
+ process.kill(process.pid, event);
6
+ process.exit(1);
7
+ };
8
+
9
+ export const handleSignals = (process: NodeJS.Process, startTime: bigint): void => {
10
+ const catchSignals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
11
+ catchSignals.map((event): NodeJS.EventEmitter => {
12
+ return process.once(event, signalReceived(process, startTime, event));
13
+ });
14
+ };
@@ -0,0 +1,3 @@
1
+ export function isTTY(process: NodeJS.Process): boolean {
2
+ return process.stdout.isTTY || false;
3
+ }
@@ -0,0 +1,29 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {execSync} from 'node:child_process';
3
+ import {resolve} from 'node:path';
4
+
5
+ describe('CLI Integration', () => {
6
+ describe('--help', () => {
7
+ it('should display help information', () => {
8
+ const result = execSync('npm run build && node ./dist/src/cli.js --help', {
9
+ encoding: 'utf-8',
10
+ cwd: resolve(__dirname, '../..'),
11
+ });
12
+
13
+ expect(result).toContain('Usage:');
14
+ expect(result).toContain('--input');
15
+ expect(result).toContain('--output');
16
+ });
17
+ });
18
+
19
+ describe('--version', () => {
20
+ it('should display version information', () => {
21
+ const result = execSync('npm run build && node ./dist/src/cli.js --version', {
22
+ encoding: 'utf-8',
23
+ cwd: resolve(__dirname, '../..'),
24
+ });
25
+
26
+ expect(result).toMatch(/\d+\.\d+\.\d+/);
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,36 @@
1
+ import {beforeEach, describe, expect, it, vi} from 'vitest';
2
+ import {Generator} from '../../src/generator.js';
3
+ import {Reporter} from '../../src/utils/reporter.js';
4
+
5
+ describe('Generator', () => {
6
+ let generator: Generator;
7
+ let mockReporter: Reporter;
8
+
9
+ beforeEach(() => {
10
+ // Create a mock reporter
11
+ mockReporter = {
12
+ info: vi.fn(),
13
+ error: vi.fn(),
14
+ warn: vi.fn(),
15
+ success: vi.fn(),
16
+ } as unknown as Reporter;
17
+
18
+ generator = new Generator('test-app', '1.0.0', mockReporter, './test-input.json', './test-output');
19
+ });
20
+
21
+ describe('constructor', () => {
22
+ it('should create a new Generator instance', () => {
23
+ expect(generator).toBeInstanceOf(Generator);
24
+ });
25
+ });
26
+
27
+ describe('run', () => {
28
+ it('should be a function', () => {
29
+ expect(typeof generator.run).toBe('function');
30
+ });
31
+
32
+ it('should have the run method defined', () => {
33
+ expect(generator).toHaveProperty('run');
34
+ });
35
+ });
36
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": false,
4
+ "alwaysStrict": true,
5
+ "baseUrl": ".",
6
+ "checkJs": false,
7
+ "downlevelIteration": true,
8
+ "esModuleInterop": true,
9
+ "exactOptionalPropertyTypes": false,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "incremental": true,
12
+ "isolatedModules": true,
13
+ "lib": ["ES2022", "DOM"],
14
+ "module": "ESNext",
15
+ "moduleResolution": "Bundler",
16
+ "noEmit": false,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noImplicitAny": true,
19
+ "noImplicitOverride": true,
20
+ "noImplicitReturns": true,
21
+ "noImplicitThis": true,
22
+ "noPropertyAccessFromIndexSignature": false,
23
+ "noUncheckedIndexedAccess": true,
24
+ "noUnusedLocals": true,
25
+ "noUnusedParameters": true,
26
+ "outDir": "dist",
27
+ "removeComments": false,
28
+ "resolveJsonModule": true,
29
+ "rootDir": ".",
30
+ "skipLibCheck": true,
31
+ "sourceMap": false,
32
+ "strict": true,
33
+ "strictBindCallApply": true,
34
+ "strictFunctionTypes": true,
35
+ "strictNullChecks": true,
36
+ "strictPropertyInitialization": true,
37
+ "target": "ES2022",
38
+ "typeRoots": ["./node_modules/@types"],
39
+ "useUnknownInCatchVariables": true,
40
+ "verbatimModuleSyntax": false
41
+ },
42
+ "exclude": ["node_modules", "dist", "build"],
43
+ "include": ["src/**/*.ts", "generated/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
44
+ }
@@ -0,0 +1,39 @@
1
+ import {defineConfig} from 'vitest/config';
2
+ import {resolve} from 'node:path';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'node',
8
+ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
9
+ exclude: ['node_modules', 'dist', 'coverage'],
10
+ coverage: {
11
+ provider: 'v8',
12
+ reporter: ['text', 'json', 'html', 'lcov'],
13
+ exclude: [
14
+ 'coverage/**',
15
+ 'dist/**',
16
+ 'node_modules/**',
17
+ '**/*.d.ts',
18
+ '**/*.config.*',
19
+ '**/*.test.*',
20
+ '**/*.spec.*',
21
+ 'scripts/**',
22
+ 'samples/**',
23
+ ],
24
+ thresholds: {
25
+ global: {
26
+ branches: 80,
27
+ functions: 80,
28
+ lines: 80,
29
+ statements: 80,
30
+ },
31
+ },
32
+ },
33
+ },
34
+ resolve: {
35
+ alias: {
36
+ '@': resolve(__dirname, 'src'),
37
+ },
38
+ },
39
+ });