File size: 6,662 Bytes
96dd062 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | /**
* 图标预处理脚本
* 在构建时自动扫描 Svelte 组件中使用的图标,并生成内联 SVG 数据
*
* 使用方法:node scripts/generate-icons.js
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { getIconData, iconToSVG, iconToHTML, replaceIDs } from "@iconify/utils";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, "..");
const SRC_DIR = join(ROOT_DIR, "src");
const OUTPUT_FILE = join(SRC_DIR, "constants", "icons.ts");
// 支持的图标集及其包名
const ICON_SETS = {
"material-symbols": "@iconify-json/material-symbols",
"fa7-solid": "@iconify-json/fa7-solid",
"fa7-brands": "@iconify-json/fa7-brands",
"fa7-regular": "@iconify-json/fa7-regular",
mdi: "@iconify-json/mdi",
"simple-icons": "@iconify-json/simple-icons",
"svg-spinners": "@iconify-json/svg-spinners",
};
// 图标集数据缓存
const iconSetCache = new Map();
/**
* 递归获取目录下所有文件
*/
function getAllFiles(dir, extensions = [".svelte"]) {
const files = [];
function walk(currentDir) {
const items = readdirSync(currentDir);
for (const item of items) {
const fullPath = join(currentDir, item);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
// 跳过 node_modules 和隐藏目录
if (!item.startsWith(".") && item !== "node_modules") {
walk(fullPath);
}
} else if (extensions.some((ext) => item.endsWith(ext))) {
files.push(fullPath);
}
}
}
walk(dir);
return files;
}
/**
* 从文件内容中提取图标名称
*/
function extractIconNames(content) {
const icons = new Set();
// 匹配各种图标使用模式
const patterns = [
// icon="xxx:yyy" 或 icon='xxx:yyy'
/icon=["']([a-z0-9-]+:[a-z0-9-]+)["']/gi,
// icon={`xxx:yyy`}
/icon=\{[`"']([a-z0-9-]+:[a-z0-9-]+)[`"']\}/gi,
// getIconSvg("xxx:yyy") 或 getIconSvg('xxx:yyy')
/getIconSvg\(["']([a-z0-9-]+:[a-z0-9-]+)["']\)/gi,
// hasIcon("xxx:yyy")
/hasIcon\(["']([a-z0-9-]+:[a-z0-9-]+)["']\)/gi,
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
icons.add(match[1]);
}
}
return icons;
}
/**
* 加载图标集数据
*/
async function loadIconSet(prefix) {
if (iconSetCache.has(prefix)) {
return iconSetCache.get(prefix);
}
const packageName = ICON_SETS[prefix];
if (!packageName) {
console.warn(`⚠️ 未知图标集: ${prefix}`);
return null;
}
try {
// 动态导入图标集 JSON
const iconSetPath = join(ROOT_DIR, "node_modules", packageName, "icons.json");
const data = JSON.parse(readFileSync(iconSetPath, "utf-8"));
iconSetCache.set(prefix, data);
return data;
} catch (error) {
console.warn(`⚠️ 无法加载图标集 ${packageName}: ${error.message}`);
return null;
}
}
/**
* 获取单个图标的 SVG
*/
async function getIconSvg(iconName) {
const [prefix, name] = iconName.split(":");
if (!prefix || !name) {
console.warn(`⚠️ 无效的图标名称: ${iconName}`);
return null;
}
const iconSet = await loadIconSet(prefix);
if (!iconSet) {
return null;
}
const iconData = getIconData(iconSet, name);
if (!iconData) {
console.warn(`⚠️ 图标未找到: ${iconName}`);
return null;
}
// 转换为 SVG
const renderData = iconToSVG(iconData, {
height: "1em",
width: "1em",
});
let svg = iconToHTML(replaceIDs(renderData.body), renderData.attributes);
// 确保支持 currentColor
if (!svg.includes("currentColor")) {
svg = svg.replace("<svg", '<svg fill="currentColor"');
}
return svg;
}
/**
* 生成 icons.ts 文件
*/
function generateIconsFile(iconsMap) {
const iconEntries = Array.from(iconsMap.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, svg]) => `\t"${name}":\n\t\t'${svg.replace(/'/g, "\\'")}'`)
.join(",\n");
const content = `/**
* 自动生成的图标数据文件
* 由 scripts/generate-icons.js 在构建时生成
* 请勿手动编辑此文件
*/
const iconSvgData: Record<string, string> = {
${iconEntries}
};
/**
* 根据 iconify 格式的图标名获取内联 SVG HTML
* @param iconName 图标名称,如 "material-symbols:search"
* @returns SVG HTML 字符串
*/
export function getIconSvg(iconName: string): string {
return iconSvgData[iconName] || "";
}
/**
* 检查图标是否可用
*/
export function hasIcon(iconName: string): boolean {
return iconName in iconSvgData;
}
/**
* 获取所有可用图标名称
*/
export function getAvailableIcons(): string[] {
return Object.keys(iconSvgData);
}
export default iconSvgData;
`;
return content;
}
/**
* 主函数
*/
async function main() {
console.log("🔍 扫描源文件中的图标使用...\n");
// 获取所有源文件
const files = getAllFiles(SRC_DIR);
console.log(`📁 找到 ${files.length} 个源文件\n`);
// 收集所有使用的图标
const allIcons = new Set();
for (const file of files) {
// 跳过 icons.ts 文件本身
if (file.endsWith("icons.ts")) continue;
const content = readFileSync(file, "utf-8");
const icons = extractIconNames(content);
for (const icon of icons) {
allIcons.add(icon);
}
}
console.log(`🎨 发现 ${allIcons.size} 个不同的图标:\n`);
// 按图标集分组显示
const iconsBySet = {};
for (const icon of allIcons) {
const [prefix] = icon.split(":");
if (!iconsBySet[prefix]) {
iconsBySet[prefix] = [];
}
iconsBySet[prefix].push(icon);
}
for (const [prefix, icons] of Object.entries(iconsBySet)) {
console.log(` ${prefix}: ${icons.length} 个图标`);
}
console.log("");
// 获取所有图标的 SVG
const iconsMap = new Map();
let successCount = 0;
let failCount = 0;
for (const iconName of allIcons) {
const svg = await getIconSvg(iconName);
if (svg) {
iconsMap.set(iconName, svg);
successCount++;
} else {
failCount++;
}
}
console.log(`✅ 成功加载 ${successCount} 个图标`);
if (failCount > 0) {
console.log(`❌ 失败 ${failCount} 个图标`);
}
// 生成输出文件
const output = generateIconsFile(iconsMap);
writeFileSync(OUTPUT_FILE, output, "utf-8");
console.log(`\n📝 已生成: ${OUTPUT_FILE}`);
console.log(`📦 文件大小: ${(Buffer.byteLength(output, "utf-8") / 1024).toFixed(2)} KB\n`);
}
main().catch(console.error);
|