Files
Maurice 242756708e ci(client): run theme:lint as a per-file ratchet with the missing token rules
theme:lint only reported a total and exited 0, skipped whole files by name and
never ran in CI. It now counts per file under src/ against
scripts/theme-baseline.json, which only shrinks, like lint:rtl: the colour
literals, arbitrary colour classes and numeric font sizes it already saw, plus
palette classes (bg-gray-100, text-indigo-600, bg-white), raw text sizes
(text-sm, text-[13px]) instead of the type tiers, z-index literals above z-50
instead of a step of the layering scale, and reads of the dark_mode setting.
The file exemptions are gone; the map, PDF and brand files carry their count in
the baseline instead. theme-lint-disable markers stay valid in a comment and
are counted against scripts/theme-disable-baseline.json, like rtl-lint-disable
in lint:rtl; a new one raises its file's entry by hand, where review sees it.
The comment handling lint:rtl already had moves into scripts/lib/source.mjs so
both read source the same way.
The check runs in the client-quality job.
2026-10-10 21:32:05 +02:00

54 lines
2.0 KiB
JavaScript

/*
* Reading source the way the line ratchets need it: with the comments blanked
* out, and telling a disable marker in a comment from the same text in code.
* Shared by lint:rtl (scripts/lib/rtl.mjs) and theme:lint (scripts/lib/theme.mjs).
*/
import ts from 'typescript';
/**
* The source with its comments blanked out, so prose like "top right" or
* "left out" is no side. TypeScript finds the comments in a .ts/.tsx file,
* which keeps `/*` and `//` inside strings and JSX text where they belong
* (accept="image/*", a URL); a stylesheet only has block comments.
*/
export function withoutComments(source, file) {
const chars = source.split('');
const blank = (pos, end) => {
for (let i = pos; i < end; i++) if (chars[i] !== '\n') chars[i] = ' ';
};
if (file.endsWith('.css')) {
for (const m of source.matchAll(/\/\*[\s\S]*?\*\//g)) blank(m.index, m.index + m[0].length);
return chars.join('');
}
const kind = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);
const seen = new Set();
const take = (ranges) => {
for (const r of ranges ?? []) {
if (seen.has(r.pos)) continue;
seen.add(r.pos);
blank(r.pos, r.end);
}
};
const visit = (node) => {
take(ts.getLeadingCommentRanges(source, node.pos));
take(ts.getTrailingCommentRanges(source, node.end));
// JSX text holds no comments, and asking for them there would read `//` in a URL as one.
if (!ts.isJsxText(node)) for (const child of node.getChildren(sf)) visit(child);
};
visit(sf);
take(ts.getLeadingCommentRanges(source, sf.endOfFileToken.pos));
return chars.join('');
}
/**
* Whether line carries marker inside a comment; in a string or in code it is no
* marker. code is the same line from withoutComments.
*/
export function markedInComment(line, code, marker) {
for (let at = line.indexOf(marker); at !== -1; at = line.indexOf(marker, at + 1)) {
if (!code.slice(at, at + marker.length).trim()) return true;
}
return false;
}