Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions scripts/check-entry-guard.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string>}
*/
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) {
Expand Down
34 changes: 30 additions & 4 deletions scripts/check-lucide-icon-record-names.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand Down
Loading