(projectRoot: string)
| 251 | * resolver does it via {@link aliasCache}). |
| 252 | */ |
| 253 | export function loadProjectAliases(projectRoot: string): AliasMap | null { |
| 254 | // `tsconfig.base.json` comes last on purpose: when a root `tsconfig.json` |
| 255 | // exists it stays authoritative and reaches the base through `extends`. |
| 256 | // The fallback is for the Nx layouts where that never happens — a |
| 257 | // solution-style root config (`references`, no `extends`, no `paths`), or |
| 258 | // no root `tsconfig.json` at all. |
| 259 | const candidates = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json']; |
| 260 | let effective: EffectiveOptions | null = null; |
| 261 | let usedFile: string | null = null; |
| 262 | for (const name of candidates) { |
| 263 | const p = path.join(projectRoot, name); |
| 264 | if (!fs.existsSync(p)) continue; |
| 265 | const opts = loadEffectiveOptions(p, new Set(), 0); |
| 266 | if (!opts) continue; |
| 267 | // Remember the first readable config so a `paths`-less project still |
| 268 | // logs the file it was judged on, but keep looking: a config that |
| 269 | // contributes no aliases must not shadow one that does. |
| 270 | if (!effective) { |
| 271 | effective = opts; |
| 272 | usedFile = name; |
| 273 | } |
| 274 | if (opts.paths) { |
| 275 | effective = opts; |
| 276 | usedFile = name; |
| 277 | break; |
| 278 | } |
| 279 | } |
| 280 | if (!effective) return null; |
| 281 | |
| 282 | // With no explicit baseUrl, `paths` targets are relative to the config that |
| 283 | // declared them — which is the project root only when that config is the |
| 284 | // root one (the pre-`extends` assumption). |
| 285 | const baseUrl = effective.baseUrl ?? effective.pathsDir ?? projectRoot; |
| 286 | |
| 287 | const paths = effective.paths; |
| 288 | if (!paths || typeof paths !== 'object') { |
| 289 | // baseUrl alone isn't an "alias" per se; with no paths we'd just |
| 290 | // be redirecting the whole tree. Skip — the existing resolver |
| 291 | // already handles relative imports. |
| 292 | return null; |
| 293 | } |
| 294 | |
| 295 | const patterns: AliasPattern[] = []; |
| 296 | for (const [pattern, targets] of Object.entries(paths)) { |
| 297 | if (!Array.isArray(targets) || targets.length === 0) continue; |
| 298 | const filtered = targets.filter((t): t is string => typeof t === 'string'); |
| 299 | if (filtered.length === 0) continue; |
| 300 | const { prefix, suffix, hasWildcard } = splitWildcard(pattern); |
| 301 | patterns.push({ prefix, suffix, hasWildcard, replacements: filtered }); |
| 302 | } |
| 303 | |
| 304 | if (patterns.length === 0) return null; |
| 305 | |
| 306 | // Specificity sort: longer prefix first; literal patterns before |
| 307 | // wildcard patterns of the same prefix length. TypeScript itself |
| 308 | // uses a similar "most specific match wins" rule. |
| 309 | patterns.sort((a, b) => { |
| 310 | if (a.prefix.length !== b.prefix.length) return b.prefix.length - a.prefix.length; |
no test coverage detected