zod-codegen 1.4.0 → 1.5.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.
- package/.github/workflows/ci.yml +18 -18
- package/.github/workflows/release.yml +8 -8
- package/CHANGELOG.md +26 -0
- package/dist/src/services/code-generator.service.d.ts +15 -0
- package/dist/src/services/code-generator.service.d.ts.map +1 -1
- package/dist/src/services/code-generator.service.js +124 -6
- package/package.json +14 -19
- package/src/services/code-generator.service.ts +160 -11
- package/tests/unit/code-generator.test.ts +199 -3
- package/tests/unit/generator.test.ts +2 -2
- package/tsconfig.json +1 -1
- package/.claude/settings.local.json +0 -43
- package/dist/scripts/update-manifest.d.ts +0 -14
- package/dist/scripts/update-manifest.d.ts.map +0 -1
- package/dist/scripts/update-manifest.js +0 -31
- package/dist/tests/integration/cli.test.d.ts +0 -2
- package/dist/tests/integration/cli.test.d.ts.map +0 -1
- package/dist/tests/integration/cli.test.js +0 -25
- package/dist/tests/unit/code-generator.test.d.ts +0 -2
- package/dist/tests/unit/code-generator.test.d.ts.map +0 -1
- package/dist/tests/unit/code-generator.test.js +0 -459
- package/dist/tests/unit/file-reader.test.d.ts +0 -2
- package/dist/tests/unit/file-reader.test.d.ts.map +0 -1
- package/dist/tests/unit/file-reader.test.js +0 -110
- package/dist/tests/unit/generator.test.d.ts +0 -2
- package/dist/tests/unit/generator.test.d.ts.map +0 -1
- package/dist/tests/unit/generator.test.js +0 -100
- package/dist/tests/unit/naming-convention.test.d.ts +0 -2
- package/dist/tests/unit/naming-convention.test.d.ts.map +0 -1
- package/dist/tests/unit/naming-convention.test.js +0 -231
- package/scripts/republish-versions.sh +0 -94
|
@@ -21,6 +21,10 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
21
21
|
private readonly namingConvention: NamingConvention | undefined;
|
|
22
22
|
private readonly operationNameTransformer: OperationNameTransformer | undefined;
|
|
23
23
|
|
|
24
|
+
// Track circular dependencies for z.lazy() wrapping
|
|
25
|
+
private circularSchemas = new Set<string>();
|
|
26
|
+
private currentSchemaName: string | null = null;
|
|
27
|
+
|
|
24
28
|
constructor(options: GeneratorOptions = {}) {
|
|
25
29
|
this.namingConvention = options.namingConvention;
|
|
26
30
|
this.operationNameTransformer = options.operationNameTransformer;
|
|
@@ -85,12 +89,25 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
85
89
|
|
|
86
90
|
private buildSchemas(openapi: OpenApiSpecType): Record<string, ts.VariableStatement> {
|
|
87
91
|
const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
|
|
88
|
-
const
|
|
92
|
+
const schemasMap = Object.fromEntries(schemasEntries);
|
|
93
|
+
|
|
94
|
+
// Detect circular dependencies before building schemas
|
|
95
|
+
this.circularSchemas = this.detectCircularDependencies(schemasMap);
|
|
96
|
+
|
|
97
|
+
const sortedSchemaNames = this.topologicalSort(schemasMap);
|
|
89
98
|
|
|
90
99
|
return sortedSchemaNames.reduce<Record<string, ts.VariableStatement>>((schemaRegistered, name) => {
|
|
91
100
|
const schema = openapi.components?.schemas?.[name];
|
|
92
101
|
if (!schema) return schemaRegistered;
|
|
93
102
|
|
|
103
|
+
// Set context for current schema being built
|
|
104
|
+
this.currentSchemaName = name;
|
|
105
|
+
|
|
106
|
+
const schemaExpression = this.buildSchema(schema);
|
|
107
|
+
|
|
108
|
+
// Clear context
|
|
109
|
+
this.currentSchemaName = null;
|
|
110
|
+
|
|
94
111
|
const variableStatement = ts.factory.createVariableStatement(
|
|
95
112
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
96
113
|
ts.factory.createVariableDeclarationList(
|
|
@@ -99,7 +116,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
99
116
|
ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)),
|
|
100
117
|
undefined,
|
|
101
118
|
undefined,
|
|
102
|
-
|
|
119
|
+
schemaExpression,
|
|
103
120
|
),
|
|
104
121
|
],
|
|
105
122
|
ts.NodeFlags.Const,
|
|
@@ -136,7 +153,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
136
153
|
const methods = this.buildClientMethods(openapi, schemas);
|
|
137
154
|
|
|
138
155
|
return ts.factory.createClassDeclaration(
|
|
139
|
-
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
|
156
|
+
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword), ts.factory.createToken(ts.SyntaxKind.DefaultKeyword)],
|
|
140
157
|
ts.factory.createIdentifier(clientName),
|
|
141
158
|
undefined,
|
|
142
159
|
undefined,
|
|
@@ -275,9 +292,9 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
275
292
|
|
|
276
293
|
private buildHttpRequestMethod(): ts.MethodDeclaration {
|
|
277
294
|
return ts.factory.createMethodDeclaration(
|
|
278
|
-
[ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
|
|
295
|
+
[ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
|
|
279
296
|
undefined,
|
|
280
|
-
ts.factory.
|
|
297
|
+
ts.factory.createIdentifier('makeRequest'),
|
|
281
298
|
undefined,
|
|
282
299
|
[this.typeBuilder.createGenericType('T')],
|
|
283
300
|
[
|
|
@@ -1002,10 +1019,7 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
1002
1019
|
|
|
1003
1020
|
// Call makeRequest
|
|
1004
1021
|
const makeRequestCall = ts.factory.createCallExpression(
|
|
1005
|
-
ts.factory.createPropertyAccessExpression(
|
|
1006
|
-
ts.factory.createThis(),
|
|
1007
|
-
ts.factory.createPrivateIdentifier('#makeRequest'),
|
|
1008
|
-
),
|
|
1022
|
+
ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier('makeRequest')),
|
|
1009
1023
|
undefined,
|
|
1010
1024
|
[ts.factory.createStringLiteral(method.toUpperCase(), true), pathExpression, optionsExpression],
|
|
1011
1025
|
);
|
|
@@ -2691,10 +2705,145 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
|
|
|
2691
2705
|
return false;
|
|
2692
2706
|
}
|
|
2693
2707
|
|
|
2694
|
-
private buildFromReference(reference: ReferenceType): ts.Identifier {
|
|
2708
|
+
private buildFromReference(reference: ReferenceType): ts.CallExpression | ts.Identifier {
|
|
2695
2709
|
const {$ref = ''} = Reference.parse(reference);
|
|
2696
2710
|
const refName = $ref.split('/').pop() ?? 'never';
|
|
2697
|
-
|
|
2711
|
+
const sanitizedRefName = this.typeBuilder.sanitizeIdentifier(refName);
|
|
2712
|
+
|
|
2713
|
+
// Check if this reference creates a circular dependency
|
|
2714
|
+
if (this.isCircularReference(refName)) {
|
|
2715
|
+
// Generate: z.lazy(() => RefSchema)
|
|
2716
|
+
return ts.factory.createCallExpression(
|
|
2717
|
+
ts.factory.createPropertyAccessExpression(
|
|
2718
|
+
ts.factory.createIdentifier('z'),
|
|
2719
|
+
ts.factory.createIdentifier('lazy'),
|
|
2720
|
+
),
|
|
2721
|
+
undefined,
|
|
2722
|
+
[
|
|
2723
|
+
ts.factory.createArrowFunction(
|
|
2724
|
+
undefined,
|
|
2725
|
+
undefined,
|
|
2726
|
+
[],
|
|
2727
|
+
undefined,
|
|
2728
|
+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
2729
|
+
ts.factory.createIdentifier(sanitizedRefName),
|
|
2730
|
+
),
|
|
2731
|
+
],
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
return ts.factory.createIdentifier(sanitizedRefName);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Determines if a reference creates a circular dependency that needs z.lazy().
|
|
2740
|
+
* A reference is circular if:
|
|
2741
|
+
* 1. It's a direct self-reference (schema references itself)
|
|
2742
|
+
* 2. It's part of a circular dependency chain (A -> B -> A)
|
|
2743
|
+
*/
|
|
2744
|
+
private isCircularReference(refName: string): boolean {
|
|
2745
|
+
// Case 1: Direct self-reference
|
|
2746
|
+
if (refName === this.currentSchemaName) {
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// Case 2: Reference to a schema that's part of a circular dependency chain
|
|
2751
|
+
// and we're currently building a schema that's also in that chain
|
|
2752
|
+
if (
|
|
2753
|
+
this.circularSchemas.has(refName) &&
|
|
2754
|
+
this.currentSchemaName !== null &&
|
|
2755
|
+
this.circularSchemas.has(this.currentSchemaName)
|
|
2756
|
+
) {
|
|
2757
|
+
return true;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* Detects schemas that are part of circular dependency chains.
|
|
2765
|
+
* Uses a modified Tarjan's algorithm to find strongly connected components (SCCs).
|
|
2766
|
+
* Schemas in SCCs with more than one node, or self-referencing schemas, are circular.
|
|
2767
|
+
*/
|
|
2768
|
+
private detectCircularDependencies(schemas: Record<string, unknown>): Set<string> {
|
|
2769
|
+
const circularSchemas = new Set<string>();
|
|
2770
|
+
|
|
2771
|
+
// Build dependency graph
|
|
2772
|
+
const graph = new Map<string, string[]>();
|
|
2773
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
2774
|
+
const dependencies = jp
|
|
2775
|
+
.query(schema, '$..["$ref"]')
|
|
2776
|
+
.filter((ref: string) => ref.startsWith('#/components/schemas/'))
|
|
2777
|
+
.map((ref: string) => ref.replace('#/components/schemas/', ''))
|
|
2778
|
+
.filter((dep: string) => dep in schemas);
|
|
2779
|
+
graph.set(name, dependencies);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// Tarjan's algorithm for finding SCCs
|
|
2783
|
+
let index = 0;
|
|
2784
|
+
const indices = new Map<string, number>();
|
|
2785
|
+
const lowlinks = new Map<string, number>();
|
|
2786
|
+
const onStack = new Set<string>();
|
|
2787
|
+
const stack: string[] = [];
|
|
2788
|
+
|
|
2789
|
+
const strongConnect = (node: string): void => {
|
|
2790
|
+
indices.set(node, index);
|
|
2791
|
+
lowlinks.set(node, index);
|
|
2792
|
+
index++;
|
|
2793
|
+
stack.push(node);
|
|
2794
|
+
onStack.add(node);
|
|
2795
|
+
|
|
2796
|
+
const neighbors = graph.get(node) ?? [];
|
|
2797
|
+
for (const neighbor of neighbors) {
|
|
2798
|
+
if (!indices.has(neighbor)) {
|
|
2799
|
+
strongConnect(neighbor);
|
|
2800
|
+
const currentLowlink = lowlinks.get(node) ?? 0;
|
|
2801
|
+
const neighborLowlink = lowlinks.get(neighbor) ?? 0;
|
|
2802
|
+
lowlinks.set(node, Math.min(currentLowlink, neighborLowlink));
|
|
2803
|
+
} else if (onStack.has(neighbor)) {
|
|
2804
|
+
const currentLowlink = lowlinks.get(node) ?? 0;
|
|
2805
|
+
const neighborIndex = indices.get(neighbor) ?? 0;
|
|
2806
|
+
lowlinks.set(node, Math.min(currentLowlink, neighborIndex));
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// If node is a root of an SCC
|
|
2811
|
+
if (lowlinks.get(node) === indices.get(node)) {
|
|
2812
|
+
const scc: string[] = [];
|
|
2813
|
+
let w: string | undefined;
|
|
2814
|
+
do {
|
|
2815
|
+
w = stack.pop();
|
|
2816
|
+
if (w !== undefined) {
|
|
2817
|
+
onStack.delete(w);
|
|
2818
|
+
scc.push(w);
|
|
2819
|
+
}
|
|
2820
|
+
} while (w !== undefined && w !== node);
|
|
2821
|
+
|
|
2822
|
+
// An SCC is circular if it has more than one node
|
|
2823
|
+
// or if it has one node that references itself
|
|
2824
|
+
if (scc.length > 1) {
|
|
2825
|
+
for (const schemaName of scc) {
|
|
2826
|
+
circularSchemas.add(schemaName);
|
|
2827
|
+
}
|
|
2828
|
+
} else if (scc.length === 1) {
|
|
2829
|
+
const schemaName = scc[0];
|
|
2830
|
+
if (schemaName !== undefined) {
|
|
2831
|
+
const deps = graph.get(schemaName) ?? [];
|
|
2832
|
+
if (deps.includes(schemaName)) {
|
|
2833
|
+
circularSchemas.add(schemaName);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
|
|
2840
|
+
for (const node of graph.keys()) {
|
|
2841
|
+
if (!indices.has(node)) {
|
|
2842
|
+
strongConnect(node);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
return circularSchemas;
|
|
2698
2847
|
}
|
|
2699
2848
|
|
|
2700
2849
|
private topologicalSort(schemas: Record<string, unknown>): string[] {
|
|
@@ -212,7 +212,7 @@ describe('TypeScriptCodeGeneratorService', () => {
|
|
|
212
212
|
expect(code).toBeTruthy();
|
|
213
213
|
expect(typeof code).toBe('string');
|
|
214
214
|
expect(code).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
215
|
-
expect(code).toContain('export class');
|
|
215
|
+
expect(code).toContain('export default class');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
it('should generate schemas for component schemas', () => {
|
|
@@ -275,7 +275,7 @@ describe('TypeScriptCodeGeneratorService', () => {
|
|
|
275
275
|
|
|
276
276
|
const code = generator.generate(spec);
|
|
277
277
|
expect(code).toContain('async getUsers');
|
|
278
|
-
expect(code).toContain('
|
|
278
|
+
expect(code).toContain('makeRequest');
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
it('should generate getBaseRequestOptions method', () => {
|
|
@@ -408,7 +408,7 @@ describe('TypeScriptCodeGeneratorService', () => {
|
|
|
408
408
|
expect(code).not.toContain('ExecutionMode: z.enum');
|
|
409
409
|
});
|
|
410
410
|
|
|
411
|
-
it('should merge baseOptions with request-specific options in
|
|
411
|
+
it('should merge baseOptions with request-specific options in makeRequest', () => {
|
|
412
412
|
const spec: OpenApiSpecType = {
|
|
413
413
|
openapi: '3.0.0',
|
|
414
414
|
info: {
|
|
@@ -506,4 +506,200 @@ describe('TypeScriptCodeGeneratorService', () => {
|
|
|
506
506
|
expect(result).toBeDefined();
|
|
507
507
|
});
|
|
508
508
|
});
|
|
509
|
+
|
|
510
|
+
describe('circular dependencies', () => {
|
|
511
|
+
it('should use z.lazy() for direct self-referencing schemas', () => {
|
|
512
|
+
const spec: OpenApiSpecType = {
|
|
513
|
+
openapi: '3.0.0',
|
|
514
|
+
info: {
|
|
515
|
+
title: 'Test API',
|
|
516
|
+
version: '1.0.0',
|
|
517
|
+
},
|
|
518
|
+
paths: {},
|
|
519
|
+
components: {
|
|
520
|
+
schemas: {
|
|
521
|
+
TreeNode: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
properties: {
|
|
524
|
+
value: {type: 'string'},
|
|
525
|
+
children: {
|
|
526
|
+
type: 'array',
|
|
527
|
+
items: {$ref: '#/components/schemas/TreeNode'},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
required: ['value'],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const code = generator.generate(spec);
|
|
537
|
+
expect(code).toContain('z.lazy(() => TreeNode)');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should use z.lazy() for indirect circular dependencies (A -> B -> A)', () => {
|
|
541
|
+
const spec: OpenApiSpecType = {
|
|
542
|
+
openapi: '3.0.0',
|
|
543
|
+
info: {
|
|
544
|
+
title: 'Test API',
|
|
545
|
+
version: '1.0.0',
|
|
546
|
+
},
|
|
547
|
+
paths: {},
|
|
548
|
+
components: {
|
|
549
|
+
schemas: {
|
|
550
|
+
Person: {
|
|
551
|
+
type: 'object',
|
|
552
|
+
properties: {
|
|
553
|
+
name: {type: 'string'},
|
|
554
|
+
bestFriend: {$ref: '#/components/schemas/Friend'},
|
|
555
|
+
},
|
|
556
|
+
required: ['name'],
|
|
557
|
+
},
|
|
558
|
+
Friend: {
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: {
|
|
561
|
+
nickname: {type: 'string'},
|
|
562
|
+
person: {$ref: '#/components/schemas/Person'},
|
|
563
|
+
},
|
|
564
|
+
required: ['nickname'],
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const code = generator.generate(spec);
|
|
571
|
+
// Both references should use z.lazy()
|
|
572
|
+
expect(code).toContain('z.lazy(() => Friend)');
|
|
573
|
+
expect(code).toContain('z.lazy(() => Person)');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should not use z.lazy() for non-circular references', () => {
|
|
577
|
+
const spec: OpenApiSpecType = {
|
|
578
|
+
openapi: '3.0.0',
|
|
579
|
+
info: {
|
|
580
|
+
title: 'Test API',
|
|
581
|
+
version: '1.0.0',
|
|
582
|
+
},
|
|
583
|
+
paths: {},
|
|
584
|
+
components: {
|
|
585
|
+
schemas: {
|
|
586
|
+
User: {
|
|
587
|
+
type: 'object',
|
|
588
|
+
properties: {
|
|
589
|
+
id: {type: 'integer'},
|
|
590
|
+
profile: {$ref: '#/components/schemas/Profile'},
|
|
591
|
+
},
|
|
592
|
+
required: ['id'],
|
|
593
|
+
},
|
|
594
|
+
Profile: {
|
|
595
|
+
type: 'object',
|
|
596
|
+
properties: {
|
|
597
|
+
name: {type: 'string'},
|
|
598
|
+
},
|
|
599
|
+
required: ['name'],
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const code = generator.generate(spec);
|
|
606
|
+
// Profile should be referenced directly, not with z.lazy()
|
|
607
|
+
expect(code).not.toContain('z.lazy(() => Profile)');
|
|
608
|
+
expect(code).toContain('profile: Profile.optional()');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should handle complex circular chains (A -> B -> C -> A)', () => {
|
|
612
|
+
const spec: OpenApiSpecType = {
|
|
613
|
+
openapi: '3.0.0',
|
|
614
|
+
info: {
|
|
615
|
+
title: 'Test API',
|
|
616
|
+
version: '1.0.0',
|
|
617
|
+
},
|
|
618
|
+
paths: {},
|
|
619
|
+
components: {
|
|
620
|
+
schemas: {
|
|
621
|
+
Alpha: {
|
|
622
|
+
type: 'object',
|
|
623
|
+
properties: {
|
|
624
|
+
beta: {$ref: '#/components/schemas/Beta'},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
Beta: {
|
|
628
|
+
type: 'object',
|
|
629
|
+
properties: {
|
|
630
|
+
gamma: {$ref: '#/components/schemas/Gamma'},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
Gamma: {
|
|
634
|
+
type: 'object',
|
|
635
|
+
properties: {
|
|
636
|
+
alpha: {$ref: '#/components/schemas/Alpha'},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const code = generator.generate(spec);
|
|
644
|
+
// All references in the cycle should use z.lazy()
|
|
645
|
+
expect(code).toContain('z.lazy(() => Beta)');
|
|
646
|
+
expect(code).toContain('z.lazy(() => Gamma)');
|
|
647
|
+
expect(code).toContain('z.lazy(() => Alpha)');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should handle self-referencing schemas in arrays', () => {
|
|
651
|
+
const spec: OpenApiSpecType = {
|
|
652
|
+
openapi: '3.0.0',
|
|
653
|
+
info: {
|
|
654
|
+
title: 'Test API',
|
|
655
|
+
version: '1.0.0',
|
|
656
|
+
},
|
|
657
|
+
paths: {},
|
|
658
|
+
components: {
|
|
659
|
+
schemas: {
|
|
660
|
+
Category: {
|
|
661
|
+
type: 'object',
|
|
662
|
+
properties: {
|
|
663
|
+
name: {type: 'string'},
|
|
664
|
+
subcategories: {
|
|
665
|
+
type: 'array',
|
|
666
|
+
items: {$ref: '#/components/schemas/Category'},
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
required: ['name'],
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const code = generator.generate(spec);
|
|
676
|
+
expect(code).toContain('z.array(z.lazy(() => Category))');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('should handle self-referencing schemas in anyOf', () => {
|
|
680
|
+
const spec: OpenApiSpecType = {
|
|
681
|
+
openapi: '3.0.0',
|
|
682
|
+
info: {
|
|
683
|
+
title: 'Test API',
|
|
684
|
+
version: '1.0.0',
|
|
685
|
+
},
|
|
686
|
+
paths: {},
|
|
687
|
+
components: {
|
|
688
|
+
schemas: {
|
|
689
|
+
Expression: {
|
|
690
|
+
type: 'object',
|
|
691
|
+
properties: {
|
|
692
|
+
value: {
|
|
693
|
+
anyOf: [{type: 'string'}, {$ref: '#/components/schemas/Expression'}],
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const code = generator.generate(spec);
|
|
702
|
+
expect(code).toContain('z.lazy(() => Expression)');
|
|
703
|
+
});
|
|
704
|
+
});
|
|
509
705
|
});
|
|
@@ -68,7 +68,7 @@ describe('Generator', () => {
|
|
|
68
68
|
// Verify file contains expected content
|
|
69
69
|
const content = readFileSync(outputFile, 'utf-8');
|
|
70
70
|
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
71
|
-
expect(content).toContain('export class');
|
|
71
|
+
expect(content).toContain('export default class');
|
|
72
72
|
expect(content).toContain('getBaseRequestOptions');
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -109,7 +109,7 @@ describe('Generator', () => {
|
|
|
109
109
|
expect(content).toMatch(/import\s*{\s*z\s*}\s*from\s*['"]zod['"]/);
|
|
110
110
|
expect(content).toContain('export const');
|
|
111
111
|
expect(content).toContain('protected getBaseRequestOptions');
|
|
112
|
-
expect(content).toContain('async
|
|
112
|
+
expect(content).toContain('protected async makeRequest');
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('should include header comments in generated file', async () => {
|
package/tsconfig.json
CHANGED
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"useUnknownInCatchVariables": true,
|
|
42
42
|
"verbatimModuleSyntax": true
|
|
43
43
|
},
|
|
44
|
-
"exclude": ["node_modules", "dist", "build", "examples/**/*.ts"],
|
|
44
|
+
"exclude": ["node_modules", "dist", "build", "examples/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"],
|
|
45
45
|
"include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
|
|
46
46
|
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm install:*)",
|
|
5
|
-
"Bash(npm uninstall:*)",
|
|
6
|
-
"Bash(npm run type-check:*)",
|
|
7
|
-
"Bash(npm run build:*)",
|
|
8
|
-
"Bash(./dist/src/cli.js --help)",
|
|
9
|
-
"Bash(./dist/src/cli.js --version)",
|
|
10
|
-
"Bash(npm run dev:*)",
|
|
11
|
-
"Bash(node:*)",
|
|
12
|
-
"Bash(npm run lint)",
|
|
13
|
-
"Bash(npx husky init:*)",
|
|
14
|
-
"Bash(chmod:*)",
|
|
15
|
-
"Bash(npm run validate:*)",
|
|
16
|
-
"Bash(npm run format:*)",
|
|
17
|
-
"Bash(npm run test:*)",
|
|
18
|
-
"Bash(npm test)",
|
|
19
|
-
"Bash(npm audit:*)",
|
|
20
|
-
"Bash(npx depcheck:*)",
|
|
21
|
-
"Bash(npm outdated)",
|
|
22
|
-
"Bash(npm update:*)",
|
|
23
|
-
"WebSearch",
|
|
24
|
-
"Bash(find:*)",
|
|
25
|
-
"Bash(npx zod-codegen:*)",
|
|
26
|
-
"Bash(DEBUG=1 node ./dist/src/cli.js --input ./samples/openapi.json --output ./test-generated)",
|
|
27
|
-
"Bash(rm:*)",
|
|
28
|
-
"Bash(npm outdated:*)",
|
|
29
|
-
"Bash(npm view:*)",
|
|
30
|
-
"Bash(git tag:*)",
|
|
31
|
-
"Bash(npm run release:*)",
|
|
32
|
-
"Bash(git log:*)",
|
|
33
|
-
"Bash(npx commitlint:*)",
|
|
34
|
-
"Bash(git rev-list:*)",
|
|
35
|
-
"Bash(./fix-commit-messages.sh:*)",
|
|
36
|
-
"Bash(git add:*)",
|
|
37
|
-
"Bash(git commit:*)",
|
|
38
|
-
"Bash(FILTER_BRANCH_SQUELCH_WARNING=1 ./fix-commit-messages.sh)"
|
|
39
|
-
],
|
|
40
|
-
"deny": [],
|
|
41
|
-
"ask": []
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
interface PackageJson {
|
|
2
|
-
name: string;
|
|
3
|
-
version: string;
|
|
4
|
-
description: string;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Type guard for the package.json object
|
|
8
|
-
*
|
|
9
|
-
* @param input Unknown input
|
|
10
|
-
* @returns true if the input is an event object
|
|
11
|
-
*/
|
|
12
|
-
export declare function isPackageJson(input: unknown): input is PackageJson;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=update-manifest.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"update-manifest.d.ts","sourceRoot":"","sources":["../../scripts/update-manifest.ts"],"names":[],"mappings":"AAIA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAclE"}
|
|
@@ -1,31 +0,0 @@
|
|
|
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);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../../tests/integration/cli.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
describe('CLI Integration', () => {
|
|
5
|
-
describe('--help', () => {
|
|
6
|
-
it('should display help information', () => {
|
|
7
|
-
const result = execSync('npm run build && node ./dist/src/cli.js --help', {
|
|
8
|
-
encoding: 'utf-8',
|
|
9
|
-
cwd: resolve(__dirname, '../..'),
|
|
10
|
-
});
|
|
11
|
-
expect(result).toContain('Usage:');
|
|
12
|
-
expect(result).toContain('--input');
|
|
13
|
-
expect(result).toContain('--output');
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
describe('--version', () => {
|
|
17
|
-
it('should display version information', () => {
|
|
18
|
-
const result = execSync('npm run build && node ./dist/src/cli.js --version', {
|
|
19
|
-
encoding: 'utf-8',
|
|
20
|
-
cwd: resolve(__dirname, '../..'),
|
|
21
|
-
});
|
|
22
|
-
expect(result).toMatch(/\d+\.\d+\.\d+/);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"code-generator.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/code-generator.test.ts"],"names":[],"mappings":""}
|