/** * Manual one-off sync — NOT wired into build or pack. * node scripts/sync-docker-icons.mjs * * Generates colored SVGs (Simple Icons, CC0) plus a few official brand assets * into public/docker-icons/, then writes domain/systemManager/dockerIconBundled.ts. */ import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as simpleIcons from 'simple-icons'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const SRC_FILE = join(ROOT, 'domain/systemManager/dockerImageIcons.ts'); const OUT_DIR = join(ROOT, 'public/docker-icons'); const MANIFEST_FILE = join(ROOT, 'domain/systemManager/dockerIconBundled.ts'); /** * Official assets not covered by Simple Icons — see public/docker-icons/NOTICE.md * Prefer SVG when the upstream project publishes one; PNG only when no SVG exists. */ const OFFICIAL_ICONS = [ { iconId: 'nacos', file: 'nacos.svg', url: 'https://raw.githubusercontent.com/nacos-group/nacos-logo/master/Nacos%20logo%20%E7%99%BD%E8%93%9D%E8%B5%84%E6%BA%90%205.svg', }, { iconId: 'polaris', file: 'polaris.svg', url: 'https://raw.githubusercontent.com/polarismesh/polaris/main/logo.svg', }, { iconId: 'memcached', file: 'memcached.png', url: 'https://memcached.org/images/memcached_link_125.png', }, ]; const slugById = parseStringRecord(SRC_FILE, 'SIMPLE_ICONS_SLUG'); const styleById = parseTileStyles(SRC_FILE); const iconsBySlug = new Map(); for (const icon of Object.values(simpleIcons)) { if (icon && typeof icon === 'object' && 'slug' in icon && 'path' in icon) { iconsBySlug.set(icon.slug, icon); } } mkdirSync(OUT_DIR, { recursive: true }); const bundled = []; const iconFiles = {}; const skipped = []; for (const [iconId, slug] of Object.entries(slugById)) { const brand = iconsBySlug.get(slug); if (!brand) { skipped.push({ iconId, slug, reason: 'slug not in simple-icons' }); continue; } const iconColor = styleById[iconId]?.iconColor ?? 'ffffff'; const fill = iconColor.startsWith('#') ? iconColor : `#${iconColor}`; const file = `${iconId}.svg`; const svg = [ ``, ` ${escapeXml(brand.title)}`, ` `, ``, '', ].join('\n'); writeFileSync(join(OUT_DIR, file), svg, 'utf8'); bundled.push(iconId); iconFiles[iconId] = file; } for (const entry of OFFICIAL_ICONS) { const response = await fetch(entry.url); if (!response.ok) { throw new Error(`Failed to download ${entry.iconId} from ${entry.url}: ${response.status}`); } const buffer = Buffer.from(await response.arrayBuffer()); writeFileSync(join(OUT_DIR, entry.file), buffer); bundled.push(entry.iconId); iconFiles[entry.iconId] = entry.file; console.log(`Downloaded official ${entry.iconId} -> ${entry.file}`); } const bundledSet = new Set(bundled); for (const file of readdirSync(OUT_DIR)) { if (file === 'NOTICE.md') continue; const iconId = file.replace(/\.(svg|png|webp)$/i, ''); const owned = bundledSet.has(iconId) && iconFiles[iconId] === file; if (!owned) { unlinkSync(join(OUT_DIR, file)); console.log(`Removed stale ${file}`); } } bundled.sort(); const fileEntries = bundled .filter((id) => iconFiles[id] !== `${id}.svg`) .map((id) => ` '${id}': '${iconFiles[id]}',`); writeFileSync( MANIFEST_FILE, [ '// Auto-generated by scripts/sync-docker-icons.mjs — do not edit.', 'export const BUNDLED_DOCKER_ICON_IDS = new Set([', ...bundled.map((id) => ` '${id}',`), ']);', '', '/** Non-default filenames (e.g. official PNG assets). Default is `${id}.svg`. */', 'export const DOCKER_ICON_FILES: Record = {', ...fileEntries, '};', '', ].join('\n'), 'utf8', ); console.log(`Wrote ${bundled.length} icons to public/docker-icons/`); console.log(`Updated ${MANIFEST_FILE}`); if (skipped.length > 0) { console.log(`Skipped ${skipped.length} (no official Simple Icons slug):`); for (const item of skipped) { console.log(` ${item.iconId} -> ${item.slug}`); } } function parseStringRecord(filePath, constName) { const src = readFileSync(filePath, 'utf8'); const start = src.indexOf(`const ${constName}`); if (start < 0) throw new Error(`Could not find ${constName} in ${filePath}`); const braceStart = src.indexOf('{', start); const braceEnd = findMatchingBrace(src, braceStart); const block = src.slice(braceStart + 1, braceEnd); const out = {}; for (const line of block.split('\n')) { const match = line.match(/^\s*([a-zA-Z0-9_]+):\s*'([^']*)',?\s*$/); if (match) out[match[1]] = match[2]; } return out; } function parseTileStyles(filePath) { const src = readFileSync(filePath, 'utf8'); const start = src.indexOf('const ICON_TILE_STYLE'); if (start < 0) throw new Error(`Could not find ICON_TILE_STYLE in ${filePath}`); const braceStart = src.indexOf('{', start); const braceEnd = findMatchingBrace(src, braceStart); const block = src.slice(braceStart + 1, braceEnd); const out = {}; const re = /^\s*([a-zA-Z0-9_]+):\s*\{\s*background:\s*'([^']*)',\s*iconColor:\s*'([^']*)'\s*\},?\s*$/; for (const line of block.split('\n')) { const match = line.match(re); if (match) { out[match[1]] = { background: match[2], iconColor: match[3] }; } } return out; } function findMatchingBrace(src, openIndex) { let depth = 0; for (let i = openIndex; i < src.length; i += 1) { if (src[i] === '{') depth += 1; else if (src[i] === '}') { depth -= 1; if (depth === 0) return i; } } throw new Error('Unbalanced braces while parsing dockerImageIcons.ts'); } function escapeXml(value) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }