Skip to content

Commit 0a0ad4a

Browse files
committed
fix: TypeScript config files cannot import from other TypeScript files
- Applied same recursive transpilation fix from container.js to config.js - When loading TypeScript config files, now recursively transpiles all imported .ts files - Handles relative imports like '../common/utils' or './helpers/custom' - Updates import paths to point to temporary .mjs files - Properly cleans up all temporary files after loading - Injects __dirname/__filename shims when needed - Works with 'type: module' in package.json - Added test case with TypeScript config importing from external TS file - Version bumped to 4.0.0-beta.13 Fixes issue where users with ESM projects (type: module) got: 'Cannot find module /path/to/file imported from codecept.conf.temp.mjs'
1 parent 1160906 commit 0a0ad4a

File tree

6 files changed

+158
-14
lines changed

6 files changed

+158
-14
lines changed

lib/config.js

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,134 @@ async function loadConfigFile(configFile) {
158158
try {
159159
// Try to load ts-node and compile the file
160160
const { transpile } = require('typescript')
161-
const tsContent = fs.readFileSync(configFile, 'utf8')
161+
162+
// Recursively transpile the file and its dependencies
163+
const transpileTS = (filePath) => {
164+
const tsContent = fs.readFileSync(filePath, 'utf8')
165+
166+
// Transpile TypeScript to JavaScript with ES module output
167+
let jsContent = transpile(tsContent, {
168+
module: 99, // ModuleKind.ESNext
169+
target: 99, // ScriptTarget.ESNext
170+
esModuleInterop: true,
171+
allowSyntheticDefaultImports: true,
172+
})
173+
174+
// Check if the code uses __dirname or __filename (CommonJS globals)
175+
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
176+
177+
if (usesCommonJSGlobals) {
178+
// Inject ESM equivalents at the top of the file
179+
const esmGlobals = `import { fileURLToPath as __fileURLToPath } from 'url';
180+
import { dirname as __dirname_fn } from 'path';
181+
const __filename = __fileURLToPath(import.meta.url);
182+
const __dirname = __dirname_fn(__filename);
162183
163-
// Transpile TypeScript to JavaScript with ES module output
164-
const jsContent = transpile(tsContent, {
165-
module: 99, // ModuleKind.ESNext
166-
target: 99, // ScriptTarget.ESNext
167-
esModuleInterop: true,
168-
allowSyntheticDefaultImports: true,
169-
})
184+
`
185+
jsContent = esmGlobals + jsContent
186+
}
187+
188+
return jsContent
189+
}
190+
191+
// Create a map to track transpiled files
192+
const transpiledFiles = new Map()
193+
const baseDir = path.dirname(configFile)
194+
195+
// Transpile main file
196+
let jsContent = transpileTS(configFile)
197+
198+
// Find and transpile all relative TypeScript imports
199+
// Match: import ... from './file' or '../file' or './file.ts'
200+
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
201+
let match
202+
const imports = []
203+
204+
while ((match = importRegex.exec(jsContent)) !== null) {
205+
imports.push(match[1])
206+
}
207+
208+
// Transpile each imported TypeScript file
209+
for (const relativeImport of imports) {
210+
let importedPath = path.resolve(baseDir, relativeImport)
211+
212+
// Handle .js extensions that might actually be .ts files
213+
if (importedPath.endsWith('.js')) {
214+
const tsVersion = importedPath.replace(/\.js$/, '.ts')
215+
if (fs.existsSync(tsVersion)) {
216+
importedPath = tsVersion
217+
}
218+
}
219+
220+
// Try adding .ts extension if file doesn't exist and no extension provided
221+
if (!path.extname(importedPath)) {
222+
if (fs.existsSync(importedPath + '.ts')) {
223+
importedPath = importedPath + '.ts'
224+
}
225+
}
226+
227+
// If it's a TypeScript file, transpile it
228+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
229+
const transpiledImportContent = transpileTS(importedPath)
230+
const tempImportFile = importedPath.replace(/\.ts$/, '.temp.mjs')
231+
fs.writeFileSync(tempImportFile, transpiledImportContent)
232+
transpiledFiles.set(importedPath, tempImportFile)
233+
}
234+
}
235+
236+
// Replace imports in the main file to point to temp .mjs files
237+
jsContent = jsContent.replace(
238+
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
239+
(match, importPath) => {
240+
let resolvedPath = path.resolve(baseDir, importPath)
241+
242+
// Handle .js extension that might be .ts
243+
if (resolvedPath.endsWith('.js')) {
244+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
245+
if (transpiledFiles.has(tsVersion)) {
246+
const tempFile = transpiledFiles.get(tsVersion)
247+
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
248+
return `from './${relPath}'`
249+
}
250+
}
251+
252+
// Try with .ts extension
253+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
254+
255+
// If we transpiled this file, use the temp file
256+
if (transpiledFiles.has(tsPath)) {
257+
const tempFile = transpiledFiles.get(tsPath)
258+
// Get relative path from main temp file to this temp file
259+
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
260+
return `from './${relPath}'`
261+
}
262+
263+
// Otherwise, keep the import as-is
264+
return match
265+
}
266+
)
170267

171268
// Create a temporary JS file with .mjs extension to force ES module treatment
172269
const tempJsFile = configFile.replace('.ts', '.temp.mjs')
173270
fs.writeFileSync(tempJsFile, jsContent)
271+
272+
// Store all temp files for cleanup
273+
const allTempFiles = [tempJsFile, ...Array.from(transpiledFiles.values())]
174274

175275
try {
176276
configModule = await import(tempJsFile)
177-
// Clean up temp file
178-
fs.unlinkSync(tempJsFile)
277+
// Clean up all temp files
278+
for (const file of allTempFiles) {
279+
if (fs.existsSync(file)) {
280+
fs.unlinkSync(file)
281+
}
282+
}
179283
} catch (err) {
180-
// Clean up temp file even on error
181-
if (fs.existsSync(tempJsFile)) {
182-
fs.unlinkSync(tempJsFile)
284+
// Clean up all temp files even on error
285+
for (const file of allTempFiles) {
286+
if (fs.existsSync(file)) {
287+
fs.unlinkSync(file)
288+
}
183289
}
184290
throw err
185291
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeceptjs",
3-
"version": "4.0.0-beta.12",
3+
"version": "4.0.0-beta.13",
44
"type": "module",
55
"description": "Supercharged End 2 End Testing Framework for NodeJS",
66
"keywords": [
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function getApiUrl(): string {
2+
return 'https://api.example.com'
3+
}
4+
5+
export function getTimeout(): number {
6+
return 5000
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "typescript-config-test",
3+
"version": "1.0.0",
4+
"type": "module"
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getApiUrl, getTimeout } from '../../common/utils'
2+
3+
export const config = {
4+
tests: './*_test.js',
5+
output: './output',
6+
helpers: {
7+
REST: {
8+
endpoint: getApiUrl(),
9+
timeout: getTimeout(),
10+
}
11+
},
12+
bootstrap: null,
13+
mocha: {},
14+
name: 'typescript-config-test'
15+
}

test/unit/config_test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,15 @@ describe('Config', () => {
6060
expect(cfg).to.contain.key('additionalValue')
6161
expect(cfg.additionalValue).to.eql(true)
6262
})
63+
64+
it('should load TypeScript config that imports other TypeScript files', async () => {
65+
const configPath = './test/data/typescript-config-imports/tests/api/codecept.conf.ts'
66+
const cfg = await config.load(configPath)
67+
68+
expect(cfg).to.be.ok
69+
expect(cfg.helpers).to.have.property('REST')
70+
expect(cfg.helpers.REST.endpoint).to.equal('https://api.example.com')
71+
expect(cfg.helpers.REST.timeout).to.equal(5000)
72+
expect(cfg.name).to.equal('typescript-config-test')
73+
})
6374
})

0 commit comments

Comments
 (0)