feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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/app-shell-styles-r11.json'), 'utf8'),
|
||||
);
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const failures = [];
|
||||
|
||||
function normalizeDeclarations(rule) {
|
||||
return rule.nodes
|
||||
.filter((node) => node.type === 'decl')
|
||||
.map((node) => `${node.prop}:${node.value}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function mediaOf(rule) {
|
||||
let parent = rule.parent;
|
||||
while (parent) {
|
||||
if (parent.type === 'atrule' && parent.name === 'media') return parent.params;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const entry = read(contract.entry);
|
||||
const shellImport = `import '@/styles/shell.css';`;
|
||||
const afterImport = `import '${contract.importAfter}';`;
|
||||
const beforeImport = `import '${contract.importBefore}';`;
|
||||
const shellImportIndex = entry.indexOf(shellImport);
|
||||
if (shellImportIndex < 0) {
|
||||
failures.push(`${contract.entry} is missing ${shellImport}`);
|
||||
} else if (
|
||||
entry.indexOf(afterImport) < 0
|
||||
|| entry.indexOf(beforeImport) < 0
|
||||
|| entry.indexOf(afterImport) >= shellImportIndex
|
||||
|| shellImportIndex >= entry.indexOf(beforeImport)
|
||||
) {
|
||||
failures.push(`${contract.shellFile} must load after reset and before legacy global styles`);
|
||||
}
|
||||
|
||||
const component = read(contract.component);
|
||||
for (const className of contract.shellClasses) {
|
||||
if (!component.includes(className)) {
|
||||
failures.push(`${contract.component} is missing shell state/class ${className}`);
|
||||
}
|
||||
}
|
||||
|
||||
const shellRoot = postcss.parse(read(contract.shellFile), { from: contract.shellFile });
|
||||
const shellRules = [];
|
||||
shellRoot.walkRules((rule) => shellRules.push(rule));
|
||||
if (shellRules.length < contract.minimumRuleCount) {
|
||||
failures.push(`${contract.shellFile} has ${shellRules.length} rules; expected at least ${contract.minimumRuleCount}`);
|
||||
}
|
||||
|
||||
const mediaQueries = new Set();
|
||||
shellRoot.walkAtRules('media', (atRule) => mediaQueries.add(atRule.params));
|
||||
for (const query of contract.requiredMediaQueries) {
|
||||
if (!mediaQueries.has(query)) failures.push(`${contract.shellFile} is missing @media ${query}`);
|
||||
}
|
||||
|
||||
for (const requirement of contract.requiredRuleFragments) {
|
||||
const matchingRules = shellRules.filter(
|
||||
(rule) => rule.selectors.includes(requirement.selector) && mediaOf(rule) === requirement.media,
|
||||
);
|
||||
if (matchingRules.length !== 1) {
|
||||
failures.push(
|
||||
`${contract.shellFile} expected one ${requirement.selector} rule`
|
||||
+ `${requirement.media ? ` in @media ${requirement.media}` : ' outside media queries'}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const declarations = normalizeDeclarations(matchingRules[0]);
|
||||
for (const fragment of requirement.includes) {
|
||||
if (!declarations.includes(fragment)) {
|
||||
failures.push(`${requirement.selector} is missing declaration ${fragment}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const legacyRoots = contract.legacyFiles.map((file) => ({
|
||||
file,
|
||||
root: postcss.parse(read(file), { from: file }),
|
||||
}));
|
||||
for (const className of [...contract.shellClasses, ...contract.legacyShellClasses]) {
|
||||
if (className === 'app-shell--mobile-nav-open') continue;
|
||||
const classPattern = new RegExp(`(^|[^a-zA-Z0-9_-])\\.${className}(?![a-zA-Z0-9_-])`);
|
||||
const owned = shellRules.some((rule) => rule.selectors.some((selector) => classPattern.test(selector)));
|
||||
if (!owned) failures.push(`${contract.shellFile} does not own .${className}`);
|
||||
|
||||
for (const legacy of legacyRoots) {
|
||||
legacy.root.walkRules((rule) => {
|
||||
if (rule.selectors.some((selector) => classPattern.test(selector))) {
|
||||
failures.push(`${legacy.file} still contains shell selector ${rule.selector}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const selector of contract.sharedLayoutSelectors) {
|
||||
const owned = shellRules.some((rule) => rule.selectors.includes(selector) && mediaOf(rule) === undefined);
|
||||
if (!owned) failures.push(`${contract.shellFile} is missing shared layout selector ${selector}`);
|
||||
for (const legacy of legacyRoots) {
|
||||
legacy.root.walkRules((rule) => {
|
||||
if (rule.selectors.includes(selector)) {
|
||||
failures.push(`${legacy.file} still owns shared layout selector ${selector}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`R11 AppShell styles verification failed:\n- ${[...new Set(failures)].join('\n- ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`R11 AppShell styles verified: ${shellRules.length} rules, `
|
||||
+ `${contract.shellClasses.length + contract.legacyShellClasses.length} shell classes, `
|
||||
+ `${contract.sharedLayoutSelectors.length} shared layout selectors and responsive states are isolated.`,
|
||||
);
|
||||
Reference in New Issue
Block a user