check-node-icons.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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. const BRAND_ICONS_FILE = path.join(ROOT, 'webui/src/config/brandIcons.ts')
  20. // Kept in step with ICON_ALIASES in webui/src/config/nodeCategories.ts - this
  21. // script has no way to read the TypeScript one.
  22. const ALIASES = {
  23. images: 'GalleryHorizontalEnd',
  24. 'octagon-x': 'XOctagon',
  25. }
  26. function pascal(name) {
  27. return name.split('-').map((w) => (w ? w[0].toUpperCase() + w.slice(1) : '')).join('')
  28. }
  29. function collectNodeFiles(dir) {
  30. const out = []
  31. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  32. const full = path.join(dir, entry.name)
  33. if (entry.isDirectory()) out.push(...collectNodeFiles(full))
  34. else if (entry.name.endsWith('.js')) out.push(full)
  35. }
  36. return out
  37. }
  38. function iconsInUse() {
  39. const found = new Map() // icon name -> [node ids]
  40. for (const file of collectNodeFiles(NODES_DIR)) {
  41. const code = fs.readFileSync(file, 'utf8')
  42. const icon = code.match(/@icon\s+([a-z0-9-]+)/i)
  43. const id = code.match(/@node\s+([a-z0-9-]+)/i)
  44. if (!icon) continue
  45. const name = icon[1].toLowerCase()
  46. if (!found.has(name)) found.set(name, [])
  47. found.get(name).push(id ? id[1] : path.basename(file))
  48. }
  49. return found
  50. }
  51. // The brand marks vendored by scripts/gen-brand-icons.js. A node may name one
  52. // of these instead of a Lucide icon, so they are not missing - they are just
  53. // resolved somewhere else.
  54. function brandIconNames() {
  55. if (!fs.existsSync(BRAND_ICONS_FILE)) return new Set()
  56. const src = fs.readFileSync(BRAND_ICONS_FILE, 'utf8')
  57. const body = src.slice(src.indexOf('BRAND_ICONS'))
  58. return new Set([...body.matchAll(/^\s*'([a-z0-9-]+)':/gm)].map((m) => m[1]))
  59. }
  60. function main() {
  61. const inUse = iconsInUse()
  62. const brands = brandIconNames()
  63. let lucide
  64. try {
  65. lucide = require(path.join(ROOT, 'webui/node_modules/lucide-react'))
  66. } catch (e) {
  67. console.error('Could not load lucide-react from webui/node_modules - run npm install first')
  68. process.exit(2)
  69. }
  70. const unresolved = []
  71. for (const name of [...inUse.keys()].sort()) {
  72. if (brands.has(name)) continue
  73. const component = ALIASES[name] || pascal(name)
  74. if (!lucide[component]) unresolved.push([name, component, inUse.get(name)])
  75. }
  76. if (unresolved.length) {
  77. console.error('These icon names do not exist in lucide-react:')
  78. for (const [name, component, nodes] of unresolved) {
  79. console.error(` @icon ${name} (tried ${component}) - used by ${nodes.join(', ')}`)
  80. }
  81. console.error('\nAdd an alias in webui/src/config/nodeCategories.ts, or change the')
  82. console.error('node to a name Lucide has: https://lucide.dev/icons')
  83. process.exit(1)
  84. }
  85. const files = collectNodeFiles(NODES_DIR)
  86. const withIcon = [...inUse.values()].reduce((n, ids) => n + ids.length, 0)
  87. const brandsUsed = [...inUse.keys()].filter((n) => brands.has(n)).length
  88. console.log(`${inUse.size} distinct icons across ${withIcon} nodes - all resolve ` +
  89. `(${brandsUsed} brand marks, ${inUse.size - brandsUsed} from Lucide)`)
  90. if (withIcon < files.length) {
  91. console.log(`${files.length - withIcon} node file(s) declare no @icon and will show their initial`)
  92. }
  93. }
  94. main()