Add CI guard validating ribbon-icon ids against Lucide (follows up #34) (#38)

scripts/check-icons.mjs validates every hardcoded Lucide icon id in main.ts (the RIBBON_ICON_CHOICES list, DEFAULT_RIBBON_ICON, and literal setIcon ids) against the offline lucide-static package, chained into npm run lint. An unknown id (like the shipped #34 bug 'tape' vs 'cassette-tape') now fails CI instead of rendering a blank icon. Strips comments before extraction and preflights missing main.ts / lucide-static with clear messages. Caveat noted in-script: catches ids that exist in no Lucide, not per-version drift vs Obsidian's bundled set.
This commit is contained in:
Charles Kelsoe 2026-07-06 11:59:55 -04:00 committed by GitHub
parent f230f3730d
commit da9550d4e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 104 additions and 1 deletions

View file

@ -4,6 +4,10 @@ All notable changes to Plaud Importer will be documented in this file.
## [Unreleased]
### Changed
- Development tooling: a CI check now validates every hardcoded ribbon-icon id against the Lucide icon set, so an invalid id (which would render a blank icon) fails the build instead of shipping. No behavior change.
## [0.24.0] - 2026-07-06
### Added

8
package-lock.json generated
View file

@ -20,6 +20,7 @@
"eslint-plugin-obsidianmd": "^0.4.1",
"globals": "^17.1.0",
"jest": "^30.2.0",
"lucide-static": "^1.23.0",
"moment": "^2.29.4",
"obsidian": "^1.13.0",
"ts-jest": "^29.4.6",
@ -7454,6 +7455,13 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-static": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.23.0.tgz",
"integrity": "sha512-V4tqLEHfvsptGaAZMD8sFJKeGsz2nTAU0+ctxTX3afao/5bs+mhrvvGDmvQmyI7YhQwpIFodZmnkPTMYyGzH4Q==",
"dev": true,
"license": "ISC"
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",

View file

@ -7,7 +7,7 @@
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"package:brat": "npm run build && node package-brat.mjs",
"lint": "eslint main.ts plaud-client.ts plaud-client-re.ts plaud-login.ts import-core.ts import-modal.ts import-runner.ts attachment-importer.ts note-writer.ts folder-catalog.ts debug-logger.ts vault-index.ts __tests__/plaud-client-re.test.ts __tests__/import-modal.test.ts __tests__/import-runner.test.ts __tests__/note-writer.test.ts __tests__/folder-catalog.test.ts __tests__/attachment-importer.test.ts __tests__/debug-logger.test.ts __tests__/vault-index.test.ts --max-warnings 0 && node scripts/check-submission.mjs",
"lint": "eslint main.ts plaud-client.ts plaud-client-re.ts plaud-login.ts import-core.ts import-modal.ts import-runner.ts attachment-importer.ts note-writer.ts folder-catalog.ts debug-logger.ts vault-index.ts __tests__/plaud-client-re.test.ts __tests__/import-modal.test.ts __tests__/import-runner.test.ts __tests__/note-writer.test.ts __tests__/folder-catalog.test.ts __tests__/attachment-importer.test.ts __tests__/debug-logger.test.ts __tests__/vault-index.test.ts --max-warnings 0 && node scripts/check-submission.mjs && node scripts/check-icons.mjs",
"test": "jest --passWithNoTests",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
@ -32,6 +32,7 @@
"eslint-plugin-obsidianmd": "^0.4.1",
"globals": "^17.1.0",
"jest": "^30.2.0",
"lucide-static": "^1.23.0",
"moment": "^2.29.4",
"obsidian": "^1.13.0",
"ts-jest": "^29.4.6",

90
scripts/check-icons.mjs Normal file
View file

@ -0,0 +1,90 @@
// Guard for the hardcoded Lucide icon ids in main.ts.
//
// Obsidian renders icons from the Lucide set; `setIcon` with an id that is not a
// real Lucide icon renders nothing (a blank ribbon icon). The ribbon-icon picker
// offers a hand-curated list of ids, and a wrong one shipped once (issue #34:
// `tape` instead of `cassette-tape`) and stayed latent for months because a unit
// test cannot reach Obsidian's runtime icon registry. This script validates every
// hardcoded id against the offline `lucide-static` package so a bad id fails CI.
//
// Caveat: lucide-static is Lucide's own release, which may differ from the exact
// version Obsidian bundles, so this catches ids that exist in NO Lucide (the
// issue #34 class), not per-version drift. The developer-dashboard preview and
// hands-on testing remain the authoritative checks. Exits non-zero on any finding
// so it chains into `npm run lint` and CI.
import { existsSync, readFileSync } from "node:fs";
const ICONS_DIR = "node_modules/lucide-static/icons";
// Preflight: a clear message beats a raw stack trace or a misleading "unknown
// icon id" report when the real cause is a missing install or a moved file.
if (!existsSync(ICONS_DIR)) {
console.error(`check-icons: ${ICONS_DIR} not found. Run \`npm install\` (lucide-static is a devDependency).`);
process.exit(1);
}
let source;
try {
source = readFileSync("main.ts", "utf8");
} catch (error) {
console.error(`check-icons: could not read main.ts (${error.message}).`);
process.exit(1);
}
// Strip comments before extracting ids, so a `];` or a commented-out setIcon call
// inside a comment cannot truncate the array block or add a phantom id. Block
// comments and whole-line `//` comments cover the styles used in main.ts; a `//`
// inside a string (e.g. a URL) is left alone because only full-line comments are
// removed.
const code = source
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "");
const ids = new Map(); // id -> reason (for the error message)
function add(id, reason) {
if (!ids.has(id)) ids.set(id, reason);
}
// 1. The curated ribbon-icon list: extract ids only from inside the
// RIBBON_ICON_CHOICES array, so command ids and other `id:` keys are not treated
// as icons. The closing `];` is required at the start of a line (the array's real
// close), a second guard against an in-body `];`.
const listMatch = code.match(/RIBBON_ICON_CHOICES[^=]*=\s*\[([\s\S]*?)\n\s*\];/);
if (!listMatch) {
console.error("check-icons: could not locate the RIBBON_ICON_CHOICES array in main.ts (did its shape change?).");
process.exit(1);
}
for (const m of listMatch[1].matchAll(/id:\s*"([a-z0-9-]+)"/g)) {
add(m[1], "RIBBON_ICON_CHOICES");
}
// 2. The default ribbon icon.
const defaultMatch = code.match(/DEFAULT_RIBBON_ICON\s*=\s*"([a-z0-9-]+)"/);
if (defaultMatch) add(defaultMatch[1], "DEFAULT_RIBBON_ICON");
// 3. Literal ids passed to setIcon(...) (both `el.setIcon("x")` and
// `setIcon(el, "x")`). Calls that pass a variable, template literal, or helper
// result (e.g. resolveRibbonIconId(...)) are skipped: they resolve to a value
// already covered above or coerced to the default at runtime. The convention this
// guard assumes is "curated list + plain quoted literals".
for (const m of code.matchAll(/setIcon\(\s*(?:[^,"'()]+,\s*)?"([a-z0-9-]+)"\s*\)/g)) {
add(m[1], 'setIcon("...")');
}
const missing = [];
for (const [id, reason] of ids) {
if (!existsSync(`${ICONS_DIR}/${id}.svg`)) {
missing.push(` - "${id}" (${reason}) is not a Lucide icon id.`);
}
}
if (missing.length > 0) {
console.error("Icon check failed: unknown Lucide icon id(s) in main.ts:");
for (const line of missing) console.error(line);
console.error("");
console.error("Find the correct id at https://lucide.dev/icons and fix it, or remove the entry.");
console.error("An unknown id makes setIcon render nothing (a blank ribbon icon).");
process.exit(1);
}
console.log(`Icon check passed: ${ids.size} hardcoded Lucide icon id(s) all valid.`);