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

127 lines
5.4 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/admin-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 globalTree = postcss.parse(globalStyle);
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 ownedClass = (className) => (
contract.ownedExactClasses.includes(className)
|| contract.ownedClassPrefixes.some((prefix) => className.startsWith(prefix))
);
const allowedContextClass = (className) => (
ownedClass(className)
|| contract.allowedContextClasses.includes(className)
|| className.startsWith('ui-')
);
const selectorClasses = (selector) => [...selector.matchAll(/\.([_a-zA-Z]+[\w-]*)/g)].map((match) => match[1]);
const selectorBranches = (rule) => rule.selectors ?? rule.selector.split(',').map((selector) => selector.trim());
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/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 shared 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) => {
if (rule.parent?.type === 'atrule' && rule.parent.name.endsWith('keyframes')) return;
ruleCount += 1;
for (const selector of selectorBranches(rule)) {
selectorCount += 1;
const classes = selectorClasses(selector);
if (!classes.some(ownedClass)) failures.push(`${contract.styleFile} contains a selector without admin ownership: ${selector}`);
if (!classes.every(allowedContextClass)) failures.push(`${contract.styleFile} contains page/client context: ${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 selector of contract.requiredSelectors) {
if (!style.includes(selector)) failures.push(`${contract.styleFile} is missing ${selector}`);
}
globalTree.walkRules((rule) => {
if (rule.parent?.type === 'atrule' && rule.parent.name.endsWith('keyframes')) return;
for (const selector of selectorBranches(rule)) {
const classes = selectorClasses(selector);
if (!classes.some(ownedClass)) continue;
if (classes.every(allowedContextClass)) {
failures.push(`${contract.globalStyleFile} still owns a pure admin shared selector: ${selector}`);
}
}
});
const adminFiles = listFiles('src/apps/admin', ['.ts', '.tsx']);
const clientFiles = listFiles('src/apps/client', ['.ts', '.tsx']);
for (const [className, minimumFiles] of Object.entries(contract.usageRequirements)) {
const count = adminFiles.filter((file) => fs.readFileSync(file, 'utf8').includes(className)).length;
if (count < minimumFiles) failures.push(`${className} is used by ${count} admin files; minimum is ${minimumFiles}`);
}
for (const file of clientFiles) {
const source = fs.readFileSync(file, 'utf8');
for (const className of contract.ownedExactClasses) {
if (source.includes(className)) failures.push(`${path.relative(root, file)} depends on admin-only class ${className}`);
}
for (const prefix of contract.ownedClassPrefixes) {
if (source.includes(prefix)) failures.push(`${path.relative(root, file)} depends on admin-only class family ${prefix}`);
}
}
for (const requirement of contract.responsiveRequirements) {
let found = false;
styleTree.walkAtRules('media', (atRule) => {
if (atRule.params !== requirement.media) return;
atRule.walkRules((rule) => {
if (selectorBranches(rule).some((selector) => selector.includes(requirement.selector))) found = true;
});
});
if (!found) failures.push(`${contract.styleFile} is missing ${requirement.selector} under @media ${requirement.media}`);
}
if (failures.length > 0) {
console.error(`R11 admin shared styles verification failed:\n- ${failures.join('\n- ')}`);
process.exit(1);
}
console.log(
`R11 admin shared styles verified: ${ruleCount} rules, ${selectorCount} selectors, `
+ `${contract.usageRequirements ? Object.keys(contract.usageRequirements).length : 0} cross-page usages and admin-only ownership are isolated.`,
);