| |
| |
| |
| |
| |
|
|
|
|
| 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()) {
|
|
|
| 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=["']([a-z0-9-]+:[a-z0-9-]+)["']/gi,
|
|
|
| /icon=\{[`"']([a-z0-9-]+:[a-z0-9-]+)[`"']\}/gi,
|
|
|
| /getIconSvg\(["']([a-z0-9-]+:[a-z0-9-]+)["']\)/gi,
|
|
|
| /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 {
|
|
|
| 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;
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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;
|
| }
|
|
|
|
|
| const renderData = iconToSVG(iconData, {
|
| height: "1em",
|
| width: "1em",
|
| });
|
|
|
| let svg = iconToHTML(replaceIDs(renderData.body), renderData.attributes);
|
|
|
|
|
| if (!svg.includes("currentColor")) {
|
| svg = svg.replace("<svg", '<svg fill="currentColor"');
|
| }
|
|
|
| return svg;
|
| }
|
|
|
| |
| |
|
|
| 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);
|
| |