|
|
@@ -0,0 +1,92 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+/**
|
|
|
+ * Checks that every icon a node asks for is one Lucide actually has.
|
|
|
+ *
|
|
|
+ * Nodes declare an icon with `@icon <name>` 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()
|