98 lines
3.5 KiB
JavaScript
98 lines
3.5 KiB
JavaScript
import { readFile, stat } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fail, pass, repositoryRoot, walk } from './lib.mjs';
|
|
|
|
const inventoryPath = path.join(repositoryRoot, 'docs/phase12/COMPONENT_INVENTORY_v1.0.json');
|
|
const inventory = JSON.parse(await readFile(inventoryPath, 'utf8'));
|
|
const errors = [];
|
|
const allowedStatus = new Set(['implemented', 'partially-implemented', 'blocked', 'excluded']);
|
|
const names = new Set();
|
|
|
|
if (inventory.phase !== 12 || !Array.isArray(inventory.entries)) {
|
|
errors.push('Component inventory must identify Phase 12 and contain an entries array.');
|
|
}
|
|
for (const entry of inventory.entries ?? []) {
|
|
if (names.has(entry.name)) errors.push(`Duplicate inventory component: ${entry.name}`);
|
|
names.add(entry.name);
|
|
if (!allowedStatus.has(entry.status))
|
|
errors.push(`${entry.name}: invalid status ${entry.status}`);
|
|
for (const field of ['sourceSpecification', 'variants', 'openIssues', 'homepageConsumers']) {
|
|
if (!Array.isArray(entry[field])) errors.push(`${entry.name}: ${field} must be an array.`);
|
|
}
|
|
if (
|
|
!entry.tests ||
|
|
!['unit', 'interaction', 'accessibility', 'visual'].every(
|
|
(key) => typeof entry.tests[key] === 'boolean',
|
|
)
|
|
) {
|
|
errors.push(`${entry.name}: test coverage flags are incomplete.`);
|
|
}
|
|
if (entry.status === 'blocked' && entry.openIssues.length === 0) {
|
|
errors.push(`${entry.name}: blocked components require an issue reference.`);
|
|
}
|
|
}
|
|
|
|
for (const required of [
|
|
'Button',
|
|
'ActionLink',
|
|
'FormField',
|
|
'Tabs',
|
|
'Accordion',
|
|
'Dialog',
|
|
'Alert',
|
|
'Card',
|
|
'Workflow',
|
|
'Comparison',
|
|
'Metric',
|
|
'CTA',
|
|
'ProductPreview',
|
|
]) {
|
|
if (!names.has(required)) errors.push(`Required component missing from inventory: ${required}`);
|
|
}
|
|
|
|
const componentFiles = await walk(path.join(repositoryRoot, 'src/components'), (file) =>
|
|
/\.(?:ts|tsx|css)$/.test(file),
|
|
);
|
|
const forbiddenRawToken = /var\(--color-(?:blue|orange|gray|green|amber|red|navy)-/g;
|
|
const forbiddenLiteralColor = /(?:#[0-9a-f]{3,8}\b|rgba?\(|hsla?\()/gi;
|
|
for (const file of componentFiles) {
|
|
const text = await readFile(file, 'utf8');
|
|
const relative = path.relative(repositoryRoot, file);
|
|
if (file.endsWith('.css') && forbiddenRawToken.test(text))
|
|
errors.push(`${relative}: consumes a raw palette token.`);
|
|
if (file.endsWith('.css') && forbiddenLiteralColor.test(text))
|
|
errors.push(`${relative}: contains a literal color.`);
|
|
if (/href=["']#["']/.test(text)) errors.push(`${relative}: contains a placeholder href="#".`);
|
|
}
|
|
|
|
for (const artifact of [
|
|
'COMPONENT_SPECIFICATION_TRACEABILITY_v1.0.csv',
|
|
'VARIANT_INVENTORY_v1.0.csv',
|
|
'ACCESSIBILITY_BEHAVIOR_MATRIX_v1.0.csv',
|
|
'RTL_BEHAVIOR_MATRIX_UPDATE_v1.0.csv',
|
|
'RESPONSIVE_VALIDATION_MATRIX_v1.0.csv',
|
|
'STORY_AND_FIXTURE_INVENTORY_v1.0.csv',
|
|
'TEST_COVERAGE_REPORT_v1.0.csv',
|
|
'COMPONENT_CATALOG_v1.0.md',
|
|
]) {
|
|
try {
|
|
await stat(path.join(repositoryRoot, 'docs/phase12', artifact));
|
|
} catch {
|
|
errors.push(`Required Phase 12 artifact missing: ${artifact}`);
|
|
}
|
|
}
|
|
|
|
const fixturePage = await readFile(
|
|
path.join(repositoryRoot, 'src/app/[locale]/component-lab/page.tsx'),
|
|
'utf8',
|
|
);
|
|
if (!fixturePage.includes("COMPONENT_FIXTURES_ENABLED !== 'true'")) {
|
|
errors.push('Component fixture route is not protected by the production environment guard.');
|
|
}
|
|
|
|
if (errors.length > 0) fail(errors);
|
|
else
|
|
pass(
|
|
`${inventory.entries.length} inventory entries and ${componentFiles.length} component source files satisfy Phase 12 static controls.`,
|
|
);
|