11#!/usr/bin/env node
2+ /**
3+ * Build SVG sprite from used icons (Lucide + Tabler + custom)
4+ */
25import fs from "fs" ;
36import path from "path" ;
47import { createRequire } from "module" ;
58import svgstore from "svgstore" ;
69import { ICONS } from "./used-icons.js" ;
710import { loadConfig } from "../dist/loadConfig.js" ;
8- import { componentNameToStaticId } from "../dist/utils.js" ;
911
1012const require = createRequire ( import . meta. url ) ;
1113
12- // Load user config (merged with defaults)
14+ // Load user config
1315const { SPRITE_PATH , CUSTOM_SVG_DIR , OUTPUT_DIR } = await loadConfig ( ) ;
1416
15- // 1️⃣ Resolve lucide-static icons
16- const sample = require . resolve ( "../../../lucide-static/icons/mail.svg" ) ;
17- const iconsDir = path . dirname ( sample ) ;
17+ // ============================================================================
18+ // Icon pack directories
19+ // ============================================================================
20+
21+ function resolveLucideIconsDir ( ) {
22+ try {
23+ // First try to find lucide-static package.json
24+ const pkgPath = require . resolve ( "lucide-static/package.json" ) ;
25+ const pkgDir = path . dirname ( pkgPath ) ;
26+ const iconsDir = path . join ( pkgDir , "icons" ) ;
27+ if ( fs . existsSync ( iconsDir ) ) {
28+ return iconsDir ;
29+ }
30+ } catch { }
31+
32+ // Fallback: check in process.cwd()/node_modules
33+ const fallbackPath = path . join ( process . cwd ( ) , "node_modules" , "lucide-static" , "icons" ) ;
34+ if ( fs . existsSync ( fallbackPath ) ) {
35+ return fallbackPath ;
36+ }
37+
38+ return null ;
39+ }
40+
41+ function resolveTablerIconsDir ( ) {
42+ try {
43+ // First try to find @tabler /icons package.json
44+ const pkgPath = require . resolve ( "@tabler/icons/package.json" ) ;
45+ const pkgDir = path . dirname ( pkgPath ) ;
46+ const outlineDir = path . join ( pkgDir , "icons" , "outline" ) ;
47+ if ( fs . existsSync ( outlineDir ) ) {
48+ return outlineDir ;
49+ }
50+ } catch { }
51+
52+ // Fallback: check in process.cwd()/node_modules
53+ const fallbackPath = path . join ( process . cwd ( ) , "node_modules" , "@tabler" , "icons" , "icons" , "outline" ) ;
54+ if ( fs . existsSync ( fallbackPath ) ) {
55+ return fallbackPath ;
56+ }
57+
58+ return null ;
59+ }
60+
61+ const lucideDir = resolveLucideIconsDir ( ) ;
62+ const tablerDir = resolveTablerIconsDir ( ) ;
63+
64+ // ============================================================================
65+ // Load component → sprite mapping (generated by gen-wrappers.js)
66+ // ============================================================================
67+
68+ import { fileURLToPath } from "url" ;
69+ const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
70+
71+ function loadSpriteMapping ( ) {
72+ const mappingFile = path . join ( __dirname , "component-sprite-map.json" ) ;
73+ if ( fs . existsSync ( mappingFile ) ) {
74+ const content = fs . readFileSync ( mappingFile , "utf8" ) ;
75+ return JSON . parse ( content ) ;
76+ }
77+ console . warn ( "⚠️ component-sprite-map.json not found. Run `npm run build` first." ) ;
78+ return { } ;
79+ }
80+
81+ const spriteMapping = loadSpriteMapping ( ) ;
82+
83+ function componentNameToSpriteId ( name ) {
84+ // Use the mapping from gen-wrappers.js (source of truth)
85+ if ( spriteMapping [ name ] ) {
86+ return spriteMapping [ name ] ;
87+ }
88+
89+ // Fallback for custom icons or unknown components
90+ console . warn ( `⚠️ No mapping found for ${ name } , using fallback conversion` ) ;
91+ const kebab = name
92+ . replace ( / ( [ a - z ] ) ( [ A - Z ] ) / g, "$1-$2" )
93+ . replace ( / ( [ A - Z ] ) ( [ A - Z ] [ a - z ] ) / g, "$1-$2" )
94+ . replace ( / ( [ a - z A - Z ] ) ( \d ) / g, "$1-$2" )
95+ . replace ( / ( \d ) ( [ a - z A - Z ] ) / g, "$1-$2" )
96+ . toLowerCase ( ) ;
97+ return { pack : "custom" , spriteId : kebab , svgFile : `${ kebab } .svg` } ;
98+ }
99+
100+ // ============================================================================
101+ // SVG Processing: Replace hardcoded values with CSS variables
102+ // ============================================================================
103+
104+ function processSvgForCssVars ( svgContent ) {
105+ // Only handle stroke-width with CSS variable (most useful, predictable)
106+ // stroke="currentColor" already works via CSS color property
107+ // fill is too complex - leave it alone
108+
109+ let processed = svgContent . replace (
110+ / s t r o k e - w i d t h = " ( [ ^ " ] + ) " / g,
111+ ( match , value ) => `stroke-width="var(--icon-stroke-width, ${ value } )"`
112+ ) ;
113+
114+ return processed ;
115+ }
116+
117+ // ============================================================================
118+ // Build sprite
119+ // ============================================================================
18120
19- // 2️⃣ Gather your needed Lucide IDs
20- const needed = new Set ( ICONS . map ( componentNameToStaticId ) ) ;
21121const store = svgstore ( {
22122 copyAttrs : [ "viewBox" , "fill" , "stroke" , "stroke-width" , "stroke-linecap" , "stroke-linejoin" , "style" , "size" ] ,
23123 svgAttrs : {
@@ -26,38 +126,91 @@ const store = svgstore({
26126 focusable : "false" ,
27127 } ,
28128} ) ;
129+
29130const found = new Set ( ) ;
131+ const missing = [ ] ;
132+
133+ // Map component names to sprite info
134+ const neededIcons = ICONS . map ( ( name ) => ( {
135+ name,
136+ ...componentNameToSpriteId ( name ) ,
137+ } ) ) ;
30138
31- // 3️⃣ Add only the Lucide icons you actually use
32- for ( const file of fs . readdirSync ( iconsDir ) ) {
33- if ( ! file . endsWith ( ".svg" ) ) continue ;
34- const id = file . slice ( 0 , - 4 ) ;
35- if ( ! needed . has ( id ) ) continue ;
36- found . add ( id ) ;
37- store . add ( id , fs . readFileSync ( path . join ( iconsDir , file ) , "utf8" ) ) ;
139+ // Add Lucide icons
140+ if ( lucideDir ) {
141+ const lucideFiles = new Set ( fs . readdirSync ( lucideDir ) . filter ( ( f ) => f . endsWith ( ".svg" ) ) ) ;
142+
143+ for ( const icon of neededIcons ) {
144+ if ( icon . pack !== "lucide" ) continue ;
145+
146+ if ( lucideFiles . has ( icon . svgFile ) ) {
147+ const svg = fs . readFileSync ( path . join ( lucideDir , icon . svgFile ) , "utf8" ) ;
148+ store . add ( icon . spriteId , processSvgForCssVars ( svg ) ) ;
149+ found . add ( icon . name ) ;
150+ } else {
151+ // Try case-insensitive match
152+ const match = [ ...lucideFiles ] . find ( ( f ) => f . toLowerCase ( ) === icon . svgFile . toLowerCase ( ) ) ;
153+ if ( match ) {
154+ const svg = fs . readFileSync ( path . join ( lucideDir , match ) , "utf8" ) ;
155+ store . add ( icon . spriteId , processSvgForCssVars ( svg ) ) ;
156+ found . add ( icon . name ) ;
157+ }
158+ }
159+ }
38160}
39161
40- // 4️⃣ Optionally include *all* SVGs from your custom folder
162+ // Add Tabler icons
163+ if ( tablerDir ) {
164+ const tablerFiles = new Set ( fs . readdirSync ( tablerDir ) . filter ( ( f ) => f . endsWith ( ".svg" ) ) ) ;
165+
166+ for ( const icon of neededIcons ) {
167+ if ( icon . pack !== "tabler" ) continue ;
168+
169+ if ( tablerFiles . has ( icon . svgFile ) ) {
170+ const svg = fs . readFileSync ( path . join ( tablerDir , icon . svgFile ) , "utf8" ) ;
171+ store . add ( icon . spriteId , processSvgForCssVars ( svg ) ) ;
172+ found . add ( icon . name ) ;
173+ } else {
174+ // Try case-insensitive match
175+ const match = [ ...tablerFiles ] . find ( ( f ) => f . toLowerCase ( ) === icon . svgFile . toLowerCase ( ) ) ;
176+ if ( match ) {
177+ const svg = fs . readFileSync ( path . join ( tablerDir , match ) , "utf8" ) ;
178+ store . add ( icon . spriteId , processSvgForCssVars ( svg ) ) ;
179+ found . add ( icon . name ) ;
180+ }
181+ }
182+ }
183+ }
184+
185+ // Add custom icons
41186const customDir = path . resolve ( process . cwd ( ) , OUTPUT_DIR , CUSTOM_SVG_DIR ) ;
42187if ( fs . existsSync ( customDir ) ) {
43188 for ( const file of fs . readdirSync ( customDir ) ) {
44189 if ( ! file . endsWith ( ".svg" ) ) continue ;
45- const id = file . slice ( 0 , - 4 ) ; // "my-icon"
190+ const id = file . slice ( 0 , - 4 ) ;
46191 const svg = fs . readFileSync ( path . join ( customDir , file ) , "utf8" ) ;
47- store . add ( id , svg ) ; // <symbol id="my-icon">…
48- // Mark custom icons as found so they don't appear as missing
192+ store . add ( id , processSvgForCssVars ( svg ) ) ;
49193 found . add ( id ) ;
50194 console . log ( `🔧 Added custom icon: ${ id } ` ) ;
51195 }
52196}
53197
54- // 5️⃣ Error on any missing Lucide icon
55- const missing = [ ...needed ] . filter ( ( id ) => ! found . has ( id ) ) ;
56- if ( missing . length ) {
57- throw new Error ( `❌ Missing icons: ${ missing . join ( ", " ) } ` ) ;
198+ // Check for missing icons
199+ for ( const icon of neededIcons ) {
200+ if ( ! found . has ( icon . name ) ) {
201+ missing . push ( `${ icon . name } (${ icon . pack } : ${ icon . svgFile } )` ) ;
202+ }
58203}
59204
60- // 6️⃣ Write a fresh sprite (inline: true drops xml/doctype)
205+ if ( missing . length > 0 ) {
206+ console . warn ( `⚠️ Missing ${ missing . length } icons:` ) ;
207+ missing . slice ( 0 , 10 ) . forEach ( ( m ) => console . warn ( ` - ${ m } ` ) ) ;
208+ if ( missing . length > 10 ) {
209+ console . warn ( ` ... and ${ missing . length - 10 } more` ) ;
210+ }
211+ }
212+
213+ // Write sprite
61214const sprite = store . toString ( { inline : true } ) ;
62215const outDir = path . join ( process . cwd ( ) , OUTPUT_DIR ) ;
63216const outFile = path . join ( outDir , SPRITE_PATH ) ;
@@ -66,4 +219,12 @@ fs.mkdirSync(outDir, { recursive: true });
66219if ( fs . existsSync ( outFile ) ) fs . unlinkSync ( outFile ) ;
67220fs . writeFileSync ( outFile , sprite , "utf8" ) ;
68221
69- console . log ( `✅ Built ${ SPRITE_PATH } with ${ found . size } Lucide + custom icons.` ) ;
222+ // Summary
223+ const lucideCount = neededIcons . filter ( ( i ) => i . pack === "lucide" && found . has ( i . name ) ) . length ;
224+ const tablerCount = neededIcons . filter ( ( i ) => i . pack === "tabler" && found . has ( i . name ) ) . length ;
225+ console . log ( `✅ Built ${ SPRITE_PATH } with ${ found . size } icons` ) ;
226+ console . log ( ` 📦 Lucide: ${ lucideCount } ` ) ;
227+ console . log ( ` 📦 Tabler: ${ tablerCount } ` ) ;
228+ if ( found . size - lucideCount - tablerCount > 0 ) {
229+ console . log ( ` 📦 Custom: ${ found . size - lucideCount - tablerCount } ` ) ;
230+ }
0 commit comments