From 15200b33f2ed0c13b94fdbfa50e1b0fdefe6db16 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 4 Oct 2026 09:13:22 +0200 Subject: [PATCH] fix(tools): follow weight variables in both readings of a keyframe A keyframe that sets a font only while it runs makes scanWeights scan the file twice: once with the keyframe over the rule that runs it, once with the rule after it. The wrapper merged only the findings and the deferred weights of the second pass, so a weight read through a variable (`font-weight: $w`, `var(--w)`, `font: $w ...`) that meets JetBrains Mono only after a non-holding animation was never followed to its definition. Merge the references too; findIndirectWeights already dedupes a reference both passes record. Definitions, call sites, loads and declaration counts do not depend on the reading. Co-Authored-By: Claude Opus 5.5 --- tools/nx/check-font-weights.mjs | 2 ++ tools/nx/check-font-weights.test.mjs | 36 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tools/nx/check-font-weights.mjs b/tools/nx/check-font-weights.mjs index c112d133d..99f2a31b2 100644 --- a/tools/nx/check-font-weights.mjs +++ b/tools/nx/check-font-weights.mjs @@ -904,6 +904,8 @@ export function scanWeights(file, written) { return { ...running, findings: union(running.findings, after.findings), + // A variable a weight reads is followed from either reading. + references: union(running.references, after.references), deferred: union(running.deferred, after.deferred), }; } diff --git a/tools/nx/check-font-weights.test.mjs b/tools/nx/check-font-weights.test.mjs index 84b4fd1fc..ffa9903f5 100644 --- a/tools/nx/check-font-weights.test.mjs +++ b/tools/nx/check-font-weights.test.mjs @@ -4360,6 +4360,42 @@ test('reads a keyframe where a rule runs it, over its own', () => { } }); +test('follows a weight variable in either reading of a keyframe that does not hold', () => { + const mono = "'JetBrains Mono'"; + const roboto = '@keyframes k { to { font-family: Roboto; } }'; + const report = (body) => + findOffScaleWeights('libs/s11/g.scss', body).findings.map( + ({ name, value, cap }) => `${name}: ${value} cap ${cap}` + ); + for (const [source, expected] of [ + // Mono once it has run: the variable meets the cap where it is set. + [ + `$w: 700; ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: $w; }`, + ['$w: 700 cap 500'], + ], + [ + `:root { --w: 700; } ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: var(--w); }`, + ['--w: 700 cap 500'], + ], + [ + `$w: 700; ${roboto} .x { animation: k 1ms; font: $w 12px/1 ${mono}; }`, + ['$w: 700 cap 500'], + ], + // Mono only while it runs. + [ + `$w: 700; .x { font-family: Roboto; animation: k 1ms; font-weight: $w; } @keyframes k { to { font-family: ${mono}; } }`, + ['$w: 700 cap 500'], + ], + // One that holds its frame keeps Roboto after it. + [ + `$w: 700; ${roboto} .x { font-family: ${mono}; animation: k 1ms forwards; font-weight: $w; }`, + [], + ], + ]) { + assert.deepEqual(report(source), expected, source); + } +}); + test('reads an `@at-root` rule where Sass writes it out', () => { const mono = "'JetBrains Mono'"; const media = '@media (min-width: 1px)';