feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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.`,
|
||||
);
|
||||
Reference in New Issue
Block a user