71 lines
5.6 KiB
JavaScript
71 lines
5.6 KiB
JavaScript
// Reproduce with installed lucide-react 1.69.0 and an extracted @tabler/icons 3.50.0
|
|
// tarball: node scripts/generate-canvas-icons.mjs /path/to/tabler/package
|
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { resolve } from "node:path";
|
|
import dynamicIconImports from "lucide-react/dynamicIconImports";
|
|
import { chinese, keywords } from "./canvas-icon-labels.mjs";
|
|
|
|
const tablerDir = process.argv[2];
|
|
if (!tablerDir) throw new Error("Pass the extracted @tabler/icons 3.50.0 directory");
|
|
const tablerPackage = JSON.parse(await readFile(resolve(tablerDir, "package.json"), "utf8"));
|
|
if (tablerPackage.version !== "3.50.0") throw new Error("Expected @tabler/icons 3.50.0");
|
|
const lucidePackage = JSON.parse(await readFile("node_modules/lucide-react/package.json", "utf8"));
|
|
if (lucidePackage.version !== "1.69.0") throw new Error("Review generated assets when upgrading Lucide");
|
|
const pascal = (slug) => { const value = slug.split("-").map((word) => word[0].toUpperCase() + word.slice(1)).join(""); return /^[A-Z]/.test(value) ? value : "Icon" + value; };
|
|
const categories = [
|
|
["arrows", /arrow|chevron|navigation|corner|undo|redo|move|expand|shrink/],
|
|
["people", /user|person|baby|smile|frown|angry|laugh|heart|hand|thumb|accessibility/],
|
|
["office", /file|folder|book|notebook|clipboard|calendar|mail|message|text|type|list|table|pencil|pen-|archive/],
|
|
["media", /image|photo|camera|video|movie|film|music|audio|volume|mic|headphone|play|pause|palette|paint|brush|crop/],
|
|
["tech", /code|terminal|cpu|circuit|robot|brain|bot|monitor|laptop|phone|computer|database|server|network|wifi|bluetooth|battery|plug|usb|chip/],
|
|
["business", /chart|graph|trend|dollar|euro|currency|coin|bank|wallet|credit|shopping|store|briefcase|receipt|badge|ticket/],
|
|
["nature", /sun|moon|cloud|snow|rain|wind|leaf|tree|flower|sprout|mountain|dog|cat|fish|bird|bug|rabbit|turtle|paw|flame|waves/],
|
|
["travel", /map|pin|compass|globe|plane|train|car|bus|bike|ship|boat|building|house|home|tent|hotel|lighthouse|rocket|ufo/],
|
|
["shapes", /circle|square|triangle|hexagon|pentagon|octagon|diamond|star|shape|cube|sphere|box/],
|
|
];
|
|
const common = new Set(["Star", "Heart", "Lightbulb", "Sparkles", "Zap", "Target", "Check", "Smile", "Coffee", "Leaf", "Sun", "Moon", "Globe", "MapPin", "ArrowRight", "BookOpen", "FileText", "Folder", "Calendar", "Users", "Image", "Camera", "Music", "Palette"]);
|
|
const entries = new Map();
|
|
for (const [slug, load] of Object.entries(dynamicIconImports)) {
|
|
const name = pascal(slug);
|
|
const { __iconData } = await load();
|
|
entries.set(name, { name, slug, source: "lucide", label: chinese[name] || slug.replaceAll("-", " "), node: __iconData.node });
|
|
}
|
|
const tabler = ["alien", "ufo", "robot", "brain", "circuit-cell", "cube-3d-sphere", "hexagon-3d", "building-lighthouse", "cat", "dog", "fish", "leaf", "sun", "moon", "rocket", "bulb", "heart", "star", "photo", "movie", "music", "palette", "coffee", "mountain"];
|
|
const tablerLabels = ["外星人", "飞碟", "机器人", "大脑", "电路", "立体球", "立体六边形", "灯塔", "猫", "狗", "鱼", "叶子", "太阳", "月亮", "火箭", "灯泡", "爱心", "星星", "照片", "电影", "音乐", "调色板", "咖啡", "山峰"];
|
|
for (const [index, slug] of tabler.entries()) {
|
|
const svg = await readFile(resolve(tablerDir, "icons/outline", slug + ".svg"), "utf8");
|
|
const node = [...svg.matchAll(/<(path|circle|ellipse|rect|line|polyline|polygon)\s+([^>]+?)\/?\s*>/g)].map((match, index) => [match[1], Object.fromEntries([
|
|
["key", String(index)], ...[...match[2].matchAll(/([\w-]+)="([^"]*)"/g)].map(([, key, value]) => [key.replace(/-([a-z])/g, (_, char) => char.toUpperCase()), value]),
|
|
])]);
|
|
if (!node.length) throw new Error("Empty Tabler SVG " + slug);
|
|
const name = "Tabler" + pascal(slug);
|
|
entries.set(name, { name, slug, source: "tabler", label: tablerLabels[index], node });
|
|
}
|
|
const records = [...entries.values()].sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
const shards = new Map();
|
|
const bucket = (name) => { let hash = 0; for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0; return hash % 96; };
|
|
const catalog = records.map(({ node, ...entry }) => {
|
|
const key = entry.source === "tabler" ? "tabler" : `lucide-${bucket(entry.name)}`;
|
|
if (!shards.has(key)) shards.set(key, {});
|
|
shards.get(key)[entry.name] = node;
|
|
return { ...entry, categories: categories.filter(([, regex]) => regex.test(entry.slug)).map(([id]) => id), common: common.has(entry.name) || entry.source === "tabler", keywords: entry.slug.split("-").map((word) => keywords[word] || "").join(" ") };
|
|
});
|
|
const assetDir = "public/canvas-icons/lucide-1.69.0-tabler-3.50.0";
|
|
await mkdir(assetDir, { recursive: true });
|
|
let totalBytes = 0;
|
|
const assets = {};
|
|
for (const [key, nodes] of shards) {
|
|
const content = JSON.stringify(nodes);
|
|
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
const filename = `${key}-${hash}.json`;
|
|
assets[key] = filename;
|
|
await writeFile(`${assetDir}/${filename}`, content + "\n");
|
|
totalBytes += Buffer.byteLength(content);
|
|
}
|
|
await writeFile("src/components/canvas/icon-catalog-data.json", JSON.stringify(catalog) + "\n");
|
|
await writeFile("src/lib/canvas-icon-assets.json", JSON.stringify({ root: "/" + assetDir.replace(/^public\//, ""), assets }) + "\n");
|
|
await writeFile(`${assetDir}/LUCIDE-LICENSE.txt`, await readFile("node_modules/lucide-react/LICENSE"));
|
|
await writeFile(`${assetDir}/TABLER-LICENSE.txt`, await readFile(resolve(tablerDir, "LICENSE")));
|
|
console.log(`${records.length} icons (${records.filter((item) => item.source === "tabler").length} Tabler), ${shards.size} data shards, ${totalBytes} uncompressed bytes`);
|