autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
180 lines (179 loc) • 7.08 kB
JavaScript
/**
* @module ImportPathResolver
* @description Phase 5: 将 import 路径解析为项目内文件路径
*
* 负责:
* - 相对路径 (./ ../) 解析
* - 文件扩展名补全 (.ts, .js, .py, ...)
* - index 文件约定 (./dir → ./dir/index.ts)
* - 外部依赖识别与过滤
* - tsconfig paths alias 支持 (@/xxx → src/xxx)
*
* 不负责:
* - webpack resolve alias (需额外配置)
* - Node.js exports map (需解析 package.json)
*/
import fs from 'node:fs';
import path from 'node:path';
export class ImportPathResolver {
fileIndex;
pathAliases;
projectRoot;
/**
* @param projectRoot 项目根目录
* @param allFiles 项目内所有文件的相对路径
*/
constructor(projectRoot, allFiles) {
this.projectRoot = projectRoot;
/** normalizedPath → actualFilePath */
this.fileIndex = new Map();
/** >} tsconfig paths 映射 */
this.pathAliases = [];
// 构建文件索引
for (const f of allFiles) {
// 完整路径
this.fileIndex.set(f, f);
// 去扩展名 → 完整路径
const base = f.replace(/\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|swift|m|dart)$/, '');
if (!this.fileIndex.has(base)) {
this.fileIndex.set(base, f);
}
// index 文件约定: src/utils/ → src/utils/index.ts
if (/\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f)) {
const dir = f.replace(/\/index\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
if (!this.fileIndex.has(dir)) {
this.fileIndex.set(dir, f);
}
}
// Python __init__.py 约定: pkg/ → pkg/__init__.py
if (f.endsWith('/__init__.py')) {
const dir = f.replace(/\/__init__\.py$/, '');
if (!this.fileIndex.has(dir)) {
this.fileIndex.set(dir, f);
}
}
}
// 自动加载 tsconfig paths
this._loadTsconfigPaths(projectRoot);
}
/**
* 从 tsconfig.json 加载 paths alias 配置
*/
_loadTsconfigPaths(projectRoot) {
const candidates = ['tsconfig.json', 'tsconfig.app.json', 'jsconfig.json'];
for (const name of candidates) {
try {
const configPath = path.join(projectRoot, name);
if (!fs.existsSync(configPath)) {
continue;
}
const raw = fs.readFileSync(configPath, 'utf-8');
// 简单的 JSON 解析 (去除注释)
const cleaned = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
const config = JSON.parse(cleaned);
const compilerOptions = config.compilerOptions || {};
const baseUrl = compilerOptions.baseUrl || '.';
const paths = compilerOptions.paths;
if (!paths) {
continue;
}
for (const [aliasPattern, targetPatterns] of Object.entries(paths)) {
// "@/*" → ["src/*"]
// "~/*" → ["src/*"]
// "@components/*" → ["src/components/*"]
const prefix = aliasPattern.replace(/\/?\*$/, '');
const targets = (Array.isArray(targetPatterns) ? targetPatterns : [targetPatterns]).map((t) => {
const target = String(t).replace(/\/?\*$/, '');
// 相对于 baseUrl 解析
return path.normalize(path.join(baseUrl, target));
});
if (prefix) {
this.pathAliases.push({ prefix, targets });
}
}
// 只加载第一个找到的配置文件
break;
}
catch (_e) {
// 配置解析失败,静默跳过
}
}
}
/**
* 解析 import 路径到项目文件
*
* @param importPath 如 "./UserRepo" 或 "../shared/utils"
* @param importerFile 当前文件路径 (相对路径)
* @returns 解析后的文件路径 (相对) 或 null (外部依赖)
*/
resolve(importPath, importerFile) {
const pathStr = String(importPath);
// 1. 跳过外部依赖 (先检查 alias,再判断外部)
// 相对路径始终尝试解析
if (pathStr.startsWith('.')) {
const importerDir = path.dirname(importerFile);
const resolved = path.normalize(path.join(importerDir, pathStr));
if (this.fileIndex.has(resolved)) {
return this.fileIndex.get(resolved);
}
return this.fileIndex.get(resolved) || null;
}
// 2. tsconfig paths alias 解析
const aliasResolved = this._resolveAlias(pathStr);
if (aliasResolved) {
return aliasResolved;
}
// 3. 如果不是 alias 且是外部依赖 → null
if (this._isExternal(pathStr)) {
return null;
}
// 4. Python 模块路径 (点分隔 → 斜线)
if (pathStr.includes('.') && !pathStr.includes('/')) {
const slashed = pathStr.replace(/\./g, '/');
if (this.fileIndex.has(slashed)) {
return this.fileIndex.get(slashed);
}
}
// 5. 直接匹配(Go 包路径、Rust crate path 等)
return this.fileIndex.get(pathStr) || null;
}
/**
* 尝试通过 tsconfig paths alias 解析
*/
_resolveAlias(importPath) {
for (const { prefix, targets } of this.pathAliases) {
if (importPath === prefix || importPath.startsWith(`${prefix}/`)) {
const remainder = importPath === prefix ? '' : importPath.slice(prefix.length + 1);
for (const target of targets) {
const resolved = remainder ? path.normalize(path.join(target, remainder)) : target;
if (this.fileIndex.has(resolved)) {
return this.fileIndex.get(resolved) ?? null;
}
}
}
}
return null;
}
/** 判断是否为外部依赖 */
_isExternal(importPath) {
// 相对路径不是外部
if (importPath.startsWith('.') || importPath.startsWith('/')) {
return false;
}
// scoped npm packages: @scope/pkg
// bare specifier: lodash, express 等
// 如果在文件索引中有匹配,说明是项目内的
if (this.fileIndex.has(importPath)) {
return false;
}
// Python 点分路径的特殊处理
if (importPath.includes('.') && !importPath.includes('/')) {
const slashed = importPath.replace(/\./g, '/');
if (this.fileIndex.has(slashed)) {
return false;
}
}
return true;
}
}
export default ImportPathResolver;