UNPKG

alias-it

Version:

Intelligent TypeScript path mapping with auto-discovery and validation

422 lines (416 loc) 15.7 kB
#!/usr/bin/env node // src/cli.ts import { Command } from "commander"; // 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(); } // src/cli.ts import * as fs2 from "fs-extra"; var program = new Command(); program.name("alias-it").description("Intelligent TypeScript path mapping with auto-discovery and validation").version("1.0.0"); program.command("discover").description("Discover potential path mappings in the project").option("--root-dir <path>", "Root directory to scan", process.cwd()).option("--output <path>", "Output file for results (JSON format)").action(async (options) => { const mapper = new PathMapper({ rootDir: options.rootDir }); const discovered = await mapper.discoverPaths(); console.log(` Found ${discovered.length} potential mappings: `); for (const item of discovered) { console.log(` @${item.alias} -> ${item.relativePath} (${item.type}, depth: ${item.depth})`); } if (options.output) { await fs2.writeJson(options.output, discovered, { spaces: 2 }); console.log(` Results saved to: ${options.output}`); } }); program.command("generate").description("Generate path mappings and optionally update tsconfig.json").option("--root-dir <path>", "Root directory to scan", process.cwd()).option("--tsconfig <path>", "Path to tsconfig.json", "tsconfig.json").option("--output <path>", "Output file for mappings (JSON format)").option("--merge", "Merge with existing mappings", true).option("--no-merge", "Replace existing mappings").action(async (options) => { const result = await generatePathMappings({ rootDir: options.rootDir }); console.log(` Generated ${Object.keys(result).length} mappings: `); for (const [alias, mapping] of Object.entries(result)) { console.log(` ${alias}: "${mapping}"`); } if (options.merge !== false) { await updateTsConfigMappings(result, options.tsconfig, true, { rootDir: options.rootDir }); console.log("\u2705 tsconfig.json updated successfully!"); } if (options.output) { await fs2.writeJson(options.output, result, { spaces: 2 }); console.log(` Mappings saved to: ${options.output}`); } }); program.command("validate").description("Validate existing path mappings in tsconfig.json").option("--root-dir <path>", "Root directory to scan", process.cwd()).option("--tsconfig <path>", "Path to tsconfig.json", "tsconfig.json").action(async (options) => { const isValid = await validatePathMappings(options.tsconfig, { rootDir: options.rootDir }); if (isValid) { console.log("\u2705 All path mappings are valid!"); } else { console.log("\u274C Some path mappings have issues."); process.exit(1); } }); program.command("suggest").description("Get suggestions for path mappings").option("--root-dir <path>", "Root directory to scan", process.cwd()).action(async (options) => { const suggestions = await getPathMappingSuggestions({ rootDir: options.rootDir }); if (suggestions.length === 0) { console.log("No suggestions available."); } else { console.log("\nSuggestions:\n"); suggestions.forEach((suggestion, index) => { console.log(` ${index + 1}. ${suggestion}`); }); } }); program.command("update").description("Update tsconfig.json with new mappings from a file").requiredOption("--output <path>", "Input file with mappings (JSON format)").option("--tsconfig <path>", "Path to tsconfig.json", "tsconfig.json").option("--merge", "Merge with existing mappings", true).option("--no-merge", "Replace existing mappings").option("--root-dir <path>", "Root directory to scan", process.cwd()).action(async (options) => { const mappings = await fs2.readJson(options.output); await updateTsConfigMappings(mappings, options.tsconfig, options.merge, { rootDir: options.rootDir }); console.log("\u2705 tsconfig.json updated successfully!"); }); program.parseAsync(process.argv); //# sourceMappingURL=cli.mjs.map