Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 128 additions & 11 deletions lib/utils/typescript.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -118,9 +189,9 @@ const __dirname = __dirname_fn(__filename);
// Transpile this file
let jsContent = transpileTS(filePath)

// Find all relative TypeScript imports in this file (both ESM imports and require() calls)
const importRegex = /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g
const requireRegex = /require\s*\(\s*['"](\.[^'"]+?)(?:\.ts)?['"]\s*\)/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 = []

Expand All @@ -136,8 +207,18 @@ const __dirname = __dirname_fn(__filename);
const fileBaseDir = path.dirname(filePath)

// Recursively transpile each imported TypeScript file
for (const { path: 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')) {
Expand Down Expand Up @@ -181,11 +262,34 @@ 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')
Expand Down Expand Up @@ -238,9 +342,19 @@ const __dirname = __dirname_fn(__filename);

// Also rewrite require() calls to point to transpiled TypeScript files
jsContent = jsContent.replace(
/require\s*\(\s*['"](\.[^'"]+?)(?:\.ts)?['"]\s*\)/g,
/require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g,
(match, requirePath) => {
let resolvedPath = path.resolve(fileBaseDir, 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')) {
Expand Down Expand Up @@ -282,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 }
}

/**
Expand Down