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