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.
Files changed (31) hide show
  1. package/.github/workflows/ci.yml +18 -18
  2. package/.github/workflows/release.yml +8 -8
  3. package/CHANGELOG.md +26 -0
  4. package/dist/src/services/code-generator.service.d.ts +15 -0
  5. package/dist/src/services/code-generator.service.d.ts.map +1 -1
  6. package/dist/src/services/code-generator.service.js +124 -6
  7. package/package.json +14 -19
  8. package/src/services/code-generator.service.ts +160 -11
  9. package/tests/unit/code-generator.test.ts +199 -3
  10. package/tests/unit/generator.test.ts +2 -2
  11. package/tsconfig.json +1 -1
  12. package/.claude/settings.local.json +0 -43
  13. package/dist/scripts/update-manifest.d.ts +0 -14
  14. package/dist/scripts/update-manifest.d.ts.map +0 -1
  15. package/dist/scripts/update-manifest.js +0 -31
  16. package/dist/tests/integration/cli.test.d.ts +0 -2
  17. package/dist/tests/integration/cli.test.d.ts.map +0 -1
  18. package/dist/tests/integration/cli.test.js +0 -25
  19. package/dist/tests/unit/code-generator.test.d.ts +0 -2
  20. package/dist/tests/unit/code-generator.test.d.ts.map +0 -1
  21. package/dist/tests/unit/code-generator.test.js +0 -459
  22. package/dist/tests/unit/file-reader.test.d.ts +0 -2
  23. package/dist/tests/unit/file-reader.test.d.ts.map +0 -1
  24. package/dist/tests/unit/file-reader.test.js +0 -110
  25. package/dist/tests/unit/generator.test.d.ts +0 -2
  26. package/dist/tests/unit/generator.test.d.ts.map +0 -1
  27. package/dist/tests/unit/generator.test.js +0 -100
  28. package/dist/tests/unit/naming-convention.test.d.ts +0 -2
  29. package/dist/tests/unit/naming-convention.test.d.ts.map +0 -1
  30. package/dist/tests/unit/naming-convention.test.js +0 -231
  31. package/scripts/republish-versions.sh +0 -94
@@ -29,25 +29,25 @@ jobs:
29
29
  uses: actions/setup-node@v5
30
30
  with:
31
31
  node-version: ${{ matrix.node-version }}
32
- cache: 'yarn'
32
+ cache: 'npm'
33
33
 
34
34
  - name: Install dependencies
35
- run: yarn install --frozen-lockfile
35
+ run: npm ci
36
36
 
37
37
  - name: Run type check
38
- run: yarn type-check
38
+ run: npm run type-check
39
39
 
40
40
  - name: Run linter
41
- run: yarn lint:check
41
+ run: npm run lint:check
42
42
 
43
43
  - name: Run formatter check
44
- run: yarn format:check
44
+ run: npm run format:check
45
45
 
46
46
  - name: Build project
47
- run: yarn build
47
+ run: npm run build
48
48
 
49
49
  - name: Run tests
50
- run: yarn test:coverage
50
+ run: npm run test:coverage
51
51
 
52
52
  - name: Upload coverage to Codecov
53
53
  if: matrix.node-version == '24.11.1' && matrix.os == 'ubuntu-latest'
@@ -70,13 +70,13 @@ jobs:
70
70
  uses: actions/setup-node@v5
71
71
  with:
72
72
  node-version: '24.11.1'
73
- cache: 'yarn'
73
+ cache: 'npm'
74
74
 
75
75
  - name: Install dependencies
76
- run: yarn install --frozen-lockfile
76
+ run: npm ci
77
77
 
78
78
  - name: Run security audit
79
- run: yarn audit --level high
79
+ run: npm audit --audit-level=high
80
80
 
81
81
  build:
82
82
  name: Build & Package
@@ -91,13 +91,13 @@ jobs:
91
91
  uses: actions/setup-node@v5
92
92
  with:
93
93
  node-version: '24.11.1'
94
- cache: 'yarn'
94
+ cache: 'npm'
95
95
 
96
96
  - name: Install dependencies
97
- run: yarn install --frozen-lockfile
97
+ run: npm ci
98
98
 
99
99
  - name: Build project
100
- run: yarn build
100
+ run: npm run build
101
101
 
102
102
  - name: Test CLI
103
103
  run: |
@@ -107,7 +107,7 @@ jobs:
107
107
  test -e "generated/type.ts"
108
108
 
109
109
  - name: Upload build artifacts
110
- uses: actions/upload-artifact@v4
110
+ uses: actions/upload-artifact@v6
111
111
  with:
112
112
  name: build-${{ github.sha }}
113
113
  path: dist/
@@ -129,15 +129,15 @@ jobs:
129
129
  uses: actions/setup-node@v5
130
130
  with:
131
131
  node-version: '24.11.1'
132
- cache: 'yarn'
132
+ cache: 'npm'
133
133
 
134
134
  - name: Install dependencies
135
- run: yarn install --frozen-lockfile
135
+ run: npm ci
136
136
 
137
137
  - name: Build project
138
- run: yarn build
138
+ run: npm run build
139
139
 
140
140
  - name: Preview semantic-release
141
141
  env:
142
142
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
143
- run: yarn release:dry
143
+ run: npm run release:dry
@@ -29,26 +29,26 @@ jobs:
29
29
  uses: actions/setup-node@v5
30
30
  with:
31
31
  node-version: '24.11.1'
32
- cache: 'yarn'
32
+ cache: 'npm'
33
33
  registry-url: 'https://registry.npmjs.org'
34
34
 
35
35
  - name: Install dependencies
36
- run: yarn install --frozen-lockfile
36
+ run: npm ci
37
37
 
38
38
  - name: Run type check
39
- run: yarn type-check
39
+ run: npm run type-check
40
40
 
41
41
  - name: Run linter check
42
- run: yarn lint:check
42
+ run: npm run lint:check
43
43
 
44
44
  - name: Run formatter check
45
- run: yarn format:check
45
+ run: npm run format:check
46
46
 
47
47
  - name: Run tests with coverage
48
- run: yarn test:coverage
48
+ run: npm run test:coverage
49
49
 
50
50
  - name: Build project
51
- run: yarn build
51
+ run: npm run build
52
52
 
53
53
  - name: Test CLI functionality
54
54
  run: |
@@ -62,4 +62,4 @@ jobs:
62
62
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63
63
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
64
64
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
65
- run: yarn release
65
+ run: npm run release
package/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 1.5.0 (2025-12-22)
2
+
3
+ - Merge pull request #44 from julienandreu/dependabot/github_actions/actions/upload-artifact-6 ([cebfd55](https://github.com/julienandreu/zod-codegen/commit/cebfd55)), closes [#44](https://github.com/julienandreu/zod-codegen/issues/44)
4
+ - Merge pull request #45 from julienandreu/dependabot/npm_and_yarn/dev-dependencies-ab633d47b1 ([8663f40](https://github.com/julienandreu/zod-codegen/commit/8663f40)), closes [#45](https://github.com/julienandreu/zod-codegen/issues/45)
5
+ - Merge pull request #46 from julienandreu/dependabot/npm_and_yarn/production-dependencies-eefc12583a ([eac5279](https://github.com/julienandreu/zod-codegen/commit/eac5279)), closes [#46](https://github.com/julienandreu/zod-codegen/issues/46)
6
+ - Merge pull request #47 from julienandreu/dependabot/npm_and_yarn/types/node-25.0.2 ([687ce64](https://github.com/julienandreu/zod-codegen/commit/687ce64)), closes [#47](https://github.com/julienandreu/zod-codegen/issues/47)
7
+ - Merge pull request #49 from julienandreu/dependabot/npm_and_yarn/production-dependencies-5419ff3310 ([1d6485c](https://github.com/julienandreu/zod-codegen/commit/1d6485c)), closes [#49](https://github.com/julienandreu/zod-codegen/issues/49)
8
+ - Merge pull request #50 from julienandreu/feat/circular-dependency-detection ([527cbb6](https://github.com/julienandreu/zod-codegen/commit/527cbb6)), closes [#50](https://github.com/julienandreu/zod-codegen/issues/50)
9
+ - feat: detect circular dependencies and wrap with z.lazy() ([4f5f545](https://github.com/julienandreu/zod-codegen/commit/4f5f545))
10
+ - chore(deps-dev): bump @types/node from 24.10.1 to 25.0.2 ([da9bbc7](https://github.com/julienandreu/zod-codegen/commit/da9bbc7))
11
+ - chore(deps-dev): bump the dev-dependencies group with 2 updates ([29ad3ef](https://github.com/julienandreu/zod-codegen/commit/29ad3ef))
12
+ - chore(deps): bump zod in the production-dependencies group ([391d6ea](https://github.com/julienandreu/zod-codegen/commit/391d6ea))
13
+ - chore(deps): bump zod in the production-dependencies group ([41177c8](https://github.com/julienandreu/zod-codegen/commit/41177c8))
14
+ - ci(deps): bump actions/upload-artifact from 4 to 6 ([c19e254](https://github.com/julienandreu/zod-codegen/commit/c19e254))
15
+
16
+ ## <small>1.4.1 (2025-12-08)</small>
17
+
18
+ - Merge branch 'refactor/build-config-and-class-exports' of github.com:julienandreu/zod-codegen into r ([bdd9bc5](https://github.com/julienandreu/zod-codegen/commit/bdd9bc5))
19
+ - Merge pull request #41 from julienandreu/dependabot/npm_and_yarn/tar-7.5.2 ([192cd47](https://github.com/julienandreu/zod-codegen/commit/192cd47)), closes [#41](https://github.com/julienandreu/zod-codegen/issues/41)
20
+ - Merge pull request #43 from julienandreu/refactor/build-config-and-class-exports ([61de21b](https://github.com/julienandreu/zod-codegen/commit/61de21b)), closes [#43](https://github.com/julienandreu/zod-codegen/issues/43)
21
+ - refactor: improve build configuration and class exports ([060f714](https://github.com/julienandreu/zod-codegen/commit/060f714))
22
+ - refactor: improve build configuration and class exports ([b8771ad](https://github.com/julienandreu/zod-codegen/commit/b8771ad))
23
+ - refactor: remove tsconfig.build.json and use tsconfig.json directly ([407c46a](https://github.com/julienandreu/zod-codegen/commit/407c46a))
24
+ - chore: restore yarn.lock for yarn 4 with corepack ([a93f018](https://github.com/julienandreu/zod-codegen/commit/a93f018))
25
+ - chore(deps): bump tar from 7.5.1 to 7.5.2 ([b867dce](https://github.com/julienandreu/zod-codegen/commit/b867dce))
26
+
1
27
  ## 1.4.0 (2025-12-01)
2
28
 
3
29
  - Merge pull request #40 from julienandreu/feat/naming-conventions-and-improvements ([d7c8146](https://github.com/julienandreu/zod-codegen/commit/d7c8146)), closes [#40](https://github.com/julienandreu/zod-codegen/issues/40)
@@ -8,6 +8,8 @@ export declare class TypeScriptCodeGeneratorService implements CodeGenerator, Sc
8
8
  private readonly printer;
9
9
  private readonly namingConvention;
10
10
  private readonly operationNameTransformer;
11
+ private circularSchemas;
12
+ private currentSchemaName;
11
13
  constructor(options?: GeneratorOptions);
12
14
  private readonly ZodAST;
13
15
  generate(spec: OpenApiSpecType): string;
@@ -52,6 +54,19 @@ export declare class TypeScriptCodeGeneratorService implements CodeGenerator, Sc
52
54
  private buildArrayTypeFromSchema;
53
55
  private isReference;
54
56
  private buildFromReference;
57
+ /**
58
+ * Determines if a reference creates a circular dependency that needs z.lazy().
59
+ * A reference is circular if:
60
+ * 1. It's a direct self-reference (schema references itself)
61
+ * 2. It's part of a circular dependency chain (A -> B -> A)
62
+ */
63
+ private isCircularReference;
64
+ /**
65
+ * Detects schemas that are part of circular dependency chains.
66
+ * Uses a modified Tarjan's algorithm to find strongly connected components (SCCs).
67
+ * Schemas in SCCs with more than one node, or self-referencing schemas, are circular.
68
+ */
69
+ private detectCircularDependencies;
55
70
  private topologicalSort;
56
71
  }
57
72
  //# sourceMappingURL=code-generator.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"code-generator.service.d.ts","sourceRoot":"","sources":["../../../src/services/code-generator.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAC,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAmB,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAC1F,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAWpE,qBAAa,8BAA+B,YAAW,aAAa,EAAE,aAAa;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuC;gBAEpE,OAAO,GAAE,gBAAqB;IAK1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGpB;IAEH,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM;IAMvC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,UAAO,GAAG,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,UAAU;IA2BhF,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,YAAY;IA8BpB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,gBAAgB;IAmGxB,OAAO,CAAC,gCAAgC;IAwBxC,OAAO,CAAC,sBAAsB;IAqmB9B,OAAO,CAAC,kBAAkB;IAsB1B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA0B9B,OAAO,CAAC,mBAAmB;IAmH3B,OAAO,CAAC,mBAAmB;IA6D3B,OAAO,CAAC,qBAAqB;IAwF7B,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,iBAAiB;IAmCzB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,eAAe;IAuDvB,OAAO,CAAC,wBAAwB;IA4IhC,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,6BAA6B;IAkPrC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuFzB,OAAO,CAAC,WAAW;IA0CnB,OAAO,CAAC,aAAa;IAmjBrB,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,oBAAoB;IAyE5B,OAAO,CAAC,8BAA8B;IActC,OAAO,CAAC,wBAAwB;IAyBhC,OAAO,CAAC,yBAAyB;IA8BjC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,eAAe;CAuCxB"}
1
+ {"version":3,"file":"code-generator.service.d.ts","sourceRoot":"","sources":["../../../src/services/code-generator.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAC,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAmB,eAAe,EAAgB,MAAM,qBAAqB,CAAC;AAC1F,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAWpE,qBAAa,8BAA+B,YAAW,aAAa,EAAE,aAAa;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAChE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAuC;IAGhF,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,iBAAiB,CAAuB;gBAEpC,OAAO,GAAE,gBAAqB;IAK1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGpB;IAEH,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM;IAMvC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,UAAO,GAAG,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,UAAU;IA2BhF,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,YAAY;IA2CpB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,gBAAgB;IAmGxB,OAAO,CAAC,gCAAgC;IAwBxC,OAAO,CAAC,sBAAsB;IAqmB9B,OAAO,CAAC,kBAAkB;IAsB1B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA0B9B,OAAO,CAAC,mBAAmB;IAgH3B,OAAO,CAAC,mBAAmB;IA6D3B,OAAO,CAAC,qBAAqB;IAwF7B,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,iBAAiB;IAmCzB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,eAAe;IAuDvB,OAAO,CAAC,wBAAwB;IA4IhC,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,6BAA6B;IAkPrC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuFzB,OAAO,CAAC,WAAW;IA0CnB,OAAO,CAAC,aAAa;IAmjBrB,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,oBAAoB;IAyE5B,OAAO,CAAC,8BAA8B;IActC,OAAO,CAAC,wBAAwB;IAyBhC,OAAO,CAAC,yBAAyB;IA8BjC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,kBAAkB;IA8B1B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAmB3B;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAiFlC,OAAO,CAAC,eAAe;CAuCxB"}
@@ -11,6 +11,9 @@ export class TypeScriptCodeGeneratorService {
11
11
  printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
12
12
  namingConvention;
13
13
  operationNameTransformer;
14
+ // Track circular dependencies for z.lazy() wrapping
15
+ circularSchemas = new Set();
16
+ currentSchemaName = null;
14
17
  constructor(options = {}) {
15
18
  this.namingConvention = options.namingConvention;
16
19
  this.operationNameTransformer = options.operationNameTransformer;
@@ -63,13 +66,21 @@ export class TypeScriptCodeGeneratorService {
63
66
  }
64
67
  buildSchemas(openapi) {
65
68
  const schemasEntries = Object.entries(openapi.components?.schemas ?? {});
66
- const sortedSchemaNames = this.topologicalSort(Object.fromEntries(schemasEntries));
69
+ const schemasMap = Object.fromEntries(schemasEntries);
70
+ // Detect circular dependencies before building schemas
71
+ this.circularSchemas = this.detectCircularDependencies(schemasMap);
72
+ const sortedSchemaNames = this.topologicalSort(schemasMap);
67
73
  return sortedSchemaNames.reduce((schemaRegistered, name) => {
68
74
  const schema = openapi.components?.schemas?.[name];
69
75
  if (!schema)
70
76
  return schemaRegistered;
77
+ // Set context for current schema being built
78
+ this.currentSchemaName = name;
79
+ const schemaExpression = this.buildSchema(schema);
80
+ // Clear context
81
+ this.currentSchemaName = null;
71
82
  const variableStatement = ts.factory.createVariableStatement([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
72
- ts.factory.createVariableDeclaration(ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)), undefined, undefined, this.buildSchema(schema)),
83
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(name)), undefined, undefined, schemaExpression),
73
84
  ], ts.NodeFlags.Const));
74
85
  return {
75
86
  ...schemaRegistered,
@@ -86,7 +97,7 @@ export class TypeScriptCodeGeneratorService {
86
97
  buildClientClass(openapi, schemas) {
87
98
  const clientName = this.generateClientName(openapi.info.title);
88
99
  const methods = this.buildClientMethods(openapi, schemas);
89
- return ts.factory.createClassDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(clientName), undefined, undefined, [
100
+ return ts.factory.createClassDeclaration([ts.factory.createToken(ts.SyntaxKind.ExportKeyword), ts.factory.createToken(ts.SyntaxKind.DefaultKeyword)], ts.factory.createIdentifier(clientName), undefined, undefined, [
90
101
  this.typeBuilder.createProperty('#baseUrl', 'string', true),
91
102
  this.buildConstructor(openapi),
92
103
  this.buildGetBaseRequestOptionsMethod(),
@@ -132,7 +143,7 @@ export class TypeScriptCodeGeneratorService {
132
143
  ]), ts.factory.createBlock([ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression([], false))], true));
133
144
  }
134
145
  buildHttpRequestMethod() {
135
- return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createPrivateIdentifier('#makeRequest'), undefined, [this.typeBuilder.createGenericType('T')], [
146
+ return ts.factory.createMethodDeclaration([ts.factory.createToken(ts.SyntaxKind.ProtectedKeyword), ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)], undefined, ts.factory.createIdentifier('makeRequest'), undefined, [this.typeBuilder.createGenericType('T')], [
136
147
  this.typeBuilder.createParameter('method', 'string'),
137
148
  this.typeBuilder.createParameter('path', 'string'),
138
149
  this.typeBuilder.createParameter('options', '{params?: Record<string, string | number | boolean>; data?: unknown; contentType?: string; headers?: Record<string, string>}', ts.factory.createObjectLiteralExpression([], false), false),
@@ -331,7 +342,7 @@ export class TypeScriptCodeGeneratorService {
331
342
  }
332
343
  const optionsExpression = ts.factory.createObjectLiteralExpression(optionsProps, false);
333
344
  // Call makeRequest
334
- const makeRequestCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createPrivateIdentifier('#makeRequest')), undefined, [ts.factory.createStringLiteral(method.toUpperCase(), true), pathExpression, optionsExpression]);
345
+ const makeRequestCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier('makeRequest')), undefined, [ts.factory.createStringLiteral(method.toUpperCase(), true), pathExpression, optionsExpression]);
335
346
  // Add Zod validation if we have a response schema
336
347
  if (responseSchema) {
337
348
  const validateCall = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(responseSchema, ts.factory.createIdentifier('parse')), undefined, [ts.factory.createAwaitExpression(makeRequestCall)]);
@@ -1223,7 +1234,114 @@ export class TypeScriptCodeGeneratorService {
1223
1234
  buildFromReference(reference) {
1224
1235
  const { $ref = '' } = Reference.parse(reference);
1225
1236
  const refName = $ref.split('/').pop() ?? 'never';
1226
- return ts.factory.createIdentifier(this.typeBuilder.sanitizeIdentifier(refName));
1237
+ const sanitizedRefName = this.typeBuilder.sanitizeIdentifier(refName);
1238
+ // Check if this reference creates a circular dependency
1239
+ if (this.isCircularReference(refName)) {
1240
+ // Generate: z.lazy(() => RefSchema)
1241
+ return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('z'), ts.factory.createIdentifier('lazy')), undefined, [
1242
+ ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createIdentifier(sanitizedRefName)),
1243
+ ]);
1244
+ }
1245
+ return ts.factory.createIdentifier(sanitizedRefName);
1246
+ }
1247
+ /**
1248
+ * Determines if a reference creates a circular dependency that needs z.lazy().
1249
+ * A reference is circular if:
1250
+ * 1. It's a direct self-reference (schema references itself)
1251
+ * 2. It's part of a circular dependency chain (A -> B -> A)
1252
+ */
1253
+ isCircularReference(refName) {
1254
+ // Case 1: Direct self-reference
1255
+ if (refName === this.currentSchemaName) {
1256
+ return true;
1257
+ }
1258
+ // Case 2: Reference to a schema that's part of a circular dependency chain
1259
+ // and we're currently building a schema that's also in that chain
1260
+ if (this.circularSchemas.has(refName) &&
1261
+ this.currentSchemaName !== null &&
1262
+ this.circularSchemas.has(this.currentSchemaName)) {
1263
+ return true;
1264
+ }
1265
+ return false;
1266
+ }
1267
+ /**
1268
+ * Detects schemas that are part of circular dependency chains.
1269
+ * Uses a modified Tarjan's algorithm to find strongly connected components (SCCs).
1270
+ * Schemas in SCCs with more than one node, or self-referencing schemas, are circular.
1271
+ */
1272
+ detectCircularDependencies(schemas) {
1273
+ const circularSchemas = new Set();
1274
+ // Build dependency graph
1275
+ const graph = new Map();
1276
+ for (const [name, schema] of Object.entries(schemas)) {
1277
+ const dependencies = jp
1278
+ .query(schema, '$..["$ref"]')
1279
+ .filter((ref) => ref.startsWith('#/components/schemas/'))
1280
+ .map((ref) => ref.replace('#/components/schemas/', ''))
1281
+ .filter((dep) => dep in schemas);
1282
+ graph.set(name, dependencies);
1283
+ }
1284
+ // Tarjan's algorithm for finding SCCs
1285
+ let index = 0;
1286
+ const indices = new Map();
1287
+ const lowlinks = new Map();
1288
+ const onStack = new Set();
1289
+ const stack = [];
1290
+ const strongConnect = (node) => {
1291
+ indices.set(node, index);
1292
+ lowlinks.set(node, index);
1293
+ index++;
1294
+ stack.push(node);
1295
+ onStack.add(node);
1296
+ const neighbors = graph.get(node) ?? [];
1297
+ for (const neighbor of neighbors) {
1298
+ if (!indices.has(neighbor)) {
1299
+ strongConnect(neighbor);
1300
+ const currentLowlink = lowlinks.get(node) ?? 0;
1301
+ const neighborLowlink = lowlinks.get(neighbor) ?? 0;
1302
+ lowlinks.set(node, Math.min(currentLowlink, neighborLowlink));
1303
+ }
1304
+ else if (onStack.has(neighbor)) {
1305
+ const currentLowlink = lowlinks.get(node) ?? 0;
1306
+ const neighborIndex = indices.get(neighbor) ?? 0;
1307
+ lowlinks.set(node, Math.min(currentLowlink, neighborIndex));
1308
+ }
1309
+ }
1310
+ // If node is a root of an SCC
1311
+ if (lowlinks.get(node) === indices.get(node)) {
1312
+ const scc = [];
1313
+ let w;
1314
+ do {
1315
+ w = stack.pop();
1316
+ if (w !== undefined) {
1317
+ onStack.delete(w);
1318
+ scc.push(w);
1319
+ }
1320
+ } while (w !== undefined && w !== node);
1321
+ // An SCC is circular if it has more than one node
1322
+ // or if it has one node that references itself
1323
+ if (scc.length > 1) {
1324
+ for (const schemaName of scc) {
1325
+ circularSchemas.add(schemaName);
1326
+ }
1327
+ }
1328
+ else if (scc.length === 1) {
1329
+ const schemaName = scc[0];
1330
+ if (schemaName !== undefined) {
1331
+ const deps = graph.get(schemaName) ?? [];
1332
+ if (deps.includes(schemaName)) {
1333
+ circularSchemas.add(schemaName);
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ };
1339
+ for (const node of graph.keys()) {
1340
+ if (!indices.has(node)) {
1341
+ strongConnect(node);
1342
+ }
1343
+ }
1344
+ return circularSchemas;
1227
1345
  }
1228
1346
  topologicalSort(schemas) {
1229
1347
  const visited = new Set();
package/package.json CHANGED
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "author": "Julien Andreu <julienandreu@me.com>",
3
- "bin": {
4
- "zod-codegen": "dist/src/cli.js"
5
- },
3
+ "bin": "dist/src/cli.js",
6
4
  "bugs": {
7
5
  "url": "https://github.com/julienandreu/zod-codegen/issues"
8
6
  },
@@ -19,7 +17,7 @@
19
17
  "typescript": "^5.9.3",
20
18
  "url-pattern": "^1.0.3",
21
19
  "yargs": "^18.0.0",
22
- "zod": "^4.1.13"
20
+ "zod": "^4.2.1"
23
21
  },
24
22
  "description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications",
25
23
  "keywords": [
@@ -36,17 +34,18 @@
36
34
  "devDependencies": {
37
35
  "@commitlint/cli": "^20.1.0",
38
36
  "@commitlint/config-conventional": "^20.0.0",
39
- "@eslint/js": "^9.39.1",
37
+ "@eslint/js": "^9.39.2",
40
38
  "@semantic-release/changelog": "^6.0.3",
41
39
  "@semantic-release/git": "^10.0.1",
42
40
  "@types/debug": "^4.1.12",
43
41
  "@types/jest": "^30.0.0",
44
42
  "@types/js-yaml": "^4.0.9",
45
43
  "@types/jsonpath": "^0.2.4",
46
- "@types/node": "^24.10.1",
44
+ "@types/node": "^25.0.2",
47
45
  "@types/yargs": "^17.0.35",
48
46
  "@vitest/coverage-v8": "^4.0.14",
49
- "eslint": "^9.39.1",
47
+ "cross-env": "^10.1.0",
48
+ "eslint": "^9.39.2",
50
49
  "eslint-config-prettier": "^10.1.8",
51
50
  "husky": "^9.1.7",
52
51
  "lint-staged": "^16.2.7",
@@ -54,8 +53,7 @@
54
53
  "ts-node": "^10.9.2",
55
54
  "typescript-eslint": "^8.46.4",
56
55
  "undici": "^7.16.0",
57
- "vitest": "^4.0.14",
58
- "yarn-audit-fix": "^10.1.1"
56
+ "vitest": "^4.0.14"
59
57
  },
60
58
  "optionalDependencies": {
61
59
  "undici": "^7.16.0"
@@ -88,15 +86,12 @@
88
86
  "tar": "^7.5.2",
89
87
  "yargs": "^18.0.0"
90
88
  },
91
- "resolutions": {
92
- "yargs": "^18.0.0"
93
- },
94
89
  "scripts": {
95
- "audit:fix": "yarn-audit-fix",
96
- "build": "rm -rf dist && tsc --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
97
- "build:native": "rm -rf dist && npx tsgo --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
90
+ "audit:fix": "npm audit fix",
91
+ "build": "NODE_OPTIONS='--no-deprecation' sh -c 'rm -rf dist && tsc --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js'",
92
+ "build:native": "rm -rf dist && NODE_OPTIONS='--no-deprecation' npx tsgo --project tsconfig.json && cp -r src/assets dist/src/ && chmod +x ./dist/src/cli.js",
98
93
  "build:watch": "tsc --project tsconfig.json --watch",
99
- "dev": "yarn build && node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ./examples/petstore && yarn format && yarn lint",
94
+ "dev": "npm run build && node ./dist/src/cli.js --input ./samples/swagger-petstore.yaml --output ./examples/petstore && npm run format && npm run lint",
100
95
  "lint": "eslint src --fix",
101
96
  "lint:check": "eslint src",
102
97
  "format": "prettier src --write --log-level error",
@@ -108,12 +103,12 @@
108
103
  "test:ui": "vitest --ui",
109
104
  "manifest:update": "ts-node scripts/update-manifest.ts",
110
105
  "prepare": "husky",
111
- "prepublishOnly": "yarn build && yarn test && yarn lint:check && yarn type-check",
106
+ "prepublishOnly": "npm run build && npm run test && npm run lint:check && npm run type-check",
112
107
  "clean": "rm -rf dist coverage node_modules/.cache",
113
- "validate": "yarn type-check && yarn lint:check && yarn format:check && yarn test",
108
+ "validate": "npm run type-check && npm run lint:check && npm run format:check && npm run test",
114
109
  "validate:examples": "tsc -p ./tsconfig.examples.json --noEmit",
115
110
  "release": "semantic-release",
116
111
  "release:dry": "semantic-release --dry-run"
117
112
  },
118
- "version": "1.4.0"
113
+ "version": "1.5.0"
119
114
  }