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.`, );