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
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.`,
|
|
);
|