A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1#!/usr/bin/env node
2
3/**
4 * Generate SVG sprite sheet from Lucide icons
5 *
6 * Usage: npm run icons:build
7 *
8 * This script auto-discovers icons used in the codebase and generates
9 * an SVG sprite file with <symbol> elements for each icon.
10 */
11
12const fs = require('fs');
13const path = require('path');
14const { globSync } = require('glob');
15
16/**
17 * Auto-discover icons from the codebase
18 */
19function discoverIcons() {
20 const icons = new Set();
21 const basePath = path.join(__dirname, '..');
22
23 // 1. Scan templates for {{ icon "name" ... }}
24 const templatePattern = /\{\{\s*icon\s+"([^"]+)"/g;
25 const templates = [
26 ...globSync('pkg/appview/templates/**/*.html', { cwd: basePath }),
27 ...globSync('pkg/hold/admin/templates/**/*.html', { cwd: basePath }),
28 ];
29 templates.forEach(file => {
30 const content = fs.readFileSync(path.join(basePath, file), 'utf8');
31 let match;
32 while ((match = templatePattern.exec(content)) !== null) {
33 icons.add(match[1]);
34 }
35 });
36
37 // 2. Scan templates and JS for icons.svg#name (direct SVG use references)
38 const svgUsePattern = /icons\.svg#([a-z0-9-]+)/g;
39 const allFiles = [
40 ...globSync('pkg/appview/templates/**/*.html', { cwd: basePath }),
41 ...globSync('pkg/hold/admin/templates/**/*.html', { cwd: basePath }),
42 ...globSync('pkg/appview/src/js/**/*.js', { cwd: basePath }),
43 ...globSync('pkg/hold/admin/src/js/**/*.js', { cwd: basePath }),
44 ];
45 allFiles.forEach(file => {
46 const content = fs.readFileSync(path.join(basePath, file), 'utf8');
47 let match;
48 while ((match = svgUsePattern.exec(content)) !== null) {
49 icons.add(match[1]);
50 }
51 });
52
53 // 3. Scan JS for iconMap object values (theme toggle, etc.)
54 const jsFiles = globSync('pkg/appview/src/js/**/*.js', { cwd: basePath });
55 const iconMapPattern = /iconMap\s*=\s*\{([^}]+)\}/g;
56 const iconValuePattern = /['"]([a-z][a-z0-9-]*)['"](?:\s*[:,])/g;
57 jsFiles.forEach(file => {
58 const content = fs.readFileSync(path.join(basePath, file), 'utf8');
59 let mapMatch;
60 while ((mapMatch = iconMapPattern.exec(content)) !== null) {
61 const mapContent = mapMatch[1];
62 let valueMatch;
63 while ((valueMatch = iconValuePattern.exec(mapContent)) !== null) {
64 // Only add values (after colon), not keys
65 icons.add(valueMatch[1]);
66 }
67 }
68 });
69
70 return Array.from(icons).sort();
71}
72
73const ICONS = discoverIcons();
74
75// Custom Helm icon (from Simple Icons - official Helm logo)
76const CUSTOM_ICONS = {
77 'helm': {
78 viewBox: '0 0 24 24',
79 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"/>'
80 }
81};
82
83// Lucide icon name to Pascal case mapping (for require path)
84function toPascalCase(str) {
85 return str.split('-').map(word =>
86 word.charAt(0).toUpperCase() + word.slice(1)
87 ).join('');
88}
89
90// Get icon content from lucide package
91function getLucideIcon(iconName) {
92 try {
93 const lucide = require('lucide');
94 const pascalName = toPascalCase(iconName);
95 const iconData = lucide[pascalName];
96
97 if (!iconData) {
98 console.warn(`Warning: Icon "${iconName}" (${pascalName}) not found in lucide package`);
99 return null;
100 }
101
102 // Lucide exports icons as arrays of [tagName, attrs] tuples
103 // e.g., [ ['path', { d: '...' }], ['circle', { cx: '...', cy: '...', r: '...' }] ]
104 const content = iconData.map(([tag, attrs]) => {
105 const attrStr = Object.entries(attrs)
106 .map(([k, v]) => `${k}="${v}"`)
107 .join(' ');
108 return `<${tag} ${attrStr}/>`;
109 }).join('');
110
111 return {
112 viewBox: '0 0 24 24',
113 content
114 };
115 } catch (err) {
116 console.error(`Error loading icon "${iconName}":`, err.message);
117 return null;
118 }
119}
120
121// Generate SVG sprite
122function generateSprite() {
123 const symbols = [];
124
125 // Process Lucide icons (skip custom icons handled below)
126 for (const iconName of ICONS) {
127 if (CUSTOM_ICONS[iconName]) continue;
128 const icon = getLucideIcon(iconName);
129 if (icon) {
130 symbols.push(` <symbol id="${iconName}" viewBox="${icon.viewBox}">${icon.content}</symbol>`);
131 }
132 }
133
134 // Process custom icons
135 for (const [name, icon] of Object.entries(CUSTOM_ICONS)) {
136 symbols.push(` <symbol id="${name}" viewBox="${icon.viewBox}">${icon.content}</symbol>`);
137 }
138
139 const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
140${symbols.join('\n')}
141</svg>`;
142
143 return sprite;
144}
145
146// Main
147const outputPaths = [
148 path.join(__dirname, '..', 'pkg', 'appview', 'public', 'icons.svg'),
149 path.join(__dirname, '..', 'pkg', 'hold', 'admin', 'public', 'icons.svg'),
150];
151
152try {
153 const sprite = generateSprite();
154 for (const outputPath of outputPaths) {
155 fs.writeFileSync(outputPath, sprite);
156 console.log(`Generated ${outputPath}`);
157 }
158 console.log(`Discovered icons (${ICONS.length}): ${ICONS.join(', ')}`);
159 console.log(`Custom icons (${Object.keys(CUSTOM_ICONS).length}): ${Object.keys(CUSTOM_ICONS).join(', ')}`);
160 console.log(`Total: ${ICONS.length + Object.keys(CUSTOM_ICONS).length} icons`);
161} catch (err) {
162 console.error('Error generating sprite:', err);
163 process.exit(1);
164}