165 lines
6.7 KiB
JavaScript
165 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Generate SVG sprite sheet from Lucide icons
|
|
*
|
|
* Usage: npm run icons:build
|
|
*
|
|
* This script auto-discovers icons used in the codebase and generates
|
|
* an SVG sprite file with <symbol> elements for each icon.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { globSync } = require('glob');
|
|
|
|
/**
|
|
* Auto-discover icons from the codebase
|
|
*/
|
|
function discoverIcons() {
|
|
const icons = new Set();
|
|
const basePath = path.join(__dirname, '..');
|
|
|
|
// 1. Scan templates for {{ icon "name" ... }}
|
|
const templatePattern = /\{\{\s*icon\s+"([^"]+)"/g;
|
|
const templates = [
|
|
...globSync('pkg/appview/templates/**/*.html', { cwd: basePath }),
|
|
...globSync('pkg/hold/admin/templates/**/*.html', { cwd: basePath }),
|
|
];
|
|
templates.forEach(file => {
|
|
const content = fs.readFileSync(path.join(basePath, file), 'utf8');
|
|
let match;
|
|
while ((match = templatePattern.exec(content)) !== null) {
|
|
icons.add(match[1]);
|
|
}
|
|
});
|
|
|
|
// 2. Scan templates and JS for icons.svg#name (direct SVG use references)
|
|
const svgUsePattern = /icons\.svg#([a-z0-9-]+)/g;
|
|
const allFiles = [
|
|
...globSync('pkg/appview/templates/**/*.html', { cwd: basePath }),
|
|
...globSync('pkg/hold/admin/templates/**/*.html', { cwd: basePath }),
|
|
...globSync('pkg/appview/src/js/**/*.js', { cwd: basePath }),
|
|
...globSync('pkg/hold/admin/src/js/**/*.js', { cwd: basePath }),
|
|
];
|
|
allFiles.forEach(file => {
|
|
const content = fs.readFileSync(path.join(basePath, file), 'utf8');
|
|
let match;
|
|
while ((match = svgUsePattern.exec(content)) !== null) {
|
|
icons.add(match[1]);
|
|
}
|
|
});
|
|
|
|
// 3. Scan JS for iconMap object values (theme toggle, etc.)
|
|
const jsFiles = globSync('pkg/appview/src/js/**/*.js', { cwd: basePath });
|
|
const iconMapPattern = /iconMap\s*=\s*\{([^}]+)\}/g;
|
|
const iconValuePattern = /['"]([a-z][a-z0-9-]*)['"](?:\s*[:,])/g;
|
|
jsFiles.forEach(file => {
|
|
const content = fs.readFileSync(path.join(basePath, file), 'utf8');
|
|
let mapMatch;
|
|
while ((mapMatch = iconMapPattern.exec(content)) !== null) {
|
|
const mapContent = mapMatch[1];
|
|
let valueMatch;
|
|
while ((valueMatch = iconValuePattern.exec(mapContent)) !== null) {
|
|
// Only add values (after colon), not keys
|
|
icons.add(valueMatch[1]);
|
|
}
|
|
}
|
|
});
|
|
|
|
return Array.from(icons).sort();
|
|
}
|
|
|
|
const ICONS = discoverIcons();
|
|
|
|
// Custom Helm icon (from Simple Icons - official Helm logo)
|
|
const CUSTOM_ICONS = {
|
|
'helm': {
|
|
viewBox: '0 0 24 24',
|
|
content: '<path d="M12.337 0c-.475 0-.861 1.016-.861 2.269 0 .527.069 1.011.183 1.396a8.514 8.514 0 0 0-3.961 1.22 5.229 5.229 0 0 0-.595-1.093c-.606-.866-1.34-1.436-1.79-1.43a.381.381 0 0 0-.217.066c-.39.273-.123 1.326.596 2.353.267.381.559.705.84.948a8.683 8.683 0 0 0-1.528 1.716h1.734a7.179 7.179 0 0 1 5.381-2.421 7.18 7.18 0 0 1 5.382 2.42h1.733a8.687 8.687 0 0 0-1.32-1.53c.35-.249.735-.643 1.078-1.133.719-1.027.986-2.08.596-2.353a.382.382 0 0 0-.217-.065c-.45-.007-1.184.563-1.79 1.43a4.897 4.897 0 0 0-.676 1.325 8.52 8.52 0 0 0-3.899-1.42c.12-.39.193-.887.193-1.429 0-1.253-.386-2.269-.862-2.269zM1.624 9.443v5.162h1.358v-1.968h1.64v1.968h1.357V9.443H4.62v1.838H2.98V9.443zm5.912 0v5.162h3.21v-1.108H8.893v-.95h1.64v-1.142h-1.64v-.84h1.853V9.443zm4.698 0v5.162h3.218v-1.362h-1.86v-3.8zm4.706 0v5.162h1.364v-2.643l1.357 1.225 1.35-1.232v2.65h1.365V9.443h-.614l-2.1 1.914-2.109-1.914zm-11.82 7.28a8.688 8.688 0 0 0 1.412 1.548 5.206 5.206 0 0 0-.841.948c-.719 1.027-.985 2.08-.596 2.353.39.273 1.289-.338 2.007-1.364a5.23 5.23 0 0 0 .595-1.092 8.514 8.514 0 0 0 3.961 1.219 5.01 5.01 0 0 0-.183 1.396c0 1.253.386 2.269.861 2.269.476 0 .862-1.016.862-2.269 0-.542-.072-1.04-.193-1.43a8.52 8.52 0 0 0 3.9-1.42c.121.4.352.865.675 1.327.719 1.026 1.617 1.637 2.007 1.364.39-.273.123-1.326-.596-2.353-.343-.49-.727-.885-1.077-1.135a8.69 8.69 0 0 0 1.202-1.36h-1.771a7.174 7.174 0 0 1-5.227 2.252 7.174 7.174 0 0 1-5.226-2.252z" fill="currentColor" stroke="none"/>'
|
|
}
|
|
};
|
|
|
|
// Lucide icon name to Pascal case mapping (for require path)
|
|
function toPascalCase(str) {
|
|
return str.split('-').map(word =>
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
).join('');
|
|
}
|
|
|
|
// Get icon content from lucide package
|
|
function getLucideIcon(iconName) {
|
|
try {
|
|
const lucide = require('lucide');
|
|
const pascalName = toPascalCase(iconName);
|
|
const iconData = lucide[pascalName];
|
|
|
|
if (!iconData) {
|
|
console.warn(`Warning: Icon "${iconName}" (${pascalName}) not found in lucide package`);
|
|
return null;
|
|
}
|
|
|
|
// Lucide exports icons as arrays of [tagName, attrs] tuples
|
|
// e.g., [ ['path', { d: '...' }], ['circle', { cx: '...', cy: '...', r: '...' }] ]
|
|
const content = iconData.map(([tag, attrs]) => {
|
|
const attrStr = Object.entries(attrs)
|
|
.map(([k, v]) => `${k}="${v}"`)
|
|
.join(' ');
|
|
return `<${tag} ${attrStr}/>`;
|
|
}).join('');
|
|
|
|
return {
|
|
viewBox: '0 0 24 24',
|
|
content
|
|
};
|
|
} catch (err) {
|
|
console.error(`Error loading icon "${iconName}":`, err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Generate SVG sprite
|
|
function generateSprite() {
|
|
const symbols = [];
|
|
|
|
// Process Lucide icons (skip custom icons handled below)
|
|
for (const iconName of ICONS) {
|
|
if (CUSTOM_ICONS[iconName]) continue;
|
|
const icon = getLucideIcon(iconName);
|
|
if (icon) {
|
|
symbols.push(` <symbol id="${iconName}" viewBox="${icon.viewBox}">${icon.content}</symbol>`);
|
|
}
|
|
}
|
|
|
|
// Process custom icons
|
|
for (const [name, icon] of Object.entries(CUSTOM_ICONS)) {
|
|
symbols.push(` <symbol id="${name}" viewBox="${icon.viewBox}">${icon.content}</symbol>`);
|
|
}
|
|
|
|
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
|
${symbols.join('\n')}
|
|
</svg>`;
|
|
|
|
return sprite;
|
|
}
|
|
|
|
// Main
|
|
const outputPaths = [
|
|
path.join(__dirname, '..', 'pkg', 'appview', 'public', 'icons.svg'),
|
|
path.join(__dirname, '..', 'pkg', 'hold', 'admin', 'public', 'icons.svg'),
|
|
];
|
|
|
|
try {
|
|
const sprite = generateSprite();
|
|
for (const outputPath of outputPaths) {
|
|
fs.writeFileSync(outputPath, sprite);
|
|
console.log(`Generated ${outputPath}`);
|
|
}
|
|
console.log(`Discovered icons (${ICONS.length}): ${ICONS.join(', ')}`);
|
|
console.log(`Custom icons (${Object.keys(CUSTOM_ICONS).length}): ${Object.keys(CUSTOM_ICONS).join(', ')}`);
|
|
console.log(`Total: ${ICONS.length + Object.keys(CUSTOM_ICONS).length} icons`);
|
|
} catch (err) {
|
|
console.error('Error generating sprite:', err);
|
|
process.exit(1);
|
|
}
|