UNPKG

alias-it

Version:

Intelligent TypeScript path mapping with auto-discovery and validation

360 lines (357 loc) 12 kB
// src/PathMapper.ts import * as path2 from "path"; // src/utils/fileUtils.ts import * as fs from "fs-extra"; import * as path from "path"; import { glob } from "glob"; var FileUtils = class { /** * Check if a path exists and is accessible */ static async pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Get all TypeScript files in a directory recursively */ static async getTypeScriptFiles(rootDir, includePatterns = ["**/*.ts", "**/*.tsx"], excludePatterns = ["**/*.d.ts", "**/node_modules/**", "**/dist/**", "**/build/**"]) { const patterns = includePatterns.map((pattern) => path.join(rootDir, pattern)); const excludePatternsWithRoot = excludePatterns.map((pattern) => path.join(rootDir, pattern)); const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { ignore: excludePatternsWithRoot, absolute: true, nodir: true }); files.push(...matches); } return [...new Set(files)]; } /** * Get directory structure up to a certain depth */ static async getDirectoryStructure(rootDir, maxDepth = 3, currentDepth = 0) { if (currentDepth >= maxDepth) { return []; } try { const items = await fs.readdir(rootDir); const directories = []; for (const item of items) { const fullPath = path.join(rootDir, item); const stat2 = await fs.stat(fullPath); if (stat2.isDirectory()) { directories.push(fullPath); const subDirs = await this.getDirectoryStructure(fullPath, maxDepth, currentDepth + 1); directories.push(...subDirs); } } return directories; } catch (error) { return []; } } /** * Generate a meaningful alias from a path */ static generateAlias(filePath, rootDir) { const relativePath = path.relative(rootDir, filePath); const withoutExt = path.parse(relativePath); let alias = withoutExt.name.replace(/[^a-zA-Z0-9]/g, " ").replace(/\s+(\w)/g, (_, char) => char.toUpperCase()).replace(/^(\w)/, (_, char) => char.toLowerCase()); if (withoutExt.ext === "") { alias = path.basename(filePath); } if (!/^[a-zA-Z]/.test(alias)) { alias = "p" + alias; } return alias; } /** * Validate if a path mapping is valid */ static validatePathMapping(mapping, rootDir) { const fullPath = path.resolve(rootDir, mapping); return fs.existsSync(fullPath); } /** * Read tsconfig.json file */ static async readTsConfig(configPath) { try { const content = await fs.readFile(configPath, "utf-8"); return JSON.parse(content); } catch (error) { throw new Error(`Failed to read tsconfig.json: ${error}`); } } /** * Write tsconfig.json file */ static async writeTsConfig(configPath, config) { try { await fs.writeFile(configPath, JSON.stringify(config, null, 2)); } catch (error) { throw new Error(`Failed to write tsconfig.json: ${error}`); } } }; // src/PathMapper.ts var PathMapper = class { constructor(options = {}) { this.rootDir = options.rootDir || process.cwd(); this.options = { rootDir: this.rootDir, includePatterns: options.includePatterns || ["**/*.ts", "**/*.tsx"], excludePatterns: options.excludePatterns || ["**/*.d.ts", "**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"], maxDepth: options.maxDepth || 3, autoGenerate: options.autoGenerate ?? true, validateExisting: options.validateExisting ?? true }; } /** * Discover potential path mappings in the project */ async discoverPaths() { const discovered = []; try { const tsFiles = await FileUtils.getTypeScriptFiles(this.rootDir, this.options.includePatterns, this.options.excludePatterns); const directories = await FileUtils.getDirectoryStructure(this.rootDir, this.options.maxDepth); for (const file of tsFiles) { const relativePath = path2.relative(this.rootDir, file); const alias = FileUtils.generateAlias(file, this.rootDir); discovered.push({ alias, path: file, relativePath, type: "file", depth: this.calculateDepth(relativePath) }); } for (const dir of directories) { const relativePath = path2.relative(this.rootDir, dir); const alias = FileUtils.generateAlias(dir, this.rootDir); discovered.push({ alias, path: dir, relativePath, type: "directory", depth: this.calculateDepth(relativePath) }); } return discovered.sort((a, b) => { if (a.depth !== b.depth) { return a.depth - b.depth; } return a.alias.localeCompare(b.alias); }); } catch (error) { throw new Error(`Failed to discover paths: ${error}`); } } /** * Generate path mappings from discovered paths */ async generateMappings() { const discovered = await this.discoverPaths(); const mappings = {}; const suggestions = []; const warnings = []; const errors = []; const aliasGroups = /* @__PURE__ */ new Map(); for (const item of discovered) { if (!aliasGroups.has(item.alias)) { aliasGroups.set(item.alias, []); } aliasGroups.get(item.alias).push(item); } for (const [alias, items] of aliasGroups) { if (items.length === 1) { const item = items[0]; mappings[`@${alias}`] = item.relativePath; if (item.depth > 2) { suggestions.push(`Consider using a shorter alias for deep path: ${item.relativePath}`); } } else { const resolved = this.resolveAliasConflict(alias, items); mappings[`@${resolved.alias}`] = resolved.path; warnings.push(`Alias conflict resolved: ${alias} -> ${resolved.alias} for ${resolved.path}`); } } return { mappings, discovered, suggestions, warnings, errors }; } /** * Validate existing path mappings in tsconfig.json */ async validateExistingMappings(tsConfigPath) { const configPath = tsConfigPath || path2.join(this.rootDir, "tsconfig.json"); const issues = []; try { if (!await FileUtils.pathExists(configPath)) { issues.push({ type: "error", message: "tsconfig.json not found", path: configPath }); return { isValid: false, issues }; } const config = await FileUtils.readTsConfig(configPath); const pathMappings = config.compilerOptions?.paths || {}; for (const [alias, mapping] of Object.entries(pathMappings)) { const mappingPath = Array.isArray(mapping) ? mapping[0] : mapping; if (!FileUtils.validatePathMapping(mappingPath, this.rootDir)) { issues.push({ type: "error", message: `Invalid path mapping: ${alias} -> ${mappingPath}`, path: mappingPath, suggestion: "Remove or fix this mapping" }); } else { const discovered = await this.discoverPaths(); const betterMapping = this.findBetterMapping(alias, mappingPath, discovered); if (betterMapping) { issues.push({ type: "info", message: `Consider using shorter path: ${betterMapping}`, path: mappingPath, suggestion: betterMapping }); } } } return { isValid: issues.filter((issue) => issue.type === "error").length === 0, issues }; } catch (error) { issues.push({ type: "error", message: `Failed to validate mappings: ${error}`, path: configPath }); return { isValid: false, issues }; } } /** * Update tsconfig.json with new path mappings */ async updateTsConfig(newMappings, tsConfigPath, merge = true) { const configPath = tsConfigPath || path2.join(this.rootDir, "tsconfig.json"); try { let config; if (await FileUtils.pathExists(configPath)) { config = await FileUtils.readTsConfig(configPath); } else { config = { compilerOptions: { target: "ES2020", module: "commonjs", strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true } }; } if (!config.compilerOptions) { config.compilerOptions = {}; } if (merge && config.compilerOptions.paths) { config.compilerOptions.paths = { ...config.compilerOptions.paths, ...newMappings }; } else { config.compilerOptions.paths = newMappings; } await FileUtils.writeTsConfig(configPath, config); } catch (error) { throw new Error(`Failed to update tsconfig.json: ${error}`); } } /** * Get intelligent suggestions for path mappings */ async getSuggestions() { const discovered = await this.discoverPaths(); const suggestions = []; const importPatterns = await this.analyzeImportPatterns(); for (const pattern of importPatterns) { if (pattern.count > 5) { suggestions.push(`Consider creating alias for: ${pattern.pattern} (used ${pattern.count} times)`); } } const commonDirs = ["components", "utils", "types", "services", "hooks", "pages"]; for (const dir of commonDirs) { const dirPath = path2.join(this.rootDir, "src", dir); if (await FileUtils.pathExists(dirPath)) { suggestions.push(`Consider adding alias for common directory: @${dir} -> src/${dir}`); } } return suggestions; } calculateDepth(relativePath) { return relativePath.split(path2.sep).length - 1; } resolveAliasConflict(alias, items) { const files = items.filter((item) => item.type === "file"); const directories = items.filter((item) => item.type === "directory"); if (files.length > 0) { const shortestFile = files.reduce((shortest, current) => current.relativePath.length < shortest.relativePath.length ? current : shortest); return { alias: shortestFile.alias, path: shortestFile.relativePath }; } if (directories.length > 0) { const shortestDir = directories.reduce((shortest, current) => current.relativePath.length < shortest.relativePath.length ? current : shortest); return { alias: shortestDir.alias, path: shortestDir.relativePath }; } return { alias: items[0].alias, path: items[0].relativePath }; } findBetterMapping(alias, currentPath, discovered) { const targetPath = path2.resolve(this.rootDir, currentPath); for (const item of discovered) { const itemFullPath = path2.resolve(this.rootDir, item.relativePath); if (itemFullPath === targetPath && item.relativePath.length < currentPath.length) { return item.relativePath; } } return null; } async analyzeImportPatterns() { return []; } }; // src/index.ts async function generatePathMappings(options) { const mapper = new PathMapper(options); const result = await mapper.generateMappings(); return result.mappings; } async function validatePathMappings(tsConfigPath, options) { const mapper = new PathMapper(options); const result = await mapper.validateExistingMappings(tsConfigPath); return result.isValid; } async function updateTsConfigMappings(mappings, tsConfigPath, merge, options) { const mapper = new PathMapper(options); await mapper.updateTsConfig(mappings, tsConfigPath, merge); } async function getPathMappingSuggestions(options) { const mapper = new PathMapper(options); return await mapper.getSuggestions(); } export { PathMapper, generatePathMappings, getPathMappingSuggestions, updateTsConfigMappings, validatePathMappings }; //# sourceMappingURL=index.mjs.map