Skip to content

Commit 7b1c520

Browse files
authored
resolve TypeScript path aliases from tsconfig.json (#5375)
1 parent fe9ff24 commit 7b1c520

File tree

1 file changed

+128
-11
lines changed

1 file changed

+128
-11
lines changed

lib/utils/typescript.js

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,60 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import { pathToFileURL } from 'url'
4+
5+
/**
6+
* Load tsconfig.json if it exists
7+
* @param {string} tsConfigPath - Path to tsconfig.json
8+
* @returns {object|null} - Parsed tsconfig or null
9+
*/
10+
function loadTsConfig(tsConfigPath) {
11+
if (!fs.existsSync(tsConfigPath)) {
12+
return null
13+
}
14+
15+
try {
16+
const tsConfigContent = fs.readFileSync(tsConfigPath, 'utf8')
17+
return JSON.parse(tsConfigContent)
18+
} catch (err) {
19+
return null
20+
}
21+
}
22+
23+
/**
24+
* Resolve TypeScript path alias to actual file path
25+
* @param {string} importPath - Import path with alias (e.g., '#config/urls')
26+
* @param {object} tsConfig - Parsed tsconfig.json
27+
* @param {string} configDir - Directory containing tsconfig.json
28+
* @returns {string|null} - Resolved file path or null if not an alias
29+
*/
30+
function resolveTsPathAlias(importPath, tsConfig, configDir) {
31+
if (!tsConfig || !tsConfig.compilerOptions || !tsConfig.compilerOptions.paths) {
32+
return null
33+
}
34+
35+
const paths = tsConfig.compilerOptions.paths
36+
37+
for (const [pattern, targets] of Object.entries(paths)) {
38+
if (!targets || targets.length === 0) {
39+
continue
40+
}
41+
42+
const patternRegex = new RegExp(
43+
'^' + pattern.replace(/\*/g, '(.*)') + '$'
44+
)
45+
const match = importPath.match(patternRegex)
46+
47+
if (match) {
48+
const wildcard = match[1] || ''
49+
const target = targets[0]
50+
const resolvedTarget = target.replace(/\*/g, wildcard)
51+
52+
return path.resolve(configDir, resolvedTarget)
53+
}
54+
}
55+
56+
return null
57+
}
358

459
/**
560
* Transpile TypeScript files to ES modules with CommonJS shim support
@@ -108,6 +163,22 @@ const __dirname = __dirname_fn(__filename);
108163
const transpiledFiles = new Map()
109164
const baseDir = path.dirname(mainFilePath)
110165

166+
// Try to find tsconfig.json by walking up the directory tree
167+
let tsConfigPath = path.join(baseDir, 'tsconfig.json')
168+
let configDir = baseDir
169+
let searchDir = baseDir
170+
171+
while (!fs.existsSync(tsConfigPath) && searchDir !== path.dirname(searchDir)) {
172+
searchDir = path.dirname(searchDir)
173+
tsConfigPath = path.join(searchDir, 'tsconfig.json')
174+
if (fs.existsSync(tsConfigPath)) {
175+
configDir = searchDir
176+
break
177+
}
178+
}
179+
180+
const tsConfig = loadTsConfig(tsConfigPath)
181+
111182
// Recursive function to transpile a file and all its TypeScript dependencies
112183
const transpileFileAndDeps = (filePath) => {
113184
// Already transpiled, skip
@@ -118,9 +189,9 @@ const __dirname = __dirname_fn(__filename);
118189
// Transpile this file
119190
let jsContent = transpileTS(filePath)
120191

121-
// Find all relative TypeScript imports in this file (both ESM imports and require() calls)
122-
const importRegex = /from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g
123-
const requireRegex = /require\s*\(\s*['"](\.[^'"]+?)(?:\.ts)?['"]\s*\)/g
192+
// Find all TypeScript imports in this file (both ESM imports and require() calls)
193+
const importRegex = /from\s+['"]([^'"]+?)['"]/g
194+
const requireRegex = /require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g
124195
let match
125196
const imports = []
126197

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

138209
// Recursively transpile each imported TypeScript file
139-
for (const { path: relativeImport } of imports) {
140-
let importedPath = path.resolve(fileBaseDir, relativeImport)
210+
for (const { path: importPath } of imports) {
211+
let importedPath = importPath
212+
213+
// Check if this is a path alias
214+
const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
215+
if (resolvedAlias) {
216+
importedPath = resolvedAlias
217+
} else if (importPath.startsWith('.')) {
218+
importedPath = path.resolve(fileBaseDir, importPath)
219+
} else {
220+
continue
221+
}
141222

142223
// Handle .js extensions that might actually be .ts files
143224
if (importedPath.endsWith('.js')) {
@@ -181,11 +262,34 @@ const __dirname = __dirname_fn(__filename);
181262

182263
// After all dependencies are transpiled, rewrite imports in this file
183264
jsContent = jsContent.replace(
184-
/from\s+['"](\.[^'"]+?)(?:\.ts)?['"]/g,
265+
/from\s+['"]([^'"]+?)['"]/g,
185266
(match, importPath) => {
186-
let resolvedPath = path.resolve(fileBaseDir, importPath)
267+
let resolvedPath = importPath
187268
const originalExt = path.extname(importPath)
188269

270+
// Check if this is a path alias
271+
const resolvedAlias = resolveTsPathAlias(importPath, tsConfig, configDir)
272+
if (resolvedAlias) {
273+
resolvedPath = resolvedAlias
274+
} else if (importPath.startsWith('.')) {
275+
resolvedPath = path.resolve(fileBaseDir, importPath)
276+
} else {
277+
return match
278+
}
279+
280+
// If resolved path is a directory, try index.ts
281+
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
282+
const indexPath = path.join(resolvedPath, 'index.ts')
283+
if (fs.existsSync(indexPath) && transpiledFiles.has(indexPath)) {
284+
const tempFile = transpiledFiles.get(indexPath)
285+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
286+
if (!relPath.startsWith('.')) {
287+
return `from './${relPath}'`
288+
}
289+
return `from '${relPath}'`
290+
}
291+
}
292+
189293
// Handle .js extension that might be .ts
190294
if (resolvedPath.endsWith('.js')) {
191295
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
@@ -238,9 +342,19 @@ const __dirname = __dirname_fn(__filename);
238342

239343
// Also rewrite require() calls to point to transpiled TypeScript files
240344
jsContent = jsContent.replace(
241-
/require\s*\(\s*['"](\.[^'"]+?)(?:\.ts)?['"]\s*\)/g,
345+
/require\s*\(\s*['"]([^'"]+?)['"]\s*\)/g,
242346
(match, requirePath) => {
243-
let resolvedPath = path.resolve(fileBaseDir, requirePath)
347+
let resolvedPath = requirePath
348+
349+
// Check if this is a path alias
350+
const resolvedAlias = resolveTsPathAlias(requirePath, tsConfig, configDir)
351+
if (resolvedAlias) {
352+
resolvedPath = resolvedAlias
353+
} else if (requirePath.startsWith('.')) {
354+
resolvedPath = path.resolve(fileBaseDir, requirePath)
355+
} else {
356+
return match
357+
}
244358

245359
// Handle .js extension that might be .ts
246360
if (resolvedPath.endsWith('.js')) {
@@ -282,10 +396,13 @@ const __dirname = __dirname_fn(__filename);
282396
// Get the main transpiled file
283397
const tempJsFile = transpiledFiles.get(mainFilePath)
284398

285-
// Store all temp files for cleanup
399+
// Convert to file:// URL for dynamic import() (required on Windows)
400+
const tempFileUrl = pathToFileURL(tempJsFile).href
401+
402+
// Store all temp files for cleanup (keep as paths, not URLs)
286403
const allTempFiles = Array.from(transpiledFiles.values())
287404

288-
return { tempFile: tempJsFile, allTempFiles, fileMapping: transpiledFiles }
405+
return { tempFile: tempFileUrl, allTempFiles, fileMapping: transpiledFiles }
289406
}
290407

291408
/**

0 commit comments

Comments
 (0)