check-node-icons.js 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. #!/usr/bin/env node
  2. /**
  3. * Checks that every icon a node asks for is one Lucide actually has.
  4. *
  5. * Nodes declare an icon with `@icon <name>` and the editor resolves the name to
  6. * a Lucide component. A name Lucide does not have does not fail anything - it
  7. * quietly becomes the default icon - so the only way to notice is to look at
  8. * every node on a canvas. This looks instead:
  9. *
  10. * node scripts/gen-node-icons.js
  11. *
  12. * Exits non-zero and names the nodes if any icon cannot be resolved, so it can
  13. * go in a pre-commit hook or CI.
  14. */
  15. const fs = require('fs')
  16. const path = require('path')
  17. const ROOT = path.resolve(__dirname, '..')
  18. const NODES_DIR = path.join(ROOT, 'nodes')
  19. // Kept in step with ICON_ALIASES in webui/src/config/nodeCategories.ts - this
  20. // script has no way to read the TypeScript one.
  21. const ALIASES = {
  22. images: 'GalleryHorizontalEnd',
  23. 'octagon-x': 'XOctagon',
  24. }
  25. function pascal(name) {
  26. return name.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : '')).join('')
  27. }
  28. function collectNodeFiles(dir) {
  29. const out = []
  30. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  31. const full = path.join(dir, entry.name)
  32. if (entry.isDirectory()) out.push(...collectNodeFiles(full))
  33. else if (entry.name.endsWith('.js')) out.push(full)
  34. }
  35. return out
  36. }
  37. function iconsInUse() {
  38. const found = new Map() // icon name -> [node ids]
  39. for (const file of collectNodeFiles(NODES_DIR)) {
  40. const code = fs.readFileSync(file, 'utf8')
  41. const icon = code.match(/@icon\s+([a-z0-9-]+)/i)
  42. const id = code.match(/@node\s+([a-z0-9-]+)/i)
  43. if (!icon) continue
  44. const name = icon[1].toLowerCase()
  45. if (!found.has(name)) found.set(name, [])
  46. found.get(name).push(id ? id[1] : path.basename(file))
  47. }
  48. return found
  49. }
  50. function main() {
  51. const inUse = iconsInUse()
  52. let lucide
  53. try {
  54. lucide = require(path.join(ROOT, 'webui/node_modules/lucide-react'))
  55. } catch (e) {
  56. console.error('Could not load lucide-react from webui/node_modules - run npm install first')
  57. process.exit(2)
  58. }
  59. const unresolved = []
  60. for (const name of [...inUse.keys()].sort()) {
  61. const component = ALIASES[name] || pascal(name)
  62. if (!lucide[component]) unresolved.push([name, component, inUse.get(name)])
  63. }
  64. if (unresolved.length) {
  65. console.error('These icon names do not exist in lucide-react:')
  66. for (const [name, component, nodes] of unresolved) {
  67. console.error(` @icon ${name} (tried ${component}) - used by ${nodes.join(', ')}`)
  68. }
  69. console.error('\nAdd an alias in webui/src/config/nodeCategories.ts, or change the')
  70. console.error('node to a name Lucide has: https://lucide.dev/icons')
  71. process.exit(1)
  72. }
  73. const files = collectNodeFiles(NODES_DIR)
  74. const withIcon = [...inUse.values()].reduce((n, ids) => n + ids.length, 0)
  75. console.log(`${inUse.size} distinct icons across ${withIcon} nodes - all resolve`)
  76. if (withIcon < files.length) {
  77. console.log(`${files.length - withIcon} node file(s) declare no @icon and will show their initial`)
  78. }
  79. }
  80. main()