diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd745f..1e534b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 1d63f49..22fe37f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3279df2..91ec8ba 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-icons.mjs b/scripts/check-icons.mjs new file mode 100644 index 0000000..a19dcce --- /dev/null +++ b/scripts/check-icons.mjs @@ -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.`);