Skip to content

Commit a2ad2aa

Browse files
authored
Merge pull request #3380 from hey-api/copilot/fix-implicit-module-resolution
Detect implicit moduleResolution from module compiler option
2 parents e57fdde + bf81851 commit a2ad2aa

3 files changed

Lines changed: 188 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
**output**: detect `importFileExtension` from tsconfig `module` option
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
5+
import { getOutput } from '../config';
6+
7+
describe('getOutput', () => {
8+
let tmpDir: string;
9+
10+
beforeEach(() => {
11+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-ts-test-'));
12+
});
13+
14+
afterEach(() => {
15+
if (fs.existsSync(tmpDir)) {
16+
fs.rmSync(tmpDir, { force: true, recursive: true });
17+
}
18+
});
19+
20+
describe('module resolution detection', () => {
21+
it('should set importFileExtension when moduleResolution is NodeNext', () => {
22+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
23+
fs.writeFileSync(
24+
tsconfigPath,
25+
JSON.stringify({
26+
compilerOptions: {
27+
moduleResolution: 'nodenext',
28+
},
29+
}),
30+
);
31+
32+
const output = getOutput({
33+
output: {
34+
path: tmpDir,
35+
tsConfigPath: tsconfigPath,
36+
},
37+
});
38+
39+
expect(output.importFileExtension).toBe('.js');
40+
});
41+
42+
it('should set importFileExtension when moduleResolution is Node16', () => {
43+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
44+
fs.writeFileSync(
45+
tsconfigPath,
46+
JSON.stringify({
47+
compilerOptions: {
48+
moduleResolution: 'node16',
49+
},
50+
}),
51+
);
52+
53+
const output = getOutput({
54+
output: {
55+
path: tmpDir,
56+
tsConfigPath: tsconfigPath,
57+
},
58+
});
59+
60+
expect(output.importFileExtension).toBe('.js');
61+
});
62+
63+
it('should set importFileExtension when module is NodeNext (implicit moduleResolution)', () => {
64+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
65+
fs.writeFileSync(
66+
tsconfigPath,
67+
JSON.stringify({
68+
compilerOptions: {
69+
module: 'nodenext',
70+
},
71+
}),
72+
);
73+
74+
const output = getOutput({
75+
output: {
76+
path: tmpDir,
77+
tsConfigPath: tsconfigPath,
78+
},
79+
});
80+
81+
expect(output.importFileExtension).toBe('.js');
82+
});
83+
84+
it('should set importFileExtension when module is Node16 (implicit moduleResolution)', () => {
85+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
86+
fs.writeFileSync(
87+
tsconfigPath,
88+
JSON.stringify({
89+
compilerOptions: {
90+
module: 'node16',
91+
},
92+
}),
93+
);
94+
95+
const output = getOutput({
96+
output: {
97+
path: tmpDir,
98+
tsConfigPath: tsconfigPath,
99+
},
100+
});
101+
102+
expect(output.importFileExtension).toBe('.js');
103+
});
104+
105+
it('should not set importFileExtension for other module types', () => {
106+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
107+
fs.writeFileSync(
108+
tsconfigPath,
109+
JSON.stringify({
110+
compilerOptions: {
111+
module: 'esnext',
112+
},
113+
}),
114+
);
115+
116+
const output = getOutput({
117+
output: {
118+
path: tmpDir,
119+
tsConfigPath: tsconfigPath,
120+
},
121+
});
122+
123+
expect(output.importFileExtension).toBeUndefined();
124+
});
125+
126+
it('should not override explicit importFileExtension setting', () => {
127+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
128+
fs.writeFileSync(
129+
tsconfigPath,
130+
JSON.stringify({
131+
compilerOptions: {
132+
module: 'nodenext',
133+
},
134+
}),
135+
);
136+
137+
const output = getOutput({
138+
output: {
139+
importFileExtension: '.ts',
140+
path: tmpDir,
141+
tsConfigPath: tsconfigPath,
142+
},
143+
});
144+
145+
expect(output.importFileExtension).toBe('.ts');
146+
});
147+
148+
it('should work when both module and moduleResolution are set', () => {
149+
const tsconfigPath = path.join(tmpDir, 'tsconfig.json');
150+
fs.writeFileSync(
151+
tsconfigPath,
152+
JSON.stringify({
153+
compilerOptions: {
154+
module: 'nodenext',
155+
moduleResolution: 'nodenext',
156+
},
157+
}),
158+
);
159+
160+
const output = getOutput({
161+
output: {
162+
path: tmpDir,
163+
tsConfigPath: tsconfigPath,
164+
},
165+
});
166+
167+
expect(output.importFileExtension).toBe('.js');
168+
});
169+
170+
it('should handle missing tsconfig gracefully', () => {
171+
const output = getOutput({
172+
output: {
173+
path: tmpDir,
174+
},
175+
});
176+
177+
expect(output.importFileExtension).toBeUndefined();
178+
});
179+
});
180+
});

packages/openapi-ts/src/config/output/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ export function getOutput(userConfig: { output: MaybeArray<string | UserOutput>
6565
if (
6666
output.importFileExtension === undefined &&
6767
(output.tsConfig?.options.moduleResolution === ts.ModuleResolutionKind.NodeNext ||
68-
output.tsConfig?.options.moduleResolution === ts.ModuleResolutionKind.Node16)
68+
output.tsConfig?.options.moduleResolution === ts.ModuleResolutionKind.Node16 ||
69+
output.tsConfig?.options.module === ts.ModuleKind.NodeNext ||
70+
output.tsConfig?.options.module === ts.ModuleKind.Node16)
6971
) {
7072
output.importFileExtension = '.js';
7173
}

0 commit comments

Comments
 (0)