/* * The matching and the comparison behind lint:rtl (scripts/rtl-lint.mjs). */ import { join } from 'node:path'; import { countMap, listFiles, lowerCounts, readBaseline, readText, reportStale, staleCounts, TEST_FILE, toKey, writeBaseline, } from './ratchet.mjs'; import { markedInComment, withoutComments } from './source.mjs'; // Tailwind utilities that name a physical side, with any variant prefix // (sm:, hover:, group-hover:) and the negative form (-ml-2). const CLASS = new RegExp( String.raw`(? { if (markedInComment(lines[i], code, DISABLE)) { markers++; return; } const patterns = isCss ? [CSS] : [CLASS, STYLE]; for (const re of patterns) for (const m of code.match(re) ?? []) uses.push(`${i + 1}: ${m.trim()}`); }); return { uses, markers }; } /** Every physical use in one file, as `line: match`. */ export function physicalUses(source, file) { return inspect(source, file).uses; } /** The physical uses and the disable markers per file under root/src, keyed by the path from src/. */ export function scan(root) { const counts = {}; const listed = {}; const markers = {}; const accept = (key) => /\.(?:tsx?|css)$/.test(key) && !TEST_FILE.test(key); for (const path of listFiles(root, ['src'], accept)) { const found = inspect(readText(path), path); const key = toKey(join(root, 'src'), path); if (found.uses.length) { counts[key] = found.uses.length; listed[key] = found.uses; } if (found.markers) markers[key] = found.markers; } return { counts, listed, markers }; } const sum = (counts) => Object.values(counts).reduce((a, b) => a + b, 0); /** * Runs the check against both baselines: rtl-baseline.json for the physical * sides, rtl-disable-baseline.json for the lines that opt out of the check. A * marker hides a side from the count, so the markers are a ratchet as well: * a file holds no more of them than its entry. Returns the exit code. */ export function check({ root, baselinePath = join(root, 'scripts/rtl-baseline.json'), disableBaselinePath = join(root, 'scripts/rtl-disable-baseline.json'), update = false, list = false, log = console.log, error = console.error, }) { const { counts, listed, markers } = scan(root); let baseline = readBaseline(baselinePath, countMap); let allowedMarkers = readBaseline(disableBaselinePath, countMap); if (list) { for (const [file, uses] of Object.entries(listed).sort()) { log(`${file} (${uses.length}, baseline ${baseline[file] ?? 0})`); for (const use of uses) log(` ${use}`); } } if (update) { baseline = lowerCounts(baseline, counts); allowedMarkers = lowerCounts(allowedMarkers, markers); writeBaseline(baselinePath, baseline); writeBaseline(disableBaselinePath, allowedMarkers); } const grown = Object.entries(counts).filter(([file, n]) => n > (baseline[file] ?? 0)); const marked = Object.entries(markers).filter(([file, n]) => n > (allowedMarkers[file] ?? 0)); const stale = staleCounts(baseline, counts); const staleMarkers = staleCounts(allowedMarkers, markers); for (const [file, n] of grown) { error( `FAIL ${file}: ${n} physical side(s), baseline ${baseline[file] ?? 0}. ` + 'Use ms-/me-/ps-/pe-/start-/end-/text-start/text-end/border-s/border-e/rounded-s/rounded-e ' + 'or marginInlineStart/paddingInlineEnd/insetInlineStart/textAlign: start. ' + `If the side is geometry, mark the line with ${DISABLE} in a comment.` ); } for (const [file, n] of marked) { error( `FAIL ${file}: ${n} line(s) marked ${DISABLE}, scripts/rtl-disable-baseline.json allows ${allowedMarkers[file] ?? 0}. ` + 'Use the logical form where the side follows the text. A side that is geometry on purpose ' + '(a map, a measured position, a time axis) needs a reviewer to raise the entry by hand.' ); } reportStale(stale, { file: 'rtl-baseline.json', command: 'lint:rtl', root: join(root, 'src'), error }); reportStale(staleMarkers, { file: 'rtl-disable-baseline.json', command: 'lint:rtl', root: join(root, 'src'), error }); log( `rtl: ${sum(counts)} physical side(s) in ${Object.keys(counts).length} file(s), baseline allows ${sum(baseline)}; ` + `${sum(markers)} line(s) marked ${DISABLE}, baseline allows ${sum(allowedMarkers)}` ); return grown.length || marked.length || stale.length || staleMarkers.length ? 1 : 0; }