Skip to content

Commit f3f3ed0

Browse files
committed
fix several 4.x issues
1 parent 6c916ce commit f3f3ed0

File tree

4 files changed

+82
-40
lines changed

4 files changed

+82
-40
lines changed

lib/config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs'
22
import path from 'path'
33
import { createRequire } from 'module'
44
import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
5-
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
5+
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
66

77
const defaultConfig = {
88
output: './_output',
@@ -159,12 +159,13 @@ async function loadConfigFile(configFile) {
159159
try {
160160
// Use the TypeScript transpilation utility
161161
const typescript = require('typescript')
162-
const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
162+
const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript)
163163

164164
try {
165165
configModule = await import(tempFile)
166166
cleanupTempFiles(allTempFiles)
167167
} catch (err) {
168+
fixErrorStack(err, fileMapping)
168169
cleanupTempFiles(allTempFiles)
169170
throw err
170171
}

lib/container.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import debugModule from 'debug'
55
const debug = debugModule('codeceptjs:container')
66
import { MetaStep } from './step.js'
77
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
8-
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
8+
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
99
import Translation from './translation.js'
1010
import MochaFactory from './mocha/factory.js'
1111
import recorder from './recorder.js'
@@ -401,18 +401,20 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
401401
// Handle TypeScript files
402402
let importPath = moduleName
403403
let tempJsFile = null
404+
let fileMapping = null
404405
const ext = path.extname(moduleName)
405406

406407
if (ext === '.ts') {
407408
try {
408409
// Use the TypeScript transpilation utility
409410
const typescript = await import('typescript')
410-
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
411+
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
411412

412413
debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
413414

414415
importPath = tempFile
415416
tempJsFile = allTempFiles
417+
fileMapping = mapping
416418
} catch (tsError) {
417419
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
418420
}
@@ -433,6 +435,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
433435
cleanupTempFiles(filesToClean)
434436
}
435437
} catch (err) {
438+
// Fix error stack to point to original .ts files
439+
if (fileMapping) {
440+
fixErrorStack(err, fileMapping)
441+
}
442+
436443
// Clean up temp files before rethrowing
437444
if (tempJsFile) {
438445
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
@@ -731,6 +738,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
731738
// Use dynamic import for both ESM and CJS modules
732739
let importPath = modulePath
733740
let tempJsFile = null
741+
let fileMapping = null
734742

735743
if (typeof importPath === 'string') {
736744
const ext = path.extname(importPath)
@@ -740,14 +748,15 @@ async function loadSupportObject(modulePath, supportObjectName) {
740748
try {
741749
// Use the TypeScript transpilation utility
742750
const typescript = await import('typescript')
743-
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
751+
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
744752

745753
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
746754

747755
// Attach cleanup handler
748756
importPath = tempFile
749757
// Store temp files list in a way that cleanup can access them
750758
tempJsFile = allTempFiles
759+
fileMapping = mapping
751760
} catch (tsError) {
752761
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
753762
}
@@ -761,6 +770,11 @@ async function loadSupportObject(modulePath, supportObjectName) {
761770
try {
762771
obj = await import(importPath)
763772
} catch (importError) {
773+
// Fix error stack to point to original .ts files
774+
if (fileMapping) {
775+
fixErrorStack(importError, fileMapping)
776+
}
777+
764778
// Clean up temp files if created before rethrowing
765779
if (tempJsFile) {
766780
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]

lib/utils/typescript.js

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ 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
10-
* @returns {Promise<{tempFile: string, allTempFiles: string[]}>} - Main temp file and all temp files created
10+
* @returns {Promise<{tempFile: string, allTempFiles: string[], fileMapping: any}>} - Main temp file and all temp files created
1111
*/
1212
export async function transpileTypeScript(mainFilePath, typescript) {
1313
const { transpile } = typescript
@@ -18,7 +18,7 @@ export async function transpileTypeScript(mainFilePath, typescript) {
1818
*/
1919
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,61 +92,61 @@ 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
112112
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
122122
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
123123
let match
124124
const imports = []
125-
125+
126126
while ((match = importRegex.exec(jsContent)) !== null) {
127127
imports.push(match[1])
128128
}
129-
129+
130130
// Get the base directory for this file
131131
const fileBaseDir = path.dirname(filePath)
132-
132+
133133
// Recursively transpile each imported TypeScript file
134134
for (const relativeImport of imports) {
135135
let importedPath = path.resolve(fileBaseDir, relativeImport)
136-
136+
137137
// Handle .js extensions that might actually be .ts files
138138
if (importedPath.endsWith('.js')) {
139139
const tsVersion = importedPath.replace(/\.js$/, '.ts')
140140
if (fs.existsSync(tsVersion)) {
141141
importedPath = tsVersion
142142
}
143143
}
144-
144+
145145
// Check for standard module extensions to determine if we should try adding .ts
146146
const ext = path.extname(importedPath)
147147
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
148148
const hasStandardExtension = standardExtensions.includes(ext.toLowerCase())
149-
149+
150150
// If it doesn't end with .ts and doesn't have a standard extension, try adding .ts
151151
if (!importedPath.endsWith('.ts') && !hasStandardExtension) {
152152
const tsPath = importedPath + '.ts'
@@ -161,20 +161,20 @@ const __dirname = __dirname_fn(__filename);
161161
}
162162
}
163163
}
164-
164+
165165
// If it's a TypeScript file, recursively transpile it and its dependencies
166166
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
167167
transpileFileAndDeps(importedPath)
168168
}
169169
}
170-
170+
171171
// After all dependencies are transpiled, rewrite imports in this file
172172
jsContent = jsContent.replace(
173173
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
174174
(match, importPath) => {
175175
let resolvedPath = path.resolve(fileBaseDir, importPath)
176176
const originalExt = path.extname(importPath)
177-
177+
178178
// Handle .js extension that might be .ts
179179
if (resolvedPath.endsWith('.js')) {
180180
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
@@ -190,10 +190,10 @@ const __dirname = __dirname_fn(__filename);
190190
// Keep .js extension as-is (might be a real .js file)
191191
return match
192192
}
193-
193+
194194
// Try with .ts extension
195195
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
196-
196+
197197
// If we transpiled this file, use the temp file
198198
if (transpiledFiles.has(tsPath)) {
199199
const tempFile = transpiledFiles.get(tsPath)
@@ -204,40 +204,67 @@ const __dirname = __dirname_fn(__filename);
204204
}
205205
return `from '${relPath}'`
206206
}
207-
207+
208208
// If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json)
209209
// add .js for ESM compatibility
210210
// This handles cases where:
211211
// 1. Import has no real extension (e.g., "./utils" or "./helper")
212212
// 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper")
213213
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
214214
const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
215-
215+
216216
if (!hasStandardExtension) {
217217
return match.replace(importPath, importPath + '.js')
218218
}
219-
219+
220220
// Otherwise, keep the import as-is
221221
return match
222222
}
223223
)
224-
224+
225225
// Write the transpiled file with updated imports
226226
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
227227
fs.writeFileSync(tempFile, jsContent)
228228
transpiledFiles.set(filePath, tempFile)
229229
}
230-
230+
231231
// Start recursive transpilation from the main file
232232
transpileFileAndDeps(mainFilePath)
233-
233+
234234
// Get the main transpiled file
235235
const tempJsFile = transpiledFiles.get(mainFilePath)
236-
236+
237237
// Store all temp files for cleanup
238238
const allTempFiles = Array.from(transpiledFiles.values())
239-
240-
return { tempFile: tempJsFile, allTempFiles }
239+
240+
return { tempFile: tempJsFile, allTempFiles, fileMapping: transpiledFiles }
241+
}
242+
243+
/**
244+
* Map error stack traces from temp .mjs files back to original .ts files
245+
* @param {Error} error - The error object to fix
246+
* @param {Map<string, string>} fileMapping - Map of original .ts files to temp .mjs files
247+
* @returns {Error} - Error with fixed stack trace
248+
*/
249+
export function fixErrorStack(error, fileMapping) {
250+
if (!error.stack || !fileMapping) return error
251+
252+
let stack = error.stack
253+
254+
// Create reverse mapping (temp.mjs -> original.ts)
255+
const reverseMap = new Map()
256+
for (const [tsFile, mjsFile] of fileMapping.entries()) {
257+
reverseMap.set(mjsFile, tsFile)
258+
}
259+
260+
// Replace all temp.mjs references with original .ts files
261+
for (const [mjsFile, tsFile] of reverseMap.entries()) {
262+
const mjsPattern = mjsFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
263+
stack = stack.replace(new RegExp(mjsPattern, 'g'), tsFile)
264+
}
265+
266+
error.stack = stack
267+
return error
241268
}
242269

243270
/**

typings/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ declare namespace CodeceptJS {
519519
retry(retries?: number): HookConfig
520520
}
521521

522-
function addStep(step: string, fn: Function): Promise<void>
522+
function addStep(step: string | RegExp, fn: Function): Promise<void>
523523
}
524524

525525
type TryTo = <T>(fn: () => Promise<T> | T) => Promise<T | false>

0 commit comments

Comments
 (0)