Files
lislgosms/tools/quality/verify-foundation-styles-r11.mjs
T

102 lines
3.8 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const contract = JSON.parse(
fs.readFileSync(path.join(root, 'docs/contracts/foundation-styles-r11.json'), 'utf8'),
);
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const failures = [];
function normalizeSelector(selector) {
return selector.replace(/\s+/g, '').trim();
}
function normalizeDeclarations(declarations) {
return declarations
.split(';')
.map((declaration) => declaration.trim())
.filter(Boolean)
.join(';');
}
function extractFlatRules(source) {
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, '');
return [...withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((match) => ({
selector: normalizeSelector(match[1]),
declarations: normalizeDeclarations(match[2]),
}));
}
const entry = read(contract.entry);
let previousImportIndex = -1;
for (const importPath of contract.importOrder) {
const statement = `import '${importPath}';`;
const importIndex = entry.indexOf(statement);
if (importIndex < 0) {
failures.push(`${contract.entry} is missing ${statement}`);
} else if (importIndex <= previousImportIndex) {
failures.push(`${importPath} is not loaded in the required foundation-to-component order`);
}
previousImportIndex = importIndex;
}
const applicationImportIndex = entry.indexOf(contract.applicationImport);
if (applicationImportIndex < 0) {
failures.push(`${contract.entry} is missing the application route import`);
} else if (applicationImportIndex <= previousImportIndex) {
failures.push('AppRoutes must enter the dependency graph after the ordered foundation/component styles');
}
const tokens = read(contract.tokensFile);
const tokenNames = [...tokens.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gim)].map((match) => match[1]);
if (tokenNames.length < contract.minimumTokenCount) {
failures.push(`${contract.tokensFile} has ${tokenNames.length} tokens; expected at least ${contract.minimumTokenCount}`);
}
for (const token of contract.requiredTokens) {
if (!tokenNames.includes(token)) failures.push(`${contract.tokensFile} is missing ${token}`);
}
const tokenRules = extractFlatRules(tokens);
if (tokenRules.length !== 1 || tokenRules[0].selector !== ':root') {
failures.push(`${contract.tokensFile} must remain a single :root design-token scope`);
}
const reset = read(contract.resetFile);
const resetRules = extractFlatRules(reset);
const expectedSelectors = Object.keys(contract.resetRuleHashes);
const actualSelectors = resetRules.map((rule) => rule.selector);
for (const selector of expectedSelectors) {
const matches = resetRules.filter((rule) => rule.selector === selector);
if (matches.length !== 1) {
failures.push(`${contract.resetFile} must contain exactly one ${selector} rule`);
continue;
}
const actualHash = crypto.createHash('sha256').update(matches[0].declarations).digest('hex');
if (actualHash !== contract.resetRuleHashes[selector]) {
failures.push(`${contract.resetFile} changed declarations for ${selector}`);
}
}
for (const selector of actualSelectors) {
if (!expectedSelectors.includes(selector)) {
failures.push(`${contract.resetFile} contains non-foundation selector ${selector}`);
}
}
const sourceRules = extractFlatRules(read(contract.sourceFile));
const remainingSelectors = new Set(sourceRules.map((rule) => rule.selector));
for (const selector of expectedSelectors) {
if (remainingSelectors.has(selector)) {
failures.push(`${contract.sourceFile} still owns reset selector ${selector}`);
}
}
if (failures.length > 0) {
console.error(`R11 foundation styles verification failed:\n- ${failures.join('\n- ')}`);
process.exit(1);
}
console.log(
`R11 foundation styles verified: ${tokenNames.length} design tokens, `
+ `${resetRules.length} reset/base rules and deterministic import order preserved.`,
);