You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

99 lines
3.5 KiB

import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import postcss from 'postcss';
const problems = [];
let checked = 0;
const definitions = new Set();
const references = [];
const drawingFiles = new Set(['GameArt.svelte', 'ParchisMap.svelte']);
const drawingSelector =
/\.(?:chess-piece|checker-piece|coordinate|parchis-token|dice-face|dice-cube|dice-stage|dice-shadow|face-\d|dice-cup|cup-shadow|throw-scene|go-target)\b/;
async function inspect(path, css) {
const file = path.split(/[\\/]/).at(-1);
if (file === 'tokens.css' || drawingFiles.has(file)) return;
const parsed = postcss.parse(css, { from: path });
parsed.walkDecls((d) => {
let inKeyframes = false;
for (let parent = d.parent; parent; parent = parent.parent)
if (parent.type === 'atrule' && /keyframes$/.test(parent.name))
inKeyframes = true;
const selector = d.parent.type === 'rule' ? d.parent.selector : '';
if (
inKeyframes ||
drawingSelector.test(selector) ||
(file === 'Dice.svelte' && /^i(?:[.:]|$)/.test(selector)) ||
d.prop.startsWith('--')
)
return;
checked++;
const value = d.value.replace(/\b0(?:px|rem)\b/g, '0');
const absoluteSize = /\b\d+(?:\.\d+)?(?:px|rem)\b/;
if (
(d.prop === 'font-size' || d.prop === 'font') &&
!value.includes('var(') &&
!/^(?:0|inherit|initial|unset)$/.test(value)
)
problems.push(
`${path}:${d.source.start.line} — ${selector}: use a type token for ${d.prop}`,
);
if (
/^(?:font-weight|line-height|letter-spacing|font-family)$/.test(d.prop) &&
!value.includes('var(') &&
!/^(?:inherit|initial|unset|normal)$/.test(value)
)
problems.push(
`${path}:${d.source.start.line} — ${selector}: use a typography token for ${d.prop}`,
);
if (
/^(?:padding|margin)(?:-.+)?$|^(?:gap|row-gap|column-gap)$/.test(
d.prop,
) &&
absoluteSize.test(value)
)
problems.push(
`${path}:${d.source.start.line} — ${selector}: use a spacing token for ${d.prop}`,
);
if (d.prop === 'border-radius' && absoluteSize.test(value))
problems.push(
`${path}:${d.source.start.line} — ${selector}: use a radius token`,
);
});
}
function collect(path, source) {
for (const match of source.matchAll(/(--[\w-]+)\s*:/g))
definitions.add(match[1]);
for (const match of source.matchAll(/style:(--[\w-]+)/g))
definitions.add(match[1]);
for (const match of source.matchAll(/var\((--[\w-]+)\s*\)/g))
references.push({ path, name: match[1] });
}
async function scan(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) await scan(path);
else if (entry.name.endsWith('.css')) {
const source = await readFile(path, 'utf8');
collect(path, source);
await inspect(path, source);
} else if (entry.name.endsWith('.svelte')) {
const source = await readFile(path, 'utf8');
collect(path, source);
const style = source.match(/<style[^>]*>([\s\S]*?)<\/style>/);
if (style) await inspect(path, style[1]);
}
}
}
await scan('src');
for (const { path, name } of references)
if (!definitions.has(name))
problems.push(`${path} — undefined CSS variable ${name}`);
if (problems.length) {
console.error(problems.join('\n'));
process.exitCode = 1;
} else
console.log(
`Design tokens: ${checked} UI declarations checked; no hardcoded type, spacing or radius values.`,
);

Powered by TurnKey Linux.