mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 16:57:09 +08:00
fix(js,ts): an import written from a jsconfig or tsconfig baseUrl links the file it names (#2649)
CRA and Vite admin templates (codedthemes' berry and mantis, uilibrary's
matx) set `"baseUrl": "src"` or `"paths": { "*": ["./src/*"] }` in a
jsconfig and write every cross-file import as `layout/MainLayout`. The
import resolver took each one for an npm package: loadProjectAliases
returned null for a config with a baseUrl and no paths, and
isExternalImport classifies a bare specifier from the root config alone,
before a nested app's config is read. The imports stayed unresolved,
their names linked by bare-name matching (vite components rendering the
remix app's namesakes), and React Router routes loaded through
`lazy(() => import('views/x'))` bound to the local Loadable value.
- path-aliases: a config with only `baseUrl` loads, marked
`declaresBaseUrl`; aliasAndBaseUrlCandidates lists the most specific
`paths` pattern's targets, then the specifier read from a declared
baseUrl (paths alone never reads bare specifiers, as in TS 4.1+).
- import-resolver: a specifier isExternalImport takes for a package is
read through the importing file's own config (nearest, else the root's)
and binds only to a file the index holds (`fileExists` also answers for
folders and node_modules). A package the file's package.json or an
enclosing one declares, or a Node built-in, never binds to a project
module that way, only to a `.d.ts` the project keeps for it (#2437's
`"*": ["./typings/*"]`). A root catch-all `"*"` no longer marks every
specifier local in isExternalImport's default mode either.
- The module ref of a one-segment import (`'config'`) links the file its
specifier resolves to, as a path-shaped one already does (#2528).
- isBoundToOutOfRepoImport keeps the `extends` edge of a class whose base
is imported through baseUrl.
- importBindingTail parks a binding under its module when the app's
config maps the specifier to an indexed file, so a sync that edits the
module relinks `import Layout from 'layout/MainLayout'` and
`{ Foo as Bar }` (#2450's rule for relative imports). An undeclared name
that maps to nothing (`express` under `"baseUrl": "."`) and a declared
package keep their own tail, which root-"*" repos had lost.
- The TypeScript and TSX extension lists probe `/index.jsx` last, as tsc
does: a route resolves its lazy module as TypeScript.
Validated against main 6071c9e4 on 21 repos (wasm path): every new
import-resolved edge lands in the file its specifier names; failed
imports naming a project file go 194/190/96 -> 0 on berry/mantis/matx;
their routes bind their lazily loaded pages and layouts (matx 7 -> 21
routes); 37 + 45 cross-app JSX renders move to the app's own components.
proshop, next-saas-starter, create-t3-turbo, excalidraw and six more are
byte-identical; root-"*" repos change only parked tails plus module
links. Same deltas with stubbed node_modules. Sync after late adds and
edits equals a fresh index, except one pre-existing barrel-edit gap that
relative imports share.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Language, Node } from '../types';
|
||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, ReExport } from './types';
|
||||
import { applyAliases } from './path-aliases';
|
||||
import { applyAliases, aliasAndBaseUrlCandidates } from './path-aliases';
|
||||
import { extractLocalExportAliases } from './alias-binding';
|
||||
import { resolveWorkspaceImport } from './workspace-packages';
|
||||
import { stripCommentsForRegex } from './strip-comments';
|
||||
@@ -32,14 +32,18 @@ import { JS_ARRAY_METHODS } from './js-builtins';
|
||||
* Extension resolution order by language
|
||||
*/
|
||||
const EXTENSION_RESOLUTION: Record<string, string[]> = {
|
||||
typescript: ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'],
|
||||
// `/index.jsx` last, as tsc probes a directory: a route table resolves the
|
||||
// module its `lazy(() => import('views/x'))` loads as TypeScript, whatever
|
||||
// the file is written in, and admin templates keep their pages in
|
||||
// `views/x/index.jsx`.
|
||||
typescript: ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js', '/index.jsx'],
|
||||
// ArkTS imports both `.ets` components and plain `.ts` logic modules —
|
||||
// HarmonyOS projects are always a mix. `/Index.ets` (capital I) is ohpm's
|
||||
// module-entry convention, hit when a bare workspace import ("data") is
|
||||
// rewritten to the member's directory; lowercase variants for safety.
|
||||
arkts: ['.ets', '.ts', '.d.ts', '.js', '/Index.ets', '/index.ets', '/index.ts', '/index.js'],
|
||||
javascript: ['.js', '.jsx', '.mjs', '.cjs', '.xsjs', '.xsjslib', '/index.js', '/index.jsx'],
|
||||
tsx: ['.tsx', '.ts', '.d.ts', '.js', '.jsx', '/index.tsx', '/index.ts', '/index.js'],
|
||||
tsx: ['.tsx', '.ts', '.d.ts', '.js', '.jsx', '/index.tsx', '/index.ts', '/index.js', '/index.jsx'],
|
||||
jsx: ['.jsx', '.js', '/index.jsx', '/index.js'],
|
||||
// SFC consumers import plain TS/JS, sibling components, and barrels
|
||||
// (`./lib` → `./lib/index.ts`). Without a list, relative imports from a
|
||||
@@ -366,9 +370,10 @@ function resolveImportPathUncached(
|
||||
// Skip external/npm packages — but pass the context so the
|
||||
// bare-specifier heuristic can consult the project's tsconfig
|
||||
// alias map first (custom prefixes like `@components/*` would
|
||||
// otherwise be misclassified as npm).
|
||||
// otherwise be misclassified as npm). What still looks like a package
|
||||
// may name a file through the importing app's own config.
|
||||
if (isExternalImport(importPath, language, context)) {
|
||||
return null;
|
||||
return resolveFromOwnConfig(importPath, fromFile, language, context);
|
||||
}
|
||||
|
||||
const projectRoot = context.getProjectRoot();
|
||||
@@ -561,11 +566,13 @@ export function isExternalImport(
|
||||
if (['fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream', 'child_process', 'buffer'].includes(importPath)) {
|
||||
return true;
|
||||
}
|
||||
// Project-defined alias prefix? Treat as local.
|
||||
// Project-defined alias prefix? Treat as local. A catch-all `"*"` has no
|
||||
// prefix and matches `react` too: what it names is left to
|
||||
// resolveFromOwnConfig, which keeps the app's packages out.
|
||||
const aliases = options.aliasPrefixes === false ? null : context?.getProjectAliases?.();
|
||||
if (aliases) {
|
||||
for (const pat of aliases.patterns) {
|
||||
if (importPath.startsWith(pat.prefix)) return false;
|
||||
if (pat.prefix !== '' && importPath.startsWith(pat.prefix)) return false;
|
||||
}
|
||||
}
|
||||
// Scoped packages or bare specifiers that don't start with aliases
|
||||
@@ -727,15 +734,7 @@ function resolveAliasedImport(
|
||||
context: ResolutionContext,
|
||||
fromFile?: string
|
||||
): string | null {
|
||||
const extensions = EXTENSION_RESOLUTION[language] || [];
|
||||
const tryWithExt = (basePath: string): string | null => {
|
||||
for (const ext of extensions) {
|
||||
const candidate = basePath + ext;
|
||||
if (context.fileExists(candidate)) return candidate;
|
||||
}
|
||||
if (context.fileExists(basePath)) return basePath;
|
||||
return findSourceForEmittedSpecifier(basePath, language, context);
|
||||
};
|
||||
const tryWithExt = (basePath: string): string | null => tryWithExtensions(basePath, language, context);
|
||||
|
||||
// 1. tsconfig/jsconfig paths: the config nearest the importing file (an
|
||||
// app of a monorepo keeps its own `@/*`), then the project root's.
|
||||
@@ -783,6 +782,84 @@ function resolveAliasedImport(
|
||||
return tryWithExt(importPath);
|
||||
}
|
||||
|
||||
/** The file `basePath` names: with the language's extensions or index files, as is, or as an emitted `.js`. */
|
||||
function tryWithExtensions(basePath: string, language: Language, context: ResolutionContext): string | null {
|
||||
for (const ext of EXTENSION_RESOLUTION[language] || []) {
|
||||
const candidate = basePath + ext;
|
||||
if (context.fileExists(candidate)) return candidate;
|
||||
}
|
||||
if (context.fileExists(basePath)) return basePath;
|
||||
return findSourceForEmittedSpecifier(basePath, language, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare specifier `isExternalImport` takes for a package, resolved through
|
||||
* the importing app's own tsconfig / jsconfig — the one nearest the file,
|
||||
* else the root's — as tsc and a paths-aware bundler resolve it: the most
|
||||
* specific `paths` pattern it matches (a catch-all `"*": ["./src/*"]`, an
|
||||
* alias only a nested app declares), then `baseUrl` (`"baseUrl": "src"`
|
||||
* makes `layout/MainLayout` read `src/layout/MainLayout`). CRA and Vite
|
||||
* admin templates import nearly everything this way.
|
||||
*
|
||||
* Only an indexed file binds: `fileExists` also answers for a folder and
|
||||
* for `node_modules`. A package — one the file's package.json or an
|
||||
* enclosing one declares, or a Node built-in — never binds to a project
|
||||
* module (`lodash` stays lodash even beside a `src/lodash/`), only to a
|
||||
* declaration file the project keeps for it, which is the package as tsc
|
||||
* reads it: `legacy-widgets` under `"*": ["./typings/*"]` is
|
||||
* `typings/legacy-widgets.d.ts`.
|
||||
*/
|
||||
function resolveFromOwnConfig(
|
||||
importPath: string,
|
||||
fromFile: string,
|
||||
language: Language,
|
||||
context: ResolutionContext
|
||||
): string | null {
|
||||
const candidates = ownConfigCandidates(importPath, fromFile, language, context);
|
||||
if (!candidates) return null;
|
||||
const indexed = (file: string): boolean => context.isIndexedFile?.(file) !== false;
|
||||
if (context.isDeclaredPackage?.(importPath, fromFile) !== false) {
|
||||
if (!EXTENSION_RESOLUTION[language]?.includes('.d.ts')) return null;
|
||||
for (const candidate of candidates) {
|
||||
const declarations = `${candidate}.d.ts`;
|
||||
if (context.fileExists(declarations) && indexed(declarations)) return declarations;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const hit = tryWithExtensions(candidate, language, context);
|
||||
if (hit && indexed(hit)) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Where the importing app's own config reads a bare specifier from (see resolveFromOwnConfig), or null when it reads none. */
|
||||
function ownConfigCandidates(
|
||||
importPath: string,
|
||||
fromFile: string,
|
||||
language: Language,
|
||||
context: ResolutionContext
|
||||
): string[] | null {
|
||||
if (!ESM_IMPORT_LANGUAGES.has(language) || !importPath || importPath.startsWith('/') || /^[A-Za-z][\w+.-]*:/.test(importPath)) return null;
|
||||
const config = context.getNearestAliases?.(fromFile) ?? context.getProjectAliases?.();
|
||||
if (!config) return null;
|
||||
const candidates = aliasAndBaseUrlCandidates(importPath, config, context.getProjectRoot());
|
||||
return candidates.length > 0 ? candidates : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a bare specifier names one of the project's files through the
|
||||
* importing app's own config: its `paths` or `baseUrl` map it to a file the
|
||||
* index holds, and no package the app declares goes by it
|
||||
* (`layout/MainLayout` under `"baseUrl": "src"`). Not a name that maps to
|
||||
* nothing yet: under `"baseUrl": "."` an undeclared `express` is far likelier
|
||||
* a package the manifest leaves out than a module still to come.
|
||||
*/
|
||||
function namesOwnConfigModule(importPath: string, fromFile: string, language: Language, context: ResolutionContext): boolean {
|
||||
return context.isDeclaredPackage?.(importPath, fromFile) === false &&
|
||||
resolveImportPath(importPath, fromFile, language, context) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* C/C++ include directory cache (keyed by project root).
|
||||
* Loaded once per resolver instance, shared across calls.
|
||||
@@ -1743,6 +1820,20 @@ export function isJsPathImportRef(ref: UnresolvedRef): boolean {
|
||||
return ref.referenceKind === 'imports' && ESM_IMPORT_LANGUAGES.has(ref.language) && isJsPathSpecifier(ref.referenceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A JS/TS `imports` reference to the module of an import named by one bare
|
||||
* segment — `'config'` in `import { DEFAULT_THEME_MODE } from 'config'` under
|
||||
* `"baseUrl": "src"`. Shaped like a package, it names a file only when
|
||||
* resolveImportPath finds one (the app's `baseUrl` or catch-all `paths`, a
|
||||
* workspace member); a real package stays unresolved. A name some import
|
||||
* binds locally is that binding, not the module, and resolves as one.
|
||||
*/
|
||||
function isJsBareModuleRef(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (ref.referenceKind !== 'imports' || !ESM_IMPORT_LANGUAGES.has(ref.language) || isJsPathSpecifier(ref.referenceName)) return false;
|
||||
const imports = context.getImportMappings(ref.filePath, ref.language);
|
||||
return imports.some((m) => m.source === ref.referenceName) && !imports.some((m) => m.localName === ref.referenceName);
|
||||
}
|
||||
|
||||
/** PHP reference kinds whose name is a class name, written as in the source. */
|
||||
const PHP_CLASS_NAME_REFS: ReadonlySet<string> = new Set(['instantiates', 'extends', 'implements', 'references']);
|
||||
|
||||
@@ -1819,7 +1910,7 @@ export function resolveViaImport(
|
||||
// found the way the runtime finds it: extensions, `index` files, path
|
||||
// aliases. Matching the basename instead missed every extensionless one
|
||||
// (the common spelling) and could land on a same-named file elsewhere.
|
||||
if (isJsPathImportRef(ref)) {
|
||||
if (isJsPathImportRef(ref) || isJsBareModuleRef(ref, context)) {
|
||||
const file = resolveImportPath(ref.referenceName, ref.filePath, ref.language, context);
|
||||
const fileNode = file && file !== ref.filePath ? context.getNodesInFile(file).find((n) => n.kind === 'file') : undefined;
|
||||
if (fileNode) return { original: ref, targetNodeId: fileNode.id, confidence: 0.9, resolvedBy: 'import' };
|
||||
@@ -3083,7 +3174,9 @@ function collectRustUseBindings(content: string): Map<string, string> {
|
||||
* has no `src/ports/` directory to walk yet is entirely in-repo — that
|
||||
* generalization measured 13 real trait implementations deleted.
|
||||
* - **ES modules** — `isExternalImport`, which already accounts for tsconfig
|
||||
* path aliases and monorepo workspace packages.
|
||||
* path aliases and monorepo workspace packages, unless the specifier still
|
||||
* names a project file through the importing app's `baseUrl` or catch-all
|
||||
* `paths` (`import Base from 'components/Base'`).
|
||||
*
|
||||
* Everything else returns false and resolves exactly as before. JVM and Python
|
||||
* imports notably do NOT go through `resolveImportPath` (they have dedicated
|
||||
@@ -3111,7 +3204,8 @@ export function isBoundToOutOfRepoImport(
|
||||
if (!ESM_IMPORT_LANGUAGES.has(ref.language)) return false;
|
||||
for (const imp of context.getImportMappings(ref.filePath, ref.language)) {
|
||||
if (imp.localName !== name) continue;
|
||||
return isExternalImport(imp.source, ref.language, context);
|
||||
return isExternalImport(imp.source, ref.language, context) &&
|
||||
resolveImportPath(imp.source, ref.filePath, ref.language, context) === null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -3151,10 +3245,13 @@ export function isBoundToOutOfRepoImport(
|
||||
* key a sync looks up for every file it adds or changes, so the file that
|
||||
* appears or gains the export finds it.
|
||||
*
|
||||
* Only for a module of the project: a package's binding keeps its tail.
|
||||
* Undefined for every other reference, and for the whole name of an
|
||||
* unaliased named import, which is the name its module declares. A Python
|
||||
* reference is {@link pythonImportBindingTail}'s, by its whole name only.
|
||||
* Only for a module of the project: a package's binding keeps its tail. A
|
||||
* bare specifier is the project's when the importing app's own config
|
||||
* (`baseUrl`, a catch-all `"*"`) maps it to a file the index holds and no
|
||||
* package the app declares goes by it. Undefined for every other reference,
|
||||
* and for the whole name of an unaliased named import, which is the name its
|
||||
* module declares. A Python reference is {@link pythonImportBindingTail}'s,
|
||||
* by its whole name only.
|
||||
*/
|
||||
export function importBindingTail(ref: UnresolvedRef, context: ResolutionContext): string | undefined {
|
||||
if (ref.language === 'python') return pythonImportBindingTail(ref, context);
|
||||
@@ -3169,7 +3266,9 @@ export function importBindingTail(ref: UnresolvedRef, context: ResolutionContext
|
||||
if (!binding || (!receiver && !binding.isDefault && !binding.isNamespace && binding.exportedName === binding.localName)) return undefined;
|
||||
if (isExternalImport(binding.source, ref.language, context) &&
|
||||
// A monorepo app's own tsconfig alias (`#/views/…`): isExternalImport reads the root's only.
|
||||
!context.getNearestAliases?.(ref.filePath)?.patterns.some((p) => p.prefix !== '' && binding.source.startsWith(p.prefix))) {
|
||||
!context.getNearestAliases?.(ref.filePath)?.patterns.some((p) => p.prefix !== '' && binding.source.startsWith(p.prefix)) &&
|
||||
// A module the app's `baseUrl` or catch-all `paths` reads (`layout/MainLayout`).
|
||||
!namesOwnConfigModule(binding.source, ref.filePath, ref.language, context)) {
|
||||
return undefined;
|
||||
}
|
||||
// `..` names a folder through the importing file's own location.
|
||||
|
||||
@@ -307,7 +307,7 @@ export class ReferenceResolver {
|
||||
// `null` = computed and absent. Treated as immutable for the
|
||||
// resolver's lifetime; callers re-create the resolver if config changes.
|
||||
private projectAliases: AliasMap | null | undefined = undefined;
|
||||
// Per directory: the aliases of the nearest non-root tsconfig declaring `paths`.
|
||||
// Per directory: the aliases of the nearest non-root tsconfig declaring `paths` or `baseUrl`.
|
||||
private dirAliases = new Map<string, AliasMap | null>();
|
||||
// Per project-relative directory ('' = the root): the module of the nearest
|
||||
// go.mod at or above it, up to the project root. Same lazy/immutable
|
||||
@@ -692,6 +692,8 @@ export class ReferenceResolver {
|
||||
if (resolved !== null && this.isIndexedFile(resolved)) return false;
|
||||
return this.isDeclaredOutsidePackage(source, fromFile);
|
||||
},
|
||||
isDeclaredPackage: (source, fromFile) => this.isDeclaredOutsidePackage(source, fromFile),
|
||||
isIndexedFile: (filePath) => this.isIndexedFile(filePath),
|
||||
getNodesInFile: (filePath: string) => {
|
||||
if (!this.nodeCache.has(filePath)) {
|
||||
this.nodeCache.set(filePath, this.queries.getNodesByFile(filePath));
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
*
|
||||
* Scope:
|
||||
* - reads tsconfig.json, then jsconfig.json, then tsconfig.base.json
|
||||
* - honours `compilerOptions.baseUrl` and `compilerOptions.paths`
|
||||
* - honours `compilerOptions.baseUrl` and `compilerOptions.paths`; a
|
||||
* `baseUrl` with no `paths` still loads, because it alone makes
|
||||
* `layout/MainLayout` name `src/layout/MainLayout` (CRA / Vite admin
|
||||
* templates)
|
||||
* - follows `extends` chains, nearest config wins (#1534) — Nx-style
|
||||
* monorepos keep every alias in a `tsconfig.base.json` the root
|
||||
* config merely inherits, so without this they resolved nothing
|
||||
@@ -49,9 +52,17 @@ export interface AliasPattern {
|
||||
export interface AliasMap {
|
||||
/** Absolute path. The directory `compilerOptions.paths` is rooted at. */
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Whether the config (or one it extends) sets `compilerOptions.baseUrl`.
|
||||
* A non-relative specifier is then also read from {@link baseUrl}, after
|
||||
* `paths`, the way tsc reads it. `paths` alone anchors only its own
|
||||
* targets (TypeScript 4.1+), so a map without a `baseUrl` never does.
|
||||
*/
|
||||
declaresBaseUrl?: boolean;
|
||||
/**
|
||||
* Patterns ordered by specificity: longer prefix first, then literal-
|
||||
* before-wildcard, so the resolver tries the most-specific match.
|
||||
* Empty for a config that sets only `baseUrl`.
|
||||
*/
|
||||
patterns: AliasPattern[];
|
||||
}
|
||||
@@ -245,7 +256,8 @@ function splitWildcard(pattern: string): {
|
||||
|
||||
/**
|
||||
* Load aliases for `projectRoot`. Returns `null` when no tsconfig /
|
||||
* jsconfig is present or when the file has no usable `paths`.
|
||||
* jsconfig is present or when the file has neither usable `paths` nor a
|
||||
* `baseUrl`.
|
||||
*
|
||||
* Cheap to call repeatedly — caching is the caller's job (the
|
||||
* resolver does it via {@link aliasCache}).
|
||||
@@ -283,25 +295,24 @@ export function loadProjectAliases(projectRoot: string): AliasMap | null {
|
||||
// declared them — which is the project root only when that config is the
|
||||
// root one (the pre-`extends` assumption).
|
||||
const baseUrl = effective.baseUrl ?? effective.pathsDir ?? projectRoot;
|
||||
|
||||
const paths = effective.paths;
|
||||
if (!paths || typeof paths !== 'object') {
|
||||
// baseUrl alone isn't an "alias" per se; with no paths we'd just
|
||||
// be redirecting the whole tree. Skip — the existing resolver
|
||||
// already handles relative imports.
|
||||
return null;
|
||||
}
|
||||
const declaresBaseUrl = effective.baseUrl !== undefined;
|
||||
|
||||
const patterns: AliasPattern[] = [];
|
||||
for (const [pattern, targets] of Object.entries(paths)) {
|
||||
if (!Array.isArray(targets) || targets.length === 0) continue;
|
||||
const filtered = targets.filter((t): t is string => typeof t === 'string');
|
||||
if (filtered.length === 0) continue;
|
||||
const { prefix, suffix, hasWildcard } = splitWildcard(pattern);
|
||||
patterns.push({ prefix, suffix, hasWildcard, replacements: filtered });
|
||||
const paths = effective.paths;
|
||||
if (paths && typeof paths === 'object') {
|
||||
for (const [pattern, targets] of Object.entries(paths)) {
|
||||
if (!Array.isArray(targets) || targets.length === 0) continue;
|
||||
const filtered = targets.filter((t): t is string => typeof t === 'string');
|
||||
if (filtered.length === 0) continue;
|
||||
const { prefix, suffix, hasWildcard } = splitWildcard(pattern);
|
||||
patterns.push({ prefix, suffix, hasWildcard, replacements: filtered });
|
||||
}
|
||||
}
|
||||
|
||||
if (patterns.length === 0) return null;
|
||||
// A `baseUrl` with no `paths` is still a map: the resolver reads a bare
|
||||
// specifier from it, but only one that names a project file and no package
|
||||
// the app depends on (see resolveFromOwnConfig in import-resolver.ts).
|
||||
if (patterns.length === 0 && !declaresBaseUrl) return null;
|
||||
|
||||
// Specificity sort: longer prefix first; literal patterns before
|
||||
// wildcard patterns of the same prefix length. TypeScript itself
|
||||
@@ -315,10 +326,11 @@ export function loadProjectAliases(projectRoot: string): AliasMap | null {
|
||||
logDebug('path-aliases loaded', {
|
||||
file: usedFile,
|
||||
baseUrl,
|
||||
declaresBaseUrl,
|
||||
patternCount: patterns.length,
|
||||
});
|
||||
|
||||
return { baseUrl, patterns };
|
||||
return { baseUrl, declaresBaseUrl, patterns };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,3 +374,24 @@ export function applyAliases(
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a non-relative specifier can live under `aliases`, in the order tsc
|
||||
* tries them: the targets of the most specific pattern it matches, then the
|
||||
* specifier read from `baseUrl` when the config declares one. Relative to
|
||||
* `projectRoot`; a candidate that escapes it is dropped.
|
||||
*/
|
||||
export function aliasAndBaseUrlCandidates(
|
||||
importPath: string,
|
||||
aliases: AliasMap,
|
||||
projectRoot: string
|
||||
): string[] {
|
||||
const out = applyAliases(importPath, aliases, projectRoot);
|
||||
if (aliases.declaresBaseUrl) {
|
||||
const relative = path.relative(projectRoot, path.resolve(aliases.baseUrl, importPath));
|
||||
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
||||
out.push(relative.replace(/\\/g, '/'));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
+19
-3
@@ -185,8 +185,23 @@ export interface ResolutionContext {
|
||||
*/
|
||||
isOutOfRepoImport?(source: string, fromFile: string, language: Language): boolean;
|
||||
/**
|
||||
* Project import-path aliases (tsconfig/jsconfig `paths`). Returns
|
||||
* `null` when the project doesn't define any. Cached per resolver
|
||||
* Whether `source`, imported from `fromFile`, is a package: one the file's
|
||||
* package.json or an enclosing one declares, a Node built-in, or a
|
||||
* runtime's virtual module (`node:`, `bun:`, `astro:`…). Such an import
|
||||
* never binds to a same-named project module, only to a declaration file
|
||||
* the project keeps for the package. Supplied by the coordinator.
|
||||
*/
|
||||
isDeclaredPackage?(source: string, fromFile: string): boolean;
|
||||
/**
|
||||
* Whether `filePath` (project-relative) is one of the files the index holds.
|
||||
* `fileExists` also answers for a directory and for a file on disk the
|
||||
* index never read, such as one under `node_modules`. Supplied by the
|
||||
* coordinator; a minimal context can omit it.
|
||||
*/
|
||||
isIndexedFile?(filePath: string): boolean;
|
||||
/**
|
||||
* Project import-path aliases (tsconfig/jsconfig `paths`, and `baseUrl`).
|
||||
* Returns `null` when the project doesn't define any. Cached per resolver
|
||||
* instance — safe to call from any resolver code path. Optional so
|
||||
* existing test fixtures and external context implementations
|
||||
* compile without modification; production resolver implements it.
|
||||
@@ -194,7 +209,8 @@ export interface ResolutionContext {
|
||||
getProjectAliases?(): import('./path-aliases').AliasMap | null;
|
||||
/**
|
||||
* The aliases of the tsconfig / jsconfig nearest `fromFile` below the
|
||||
* project root that declares `paths` — a monorepo app's own `@/*` — or null.
|
||||
* project root that declares `paths` or `baseUrl` — a monorepo app's own
|
||||
* `@/*`, a nested Vite app's `"baseUrl": "src"` — or null.
|
||||
*/
|
||||
getNearestAliases?(fromFile: string): import('./path-aliases').AliasMap | null;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user