|
| 1 | +const packageName = 'lucide-static' |
| 2 | +const distName = 'lucide-icons' |
| 3 | +const iconSetName = 'Lucide' |
| 4 | +const prefix = 'luc' |
| 5 | + |
| 6 | +// ------------ |
| 7 | + |
| 8 | +const { globSync } = require('tinyglobby') |
| 9 | +const { copySync } = require('fs-extra') |
| 10 | +const { writeFileSync } = require('fs') |
| 11 | +const { resolve, join } = require('path') |
| 12 | + |
| 13 | +const skipped = [] |
| 14 | +const distFolder = resolve(__dirname, `../${ distName }`) |
| 15 | +const { defaultNameMapper, writeExports, extractSvg } = require('./utils') |
| 16 | +const { readFileSync } = require('fs') |
| 17 | + |
| 18 | +// Custom extract function for Lucide icons that handles relative paths correctly |
| 19 | +function extractLucide(filePath, name) { |
| 20 | + const content = readFileSync(filePath, 'utf-8') |
| 21 | + |
| 22 | + // The real issue is that the utils path decoder adds M0 0z prefix for paths starting with 'm' |
| 23 | + // We need to override this behavior temporarily |
| 24 | + const utils = require('./utils') |
| 25 | + |
| 26 | + // Store the original extractSvg function |
| 27 | + const originalExtractSvg = utils.extractSvg |
| 28 | + |
| 29 | + // Create a modified version that doesn't add M0 0z prefix |
| 30 | + function customExtractSvg(content, name) { |
| 31 | + const { parseSvgContent } = utils |
| 32 | + |
| 33 | + // Monkey-patch the path decoder temporarily |
| 34 | + const xmldom = require('@xmldom/xmldom') |
| 35 | + const Parser = new xmldom.DOMParser() |
| 36 | + |
| 37 | + // Override just the path processing part |
| 38 | + const originalParseSvgContent = parseSvgContent |
| 39 | + |
| 40 | + // Custom parseSvgContent that avoids the M0 0z issue |
| 41 | + function lucideParseSvgContent(name, content) { |
| 42 | + let viewBox |
| 43 | + const pathsDefinitions = [] |
| 44 | + |
| 45 | + try { |
| 46 | + const dom = Parser.parseFromString(content, 'text/xml') |
| 47 | + viewBox = dom.documentElement.getAttribute('viewBox') |
| 48 | + |
| 49 | + if (!viewBox) { |
| 50 | + const width = parseFloat(dom.documentElement.getAttribute('width') || '0') |
| 51 | + const height = parseFloat(dom.documentElement.getAttribute('height') || '0') |
| 52 | + if (width > 0 && height > 0) { |
| 53 | + viewBox = `0 0 ${width} ${height}` |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // Parse all elements (paths, circles, rects, etc.) but without the M0 0z issue |
| 58 | + function parseElement(el) { |
| 59 | + const type = el.nodeName |
| 60 | + |
| 61 | + if (el.getAttribute === undefined || el.getAttribute('opacity') === '0') return |
| 62 | + |
| 63 | + const typeExceptions = ['g', 'svg', 'defs', 'style', 'title'] |
| 64 | + |
| 65 | + if (!typeExceptions.includes(type)) { |
| 66 | + let pathData = '' |
| 67 | + |
| 68 | + if (type === 'path') { |
| 69 | + const points = el.getAttribute('d')?.trim() |
| 70 | + if (points) { |
| 71 | + // Don't add M0 0z prefix for relative paths! |
| 72 | + pathData = points |
| 73 | + } |
| 74 | + } else if (type === 'circle') { |
| 75 | + const cx = parseFloat(el.getAttribute('cx') || 0) |
| 76 | + const cy = parseFloat(el.getAttribute('cy') || 0) |
| 77 | + const r = parseFloat(el.getAttribute('r') || 0) |
| 78 | + pathData = `M${cx} ${cy} m-${r}, 0 a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 ${-r * 2},0` |
| 79 | + } else if (type === 'rect') { |
| 80 | + const x = parseFloat(el.getAttribute('x') || 0) |
| 81 | + const y = parseFloat(el.getAttribute('y') || 0) |
| 82 | + const width = parseFloat(el.getAttribute('width') || 0) |
| 83 | + const height = parseFloat(el.getAttribute('height') || 0) |
| 84 | + pathData = `M${x} ${y}H${x + width}V${y + height}H${x}Z` |
| 85 | + } else if (type === 'line') { |
| 86 | + const x1 = parseFloat(el.getAttribute('x1') || 0) |
| 87 | + const y1 = parseFloat(el.getAttribute('y1') || 0) |
| 88 | + const x2 = parseFloat(el.getAttribute('x2') || 0) |
| 89 | + const y2 = parseFloat(el.getAttribute('y2') || 0) |
| 90 | + pathData = `M${x1},${y1}L${x2},${y2}` |
| 91 | + } else if (type === 'polygon') { |
| 92 | + const points = el.getAttribute('points')?.trim() |
| 93 | + if (points) { |
| 94 | + // Convert polygon points to path data |
| 95 | + const coords = points.split(/[\s,]+/).filter(p => p.length > 0) |
| 96 | + if (coords.length >= 4 && coords.length % 2 === 0) { |
| 97 | + pathData = `M${coords[0]} ${coords[1]}` |
| 98 | + for (let i = 2; i < coords.length; i += 2) { |
| 99 | + pathData += `L${coords[i]} ${coords[i + 1]}` |
| 100 | + } |
| 101 | + pathData += 'Z' // Close the polygon |
| 102 | + } |
| 103 | + } |
| 104 | + } else if (type === 'ellipse') { |
| 105 | + const cx = parseFloat(el.getAttribute('cx') || 0) |
| 106 | + const cy = parseFloat(el.getAttribute('cy') || 0) |
| 107 | + const rx = parseFloat(el.getAttribute('rx') || 0) |
| 108 | + const ry = parseFloat(el.getAttribute('ry') || 0) |
| 109 | + // Convert ellipse to path using arc commands |
| 110 | + pathData = `M${cx - rx} ${cy} A${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A${rx} ${ry} 0 1 0 ${cx - rx} ${cy}` |
| 111 | + } |
| 112 | + |
| 113 | + if (pathData) { |
| 114 | + // Get styles from SVG attributes |
| 115 | + const fill = el.getAttribute('fill') || dom.documentElement.getAttribute('fill') || 'none' |
| 116 | + const stroke = el.getAttribute('stroke') || dom.documentElement.getAttribute('stroke') || 'currentColor' |
| 117 | + const strokeWidth = el.getAttribute('stroke-width') || dom.documentElement.getAttribute('stroke-width') || '2' |
| 118 | + const strokeLinecap = el.getAttribute('stroke-linecap') || dom.documentElement.getAttribute('stroke-linecap') || 'round' |
| 119 | + const strokeLinejoin = el.getAttribute('stroke-linejoin') || dom.documentElement.getAttribute('stroke-linejoin') || 'round' |
| 120 | + |
| 121 | + const style = `fill:${fill};stroke:${stroke};stroke-width:${strokeWidth};stroke-linecap:${strokeLinecap};stroke-linejoin:${strokeLinejoin};` |
| 122 | + |
| 123 | + pathsDefinitions.push({ |
| 124 | + path: pathData, |
| 125 | + style: style, |
| 126 | + transform: el.getAttribute('transform') || '' |
| 127 | + }) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + // Recursively process child elements |
| 132 | + Array.from(el.childNodes).forEach(child => { |
| 133 | + if (child.nodeType === 1) { // Element node |
| 134 | + parseElement(child) |
| 135 | + } |
| 136 | + }) |
| 137 | + } |
| 138 | + |
| 139 | + parseElement(dom.documentElement) |
| 140 | + |
| 141 | + } catch (err) { |
| 142 | + console.error(`[Error] "${name}" could not be parsed: ${err.message}`) |
| 143 | + throw err |
| 144 | + } |
| 145 | + |
| 146 | + if (pathsDefinitions.length === 0) { |
| 147 | + throw new Error(`Could not infer any paths for "${name}"`) |
| 148 | + } |
| 149 | + |
| 150 | + const tmpView = viewBox !== '0 0 24 24' && viewBox ? `|${viewBox}` : '' |
| 151 | + |
| 152 | + const result = { |
| 153 | + viewBox: tmpView |
| 154 | + } |
| 155 | + |
| 156 | + if (pathsDefinitions.every((def) => !def.style && !def.transform)) { |
| 157 | + result.paths = pathsDefinitions.map((def) => def.path).join('') |
| 158 | + } else { |
| 159 | + result.paths = pathsDefinitions |
| 160 | + .map((def) => { |
| 161 | + let stylePart = def.style ? `@@${def.style}` : '' |
| 162 | + const transformPart = def.transform ? `@@${def.transform}` : '' |
| 163 | + |
| 164 | + if (!def.style && def.transform) { |
| 165 | + stylePart = '@@' |
| 166 | + } |
| 167 | + |
| 168 | + return `${def.path}${stylePart}${transformPart}` |
| 169 | + }) |
| 170 | + .join('&&') |
| 171 | + } |
| 172 | + |
| 173 | + return result |
| 174 | + } |
| 175 | + |
| 176 | + const { paths, viewBox } = lucideParseSvgContent(name, content) |
| 177 | + const path = paths.replace(/[\r\n\t]+/gi, ',').replace(/,,/gi, ',') |
| 178 | + |
| 179 | + return { |
| 180 | + svgDef: `export const ${name} = '${path}${viewBox}'`, |
| 181 | + typeDef: `export declare const ${name}: string;` |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + return customExtractSvg(content, name) |
| 186 | +} |
| 187 | + |
| 188 | +const svgFolder = resolve( |
| 189 | + __dirname, |
| 190 | + `../node_modules/${ packageName }/icons/` |
| 191 | +) |
| 192 | +const svgFiles = globSync(svgFolder + '/*.svg') |
| 193 | +let iconNames = new Set() |
| 194 | + |
| 195 | +const svgExports = [] |
| 196 | +const typeExports = [] |
| 197 | + |
| 198 | +svgFiles.forEach((file) => { |
| 199 | + const name = defaultNameMapper(file, prefix) |
| 200 | + |
| 201 | + if (iconNames.has(name)) return |
| 202 | + |
| 203 | + try { |
| 204 | + const { svgDef, typeDef } = extractLucide(file, name) |
| 205 | + svgExports.push(svgDef) |
| 206 | + typeExports.push(typeDef) |
| 207 | + |
| 208 | + iconNames.add(name) |
| 209 | + } |
| 210 | + catch (err) { |
| 211 | + console.error(err) |
| 212 | + skipped.push(name) |
| 213 | + } |
| 214 | +}) |
| 215 | + |
| 216 | +iconNames = [ ...iconNames ] |
| 217 | +svgExports.sort((a, b) => { |
| 218 | + return ('' + a).localeCompare(b) |
| 219 | +}) |
| 220 | +typeExports.sort((a, b) => { |
| 221 | + return ('' + a).localeCompare(b) |
| 222 | +}) |
| 223 | +iconNames.sort((a, b) => { |
| 224 | + return ('' + a).localeCompare(b) |
| 225 | +}) |
| 226 | + |
| 227 | +writeExports( |
| 228 | + iconSetName, |
| 229 | + packageName, |
| 230 | + distFolder, |
| 231 | + svgExports, |
| 232 | + typeExports, |
| 233 | + skipped |
| 234 | +) |
| 235 | + |
| 236 | +copySync( |
| 237 | + resolve(__dirname, `../node_modules/${ packageName }/LICENSE`), |
| 238 | + resolve(__dirname, `../${ distName }/LICENSE`) |
| 239 | +) |
| 240 | + |
| 241 | +// write the JSON file |
| 242 | +const file = resolve(__dirname, join('..', distName, 'icons.json')) |
| 243 | +writeFileSync(file, JSON.stringify([ ...iconNames ].sort(), null, 2), 'utf-8') |
| 244 | + |
| 245 | +console.log(`${ distName } done with ${ iconNames.length } icons`) |
0 commit comments