|
| 1 | +#!/usr/bin/env node |
| 2 | +// 从本地安装的 Wappalyzer 扩展抽取与我们 rules name 命中的图标,放到 public/icons/tech/<slug>.{svg,png} |
| 3 | +// 用法:WAPPALYZER_ICON_DIR=<wappalyzer 安装目录的 images/icons> node build-scripts/extract-wappalyzer-icons.mjs |
| 4 | +// 默认到百分浏览器(Cent Browser)的 Wappalyzer 6.12.2 安装路径找 |
| 5 | +import fs from 'node:fs' |
| 6 | +import path from 'node:path' |
| 7 | +import { fileURLToPath } from 'node:url' |
| 8 | + |
| 9 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
| 10 | +const repoRoot = path.resolve(__dirname, '..') |
| 11 | + |
| 12 | +const DEFAULT_DIR = |
| 13 | + 'C:/Users/19622/AppData/Local/CentBrowser/User Data/Default/Extensions/gppongmhjkpfnbhagpmjfkannfbllamg/6.12.2_0/images/icons' |
| 14 | +const ICON_DIR = process.env.WAPPALYZER_ICON_DIR || DEFAULT_DIR |
| 15 | +const RULES_DIR = path.join(repoRoot, 'public', 'rules') |
| 16 | +const OUTPUT_DIR = path.join(repoRoot, 'public', 'icons', 'tech') |
| 17 | + |
| 18 | +if (!fs.existsSync(ICON_DIR)) { |
| 19 | + console.error(`找不到 Wappalyzer 图标目录:${ICON_DIR}`) |
| 20 | + console.error('请设置环境变量 WAPPALYZER_ICON_DIR 指向本地安装的 images/icons 目录') |
| 21 | + process.exit(1) |
| 22 | +} |
| 23 | + |
| 24 | +const collectNames = (node, out) => { |
| 25 | + if (!node || typeof node !== 'object') return |
| 26 | + if (Array.isArray(node)) { |
| 27 | + for (const i of node) collectNames(i, out) |
| 28 | + return |
| 29 | + } |
| 30 | + if (typeof node.name === 'string') out.add(node.name.trim()) |
| 31 | + for (const v of Object.values(node)) collectNames(v, out) |
| 32 | +} |
| 33 | + |
| 34 | +const walk = (dir, files = []) => { |
| 35 | + for (const e of fs.readdirSync(dir)) { |
| 36 | + const p = path.join(dir, e) |
| 37 | + if (fs.statSync(p).isDirectory()) walk(p, files) |
| 38 | + else if (e.endsWith('.json')) files.push(p) |
| 39 | + } |
| 40 | + return files |
| 41 | +} |
| 42 | + |
| 43 | +const slugify = raw => |
| 44 | + String(raw || '') |
| 45 | + .toLowerCase() |
| 46 | + .replace(/[^a-z0-9]/g, '') |
| 47 | + |
| 48 | +// 收 Wappalyzer 图标库:filename → [候选文件名(可能多种扩展名)] |
| 49 | +const iconBySlug = new Map() |
| 50 | +for (const f of fs.readdirSync(ICON_DIR)) { |
| 51 | + if (!f.endsWith('.svg') && !f.endsWith('.png')) continue |
| 52 | + const slug = slugify(f.replace(/\.(svg|png)$/i, '')) |
| 53 | + if (!slug) continue |
| 54 | + if (!iconBySlug.has(slug)) iconBySlug.set(slug, []) |
| 55 | + iconBySlug.get(slug).push(f) |
| 56 | +} |
| 57 | + |
| 58 | +// 收我们 rules 里所有 name |
| 59 | +const ruleNames = new Set() |
| 60 | +for (const f of walk(RULES_DIR)) { |
| 61 | + try { |
| 62 | + collectNames(JSON.parse(fs.readFileSync(f, 'utf8')), ruleNames) |
| 63 | + } catch { |
| 64 | + // ignore parse errors |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +// 清掉旧的输出目录,重新抽 |
| 69 | +if (fs.existsSync(OUTPUT_DIR)) fs.rmSync(OUTPUT_DIR, { recursive: true, force: true }) |
| 70 | +fs.mkdirSync(OUTPUT_DIR, { recursive: true }) |
| 71 | + |
| 72 | +let copied = 0 |
| 73 | +let svgCount = 0 |
| 74 | +let pngCount = 0 |
| 75 | +let totalBytes = 0 |
| 76 | +const manifest = {} |
| 77 | +const seenSlugs = new Set() |
| 78 | +for (const name of ruleNames) { |
| 79 | + // 跟我们 TechChip 里的 slugify 算法保持一致:取 / 之前的部分,小写 + 去掉所有非字母数字 |
| 80 | + const slug = slugify(name.split('/')[0]) |
| 81 | + if (!slug || seenSlugs.has(slug)) continue |
| 82 | + const candidates = iconBySlug.get(slug) |
| 83 | + if (!candidates) continue |
| 84 | + |
| 85 | + // 优先 svg |
| 86 | + const file = candidates.find(c => c.endsWith('.svg')) || candidates[0] |
| 87 | + const ext = path.extname(file).toLowerCase().slice(1) |
| 88 | + const dst = path.join(OUTPUT_DIR, slug + '.' + ext) |
| 89 | + fs.copyFileSync(path.join(ICON_DIR, file), dst) |
| 90 | + seenSlugs.add(slug) |
| 91 | + manifest[slug] = ext |
| 92 | + copied++ |
| 93 | + totalBytes += fs.statSync(dst).size |
| 94 | + if (ext === 'svg') svgCount++ |
| 95 | + else pngCount++ |
| 96 | +} |
| 97 | + |
| 98 | +// 写出 manifest,运行时 TechChip 用它判断本地是否有图标、是 svg 还是 png,避免无意义 404 |
| 99 | +const sortedManifest = {} |
| 100 | +for (const slug of Object.keys(manifest).sort()) sortedManifest[slug] = manifest[slug] |
| 101 | +const manifestPath = path.join(repoRoot, 'src', 'ui', 'components', 'local-icon-manifest.json') |
| 102 | +fs.writeFileSync(manifestPath, JSON.stringify(sortedManifest) + '\n', 'utf8') |
| 103 | + |
| 104 | +console.log(`抽取完成:${copied} 个图标 (svg: ${svgCount}, png: ${pngCount})`) |
| 105 | +console.log(`输出目录:${OUTPUT_DIR}`) |
| 106 | +console.log(`总大小:${(totalBytes / 1024 / 1024).toFixed(2)} MB`) |
| 107 | +console.log(`manifest:${manifestPath}`) |
0 commit comments