diff --git a/lib/config.js b/lib/config.js index ea9f460c7..0b3372e32 100644 --- a/lib/config.js +++ b/lib/config.js @@ -156,27 +156,32 @@ async function loadConfigFile(configFile) { try { // For .ts files, try to compile and load as JavaScript if (extensionName === '.ts') { + let transpileError = null + let tempFile = null + let allTempFiles = null + let fileMapping = null + try { // Use the TypeScript transpilation utility const typescript = require('typescript') - const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript) - - try { - configModule = await import(tempFile) - cleanupTempFiles(allTempFiles) - } catch (err) { + const result = await transpileTypeScript(configFile, typescript) + tempFile = result.tempFile + allTempFiles = result.allTempFiles + fileMapping = result.fileMapping + + configModule = await import(tempFile) + cleanupTempFiles(allTempFiles) + } catch (err) { + transpileError = err + if (fileMapping) { fixErrorStack(err, fileMapping) - cleanupTempFiles(allTempFiles) - throw err } - } catch (tsError) { - // If TypeScript compilation fails, fallback to ts-node - try { - require('ts-node/register') - configModule = require(configFile) - } catch (tsNodeError) { - throw new Error(`Failed to load TypeScript config: ${tsError.message}`) + if (allTempFiles) { + cleanupTempFiles(allTempFiles) } + // Throw immediately with the actual error - don't fall back to ts-node + // as it will mask the real error with "Unexpected token 'export'" + throw err } } else { // Try ESM import first for JS files diff --git a/lib/utils/typescript.js b/lib/utils/typescript.js index 3f134c7ea..5f9d0d417 100644 --- a/lib/utils/typescript.js +++ b/lib/utils/typescript.js @@ -1,5 +1,60 @@ import fs from 'fs' import path from 'path' +import { pathToFileURL } from 'url' + +/** + * Load tsconfig.json if it exists + * @param {string} tsConfigPath - Path to tsconfig.json + * @returns {object|null} - Parsed tsconfig or null + */ +function loadTsConfig(tsConfigPath) { + if (!fs.existsSync(tsConfigPath)) { + return null + } + + try { + const tsConfigContent = fs.readFileSync(tsConfigPath, 'utf8') + return JSON.parse(tsConfigContent) + } catch (err) { + return null + } +} + +/** + * Resolve TypeScript path alias to actual file path + * @param {string} importPath - Import path with alias (e.g., '#config/urls') + * @param {object} tsConfig - Parsed tsconfig.json + * @param {string} configDir - Directory containing tsconfig.json + * @returns {string|null} - Resolved file path or null if not an alias + */ +function resolveTsPathAlias(importPath, tsConfig, configDir) { + if (!tsConfig || !tsConfig.compilerOptions || !tsConfig.compilerOptions.paths) { + return null + } + + const paths = tsConfig.compilerOptions.paths + + for (const [pattern, targets] of Object.entries(paths)) { + if (!targets || targets.length === 0) { + continue + } + + const patternRegex = new RegExp( + '^' + pattern.replace(/\*/g, '(.*)') + '$' + ) + const match = importPath.match(patternRegex) + + if (match) { + const wildcard = match[1] || '' + const target = targets[0] + const resolvedTarget = target.replace(/\*/g, wildcard) + + return path.resolve(configDir, resolvedTarget) + } + } + + return null +} /** * Transpile TypeScript files to ES modules with CommonJS shim support @@ -108,6 +163,22 @@ const __dirname = __dirname_fn(__filename); const transpiledFiles = new Map() const baseDir = path.dirname(mainFilePath) + // Try to find tsconfig.json by walking up the directory tree + let tsConfigPath = path.join(baseDir, 'tsconfig.json') + let configDir = baseDir + let searchDir = baseDir + + while (!fs.existsSync(tsConfigPath) && searchDir !== path.dirname(searchDir)) { + searchDir = path.dirname(searchDir) + tsConfigPath = path.join(searchDir, 'tsconfig.json') + if (fs.existsSync(tsConfigPath)) { + configDir = searchDir + break + } + } + + const tsConfig = loadTsConfig(tsConfigPath) + // Recursive function to transpile a file and all its TypeScript dependencies const transpileFileAndDeps = (filePath) => { // Already transpiled, skip @@ -118,21 +189,36 @@ const __dirname = __dirname_fn(__filename); // Transpile this file let jsContent = transpileTS(filePath) - // Find all relative TypeScript imports in this file - const importRegex = /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g + // Find all TypeScript imports in this file (both ESM imports and require() calls) + const importRegex = /from\s+['"]([^'"]+?)['"]/g + const requireRegex = /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g let match const imports = [] while ((match = importRegex.exec(jsContent)) !== null) { - imports.push(match[1]) + imports.push({ path: match[1], type: 'import' }) + } + + while ((match = requireRegex.exec(jsContent)) !== null) { + imports.push({ path: match[1], type: 'require' }) } // Get the base directory for this file const fileBaseDir = path.dirname(filePath) // Recursively transpile each imported TypeScript file - for (const relativeImport of imports) { - let importedPath = path.resolve(fileBaseDir, relativeImport) + for (const { path: importPath } of imports) { + let importedPath = importPath + + // Check if this is a path alias + const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir) + if (resolvedAlias) { + importedPath = resolvedAlias + } else if (importPath.startsWith('.')) { + importedPath = path.resolve(fileBaseDir, importPath) + } else { + continue + } // Handle .js extensions that might actually be .ts files if (importedPath.endsWith('.js')) { @@ -153,11 +239,17 @@ const __dirname = __dirname_fn(__filename); if (fs.existsSync(tsPath)) { importedPath = tsPath } else { - // Try .js extension as well - const jsPath = importedPath + '.js' - if (fs.existsSync(jsPath)) { - // Skip .js files, they don't need transpilation - continue + // Try index.ts for directory imports + const indexTsPath = path.join(importedPath, 'index.ts') + if (fs.existsSync(indexTsPath)) { + importedPath = indexTsPath + } else { + // Try .js extension as well + const jsPath = importedPath + '.js' + if (fs.existsSync(jsPath)) { + // Skip .js files, they don't need transpilation + continue + } } } } @@ -170,24 +262,45 @@ const __dirname = __dirname_fn(__filename); // After all dependencies are transpiled, rewrite imports in this file jsContent = jsContent.replace( - /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g, + /from\s+['"]([^'"]+?)['"]/g, (match, importPath) => { - let resolvedPath = path.resolve(fileBaseDir, importPath) + let resolvedPath = importPath const originalExt = path.extname(importPath) + // Check if this is a path alias + const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir) + if (resolvedAlias) { + resolvedPath = resolvedAlias + } else if (importPath.startsWith('.')) { + resolvedPath = path.resolve(fileBaseDir, importPath) + } else { + return match + } + + // If resolved path is a directory, try index.ts + if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) { + const indexPath = path.join(resolvedPath, 'index.ts') + if (fs.existsSync(indexPath) && transpiledFiles.has(indexPath)) { + const tempFile = transpiledFiles.get(indexPath) + const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') + if (!relPath.startsWith('.')) { + return `from './${relPath}'` + } + return `from '${relPath}'` + } + } + // Handle .js extension that might be .ts if (resolvedPath.endsWith('.js')) { const tsVersion = resolvedPath.replace(/\.js$/, '.ts') if (transpiledFiles.has(tsVersion)) { const tempFile = transpiledFiles.get(tsVersion) const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') - // Ensure the path starts with ./ if (!relPath.startsWith('.')) { return `from './${relPath}'` } return `from '${relPath}'` } - // Keep .js extension as-is (might be a real .js file) return match } @@ -198,18 +311,24 @@ const __dirname = __dirname_fn(__filename); if (transpiledFiles.has(tsPath)) { const tempFile = transpiledFiles.get(tsPath) const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') - // Ensure the path starts with ./ if (!relPath.startsWith('.')) { return `from './${relPath}'` } return `from '${relPath}'` } - // If the import doesn't have a standard module extension (.js, .mjs, .cjs, .json) - // add .js for ESM compatibility - // This handles cases where: - // 1. Import has no real extension (e.g., "./utils" or "./helper") - // 2. Import has a non-standard extension that's part of the name (e.g., "./abstract.helper") + // Try index.ts for directory imports + const indexTsPath = path.join(resolvedPath, 'index.ts') + if (transpiledFiles.has(indexTsPath)) { + const tempFile = transpiledFiles.get(indexTsPath) + const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') + if (!relPath.startsWith('.')) { + return `from './${relPath}'` + } + return `from '${relPath}'` + } + + // If the import doesn't have a standard module extension, add .js for ESM compatibility const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node'] const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase()) @@ -217,7 +336,50 @@ const __dirname = __dirname_fn(__filename); return match.replace(importPath, importPath + '.js') } - // Otherwise, keep the import as-is + return match + } + ) + + // Also rewrite require() calls to point to transpiled TypeScript files + jsContent = jsContent.replace( + /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g, + (match, requirePath) => { + let resolvedPath = requirePath + + // Check if this is a path alias + const resolvedAlias = resolveTsPathAlias(requirePath, tsConfig, configDir) + if (resolvedAlias) { + resolvedPath = resolvedAlias + } else if (requirePath.startsWith('.')) { + resolvedPath = path.resolve(fileBaseDir, requirePath) + } else { + return match + } + + // Handle .js extension that might be .ts + if (resolvedPath.endsWith('.js')) { + const tsVersion = resolvedPath.replace(/\.js$/, '.ts') + if (transpiledFiles.has(tsVersion)) { + const tempFile = transpiledFiles.get(tsVersion) + const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') + const finalPath = relPath.startsWith('.') ? relPath : './' + relPath + return `require('${finalPath}')` + } + return match + } + + // Try with .ts extension + const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts' + + // If we transpiled this file, use the temp file + if (transpiledFiles.has(tsPath)) { + const tempFile = transpiledFiles.get(tsPath) + const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/') + const finalPath = relPath.startsWith('.') ? relPath : './' + relPath + return `require('${finalPath}')` + } + + // Otherwise, keep the require as-is return match } ) @@ -234,10 +396,13 @@ const __dirname = __dirname_fn(__filename); // Get the main transpiled file const tempJsFile = transpiledFiles.get(mainFilePath) - // Store all temp files for cleanup + // Convert to file:// URL for dynamic import() (required on Windows) + const tempFileUrl = pathToFileURL(tempJsFile).href + + // Store all temp files for cleanup (keep as paths, not URLs) const allTempFiles = Array.from(transpiledFiles.values()) - return { tempFile: tempJsFile, allTempFiles, fileMapping: transpiledFiles } + return { tempFile: tempFileUrl, allTempFiles, fileMapping: transpiledFiles } } /** diff --git a/package.json b/package.json index 49e538dc2..c7691c160 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "4.0.2-beta.17", + "version": "4.0.2-beta.19", "type": "module", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [