zod-codegen 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [1.7.1](https://github.com/julienandreu/zod-codegen/compare/v1.7.0...v1.7.1) (2026-03-10)
2
+
3
+ ### 🐛 Bug Fixes
4
+
5
+ - properly handle URL inputs with authentication support ([#87](https://github.com/julienandreu/zod-codegen/issues/87)) ([49ba2c4](https://github.com/julienandreu/zod-codegen/commit/49ba2c483458397d9f12e03b8f6c634981e4454a))
6
+
1
7
  ## [1.7.0](https://github.com/julienandreu/zod-codegen/compare/v1.6.3...v1.7.0) (2026-03-09)
2
8
 
3
9
  ### 🚀 Features
@@ -1,7 +1,9 @@
1
1
  import type { OpenApiFileParser, OpenApiFileReader } from '../interfaces/file-reader';
2
2
  import type { OpenApiSpecType } from '../types/openapi';
3
3
  export declare class SyncFileReaderService implements OpenApiFileReader {
4
+ private isUrl;
4
5
  readFile(path: string): Promise<string>;
6
+ private fetchUrl;
5
7
  }
6
8
  export declare class OpenApiFileParserService implements OpenApiFileParser<OpenApiSpecType> {
7
9
  parse(input: unknown): OpenApiSpecType;
@@ -1 +1 @@
1
- {"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAgB9C;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
1
+ {"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D,OAAO,CAAC,KAAK;IASP,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAQ/B,QAAQ;CAkBvB;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
@@ -2,21 +2,35 @@ import { load } from 'js-yaml';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { OpenApiSpec } from '../types/openapi.js';
4
4
  export class SyncFileReaderService {
5
- async readFile(path) {
6
- // Check if path is a URL
5
+ isUrl(path) {
7
6
  try {
8
7
  const url = new URL(path);
9
- // If it's a valid URL, fetch it
10
- const response = await fetch(url.toString());
11
- if (!response.ok) {
12
- throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
13
- }
14
- return await response.text();
8
+ return url.protocol === 'http:' || url.protocol === 'https:';
15
9
  }
16
10
  catch {
17
- // If URL parsing fails, treat it as a local file path
18
- return readFileSync(path, 'utf8');
11
+ return false;
12
+ }
13
+ }
14
+ async readFile(path) {
15
+ if (this.isUrl(path)) {
16
+ return await this.fetchUrl(path);
17
+ }
18
+ return readFileSync(path, 'utf8');
19
+ }
20
+ async fetchUrl(path) {
21
+ const url = new URL(path);
22
+ const headers = {};
23
+ if (url.username || url.password) {
24
+ const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
25
+ headers['Authorization'] = `Basic ${btoa(credentials)}`;
26
+ url.username = '';
27
+ url.password = '';
28
+ }
29
+ const response = await fetch(url.toString(), { headers });
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
19
32
  }
33
+ return await response.text();
20
34
  }
21
35
  }
22
36
  export class OpenApiFileParserService {
@@ -106,5 +106,18 @@ describe('CLI Comprehensive Integration', () => {
106
106
  const outputFile = resolve(testOutputDir, 'api.ts');
107
107
  expect(existsSync(outputFile)).toBe(true);
108
108
  });
109
+ it('should generate code from a remote URL', () => {
110
+ execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
111
+ encoding: 'utf-8',
112
+ cwd,
113
+ timeout: 30000
114
+ });
115
+ const outputFile = resolve(testOutputDir, 'api.ts');
116
+ expect(existsSync(outputFile)).toBe(true);
117
+ const content = readFileSync(outputFile, 'utf-8');
118
+ expect(content).toContain('export default class');
119
+ expect(content).toContain('class ResponseValidationError<T> extends Error');
120
+ expect(content).toContain('import { z }');
121
+ });
109
122
  });
110
123
  });
@@ -1,8 +1,7 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
4
  import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
3
- import { join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname } from 'node:path';
6
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
6
  describe('SyncFileReaderService', () => {
8
7
  let reader;
@@ -25,7 +24,6 @@ describe('SyncFileReaderService', () => {
25
24
  await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
26
25
  });
27
26
  it('should handle URLs', async () => {
28
- // Mock fetch for URL test
29
27
  const originalFetch = global.fetch;
30
28
  global.fetch = vi.fn().mockResolvedValue({
31
29
  ok: true,
@@ -33,6 +31,7 @@ describe('SyncFileReaderService', () => {
33
31
  });
34
32
  const content = await reader.readFile('https://example.com/openapi.json');
35
33
  expect(content).toBeTruthy();
34
+ expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
36
35
  global.fetch = originalFetch;
37
36
  });
38
37
  it('should handle URL fetch errors with non-ok responses', async () => {
@@ -43,11 +42,40 @@ describe('SyncFileReaderService', () => {
43
42
  statusText: 'Not Found'
44
43
  });
45
44
  global.fetch = mockFetch;
46
- await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
47
- // Verify fetch was called
48
- expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
45
+ await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
46
+ global.fetch = originalFetch;
47
+ });
48
+ it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
49
+ const originalFetch = global.fetch;
50
+ global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
51
+ await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
52
+ global.fetch = originalFetch;
53
+ });
54
+ it('should extract basic auth credentials from URL into Authorization header', async () => {
55
+ const originalFetch = global.fetch;
56
+ const mockFetch = vi.fn().mockResolvedValue({
57
+ ok: true,
58
+ text: async () => '{}'
59
+ });
60
+ global.fetch = mockFetch;
61
+ await reader.readFile('https://user:pass@example.com/openapi.json');
62
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
63
+ global.fetch = originalFetch;
64
+ });
65
+ it('should decode percent-encoded credentials from URL', async () => {
66
+ const originalFetch = global.fetch;
67
+ const mockFetch = vi.fn().mockResolvedValue({
68
+ ok: true,
69
+ text: async () => '{}'
70
+ });
71
+ global.fetch = mockFetch;
72
+ await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
73
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
49
74
  global.fetch = originalFetch;
50
75
  });
76
+ it('should not treat non-http schemes as URLs', async () => {
77
+ await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
78
+ });
51
79
  });
52
80
  });
53
81
  describe('OpenApiFileParserService', () => {
package/package.json CHANGED
@@ -101,5 +101,5 @@
101
101
  "release": "semantic-release",
102
102
  "release:dry": "semantic-release --dry-run"
103
103
  },
104
- "version": "1.7.0"
104
+ "version": "1.7.1"
105
105
  }
@@ -5,21 +5,40 @@ import type { OpenApiSpecType } from '../types/openapi';
5
5
  import { OpenApiSpec } from '../types/openapi';
6
6
 
7
7
  export class SyncFileReaderService implements OpenApiFileReader {
8
- async readFile(path: string): Promise<string> {
9
- // Check if path is a URL
8
+ private isUrl(path: string): boolean {
10
9
  try {
11
10
  const url = new URL(path);
12
- // If it's a valid URL, fetch it
13
- const response = await fetch(url.toString());
14
- if (!response.ok) {
15
- throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
16
- }
17
-
18
- return await response.text();
11
+ return url.protocol === 'http:' || url.protocol === 'https:';
19
12
  } catch {
20
- // If URL parsing fails, treat it as a local file path
21
- return readFileSync(path, 'utf8');
13
+ return false;
14
+ }
15
+ }
16
+
17
+ async readFile(path: string): Promise<string> {
18
+ if (this.isUrl(path)) {
19
+ return await this.fetchUrl(path);
20
+ }
21
+
22
+ return readFileSync(path, 'utf8');
23
+ }
24
+
25
+ private async fetchUrl(path: string): Promise<string> {
26
+ const url = new URL(path);
27
+
28
+ const headers: Record<string, string> = {};
29
+ if (url.username || url.password) {
30
+ const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
31
+ headers['Authorization'] = `Basic ${btoa(credentials)}`;
32
+ url.username = '';
33
+ url.password = '';
22
34
  }
35
+
36
+ const response = await fetch(url.toString(), { headers });
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
39
+ }
40
+
41
+ return await response.text();
23
42
  }
24
43
  }
25
44
 
@@ -125,5 +125,21 @@ describe('CLI Comprehensive Integration', () => {
125
125
  const outputFile = resolve(testOutputDir, 'api.ts');
126
126
  expect(existsSync(outputFile)).toBe(true);
127
127
  });
128
+
129
+ it('should generate code from a remote URL', () => {
130
+ execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
131
+ encoding: 'utf-8',
132
+ cwd,
133
+ timeout: 30000
134
+ });
135
+
136
+ const outputFile = resolve(testOutputDir, 'api.ts');
137
+ expect(existsSync(outputFile)).toBe(true);
138
+
139
+ const content = readFileSync(outputFile, 'utf-8');
140
+ expect(content).toContain('export default class');
141
+ expect(content).toContain('class ResponseValidationError<T> extends Error');
142
+ expect(content).toContain('import { z }');
143
+ });
128
144
  });
129
145
  });
@@ -1,8 +1,7 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
4
  import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
3
- import { join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname } from 'node:path';
6
5
 
7
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
7
 
@@ -32,7 +31,6 @@ describe('SyncFileReaderService', () => {
32
31
  });
33
32
 
34
33
  it('should handle URLs', async () => {
35
- // Mock fetch for URL test
36
34
  const originalFetch = global.fetch;
37
35
  global.fetch = vi.fn().mockResolvedValue({
38
36
  ok: true,
@@ -41,6 +39,7 @@ describe('SyncFileReaderService', () => {
41
39
 
42
40
  const content = await reader.readFile('https://example.com/openapi.json');
43
41
  expect(content).toBeTruthy();
42
+ expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
44
43
 
45
44
  global.fetch = originalFetch;
46
45
  });
@@ -54,12 +53,53 @@ describe('SyncFileReaderService', () => {
54
53
  } as Response);
55
54
  global.fetch = mockFetch;
56
55
 
57
- await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
56
+ await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
57
+
58
+ global.fetch = originalFetch;
59
+ });
60
+
61
+ it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
62
+ const originalFetch = global.fetch;
63
+ global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
64
+
65
+ await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
66
+
67
+ global.fetch = originalFetch;
68
+ });
69
+
70
+ it('should extract basic auth credentials from URL into Authorization header', async () => {
71
+ const originalFetch = global.fetch;
72
+ const mockFetch = vi.fn().mockResolvedValue({
73
+ ok: true,
74
+ text: async () => '{}'
75
+ } as Response);
76
+ global.fetch = mockFetch;
77
+
78
+ await reader.readFile('https://user:pass@example.com/openapi.json');
79
+
80
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
81
+
82
+ global.fetch = originalFetch;
83
+ });
84
+
85
+ it('should decode percent-encoded credentials from URL', async () => {
86
+ const originalFetch = global.fetch;
87
+ const mockFetch = vi.fn().mockResolvedValue({
88
+ ok: true,
89
+ text: async () => '{}'
90
+ } as Response);
91
+ global.fetch = mockFetch;
92
+
93
+ await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
94
+
95
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
58
96
 
59
- // Verify fetch was called
60
- expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
61
97
  global.fetch = originalFetch;
62
98
  });
99
+
100
+ it('should not treat non-http schemes as URLs', async () => {
101
+ await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
102
+ });
63
103
  });
64
104
  });
65
105