Files
lislgosms/tools/quality/verify-client-shared-styles-r11.mjs

103 lines
4.7 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import postcss from 'postcss';
const root = process.cwd();
const contract = JSON.parse(fs.readFileSync(path.join(root, 'docs/contracts/client-shared-styles-r11.json'), 'utf8'));
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const failures = [];
const style = read(contract.styleFile);
const globalStyle = read(contract.globalStyleFile);
const entry = read(contract.entryFile);
const styleTree = postcss.parse(style);
const listFiles = (directory, extensions) => {
const result = [];
const visit = (current) => {
for (const item of fs.readdirSync(current, { withFileTypes: true })) {
const absolute = path.join(current, item.name);
if (item.isDirectory()) visit(absolute);
else if (extensions.some((extension) => item.name.endsWith(extension))) result.push(absolute);
}
};
visit(path.join(root, directory));
return result;
};
const clientFiles = listFiles('src/apps/client', ['.ts', '.tsx']);
const adminFiles = listFiles('src/apps/admin', ['.ts', '.tsx']);
const countUsage = (files, className) => files.filter((file) => fs.readFileSync(file, 'utf8').includes(className)).length;
const selectorClasses = (selector) => [...selector.matchAll(/\.([_a-zA-Z]+[\w-]*)/g)].map((match) => match[1]);
if (!style.includes(contract.marker)) failures.push(`${contract.styleFile} is missing its ownership marker`);
const importOrder = [
"@/styles/tokens.css",
"@/styles/reset.css",
"@/styles/shell.css",
"@/styles/global.css",
"@/styles/admin.css",
"@/styles/client.css",
"@/styles/components.css",
];
let previousIndex = -1;
for (const importPath of importOrder) {
const index = entry.indexOf(`import '${importPath}';`);
if (index < 0) failures.push(`${contract.entryFile} is missing ${importPath}`);
if (index <= previousIndex) failures.push(`${contract.entryFile} has an invalid style import order at ${importPath}`);
previousIndex = index;
}
if (entry.indexOf("import { AppRoutes }") <= previousIndex) {
failures.push('AppRoutes must load after the complete shared style dependency chain');
}
let ruleCount = 0;
let selectorCount = 0;
styleTree.walkRules((rule) => {
ruleCount += 1;
for (const selector of rule.selectors ?? [rule.selector]) {
selectorCount += 1;
const classes = selectorClasses(selector);
if (classes.length === 0 || !classes.every((className) => contract.ownedClasses.includes(className))) {
failures.push(`${contract.styleFile} contains a non-client-shared selector: ${selector}`);
}
}
});
if (ruleCount < contract.minimumRuleCount) failures.push(`${contract.styleFile} has ${ruleCount} rules; minimum is ${contract.minimumRuleCount}`);
if (selectorCount < contract.minimumSelectorCount) failures.push(`${contract.styleFile} has ${selectorCount} selectors; minimum is ${contract.minimumSelectorCount}`);
for (const [className, minimumFiles] of Object.entries(contract.usageRequirements)) {
const clientCount = countUsage(clientFiles, className);
const adminCount = countUsage(adminFiles, className);
if (clientCount < minimumFiles) failures.push(`${className} is used by ${clientCount} client files; minimum is ${minimumFiles}`);
if (adminCount !== 0) failures.push(`${className} is unexpectedly used by ${adminCount} admin files`);
const pureSelector = new RegExp(`^\\s*\\.${className}\\s*\\{`, 'm');
if (pureSelector.test(globalStyle)) failures.push(`${contract.globalStyleFile} still owns .${className}`);
}
for (const selector of contract.requiredGlobalContextSelectors) {
if (!globalStyle.includes(selector)) failures.push(`${contract.globalStyleFile} lost contextual override ${selector}`);
}
for (const [className, requirement] of Object.entries(contract.crossPortalClassesKeptGlobal)) {
if (!globalStyle.includes(`.${className}`)) failures.push(`${contract.globalStyleFile} lost cross-portal class .${className}`);
const clientCount = countUsage(clientFiles, className);
const adminCount = countUsage(adminFiles, className);
if (clientCount < requirement.minimumClientFiles) failures.push(`${className} client usage dropped to ${clientCount}`);
if (adminCount < requirement.minimumAdminFiles) failures.push(`${className} admin usage dropped to ${adminCount}`);
}
for (const className of contract.singlePageClassesKeptGlobal) {
if (!globalStyle.includes(`.${className}`)) failures.push(`${contract.globalStyleFile} lost single-page class .${className}`);
}
if (failures.length > 0) {
console.error(`R11 client shared styles verification failed:\n- ${failures.join('\n- ')}`);
process.exit(1);
}
console.log(
`R11 client shared styles verified: ${ruleCount} rule, ${selectorCount} selector, `
+ `${Object.keys(contract.crossPortalClassesKeptGlobal).length} cross-portal boundaries and single-page ownership are preserved.`,
);