Skip to content

Commit 59342cd

Browse files
committed
resolve TypeScript path aliases from tsconfig.json
1 parent 87fec45 commit 59342cd

File tree

2 files changed

+143
-32
lines changed

2 files changed

+143
-32
lines changed

lib/utils/typescript.js

Lines changed: 142 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import { loadConfig, createMatchPath } from 'tsconfig-paths'
34

45
/**
56
* Transpile TypeScript files to ES modules with CommonJS shim support
@@ -107,6 +108,53 @@ const __dirname = __dirname_fn(__filename);
107108
// Create a map to track transpiled files
108109
const transpiledFiles = new Map()
109110
const baseDir = path.dirname(mainFilePath)
111+
const pathsMatcher = loadTsConfigPaths(baseDir)
112+
113+
/**
114+
* Load tsconfig.json and create path matcher for resolving aliases
115+
*/
116+
function loadTsConfigPaths(startDir) {
117+
try {
118+
let currentDir = startDir
119+
let configPath = null
120+
while (currentDir && currentDir !== path.parse(currentDir).root) {
121+
const testPath = path.join(currentDir, 'tsconfig.json')
122+
if (fs.existsSync(testPath)) {
123+
configPath = testPath
124+
break
125+
}
126+
currentDir = path.dirname(currentDir)
127+
}
128+
129+
if (!configPath) return null
130+
131+
const configResult = loadConfig(configPath)
132+
133+
if (configResult.resultType !== 'success') return null
134+
135+
const { paths, absoluteBaseUrl } = configResult
136+
if (!paths) return null
137+
138+
const matchPath = createMatchPath(
139+
absoluteBaseUrl,
140+
paths,
141+
undefined,
142+
false
143+
)
144+
145+
return {
146+
matchPath: (importPath) => {
147+
for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.json', '']) {
148+
const resolved = matchPath(importPath + ext)
149+
if (resolved) return resolved
150+
}
151+
return null
152+
}
153+
}
154+
} catch (error) {
155+
return null
156+
}
157+
}
110158

111159
// Recursive function to transpile a file and all its TypeScript dependencies
112160
const transpileFileAndDeps = (filePath) => {
@@ -118,8 +166,7 @@ const __dirname = __dirname_fn(__filename);
118166
// Transpile this file
119167
let jsContent = transpileTS(filePath)
120168

121-
// Find all relative TypeScript imports in this file
122-
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
169+
const importRegex = /from\s+['"]([^'"]+?)['"]/g
123170
let match
124171
const imports = []
125172

@@ -131,50 +178,113 @@ const __dirname = __dirname_fn(__filename);
131178
const fileBaseDir = path.dirname(filePath)
132179

133180
// Recursively transpile each imported TypeScript file
134-
for (const relativeImport of imports) {
135-
let importedPath = path.resolve(fileBaseDir, relativeImport)
136-
137-
// Handle .js extensions that might actually be .ts files
138-
if (importedPath.endsWith('.js')) {
139-
const tsVersion = importedPath.replace(/\.js$/, '.ts')
140-
if (fs.existsSync(tsVersion)) {
141-
importedPath = tsVersion
142-
}
181+
for (const importPath of imports) {
182+
if (importPath.startsWith('node:')) {
183+
continue
143184
}
144185

145-
// Check for standard module extensions to determine if we should try adding .ts
146-
const ext = path.extname(importedPath)
147-
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
148-
const hasStandardExtension = standardExtensions.includes(ext.toLowerCase())
149-
150-
// If it doesn't end with .ts and doesn't have a standard extension, try adding .ts
151-
if (!importedPath.endsWith('.ts') && !hasStandardExtension) {
152-
const tsPath = importedPath + '.ts'
153-
if (fs.existsSync(tsPath)) {
154-
importedPath = tsPath
155-
} else {
156-
// Try .js extension as well
157-
const jsPath = importedPath + '.js'
158-
if (fs.existsSync(jsPath)) {
159-
// Skip .js files, they don't need transpilation
160-
continue
186+
if (importPath.startsWith('.')) {
187+
let importedPath = path.resolve(fileBaseDir, importPath)
188+
189+
// Handle .js extensions that might actually be .ts files
190+
if (importedPath.endsWith('.js')) {
191+
const tsVersion = importedPath.replace(/\.js$/, '.ts')
192+
if (fs.existsSync(tsVersion)) {
193+
importedPath = tsVersion
194+
}
195+
}
196+
197+
// Check for standard module extensions to determine if we should try adding .ts
198+
const ext = path.extname(importedPath)
199+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
200+
const hasStandardExtension = standardExtensions.includes(ext.toLowerCase())
201+
202+
// If it doesn't end with .ts and doesn't have a standard extension, try adding .ts
203+
if (!importedPath.endsWith('.ts') && !hasStandardExtension) {
204+
const tsPath = importedPath + '.ts'
205+
if (fs.existsSync(tsPath)) {
206+
importedPath = tsPath
207+
} else {
208+
// Try .js extension as well
209+
const jsPath = importedPath + '.js'
210+
if (fs.existsSync(jsPath)) {
211+
// Skip .js files, they don't need transpilation
212+
continue
213+
}
161214
}
162215
}
216+
217+
// If it's a TypeScript file, recursively transpile it and its dependencies
218+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
219+
transpileFileAndDeps(importedPath)
220+
}
221+
222+
continue
163223
}
164224

165-
// If it's a TypeScript file, recursively transpile it and its dependencies
166-
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
167-
transpileFileAndDeps(importedPath)
225+
// Try to resolve as a path alias (non-relative import)
226+
const resolvedAlias = pathsMatcher ? pathsMatcher.matchPath(importPath) : null
227+
if (resolvedAlias) {
228+
let importedPath = resolvedAlias
229+
230+
if (!importedPath.endsWith('.ts') && !path.extname(importedPath)) {
231+
const tsPath = importedPath + '.ts'
232+
if (fs.existsSync(tsPath)) {
233+
importedPath = tsPath
234+
}
235+
}
236+
237+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
238+
transpileFileAndDeps(importedPath)
239+
}
240+
241+
continue
168242
}
169243
}
170244

171245
// After all dependencies are transpiled, rewrite imports in this file
172246
jsContent = jsContent.replace(
173-
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
247+
/from\s+['"]([^'"]+?)['"]/g,
174248
(match, importPath) => {
175-
let resolvedPath = path.resolve(fileBaseDir, importPath)
176249
const originalExt = path.extname(importPath)
177250

251+
// Check if this is a path alias (non-relative import)
252+
if (!importPath.startsWith('.')) {
253+
const resolvedAlias = pathsMatcher ? pathsMatcher.matchPath(importPath) : null
254+
if (resolvedAlias) {
255+
let tsPath = resolvedAlias.endsWith('.ts') ? resolvedAlias : resolvedAlias + '.ts'
256+
257+
if (transpiledFiles.has(tsPath)) {
258+
const tempFile = transpiledFiles.get(tsPath)
259+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
260+
if (!relPath.startsWith('.')) {
261+
return `from './${relPath}'`
262+
}
263+
return `from '${relPath}'`
264+
}
265+
266+
if (fs.existsSync(tsPath)) {
267+
const jsPath = tsPath.replace(/\.ts$/, '.js')
268+
const relPath = path.relative(fileBaseDir, jsPath).replace(/\\/g, '/')
269+
if (!relPath.startsWith('.')) {
270+
return `from './${relPath}'`
271+
}
272+
return `from '${relPath}'`
273+
}
274+
}
275+
276+
const standardExtensions = ['.js', '.mjs', '.cjs', '.json', '.node']
277+
const hasStandardExtension = standardExtensions.includes(originalExt.toLowerCase())
278+
279+
if (!hasStandardExtension && !importPath.startsWith('node:')) {
280+
return match.replace(importPath, importPath + '.js')
281+
}
282+
283+
return match
284+
}
285+
286+
let resolvedPath = path.resolve(fileBaseDir, importPath)
287+
178288
// Handle .js extension that might be .ts
179289
if (resolvedPath.endsWith('.js')) {
180290
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
"sinon-chai": "^4.0.1",
182182
"ts-morph": "27.0.2",
183183
"ts-node": "10.9.2",
184+
"tsconfig-paths": "^4.2.0",
184185
"tsd": "^0.33.0",
185186
"tsd-jsdoc": "2.5.0",
186187
"tsx": "^4.19.2",

0 commit comments

Comments
 (0)