diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index 0b4e19154a..61de3b7df3 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -604,31 +604,34 @@ export function importUnsafeStatements(source) { * never a line in here. An entry whose file has since been fixed fails as STALE * and names itself, which is what stops this from rotting into an allowlist. * - * ONE entry, and that is the point: this rule recognises a hand-typed guard AS - * a guard (see `guardAliases`), so the 29 badly-spelled files are rule 1's - * business and do not appear here. `check-lucide-icon-record-names.mjs` builds - * two lookup maps in top-level `for` loops at :243 and :245, outside any guard, - * and really does run them inside an importer. - * - * ⚠️ Its remedy is NOT "move the loops behind the guard". Those maps are read by - * the exported `liveSpellingFor` / `describeName`, which importers really call, - * so guarding them empties the maps for every importer. Measured on objectui#6092's - * branch rather than reasoned: with the two loops moved inside the guard, rule 2 - * goes green and reports the entry as STALE -- and - * `scripts/__tests__/check-lucide-icon-record-names.test.ts` fails 5 of 25. - * The failure is not merely a red test. `describeName('BarChart3')` stops saying - * "write `chart-column`" and says "no live key names the same glyph" instead: - * a WRONG diagnosis for a real violation, printed by a gate that still exits 1, - * which is a worse outcome than the unguarded loops. - * - * So objectui#6092 ruled the restructuring out of scope rather than trade a - * working import for a baseline line. The entry stays, and its remedy is a - * judgement someone still has to make -- which is exactly what the rest of this - * comment says a debt line must not be. It is the one line here that owes a - * card, not a one-liner. A lazy build of the two maps inside `liveSpellingFor` - * would satisfy both, and is the shape that card should consider. + * EMPTY, and that is now the point. It stays as an empty set rather than being + * deleted: an empty debt list is the state this rule exists to reach, and the + * set still has to be here for a re-added line to be reconciled — FRESH and + * STALE are both pinned by the self-test in exactly that state. Nothing appears + * here because this rule recognises a hand-typed guard AS a guard (see + * `guardAliases`), so a badly-spelled guard is rule 1's business, not this + * list's. + * + * Its one entry was `check-lucide-icon-record-names.mjs`, whose two lookup maps + * ran in top-level `for` loops. They are now built LAZILY on first read inside + * `liveSpellingFor`, memoised (objectui#6147), so the line is gone. + * + * ⚠️ WHICH remedy emptied it is the load-bearing part, and it is why that line + * owed a card instead of a one-liner. Moving those loops behind that file's + * ENTRY GUARD empties this list too. Measured on objectui#6092's branch rather + * than reasoned: rule 2 goes green and reports the entry as STALE, and + * `scripts/__tests__/check-lucide-icon-record-names.test.ts` fails 5 of 25 — + * and the failure is not merely a red test. With the maps empty for importers, + * `describeName('BarChart3')` stops saying "write `chart-column`" and says "no + * live key names the same glyph" instead: a WRONG diagnosis for a real + * violation, printed by a gate that still exits 1, which is a worse outcome + * than the unguarded loops. So an empty list is not by itself evidence that + * anything improved — the remedy has to preserve what every importer already + * got. That file's own comment carries the measurement. + * + * @type {Set} */ -const KNOWN_IMPORT_UNSAFE = new Set(['scripts/check-lucide-icon-record-names.mjs']); +const KNOWN_IMPORT_UNSAFE = new Set(); /** Every exporting file, with the statements that would run on import. */ function importSafetyCensus(files) { diff --git a/scripts/check-lucide-icon-record-names.mjs b/scripts/check-lucide-icon-record-names.mjs index 620c684913..fc45da4551 100644 --- a/scripts/check-lucide-icon-record-names.mjs +++ b/scripts/check-lucide-icon-record-names.mjs @@ -239,13 +239,39 @@ export const isLiveKey = (name) => Object.prototype.hasOwnProperty.call(icons, t // Derive the live spelling of a retired name BY IDENTITY, never from a list: // lucide keeps the retired export pointing at the same object as its live key. -const keyByComponent = new Map(); -for (const [key, component] of Object.entries(icons)) if (!keyByComponent.has(component)) keyByComponent.set(component, key); -const kebabByKey = new Map(); -for (const kebab of iconNames) kebabByKey.set(toRecordKey(kebab), kebab); +// +// Built LAZILY on first read and memoised, never at module top level. This file +// exports bindings, so whatever its top level ran would run inside every +// importer — the rule `check-entry-guard.mjs` calls import-safety (objectui#6133). +// +// ⛔ Do NOT "fix" this by moving the build behind the entry guard instead. That +// was measured, not reasoned about, and rejected (objectui#6092 PR 2's ablation, +// recorded on objectui#6147): the maps stay EMPTY for importers, the suite fails +// 5 of 25, and `describeName('BarChart3')` stops saying ``write `chart-column` `` +// and says "no live key names the same glyph" instead — a WRONG DIAGNOSIS for a +// real violation, printed by a gate that still exits 1. A green baseline bought +// with a lying error message is worse than an honest debt line. Lazy satisfies +// both sides: nothing runs on import, and every reader gets the built maps. +// +// ⛔ This does not make the module cheap to import, and is not trying to: +// `icons`/`iconNames` are top-level `await import('lucide-react')` above. That +// cost is a separate question (objectui#6147) and is deliberately left alone. +let memoisedLookups = null; + +/** The two identity maps, built ONCE on the first read and memoised after. */ +function glyphLookups() { + if (memoisedLookups) return memoisedLookups; + const keyByComponent = new Map(); + for (const [key, component] of Object.entries(icons)) if (!keyByComponent.has(component)) keyByComponent.set(component, key); + const kebabByKey = new Map(); + for (const kebab of iconNames) kebabByKey.set(toRecordKey(kebab), kebab); + memoisedLookups = { keyByComponent, kebabByKey }; + return memoisedLookups; +} /** `{ key, kebab }` of the live spelling naming the SAME glyph, or null. */ export function liveSpellingFor(name) { + const { keyByComponent, kebabByKey } = glyphLookups(); const retiredExport = lucide[toRecordKey(name)]; if (!retiredExport) return null; const liveKey = keyByComponent.get(retiredExport);