Skip to content

Commit 981ec98

Browse files
committed
refactor: extract TypeScript transpilation logic into reusable utility
- Created lib/utils/typescript.js with transpileTypeScript() and cleanupTempFiles() - Refactored lib/config.js to use the new utility (reduced ~160 lines to ~10) - Refactored lib/container.js to use the new utility (reduced ~150 lines to ~10) - DRY principle: single source of truth for TS transpilation logic - Added support for require(), module.exports, __dirname, __filename in TS files - All tests pass (config + container TypeScript support) - Version remains at 4.0.0-beta.14
1 parent 0a0ad4a commit 981ec98

File tree

9 files changed

+253
-262
lines changed

9 files changed

+253
-262
lines changed

lib/config.js

Lines changed: 7 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +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'
56

67
const defaultConfig = {
78
output: './_output',
@@ -156,137 +157,15 @@ async function loadConfigFile(configFile) {
156157
// For .ts files, try to compile and load as JavaScript
157158
if (extensionName === '.ts') {
158159
try {
159-
// Try to load ts-node and compile the file
160-
const { transpile } = require('typescript')
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);
183-
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-
)
267-
268-
// Create a temporary JS file with .mjs extension to force ES module treatment
269-
const tempJsFile = configFile.replace('.ts', '.temp.mjs')
270-
fs.writeFileSync(tempJsFile, jsContent)
271-
272-
// Store all temp files for cleanup
273-
const allTempFiles = [tempJsFile, ...Array.from(transpiledFiles.values())]
160+
// Use the TypeScript transpilation utility
161+
const typescript = require('typescript')
162+
const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
274163

275164
try {
276-
configModule = await import(tempJsFile)
277-
// Clean up all temp files
278-
for (const file of allTempFiles) {
279-
if (fs.existsSync(file)) {
280-
fs.unlinkSync(file)
281-
}
282-
}
165+
configModule = await import(tempFile)
166+
cleanupTempFiles(allTempFiles)
283167
} catch (err) {
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-
}
289-
}
168+
cleanupTempFiles(allTempFiles)
290169
throw err
291170
}
292171
} catch (tsError) {

lib/container.js

Lines changed: 8 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +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'
89
import Translation from './translation.js'
910
import MochaFactory from './mocha/factory.js'
1011
import recorder from './recorder.js'
@@ -683,124 +684,14 @@ async function loadSupportObject(modulePath, supportObjectName) {
683684
// Handle TypeScript files
684685
if (ext === '.ts') {
685686
try {
686-
const { transpile } = await import('typescript')
687+
// Use the TypeScript transpilation utility
688+
const typescript = await import('typescript')
689+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
687690

688-
// Recursively transpile the file and its dependencies
689-
const transpileTS = (filePath) => {
690-
const tsContent = fs.readFileSync(filePath, 'utf8')
691-
692-
// Transpile TypeScript to JavaScript with ES module output
693-
let jsContent = transpile(tsContent, {
694-
module: 99, // ModuleKind.ESNext
695-
target: 99, // ScriptTarget.ESNext
696-
esModuleInterop: true,
697-
allowSyntheticDefaultImports: true,
698-
})
699-
700-
// Check if the code uses __dirname or __filename (CommonJS globals)
701-
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
702-
703-
if (usesCommonJSGlobals) {
704-
// Inject ESM equivalents at the top of the file
705-
const esmGlobals = `import { fileURLToPath as __fileURLToPath } from 'url';
706-
import { dirname as __dirname_fn } from 'path';
707-
const __filename = __fileURLToPath(import.meta.url);
708-
const __dirname = __dirname_fn(__filename);
709-
710-
`
711-
jsContent = esmGlobals + jsContent
712-
}
713-
714-
return jsContent
715-
}
716-
717-
// Create a map to track transpiled files
718-
const transpiledFiles = new Map()
719-
const baseDir = path.dirname(importPath)
720-
721-
// Transpile main file
722-
let jsContent = transpileTS(importPath)
723-
724-
// Find and transpile all relative TypeScript imports
725-
// Match: import ... from './file' or '../file' or './file.ts'
726-
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
727-
let match
728-
const imports = []
729-
730-
while ((match = importRegex.exec(jsContent)) !== null) {
731-
imports.push(match[1])
732-
}
733-
734-
// Transpile each imported TypeScript file
735-
for (const relativeImport of imports) {
736-
let importedPath = path.resolve(baseDir, relativeImport)
737-
738-
// Handle .js extensions that might actually be .ts files
739-
if (importedPath.endsWith('.js')) {
740-
const tsVersion = importedPath.replace(/\.js$/, '.ts')
741-
if (fs.existsSync(tsVersion)) {
742-
importedPath = tsVersion
743-
}
744-
}
745-
746-
// Try adding .ts extension if file doesn't exist and no extension provided
747-
if (!path.extname(importedPath)) {
748-
if (fs.existsSync(importedPath + '.ts')) {
749-
importedPath = importedPath + '.ts'
750-
}
751-
}
752-
753-
// If it's a TypeScript file, transpile it
754-
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
755-
const transpiledImportContent = transpileTS(importedPath)
756-
const tempImportFile = importedPath.replace(/\.ts$/, '.temp.mjs')
757-
fs.writeFileSync(tempImportFile, transpiledImportContent)
758-
transpiledFiles.set(importedPath, tempImportFile)
759-
debug(`Transpiled dependency: ${importedPath} -> ${tempImportFile}`)
760-
}
761-
}
762-
763-
// Replace imports in the main file to point to temp .mjs files
764-
jsContent = jsContent.replace(
765-
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
766-
(match, importPath) => {
767-
let resolvedPath = path.resolve(baseDir, importPath)
768-
769-
// Handle .js extension that might be .ts
770-
if (resolvedPath.endsWith('.js')) {
771-
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
772-
if (transpiledFiles.has(tsVersion)) {
773-
const tempFile = transpiledFiles.get(tsVersion)
774-
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
775-
return `from './${relPath}'`
776-
}
777-
}
778-
779-
// Try with .ts extension
780-
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
781-
782-
// If we transpiled this file, use the temp file
783-
if (transpiledFiles.has(tsPath)) {
784-
const tempFile = transpiledFiles.get(tsPath)
785-
// Get relative path from main temp file to this temp file
786-
const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
787-
return `from './${relPath}'`
788-
}
789-
790-
// Otherwise, keep the import as-is
791-
return match
792-
}
793-
)
794-
795-
// Create a temporary JS file with .mjs extension for the main file
796-
tempJsFile = importPath.replace(/\.ts$/, '.temp.mjs')
797-
fs.writeFileSync(tempJsFile, jsContent)
798-
799-
// Store all temp files for cleanup
800-
const allTempFiles = [tempJsFile, ...Array.from(transpiledFiles.values())]
691+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
801692

802693
// Attach cleanup handler
803-
importPath = tempJsFile
694+
importPath = tempFile
804695
// Store temp files list in a way that cleanup can access them
805696
tempJsFile = allTempFiles
806697

@@ -820,30 +711,14 @@ const __dirname = __dirname_fn(__filename);
820711
// Clean up temp files if created before rethrowing
821712
if (tempJsFile) {
822713
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
823-
for (const file of filesToClean) {
824-
try {
825-
if (fs.existsSync(file)) {
826-
fs.unlinkSync(file)
827-
}
828-
} catch (cleanupError) {
829-
// Ignore cleanup errors
830-
}
831-
}
714+
cleanupTempFiles(filesToClean)
832715
}
833716
throw importError
834717
} finally {
835718
// Clean up temp files if created
836719
if (tempJsFile) {
837720
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
838-
for (const file of filesToClean) {
839-
try {
840-
if (fs.existsSync(file)) {
841-
fs.unlinkSync(file)
842-
}
843-
} catch (cleanupError) {
844-
// Ignore cleanup errors
845-
}
846-
}
721+
cleanupTempFiles(filesToClean)
847722
}
848723
}
849724

0 commit comments

Comments
 (0)