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/client-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 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 clientFiles = listFiles('src/apps/client', ['.ts', '.tsx']); const adminFiles = listFiles('src/apps/admin', ['.ts', '.tsx']); const countUsage = (files, className) => files.filter((file) => fs.readFileSync(file, 'utf8').includes(className)).length; const selectorClasses = (selector) => [...selector.matchAll(/\.([_a-zA-Z]+[\w-]*)/g)].map((match) => match[1]); 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/client.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 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) => { ruleCount += 1; for (const selector of rule.selectors ?? [rule.selector]) { selectorCount += 1; const classes = selectorClasses(selector); if (classes.length === 0 || !classes.every((className) => contract.ownedClasses.includes(className))) { failures.push(`${contract.styleFile} contains a non-client-shared selector: ${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 [className, minimumFiles] of Object.entries(contract.usageRequirements)) { const clientCount = countUsage(clientFiles, className); const adminCount = countUsage(adminFiles, className); if (clientCount < minimumFiles) failures.push(`${className} is used by ${clientCount} client files; minimum is ${minimumFiles}`); if (adminCount !== 0) failures.push(`${className} is unexpectedly used by ${adminCount} admin files`); const pureSelector = new RegExp(`^\\s*\\.${className}\\s*\\{`, 'm'); if (pureSelector.test(globalStyle)) failures.push(`${contract.globalStyleFile} still owns .${className}`); } for (const selector of contract.requiredGlobalContextSelectors) { if (!globalStyle.includes(selector)) failures.push(`${contract.globalStyleFile} lost contextual override ${selector}`); } for (const [className, requirement] of Object.entries(contract.crossPortalClassesKeptGlobal)) { if (!globalStyle.includes(`.${className}`)) failures.push(`${contract.globalStyleFile} lost cross-portal class .${className}`); const clientCount = countUsage(clientFiles, className); const adminCount = countUsage(adminFiles, className); if (clientCount < requirement.minimumClientFiles) failures.push(`${className} client usage dropped to ${clientCount}`); if (adminCount < requirement.minimumAdminFiles) failures.push(`${className} admin usage dropped to ${adminCount}`); } for (const className of contract.singlePageClassesKeptGlobal) { if (!globalStyle.includes(`.${className}`)) failures.push(`${contract.globalStyleFile} lost single-page class .${className}`); } if (failures.length > 0) { console.error(`R11 client shared styles verification failed:\n- ${failures.join('\n- ')}`); process.exit(1); } console.log( `R11 client shared styles verified: ${ruleCount} rule, ${selectorCount} selector, ` + `${Object.keys(contract.crossPortalClassesKeptGlobal).length} cross-portal boundaries and single-page ownership are preserved.`, );