Skip to content

Commit 6c5537b

Browse files
committed
fix: TypeScript helper transpilation for multi-file imports
- Fixed import detection regex to match ./ and ../ patterns correctly - Fixed extension handling for files like ./abstract.helper.ts - Fixed import path calculation to use temp file directory - Added recursive transpilation for imported TypeScript dependencies - Import paths are now correctly rewritten to .temp.mjs files Resolves issue where custom TypeScript helpers could not import from other TypeScript files, resulting in 'Cannot find module' errors. Version bump to 4.0.0-beta.22
1 parent f74d0f3 commit 6c5537b

File tree

5 files changed

+96
-67
lines changed

5 files changed

+96
-67
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 4.0.0-beta.22
2+
3+
### 🐛 Bug Fixes
4+
5+
- Fixed TypeScript helper transpilation to support imports from other TypeScript files
6+
- Custom helpers can now import from other TypeScript files (e.g., `import { AbstractHelper } from './abstract.helper'`)
7+
- Automatically transpiles imported TypeScript dependencies recursively
8+
- Correctly rewrites import paths in transpiled `.temp.mjs` files
9+
110
## 3.7.5
211

312
❤️ Thanks all to those who contributed to make this release! ❤️

lib/utils/typescript.js

Lines changed: 68 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from 'path'
44
/**
55
* Transpile TypeScript files to ES modules with CommonJS shim support
66
* Handles recursive transpilation of imported TypeScript files
7-
*
7+
*
88
* @param {string} mainFilePath - Path to the main TypeScript file to transpile
99
* @param {object} typescript - TypeScript compiler instance
1010
* @returns {Promise<{tempFile: string, allTempFiles: string[]}>} - Main temp file and all temp files created
@@ -16,9 +16,9 @@ export async function transpileTypeScript(mainFilePath, typescript) {
1616
* Transpile a single TypeScript file to JavaScript
1717
* Injects CommonJS shims (require, module, exports, __dirname, __filename) as needed
1818
*/
19-
const transpileTS = (filePath) => {
19+
const transpileTS = filePath => {
2020
const tsContent = fs.readFileSync(filePath, 'utf8')
21-
21+
2222
// Transpile TypeScript to JavaScript with ES module output
2323
let jsContent = transpile(tsContent, {
2424
module: 99, // ModuleKind.ESNext
@@ -29,16 +29,16 @@ export async function transpileTypeScript(mainFilePath, typescript) {
2929
suppressOutputPathCheck: true,
3030
skipLibCheck: true,
3131
})
32-
32+
3333
// Check if the code uses CommonJS globals
3434
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
3535
const usesRequire = /\brequire\s*\(/.test(jsContent)
3636
const usesModuleExports = /\b(module\.exports|exports\.)/.test(jsContent)
37-
37+
3838
if (usesCommonJSGlobals || usesRequire || usesModuleExports) {
3939
// Inject ESM equivalents at the top of the file
4040
let esmGlobals = ''
41-
41+
4242
if (usesRequire || usesModuleExports) {
4343
// IMPORTANT: Use the original .ts file path as the base for require()
4444
// This ensures dynamic require() calls work with relative paths from the original file location
@@ -81,7 +81,7 @@ const exports = module.exports;
8181
8282
`
8383
}
84-
84+
8585
if (usesCommonJSGlobals) {
8686
// For __dirname and __filename, also use the original file path
8787
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
@@ -92,56 +92,57 @@ const __dirname = __dirname_fn(__filename);
9292
9393
`
9494
}
95-
95+
9696
jsContent = esmGlobals + jsContent
97-
97+
9898
// If module.exports is used, we need to export it as default
9999
if (usesModuleExports) {
100100
jsContent += `\nexport default module.exports;\n`
101101
}
102102
}
103-
103+
104104
return jsContent
105105
}
106-
106+
107107
// Create a map to track transpiled files
108108
const transpiledFiles = new Map()
109109
const baseDir = path.dirname(mainFilePath)
110-
110+
111111
// Recursive function to transpile a file and all its TypeScript dependencies
112-
const transpileFileAndDeps = (filePath) => {
112+
const transpileFileAndDeps = filePath => {
113113
// Already transpiled, skip
114114
if (transpiledFiles.has(filePath)) {
115115
return
116116
}
117-
117+
118118
// Transpile this file
119119
let jsContent = transpileTS(filePath)
120-
120+
121121
// Find all relative TypeScript imports in this file
122-
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
122+
// Match imports that start with ./ or ../
123+
const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?:\.ts)?['"]/g
123124
let match
124125
const imports = []
125-
126+
126127
while ((match = importRegex.exec(jsContent)) !== null) {
127128
imports.push(match[1])
128129
}
129-
130+
130131
// Get the base directory for this file
131132
const fileBaseDir = path.dirname(filePath)
132-
133+
133134
// Recursively transpile each imported TypeScript file
134135
for (const relativeImport of imports) {
135136
let importedPath = path.resolve(fileBaseDir, relativeImport)
136-
137+
137138
// Handle .js extensions that might actually be .ts files
138139
if (importedPath.endsWith('.js')) {
139140
const tsVersion = importedPath.replace(/\.js$/, '.ts')
140141
if (fs.existsSync(tsVersion)) {
141142
importedPath = tsVersion
142143
}
143144
}
144-
145+
145146
// Try adding .ts extension if file doesn't exist and no extension provided
146147
if (!path.extname(importedPath)) {
147148
const tsPath = importedPath + '.ts'
@@ -155,68 +156,76 @@ const __dirname = __dirname_fn(__filename);
155156
continue
156157
}
157158
}
159+
} else if (importedPath.match(/\.[^./\\]+$/)) {
160+
// Has an extension that's not .ts - check if .ts version exists by appending .ts
161+
const tsPath = importedPath + '.ts'
162+
if (fs.existsSync(tsPath)) {
163+
importedPath = tsPath
164+
}
158165
}
159-
166+
160167
// If it's a TypeScript file, recursively transpile it and its dependencies
161168
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
162169
transpileFileAndDeps(importedPath)
163170
}
164171
}
165-
172+
166173
// After all dependencies are transpiled, rewrite imports in this file
167-
jsContent = jsContent.replace(
168-
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
169-
(match, importPath) => {
170-
let resolvedPath = path.resolve(fileBaseDir, importPath)
171-
172-
// Handle .js extension that might be .ts
173-
if (resolvedPath.endsWith('.js')) {
174-
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
175-
if (transpiledFiles.has(tsVersion)) {
176-
const tempFile = transpiledFiles.get(tsVersion)
177-
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
178-
// Ensure the path starts with ./
179-
if (!relPath.startsWith('.')) {
180-
return `from './${relPath}'`
181-
}
182-
return `from '${relPath}'`
183-
}
184-
}
185-
186-
// Try with .ts extension
187-
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
188-
189-
// If we transpiled this file, use the temp file
190-
if (transpiledFiles.has(tsPath)) {
191-
const tempFile = transpiledFiles.get(tsPath)
192-
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
174+
// IMPORTANT: We need to calculate temp file location first so we can compute correct relative paths
175+
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
176+
const tempFileDir = path.dirname(tempFile)
177+
178+
jsContent = jsContent.replace(/from\s+['"](\.\.?\/[^'"]+?)(?:\.ts)?['"]/g, (match, importPath) => {
179+
let resolvedPath = path.resolve(fileBaseDir, importPath)
180+
181+
// Handle .js extension that might be .ts
182+
if (resolvedPath.endsWith('.js')) {
183+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
184+
if (transpiledFiles.has(tsVersion)) {
185+
const importedTempFile = transpiledFiles.get(tsVersion)
186+
// Calculate relative path from THIS temp file to the imported temp file
187+
const relPath = path.relative(tempFileDir, importedTempFile).replace(/\\/g, '/')
193188
// Ensure the path starts with ./
194189
if (!relPath.startsWith('.')) {
195190
return `from './${relPath}'`
196191
}
197192
return `from '${relPath}'`
198193
}
199-
200-
// Otherwise, keep the import as-is
201-
return match
202194
}
203-
)
204-
195+
196+
// Try with .ts extension
197+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
198+
199+
// If we transpiled this file, use the temp file
200+
if (transpiledFiles.has(tsPath)) {
201+
const importedTempFile = transpiledFiles.get(tsPath)
202+
// Calculate relative path from THIS temp file to the imported temp file
203+
const relPath = path.relative(tempFileDir, importedTempFile).replace(/\\/g, '/')
204+
// Ensure the path starts with ./
205+
if (!relPath.startsWith('.')) {
206+
return `from './${relPath}'`
207+
}
208+
return `from '${relPath}'`
209+
}
210+
211+
// Otherwise, keep the import as-is (for npm packages)
212+
return match
213+
})
214+
205215
// Write the transpiled file with updated imports
206-
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
207216
fs.writeFileSync(tempFile, jsContent)
208217
transpiledFiles.set(filePath, tempFile)
209218
}
210-
219+
211220
// Start recursive transpilation from the main file
212221
transpileFileAndDeps(mainFilePath)
213-
222+
214223
// Get the main transpiled file
215224
const tempJsFile = transpiledFiles.get(mainFilePath)
216-
225+
217226
// Store all temp files for cleanup
218227
const allTempFiles = Array.from(transpiledFiles.values())
219-
228+
220229
return { tempFile: tempJsFile, allTempFiles }
221230
}
222231

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.21",
3+
"version": "4.0.0-beta.22",
44
"type": "module",
55
"description": "Supercharged End 2 End Testing Framework for NodeJS",
66
"keywords": [
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Helper from '@codeceptjs/helper'
2+
3+
export abstract class AbstractHelper extends Helper {
4+
protected getHelperName(): string {
5+
return this.constructor.name
6+
}
7+
8+
protected log(message: string): void {
9+
console.log(`[${this.getHelperName()}] ${message}`)
10+
}
11+
}
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import Helper from '@codeceptjs/helper';
1+
import { AbstractHelper } from './abstract.helper'
22

33
interface TestData {
4-
message: string;
5-
count: number;
4+
message: string
5+
count: number
66
}
77

8-
class CustomHelper extends Helper {
8+
class CustomHelper extends AbstractHelper {
99
async logTestData(data: TestData): Promise<void> {
10-
console.log(`Message: ${data.message}, Count: ${data.count}`);
10+
this.log(`Message: ${data.message}, Count: ${data.count}`)
1111
}
1212

1313
getGreeting(name: string): string {
14-
return `Hello, ${name}!`;
14+
return `Hello, ${name}!`
1515
}
1616
}
1717

18-
export default CustomHelper;
18+
export default CustomHelper

0 commit comments

Comments
 (0)