2026-02-05 13:40:43 +00:00
|
|
|
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
2025-12-19 15:09:24 +00:00
|
|
|
|
import { join, relative } from 'node:path';
|
|
|
|
|
|
|
|
|
|
|
|
interface ReplaceConfig {
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** List of components to replace */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
components: string[];
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** Whether to run in dry-run mode (preview only, no actual modifications) */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
dryRun?: boolean;
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** File extension whitelist */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
fileExtensions?: string[];
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** Source package name */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
fromPackage: string;
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** Directory to scan */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
targetDir: string;
|
2026-04-09 06:38:29 +00:00
|
|
|
|
/** Target package name */
|
2025-12-19 15:09:24 +00:00
|
|
|
|
toPackage: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-09 06:38:29 +00:00
|
|
|
|
* Recursively get all files in a directory
|
2025-12-19 15:09:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function getAllFiles(dir: string, extensions: string[]): string[] {
|
|
|
|
|
|
const files: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
function walk(currentPath: string) {
|
|
|
|
|
|
const items = readdirSync(currentPath);
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
const fullPath = join(currentPath, item);
|
|
|
|
|
|
const stat = statSync(fullPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (stat.isDirectory()) {
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Skip directories like node_modules
|
2025-12-19 15:09:24 +00:00
|
|
|
|
if (!['node_modules', '.git', 'dist', 'build', '.next'].includes(item)) {
|
|
|
|
|
|
walk(fullPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (stat.isFile()) {
|
|
|
|
|
|
const hasValidExtension = extensions.some((ext) => fullPath.endsWith(ext));
|
|
|
|
|
|
if (hasValidExtension) {
|
|
|
|
|
|
files.push(fullPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
walk(dir);
|
|
|
|
|
|
return files;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-09 06:38:29 +00:00
|
|
|
|
* Parse import statements and extract imported components
|
2025-12-19 15:09:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function parseImportStatement(line: string, packageName: string) {
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Match import { ... } from 'package'
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const importRegex = new RegExp(
|
|
|
|
|
|
`import\\s+{([^}]+)}\\s+from\\s+['"]${packageName.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&')}['"]`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const match = line.match(importRegex);
|
|
|
|
|
|
|
|
|
|
|
|
if (!match) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const importContent = match[1];
|
|
|
|
|
|
const components = importContent
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
const trimmed = item.trim();
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Handle as aliases: ComponentName as AliasName
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const asMatch = trimmed.match(/^(\w+)(?:\s+as\s+(\w+))?/);
|
|
|
|
|
|
return asMatch
|
|
|
|
|
|
? {
|
|
|
|
|
|
alias: asMatch[2] || null,
|
|
|
|
|
|
name: asMatch[1],
|
|
|
|
|
|
raw: trimmed,
|
|
|
|
|
|
}
|
|
|
|
|
|
: null;
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean) as Array<{ alias: string | null; name: string; raw: string }>;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
components,
|
|
|
|
|
|
fullMatch: match[0],
|
|
|
|
|
|
indentation: line.match(/^\s*/)?.[0] || '',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-09 06:38:29 +00:00
|
|
|
|
* Replace import statements in a file
|
2025-12-19 15:09:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function replaceImportsInFile(filePath: string, config: ReplaceConfig): boolean {
|
|
|
|
|
|
const content = readFileSync(filePath, 'utf8');
|
|
|
|
|
|
const lines = content.split('\n');
|
|
|
|
|
|
let modified = false;
|
|
|
|
|
|
const newLines: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
const parsed = parseImportStatement(line, config.fromPackage);
|
|
|
|
|
|
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
|
newLines.push(line);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Find components to replace and components to keep
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const toReplace = parsed.components.filter((comp) => config.components.includes(comp.name));
|
|
|
|
|
|
const toKeep = parsed.components.filter((comp) => !config.components.includes(comp.name));
|
|
|
|
|
|
|
|
|
|
|
|
if (toReplace.length === 0) {
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// No components to replace
|
2025-12-19 15:09:24 +00:00
|
|
|
|
newLines.push(line);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modified = true;
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Generate new import statement
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const { indentation } = parsed;
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// If there are components to keep, preserve the original import
|
2025-12-19 15:09:24 +00:00
|
|
|
|
if (toKeep.length > 0) {
|
|
|
|
|
|
const keepImports = toKeep.map((c) => c.raw).join(', ');
|
|
|
|
|
|
newLines.push(`${indentation}import { ${keepImports} } from '${config.fromPackage}';`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Add new import
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const replaceImports = toReplace.map((c) => c.raw).join(', ');
|
|
|
|
|
|
newLines.push(`${indentation}import { ${replaceImports} } from '${config.toPackage}';`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (modified) {
|
|
|
|
|
|
const newContent = newLines.join('\n');
|
|
|
|
|
|
if (!config.dryRun) {
|
|
|
|
|
|
writeFileSync(filePath, newContent, 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-09 06:38:29 +00:00
|
|
|
|
* Execute replacement
|
2025-12-19 15:09:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function executeReplace(config: ReplaceConfig) {
|
|
|
|
|
|
const extensions = config.fileExtensions || ['.ts', '.tsx', '.js', '.jsx'];
|
|
|
|
|
|
const files = getAllFiles(config.targetDir, extensions);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n🔍 扫描目录: ${config.targetDir}`);
|
|
|
|
|
|
console.log(`📦 从 "${config.fromPackage}" 替换到 "${config.toPackage}"`);
|
|
|
|
|
|
console.log(`🎯 目标组件: ${config.components.join(', ')}`);
|
|
|
|
|
|
console.log(`📄 找到 ${files.length} 个文件\n`);
|
|
|
|
|
|
|
|
|
|
|
|
if (config.dryRun) {
|
|
|
|
|
|
console.log('🔔 [DRY RUN 模式] 仅预览,不会实际修改文件\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let modifiedCount = 0;
|
|
|
|
|
|
const modifiedFiles: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
const wasModified = replaceImportsInFile(file, config);
|
|
|
|
|
|
if (wasModified) {
|
|
|
|
|
|
modifiedCount++;
|
|
|
|
|
|
modifiedFiles.push(relative(process.cwd(), file));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('\n✅ 完成!');
|
|
|
|
|
|
console.log(`📝 修改了 ${modifiedCount} 个文件\n`);
|
|
|
|
|
|
|
|
|
|
|
|
if (modifiedFiles.length > 0) {
|
|
|
|
|
|
console.log('修改的文件:');
|
|
|
|
|
|
for (const file of modifiedFiles) {
|
|
|
|
|
|
console.log(` - ${file}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// ============ Main function ============
|
2025-12-19 15:09:24 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-09 06:38:29 +00:00
|
|
|
|
* Parse configuration from command line arguments
|
2025-12-19 15:09:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function parseArgs(): ReplaceConfig | null {
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
|
|
|
|
|
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
|
|
|
console.log(`
|
|
|
|
|
|
使用方法:
|
|
|
|
|
|
bun run scripts/replaceComponentImports.ts [选项]
|
|
|
|
|
|
|
|
|
|
|
|
选项:
|
|
|
|
|
|
--components <comp1,comp2,...> 要替换的组件列表(逗号分隔)
|
|
|
|
|
|
--from <package> 原始包名
|
|
|
|
|
|
--to <package> 目标包名
|
|
|
|
|
|
--dir <directory> 要扫描的目录(默认: src)
|
|
|
|
|
|
--ext <.ext1,.ext2,...> 文件扩展名(默认: .ts,.tsx,.js,.jsx)
|
|
|
|
|
|
--dry-run 仅预览,不实际修改文件
|
|
|
|
|
|
--help, -h 显示帮助信息
|
|
|
|
|
|
|
|
|
|
|
|
示例:
|
|
|
|
|
|
# 将 antd 的 Skeleton 和 Empty 替换为 @lobehub/ui
|
|
|
|
|
|
bun run scripts/replaceComponentImports.ts \\
|
|
|
|
|
|
--components Skeleton,Empty \\
|
|
|
|
|
|
--from antd \\
|
|
|
|
|
|
--to @lobehub/ui \\
|
|
|
|
|
|
--dir src
|
|
|
|
|
|
|
|
|
|
|
|
# 仅预览,不修改
|
|
|
|
|
|
bun run scripts/replaceComponentImports.ts \\
|
|
|
|
|
|
--components Skeleton,Empty \\
|
|
|
|
|
|
--from antd \\
|
|
|
|
|
|
--to @lobehub/ui \\
|
|
|
|
|
|
--dry-run
|
|
|
|
|
|
`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getArgValue = (flag: string): string | undefined => {
|
|
|
|
|
|
const index = args.indexOf(flag);
|
|
|
|
|
|
return index !== -1 && index + 1 < args.length ? args[index + 1] : undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const componentsStr = getArgValue('--components');
|
|
|
|
|
|
const fromPackage = getArgValue('--from');
|
|
|
|
|
|
const toPackage = getArgValue('--to');
|
|
|
|
|
|
const targetDir = getArgValue('--dir') || 'src';
|
|
|
|
|
|
const extStr = getArgValue('--ext');
|
|
|
|
|
|
const dryRun = args.includes('--dry-run');
|
|
|
|
|
|
|
|
|
|
|
|
if (!componentsStr || !fromPackage || !toPackage) {
|
|
|
|
|
|
console.error('❌ 错误: 必须指定 --components, --from 和 --to 参数');
|
|
|
|
|
|
console.error('使用 --help 查看帮助信息');
|
2026-02-05 13:40:43 +00:00
|
|
|
|
|
2025-12-19 15:09:24 +00:00
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
components: componentsStr.split(',').map((c) => c.trim()),
|
|
|
|
|
|
dryRun,
|
|
|
|
|
|
fileExtensions: extStr ? extStr.split(',').map((e) => e.trim()) : undefined,
|
|
|
|
|
|
fromPackage,
|
|
|
|
|
|
targetDir,
|
|
|
|
|
|
toPackage,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
|
// Execute script
|
2025-12-19 15:09:24 +00:00
|
|
|
|
const config = parseArgs();
|
|
|
|
|
|
if (config) {
|
|
|
|
|
|
executeReplace(config);
|
|
|
|
|
|
}
|