Skip to content

Commit b97fe90

Browse files
committed
fix: TypeScript files with directory-style imports (no .ts extension) now work correctly
- Refactored transpilation to recursively process all TypeScript dependencies - Each imported .ts file now has its imports rewritten before being saved - This ensures that when A imports B and B imports C, all three files are transpiled and their imports are updated correctly - Fixes issue where 'import X from ../file' would fail with 'Directory import not supported' - Added test case for nested TypeScript imports without extensions
1 parent ba1aa04 commit b97fe90

File tree

6 files changed

+136
-77
lines changed

6 files changed

+136
-77
lines changed

lib/utils/typescript.js

Lines changed: 94 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -74,97 +74,114 @@ const __dirname = __dirname_fn(__filename);
7474
const transpiledFiles = new Map()
7575
const baseDir = path.dirname(mainFilePath)
7676

77-
// Transpile main file
78-
let jsContent = transpileTS(mainFilePath)
79-
80-
// Find and transpile all relative TypeScript imports
81-
// Match: import ... from './file' or '../file' or './file.ts'
82-
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
83-
let match
84-
const imports = []
85-
86-
while ((match = importRegex.exec(jsContent)) !== null) {
87-
imports.push(match[1])
88-
}
89-
90-
// Transpile each imported TypeScript file
91-
for (const relativeImport of imports) {
92-
let importedPath = path.resolve(baseDir, relativeImport)
93-
94-
// Handle .js extensions that might actually be .ts files
95-
if (importedPath.endsWith('.js')) {
96-
const tsVersion = importedPath.replace(/\.js$/, '.ts')
97-
if (fs.existsSync(tsVersion)) {
98-
importedPath = tsVersion
99-
}
77+
// Recursive function to transpile a file and all its TypeScript dependencies
78+
const transpileFileAndDeps = (filePath) => {
79+
// Already transpiled, skip
80+
if (transpiledFiles.has(filePath)) {
81+
return
10082
}
10183

102-
// Try adding .ts extension if file doesn't exist and no extension provided
103-
if (!path.extname(importedPath)) {
104-
const tsPath = importedPath + '.ts'
105-
if (fs.existsSync(tsPath)) {
106-
importedPath = tsPath
107-
} else {
108-
// Try .js extension as well
109-
const jsPath = importedPath + '.js'
110-
if (fs.existsSync(jsPath)) {
111-
// Skip .js files, they don't need transpilation
112-
continue
113-
}
114-
}
115-
}
84+
// Transpile this file
85+
let jsContent = transpileTS(filePath)
86+
87+
// Find all relative TypeScript imports in this file
88+
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
89+
let match
90+
const imports = []
11691

117-
// If it's a TypeScript file, transpile it
118-
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
119-
const transpiledImportContent = transpileTS(importedPath)
120-
const tempImportFile = importedPath.replace(/\.ts$/, '.temp.mjs')
121-
fs.writeFileSync(tempImportFile, transpiledImportContent)
122-
transpiledFiles.set(importedPath, tempImportFile)
92+
while ((match = importRegex.exec(jsContent)) !== null) {
93+
imports.push(match[1])
12394
}
124-
}
125-
126-
// Replace imports in the main file to point to temp .mjs files
127-
jsContent = jsContent.replace(
128-
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
129-
(match, importPath) => {
130-
let resolvedPath = path.resolve(baseDir, importPath)
95+
96+
// Get the base directory for this file
97+
const fileBaseDir = path.dirname(filePath)
98+
99+
// Recursively transpile each imported TypeScript file
100+
for (const relativeImport of imports) {
101+
let importedPath = path.resolve(fileBaseDir, relativeImport)
131102

132-
// Handle .js extension that might be .ts
133-
if (resolvedPath.endsWith('.js')) {
134-
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
135-
if (transpiledFiles.has(tsVersion)) {
136-
const tempFile = transpiledFiles.get(tsVersion)
137-
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
138-
return `from './${relPath}'`
103+
// Handle .js extensions that might actually be .ts files
104+
if (importedPath.endsWith('.js')) {
105+
const tsVersion = importedPath.replace(/\.js$/, '.ts')
106+
if (fs.existsSync(tsVersion)) {
107+
importedPath = tsVersion
139108
}
140109
}
141110

142-
// Try with .ts extension
143-
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
144-
145-
// If we transpiled this file, use the temp file
146-
if (transpiledFiles.has(tsPath)) {
147-
const tempFile = transpiledFiles.get(tsPath)
148-
// Get relative path from main temp file to this temp file
149-
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
150-
// Ensure the path starts with ./
151-
if (!relPath.startsWith('.')) {
152-
return `from './${relPath}'`
111+
// Try adding .ts extension if file doesn't exist and no extension provided
112+
if (!path.extname(importedPath)) {
113+
const tsPath = importedPath + '.ts'
114+
if (fs.existsSync(tsPath)) {
115+
importedPath = tsPath
116+
} else {
117+
// Try .js extension as well
118+
const jsPath = importedPath + '.js'
119+
if (fs.existsSync(jsPath)) {
120+
// Skip .js files, they don't need transpilation
121+
continue
122+
}
153123
}
154-
return `from '${relPath}'`
155124
}
156125

157-
// Otherwise, keep the import as-is
158-
return match
126+
// If it's a TypeScript file, recursively transpile it and its dependencies
127+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
128+
transpileFileAndDeps(importedPath)
129+
}
159130
}
160-
)
161-
162-
// Create a temporary JS file with .mjs extension for the main file
163-
const tempJsFile = mainFilePath.replace(/\.ts$/, '.temp.mjs')
164-
fs.writeFileSync(tempJsFile, jsContent)
131+
132+
// After all dependencies are transpiled, rewrite imports in this file
133+
jsContent = jsContent.replace(
134+
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
135+
(match, importPath) => {
136+
let resolvedPath = path.resolve(fileBaseDir, importPath)
137+
138+
// Handle .js extension that might be .ts
139+
if (resolvedPath.endsWith('.js')) {
140+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
141+
if (transpiledFiles.has(tsVersion)) {
142+
const tempFile = transpiledFiles.get(tsVersion)
143+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
144+
// Ensure the path starts with ./
145+
if (!relPath.startsWith('.')) {
146+
return `from './${relPath}'`
147+
}
148+
return `from '${relPath}'`
149+
}
150+
}
151+
152+
// Try with .ts extension
153+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
154+
155+
// If we transpiled this file, use the temp file
156+
if (transpiledFiles.has(tsPath)) {
157+
const tempFile = transpiledFiles.get(tsPath)
158+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
159+
// Ensure the path starts with ./
160+
if (!relPath.startsWith('.')) {
161+
return `from './${relPath}'`
162+
}
163+
return `from '${relPath}'`
164+
}
165+
166+
// Otherwise, keep the import as-is
167+
return match
168+
}
169+
)
170+
171+
// Write the transpiled file with updated imports
172+
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
173+
fs.writeFileSync(tempFile, jsContent)
174+
transpiledFiles.set(filePath, tempFile)
175+
}
176+
177+
// Start recursive transpilation from the main file
178+
transpileFileAndDeps(mainFilePath)
179+
180+
// Get the main transpiled file
181+
const tempJsFile = transpiledFiles.get(mainFilePath)
165182

166183
// Store all temp files for cleanup
167-
const allTempFiles = [tempJsFile, ...Array.from(transpiledFiles.values())]
184+
const allTempFiles = Array.from(transpiledFiles.values())
168185

169186
return { tempFile: tempJsFile, allTempFiles }
170187
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Import from root level without .ts extension (TypeScript style)
2+
import { apiUrl, timeout } from '../environments';
3+
4+
export function getConfig() {
5+
return {
6+
endpoint: apiUrl,
7+
timeout: timeout
8+
};
9+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const apiUrl = 'https://api.example.com';
2+
export const timeout = 5000;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "typescript-directory-import",
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 { getConfig } from '../../common/utils';
2+
3+
const apiConfig = getConfig();
4+
5+
export const config = {
6+
tests: './*_test.js',
7+
output: './output',
8+
helpers: {
9+
REST: {
10+
endpoint: apiConfig.endpoint,
11+
timeout: apiConfig.timeout
12+
}
13+
},
14+
name: 'typescript-directory-import-test'
15+
};

test/unit/config_test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,15 @@ describe('Config', () => {
9595
expect(cfg.name).to.equal('typescript-dynamic-require-test')
9696
delete process.env.E2E_ENV
9797
})
98+
99+
it('should load TypeScript config with directory-style imports (no .ts extension)', async () => {
100+
const configPath = './test/data/typescript-directory-import/test/api/codecept.conf.ts'
101+
const cfg = await config.load(configPath)
102+
103+
expect(cfg).to.be.ok
104+
expect(cfg.helpers).to.have.property('REST')
105+
expect(cfg.helpers.REST.endpoint).to.equal('https://api.example.com')
106+
expect(cfg.helpers.REST.timeout).to.equal(5000)
107+
expect(cfg.name).to.equal('typescript-directory-import-test')
108+
})
98109
})

0 commit comments

Comments
 (0)