feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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/shared-components-r11.json'), 'utf8'),
|
||||
);
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const failures = [];
|
||||
|
||||
function mediaOf(rule) {
|
||||
let parent = rule.parent;
|
||||
while (parent) {
|
||||
if (parent.type === 'atrule' && parent.name === 'media') return parent.params;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizedDeclarations(rule) {
|
||||
return rule.nodes
|
||||
.filter((node) => node.type === 'decl')
|
||||
.map((node) => `${node.prop}:${node.value}${node.important ? ' !important' : ''}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function selectorClasses(selector) {
|
||||
return (selector.match(/\.([a-zA-Z0-9_-]+)/g) ?? []).map((value) => value.slice(1));
|
||||
}
|
||||
|
||||
function isPureSharedSelector(selector) {
|
||||
const classes = selectorClasses(selector);
|
||||
const generic = new Set(contract.genericClasses);
|
||||
const states = new Set(contract.allowedStateClasses);
|
||||
const ownsSharedClass = classes.some((className) => className.startsWith('ui-') || generic.has(className));
|
||||
return classes.length > 0
|
||||
&& ownsSharedClass
|
||||
&& classes.every((className) => className.startsWith('ui-') || generic.has(className) || states.has(className));
|
||||
}
|
||||
|
||||
const entry = read(contract.entry);
|
||||
const globalImport = entry.indexOf(`import '@/styles/global.css';`);
|
||||
const componentImport = entry.indexOf(`import '@/styles/components.css';`);
|
||||
if (globalImport < 0 || componentImport < 0 || globalImport >= componentImport) {
|
||||
failures.push(`${contract.componentsFile} must load after ${contract.legacyFile}`);
|
||||
}
|
||||
|
||||
const componentsSource = read(contract.componentsFile);
|
||||
const markerIndexes = [
|
||||
componentsSource.indexOf(contract.compatibilityMarker),
|
||||
componentsSource.indexOf(contract.canonicalMarker),
|
||||
componentsSource.indexOf(contract.responsiveMarker),
|
||||
];
|
||||
if (markerIndexes.some((index) => index < 0) || !(markerIndexes[0] < markerIndexes[1] && markerIndexes[1] < markerIndexes[2])) {
|
||||
failures.push('compatibility, canonical and responsive component sections are missing or out of order');
|
||||
}
|
||||
|
||||
const componentsRoot = postcss.parse(componentsSource, { from: contract.componentsFile });
|
||||
const componentRules = [];
|
||||
componentsRoot.walkRules((rule) => componentRules.push(rule));
|
||||
const selectorCount = componentRules.reduce((total, rule) => total + rule.selectors.length, 0);
|
||||
if (componentRules.length < contract.minimumRuleCount) {
|
||||
failures.push(`${contract.componentsFile} has ${componentRules.length} rules; expected at least ${contract.minimumRuleCount}`);
|
||||
}
|
||||
if (selectorCount < contract.minimumSelectorCount) {
|
||||
failures.push(`${contract.componentsFile} has ${selectorCount} selectors; expected at least ${contract.minimumSelectorCount}`);
|
||||
}
|
||||
|
||||
for (const requirement of contract.requiredRuleFragments) {
|
||||
const matches = componentRules.filter(
|
||||
(rule) => rule.selectors.includes(requirement.selector) && mediaOf(rule) === requirement.media,
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
failures.push(
|
||||
`${contract.componentsFile} is missing ${requirement.selector}`
|
||||
+ `${requirement.media ? ` in @media ${requirement.media}` : ' outside media queries'}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const declarations = matches.map(normalizedDeclarations).join(';');
|
||||
for (const fragment of requirement.includes) {
|
||||
if (!declarations.includes(fragment)) {
|
||||
failures.push(`${requirement.selector} is missing declaration ${fragment}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const legacyRoot = postcss.parse(read(contract.legacyFile), { from: contract.legacyFile });
|
||||
legacyRoot.walkRules((rule) => {
|
||||
for (const selector of rule.selectors) {
|
||||
if (isPureSharedSelector(selector)) {
|
||||
failures.push(`${contract.legacyFile} still owns shared component selector ${selector}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const [file, classNames] of Object.entries(contract.requiredComponentBindings)) {
|
||||
const source = read(file);
|
||||
for (const className of classNames) {
|
||||
if (!source.includes(className)) failures.push(`${file} is missing component class ${className}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`R11 shared components verification failed:\n- ${[...new Set(failures)].join('\n- ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`R11 shared components verified: ${componentRules.length} rules, ${selectorCount} selectors, `
|
||||
+ `${contract.genericClasses.length} generic class families and desktop/mobile ownership are isolated.`,
|
||||
);
|
||||
Reference in New Issue
Block a user