diff --git a/tools/nx/check-font-weights.mjs b/tools/nx/check-font-weights.mjs index 2f26d2b26..50355caa0 100644 --- a/tools/nx/check-font-weights.mjs +++ b/tools/nx/check-font-weights.mjs @@ -757,6 +757,11 @@ function capOnly(term) { return Boolean(term.cap) && !offScale; } +/** What a finding reports (see `describeFinding`): one key, one message. */ +function findingKey({ file, line, name, value, cap, computed }) { + return JSON.stringify([file, line, name, value, cap, computed]); +} + /** A string literal in code, whole: its quote and its text. */ const STRING_LITERAL = /^\s*(['"`])([\s\S]*)\1\s*$/; @@ -940,6 +945,8 @@ export function scanWeights(file, written, modules = {}) { 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), // A call meets the families of either reading. includeCalls: new Map( @@ -2362,7 +2369,13 @@ export function findIndirectWeights(scans) { ); } } - return findings; + // A definition several references reach (two rules reading one Sass + // variable, or both readings of a keyframe) is reported once. + return [ + ...new Map( + findings.map((finding) => [findingKey(finding), finding]) + ).values(), + ]; } /** @@ -2448,13 +2461,11 @@ export function findWorkspaceWeights(sources) { ...scans.flatMap((scan) => scan.findings), ...findIndirectWeights(scans), ]; - const keyOf = ({ file, line, name, value, cap, computed }) => - JSON.stringify([file, line, name, value, cap, computed]); - const seen = new Set(all.filter((f) => !f.landed).map(keyOf)); + const seen = new Set(all.filter((f) => !f.landed).map(findingKey)); const findings = []; for (const { landed, ...finding } of all) { if (landed) { - const key = keyOf(finding); + const key = findingKey(finding); if (seen.has(key)) continue; seen.add(key); } diff --git a/tools/nx/check-font-weights.test.mjs b/tools/nx/check-font-weights.test.mjs index 79eecf32b..a06503085 100644 --- a/tools/nx/check-font-weights.test.mjs +++ b/tools/nx/check-font-weights.test.mjs @@ -197,6 +197,27 @@ test('follows variables a weight refers to, through chains and files', () => { ); }); +test('reports a definition once however many references reach it', () => { + const mono = "'JetBrains Mono'"; + for (const [source, expected] of [ + ['$w: 650; .a { font-weight: $w; } .b { font-weight: $w; }', '650'], + [ + '$w: calc(400 + 100); .a { font-weight: $w; } .b { font-weight: $w; }', + 'calc(400 + 100)', + ], + [ + `$w: calc(400 + 100); .a { font-family: ${mono}; font-weight: $w; } .b { font-weight: $w; }`, + 'calc(400 + 100)', + ], + ]) { + assert.deepEqual( + offScale('libs/a.scss', source), + [`1 $w: ${expected}`], + source + ); + } +}); + test('checks Angular style bindings and literal DOM writes', () => { const template = [ '

a

', @@ -4939,6 +4960,63 @@ 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 ${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'], + ], + // A computed weight is one finding, whichever reading reaches it. + [ + `$w: calc(400 + 100); ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: $w; }`, + ['$w: calc(400 + 100)'], + ], + // 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); + } + // And where another module's mixin runs it. + for (const [run, expected] of [ + ['k 1ms', ['libs/s11/c.scss:2 $w: 700']], + ['k 1ms forwards', []], + ]) { + const face = `@mixin face { font-family: ${mono}; animation: ${run}; }`; + assert.deepEqual( + workspace({ + 'libs/s11/_type.scss': `${roboto}\n${face}`, + 'libs/s11/c.scss': `@use 'type';\n$w: 700;\n.x { @include type.face; font-weight: $w; }`, + }), + expected, + run + ); + } +}); + test('reads an `@at-root` rule where Sass writes it out', () => { const mono = "'JetBrains Mono'"; const media = '@media (min-width: 1px)';