#!/usr/bin/env node /** * Checks that every icon a node asks for is one Lucide actually has. * * Nodes declare an icon with `@icon ` and the editor resolves the name to * a Lucide component. A name Lucide does not have does not fail anything - it * quietly becomes the default icon - so the only way to notice is to look at * every node on a canvas. This looks instead: * * node scripts/gen-node-icons.js * * Exits non-zero and names the nodes if any icon cannot be resolved, so it can * go in a pre-commit hook or CI. */ const fs = require('fs') const path = require('path') const ROOT = path.resolve(__dirname, '..') const NODES_DIR = path.join(ROOT, 'nodes') // Kept in step with ICON_ALIASES in webui/src/config/nodeCategories.ts - this // script has no way to read the TypeScript one. const ALIASES = { images: 'GalleryHorizontalEnd', 'octagon-x': 'XOctagon', } function pascal(name) { return name.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : '')).join('') } function collectNodeFiles(dir) { const out = [] for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name) if (entry.isDirectory()) out.push(...collectNodeFiles(full)) else if (entry.name.endsWith('.js')) out.push(full) } return out } function iconsInUse() { const found = new Map() // icon name -> [node ids] for (const file of collectNodeFiles(NODES_DIR)) { const code = fs.readFileSync(file, 'utf8') const icon = code.match(/@icon\s+([a-z0-9-]+)/i) const id = code.match(/@node\s+([a-z0-9-]+)/i) if (!icon) continue const name = icon[1].toLowerCase() if (!found.has(name)) found.set(name, []) found.get(name).push(id ? id[1] : path.basename(file)) } return found } function main() { const inUse = iconsInUse() let lucide try { lucide = require(path.join(ROOT, 'webui/node_modules/lucide-react')) } catch (e) { console.error('Could not load lucide-react from webui/node_modules - run npm install first') process.exit(2) } const unresolved = [] for (const name of [...inUse.keys()].sort()) { const component = ALIASES[name] || pascal(name) if (!lucide[component]) unresolved.push([name, component, inUse.get(name)]) } if (unresolved.length) { console.error('These icon names do not exist in lucide-react:') for (const [name, component, nodes] of unresolved) { console.error(` @icon ${name} (tried ${component}) - used by ${nodes.join(', ')}`) } console.error('\nAdd an alias in webui/src/config/nodeCategories.ts, or change the') console.error('node to a name Lucide has: https://lucide.dev/icons') process.exit(1) } const files = collectNodeFiles(NODES_DIR) const withIcon = [...inUse.values()].reduce((n, ids) => n + ids.length, 0) console.log(`${inUse.size} distinct icons across ${withIcon} nodes - all resolve`) if (withIcon < files.length) { console.log(`${files.length - withIcon} node file(s) declare no @icon and will show their initial`) } } main()