alias-it
Version:
Intelligent TypeScript path mapping with auto-discovery and validation
422 lines (416 loc) • 15.7 kB
JavaScript
// 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