102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const root = process.cwd();
|
|
const contract = JSON.parse(
|
|
fs.readFileSync(path.join(root, 'docs/contracts/foundation-styles-r11.json'), 'utf8'),
|
|
);
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
const failures = [];
|
|
|
|
function normalizeSelector(selector) {
|
|
return selector.replace(/\s+/g, '').trim();
|
|
}
|
|
|
|
function normalizeDeclarations(declarations) {
|
|
return declarations
|
|
.split(';')
|
|
.map((declaration) => declaration.trim())
|
|
.filter(Boolean)
|
|
.join(';');
|
|
}
|
|
|
|
function extractFlatRules(source) {
|
|
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
return [...withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((match) => ({
|
|
selector: normalizeSelector(match[1]),
|
|
declarations: normalizeDeclarations(match[2]),
|
|
}));
|
|
}
|
|
|
|
const entry = read(contract.entry);
|
|
let previousImportIndex = -1;
|
|
for (const importPath of contract.importOrder) {
|
|
const statement = `import '${importPath}';`;
|
|
const importIndex = entry.indexOf(statement);
|
|
if (importIndex < 0) {
|
|
failures.push(`${contract.entry} is missing ${statement}`);
|
|
} else if (importIndex <= previousImportIndex) {
|
|
failures.push(`${importPath} is not loaded in the required foundation-to-component order`);
|
|
}
|
|
previousImportIndex = importIndex;
|
|
}
|
|
const applicationImportIndex = entry.indexOf(contract.applicationImport);
|
|
if (applicationImportIndex < 0) {
|
|
failures.push(`${contract.entry} is missing the application route import`);
|
|
} else if (applicationImportIndex <= previousImportIndex) {
|
|
failures.push('AppRoutes must enter the dependency graph after the ordered foundation/component styles');
|
|
}
|
|
|
|
const tokens = read(contract.tokensFile);
|
|
const tokenNames = [...tokens.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gim)].map((match) => match[1]);
|
|
if (tokenNames.length < contract.minimumTokenCount) {
|
|
failures.push(`${contract.tokensFile} has ${tokenNames.length} tokens; expected at least ${contract.minimumTokenCount}`);
|
|
}
|
|
for (const token of contract.requiredTokens) {
|
|
if (!tokenNames.includes(token)) failures.push(`${contract.tokensFile} is missing ${token}`);
|
|
}
|
|
const tokenRules = extractFlatRules(tokens);
|
|
if (tokenRules.length !== 1 || tokenRules[0].selector !== ':root') {
|
|
failures.push(`${contract.tokensFile} must remain a single :root design-token scope`);
|
|
}
|
|
|
|
const reset = read(contract.resetFile);
|
|
const resetRules = extractFlatRules(reset);
|
|
const expectedSelectors = Object.keys(contract.resetRuleHashes);
|
|
const actualSelectors = resetRules.map((rule) => rule.selector);
|
|
for (const selector of expectedSelectors) {
|
|
const matches = resetRules.filter((rule) => rule.selector === selector);
|
|
if (matches.length !== 1) {
|
|
failures.push(`${contract.resetFile} must contain exactly one ${selector} rule`);
|
|
continue;
|
|
}
|
|
const actualHash = crypto.createHash('sha256').update(matches[0].declarations).digest('hex');
|
|
if (actualHash !== contract.resetRuleHashes[selector]) {
|
|
failures.push(`${contract.resetFile} changed declarations for ${selector}`);
|
|
}
|
|
}
|
|
for (const selector of actualSelectors) {
|
|
if (!expectedSelectors.includes(selector)) {
|
|
failures.push(`${contract.resetFile} contains non-foundation selector ${selector}`);
|
|
}
|
|
}
|
|
|
|
const sourceRules = extractFlatRules(read(contract.sourceFile));
|
|
const remainingSelectors = new Set(sourceRules.map((rule) => rule.selector));
|
|
for (const selector of expectedSelectors) {
|
|
if (remainingSelectors.has(selector)) {
|
|
failures.push(`${contract.sourceFile} still owns reset selector ${selector}`);
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`R11 foundation styles verification failed:\n- ${failures.join('\n- ')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`R11 foundation styles verified: ${tokenNames.length} design tokens, `
|
|
+ `${resetRules.length} reset/base rules and deterministic import order preserved.`,
|
|
);
|