mirror of
https://github.com/hanzoai/icons.git
synced 2026-09-24 16:59:52 +00:00
Fork of lobe-icons (MIT) rebranded for Hanzo. Renamed all 6 published packages (@hanzoai/icons + -static-svg/png/webp/avatar + -rn) and every import across src/ + docs, repo URLs (lobehub/lobe-icons → hanzoai/icons), and prose (LobeHub → Hanzo). Fresh Hanzo Icons README dropping the upstream marketing chrome. Preserved: the MIT LICENSE + original copyright (required by the fork), the LobeHub brand icon itself (src/LobeHub/ — content, like any other brand), and external @lobehub/* build/demo deps (ui, lint, docs-kit, assets-logo, fluent-emoji-3d) that have no @hanzoai equivalent. CHANGELOG left as historical.
458 lines
12 KiB
TypeScript
458 lines
12 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync, readdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import FeatureConfigSyncer from './feature-config-syncer';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
interface ConversionStats {
|
|
alreadyConverted: string[];
|
|
converted: string[];
|
|
failed: string[];
|
|
notFound: string[];
|
|
pending: string[];
|
|
skipped: string[];
|
|
total: number;
|
|
}
|
|
|
|
interface ConversionOptions {
|
|
buildTest?: boolean;
|
|
dryRun?: boolean;
|
|
forceUpdate?: boolean;
|
|
lintCheck?: boolean;
|
|
maxIcons?: number;
|
|
skipExisting?: boolean;
|
|
}
|
|
|
|
class AutoConverter {
|
|
private projectRoot: string;
|
|
private srcDir: string;
|
|
private rnIconsDir: string;
|
|
private stats: ConversionStats;
|
|
|
|
constructor() {
|
|
this.projectRoot = join(__dirname, '../../');
|
|
this.srcDir = join(this.projectRoot, 'src');
|
|
this.rnIconsDir = join(this.projectRoot, 'packages/react-native/src/icons');
|
|
this.stats = {
|
|
alreadyConverted: [],
|
|
converted: [],
|
|
failed: [],
|
|
notFound: [],
|
|
pending: [],
|
|
skipped: [],
|
|
total: 0,
|
|
};
|
|
}
|
|
|
|
// 获取所有Web图标
|
|
private getAllWebIcons(): string[] {
|
|
try {
|
|
return readdirSync(this.srcDir)
|
|
.filter((name) => {
|
|
const iconPath = join(this.srcDir, name);
|
|
const componentsPath = join(iconPath, 'components');
|
|
// 过滤掉非图标目录
|
|
const excludeDirs = ['components', 'features', 'hooks', 'platform', 'types', 'utils'];
|
|
return !excludeDirs.includes(name) && existsSync(componentsPath);
|
|
})
|
|
.sort();
|
|
} catch (error) {
|
|
console.error('❌ 获取Web图标列表失败:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 获取所有已转换的RN图标
|
|
private getConvertedRNIcons(): string[] {
|
|
try {
|
|
if (!existsSync(this.rnIconsDir)) {
|
|
return [];
|
|
}
|
|
return readdirSync(this.rnIconsDir)
|
|
.filter((name) => {
|
|
const iconPath = join(this.rnIconsDir, name);
|
|
const indexPath = join(iconPath, 'index.ts');
|
|
return existsSync(indexPath);
|
|
})
|
|
.sort();
|
|
} catch (error) {
|
|
console.error('❌ 获取RN图标列表失败:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 检测图标转换状态
|
|
private analyzeConversionStatus(options: ConversionOptions) {
|
|
console.log('🔍 分析图标转换状态...\n');
|
|
|
|
const webIcons = this.getAllWebIcons();
|
|
const rnIcons = new Set(this.getConvertedRNIcons());
|
|
|
|
this.stats.total = webIcons.length;
|
|
|
|
if (options.forceUpdate) {
|
|
// 强制模式:转换所有图标
|
|
this.stats.pending = [...webIcons];
|
|
this.stats.alreadyConverted = [];
|
|
} else {
|
|
// 正常模式:只转换未转换的图标
|
|
this.stats.pending = webIcons.filter((icon) => !rnIcons.has(icon));
|
|
this.stats.alreadyConverted = webIcons.filter((icon) => rnIcons.has(icon));
|
|
}
|
|
|
|
// 如果设置了最大转换数量,只转换前N个
|
|
if (options.maxIcons && options.maxIcons > 0) {
|
|
this.stats.pending = this.stats.pending.slice(0, options.maxIcons);
|
|
// 如果是强制模式,需要重新计算已转换的数量
|
|
if (options.forceUpdate) {
|
|
const remainingIcons = webIcons.slice(options.maxIcons);
|
|
this.stats.alreadyConverted = remainingIcons.filter((icon) => rnIcons.has(icon));
|
|
}
|
|
}
|
|
|
|
console.log('📊 转换状态分析:');
|
|
console.log(`📦 总图标数: ${this.stats.total}`);
|
|
console.log(`✅ 已转换: ${this.stats.alreadyConverted.length}`);
|
|
console.log(`⏳ 待转换: ${this.stats.pending.length}`);
|
|
console.log('');
|
|
|
|
if (this.stats.pending.length === 0) {
|
|
console.log('🎉 所有图标已转换完成!');
|
|
return false;
|
|
}
|
|
|
|
console.log('📝 待转换图标列表:');
|
|
this.stats.pending.forEach((icon, index) => {
|
|
console.log(`${index + 1}. ${icon}`);
|
|
});
|
|
console.log('');
|
|
|
|
return true;
|
|
}
|
|
|
|
// 运行批量转换
|
|
private async runBatchConversion(options: ConversionOptions): Promise<boolean> {
|
|
if (this.stats.pending.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
console.log(`🚀 开始转换 ${this.stats.pending.length} 个图标...\n`);
|
|
|
|
if (options.dryRun) {
|
|
console.log('🔄 预演模式:不会实际转换文件');
|
|
this.stats.converted = [...this.stats.pending];
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// 调用批量转换器
|
|
const iconsToConvert = this.stats.pending.join(' ');
|
|
const command = `npx tsx batch-converter.ts ${iconsToConvert}`;
|
|
|
|
console.log(`📝 执行命令: ${command}`);
|
|
|
|
execSync(command, {
|
|
cwd: __dirname,
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
this.stats.converted = [...this.stats.pending];
|
|
console.log('\n✅ 批量转换完成');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('❌ 批量转换失败:', error);
|
|
this.stats.failed = [...this.stats.pending];
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 更新导出文件
|
|
private async updateExports(options: ConversionOptions): Promise<boolean> {
|
|
console.log('\n📦 更新导出文件...');
|
|
|
|
if (options.dryRun) {
|
|
console.log('🔄 预演模式:不会实际更新导出文件');
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const command = 'npx tsx export-updater.ts';
|
|
|
|
execSync(command, {
|
|
cwd: __dirname,
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
console.log('✅ 导出文件更新完成');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('❌ 导出文件更新失败:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 同步特性配置文件
|
|
private async syncFeatureConfigs(options: ConversionOptions): Promise<boolean> {
|
|
if (options.dryRun) {
|
|
console.log('\n🔄 预演模式:不会实际同步配置文件');
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const syncer = new FeatureConfigSyncer();
|
|
const success = await syncer.syncAll();
|
|
return success;
|
|
} catch (error) {
|
|
console.error('❌ 配置文件同步失败:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 运行构建测试
|
|
private async runBuildTest(options: ConversionOptions): Promise<boolean> {
|
|
if (!options.buildTest) {
|
|
return true;
|
|
}
|
|
|
|
console.log('\n🧪 运行构建测试...');
|
|
|
|
if (options.dryRun) {
|
|
console.log('🔄 预演模式:不会实际运行构建测试');
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const command = 'npm run build';
|
|
|
|
execSync(command, {
|
|
cwd: join(this.projectRoot, 'packages/react-native'),
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
console.log('✅ 构建测试通过');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('❌ 构建测试失败:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 运行代码检查
|
|
private async runLintCheck(options: ConversionOptions): Promise<boolean> {
|
|
if (!options.lintCheck) {
|
|
return true;
|
|
}
|
|
|
|
console.log('\n🔧 运行代码检查...');
|
|
|
|
if (options.dryRun) {
|
|
console.log('🔄 预演模式:不会实际运行代码检查');
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const command = 'npm run lint';
|
|
|
|
execSync(command, {
|
|
cwd: join(this.projectRoot, 'packages/react-native'),
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
console.log('✅ 代码检查通过');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('❌ 代码检查失败:', error);
|
|
console.log('💡 可以尝试运行 npm run lint:fix 自动修复部分问题');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 显示最终统计
|
|
private showFinalStats() {
|
|
console.log('\n📊 转换完成统计:');
|
|
console.log(`📦 总图标数: ${this.stats.total}`);
|
|
console.log(`✅ 本次转换: ${this.stats.converted.length}`);
|
|
console.log(`❌ 转换失败: ${this.stats.failed.length}`);
|
|
console.log(`⏭️ 已跳过: ${this.stats.skipped.length}`);
|
|
console.log(`🔄 已存在: ${this.stats.alreadyConverted.length}`);
|
|
|
|
if (this.stats.failed.length > 0) {
|
|
console.log('\n❌ 失败的图标:');
|
|
this.stats.failed.forEach((icon) => console.log(` - ${icon}`));
|
|
}
|
|
|
|
if (this.stats.converted.length > 0) {
|
|
console.log('\n✅ 成功转换的图标:');
|
|
this.stats.converted.slice(0, 10).forEach((icon) => console.log(` - ${icon}`));
|
|
if (this.stats.converted.length > 10) {
|
|
console.log(` ... 还有 ${this.stats.converted.length - 10} 个图标`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 主执行函数
|
|
async execute(options: ConversionOptions = {}): Promise<boolean> {
|
|
const startTime = Date.now();
|
|
|
|
console.log('🚀 Hanzo Icons 自动转换器');
|
|
console.log('================================\n');
|
|
|
|
// 分析转换状态
|
|
const hasWork = this.analyzeConversionStatus(options);
|
|
|
|
if (!hasWork) {
|
|
return true;
|
|
}
|
|
|
|
let success = true;
|
|
|
|
// 执行转换流程
|
|
if (success) {
|
|
success = await this.runBatchConversion(options);
|
|
}
|
|
|
|
if (success) {
|
|
success = await this.updateExports(options);
|
|
}
|
|
|
|
if (success) {
|
|
success = await this.syncFeatureConfigs(options);
|
|
}
|
|
|
|
if (success && options.lintCheck) {
|
|
success = await this.runLintCheck(options);
|
|
}
|
|
|
|
if (success && options.buildTest) {
|
|
success = await this.runBuildTest(options);
|
|
}
|
|
|
|
// 显示统计
|
|
this.showFinalStats();
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`\n⏱️ 总耗时: ${(duration / 1000).toFixed(2)}秒`);
|
|
|
|
if (success) {
|
|
console.log('\n🎉 自动转换完成!');
|
|
} else {
|
|
console.log('\n💥 转换过程中出现错误');
|
|
}
|
|
|
|
return success;
|
|
}
|
|
}
|
|
|
|
// 显示帮助信息
|
|
function showHelp() {
|
|
console.log(`
|
|
🚀 Hanzo Icons 自动转换器
|
|
|
|
用法:
|
|
npx tsx auto-converter.ts [选项]
|
|
|
|
选项:
|
|
-n, --dry-run 预演模式,不实际执行转换
|
|
-t, --build-test 转换后运行构建测试
|
|
-l, --lint 转换后运行代码检查
|
|
-f, --force 强制更新,包括已存在的图标
|
|
-m, --max <数量> 限制转换的图标数量
|
|
-h, --help 显示帮助信息
|
|
|
|
示例:
|
|
npx tsx auto-converter.ts # 转换所有未转换的图标
|
|
npx tsx auto-converter.ts --dry-run # 预览要转换的图标
|
|
npx tsx auto-converter.ts --max 10 # 只转换前10个图标
|
|
npx tsx auto-converter.ts --lint # 转换后运行代码检查
|
|
npx tsx auto-converter.ts --build-test # 转换后运行构建测试
|
|
npx tsx auto-converter.ts --lint --build-test # 转换后运行代码检查和构建测试
|
|
npx tsx auto-converter.ts --force # 强制重新转换所有图标
|
|
`);
|
|
}
|
|
|
|
// 命令行参数解析
|
|
function parseArgs(): ConversionOptions {
|
|
const args = process.argv.slice(2);
|
|
const options: ConversionOptions = {
|
|
buildTest: false,
|
|
dryRun: false,
|
|
forceUpdate: false,
|
|
lintCheck: false,
|
|
skipExisting: true,
|
|
};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
switch (arg) {
|
|
case '--dry-run':
|
|
case '-n': {
|
|
options.dryRun = true;
|
|
break;
|
|
}
|
|
case '--build-test':
|
|
case '-t': {
|
|
options.buildTest = true;
|
|
break;
|
|
}
|
|
case '--lint':
|
|
case '-l': {
|
|
options.lintCheck = true;
|
|
break;
|
|
}
|
|
case '--force':
|
|
case '-f': {
|
|
options.forceUpdate = true;
|
|
options.skipExisting = false;
|
|
break;
|
|
}
|
|
case '--max':
|
|
case '-m': {
|
|
const maxValue = args[i + 1];
|
|
if (maxValue && !isNaN(Number(maxValue))) {
|
|
options.maxIcons = Number(maxValue);
|
|
i++; // 跳过下一个参数
|
|
}
|
|
break;
|
|
}
|
|
case '--help':
|
|
case '-h': {
|
|
showHelp();
|
|
process.exit(0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
// 主函数
|
|
async function main() {
|
|
try {
|
|
const options = parseArgs();
|
|
const converter = new AutoConverter();
|
|
const success = await converter.execute(options);
|
|
|
|
process.exit(success ? 0 : 1);
|
|
} catch (error) {
|
|
console.error('💥 执行失败:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
|
main().catch((error) => {
|
|
console.error('❌ 主函数执行失败:', error);
|
|
process.exit(1);
|
|
});
|