/** * Lint every eidos component CSS against its morfo contract. * * Usage: npx tsx scripts/eidos-lint-all.ts * * Prints a per-component summary so the drift between eidos and the * morfo declarations is visible at a glance. */ import { existsSync, readFileSync, readdirSync } from 'node:fs' import { resolve } from 'node:path' import { compileMorfo } from '../src/uix/morfo/compile' import { lintEidosCss } from '../src/uix/eidos/lint' import type { Morfo } from '../src/uix/morfo/types' const EIDOS_ONLY_ATTRS = new Set([ 'data-archetype', 'data-block', 'data-collapsed', 'data-color', 'data-columns', 'data-dragging', 'data-event', 'data-event-family', 'data-event-id', 'data-event-intent', 'data-event-phase', 'data-fitted', 'data-icon-only', 'data-indicator-placement', 'data-motion', 'data-motion-disabled', 'data-position', 'data-ready', 'data-scrollable', 'data-sheet', 'data-size', 'data-floating-wrapper', 'data-svg', 'data-svg-direction', 'data-variant' ]) const RUNTIME_SIGNAL_ATTRS = new Set([ 'data-disabled', 'data-invalid', 'data-last-action', 'data-orientation', 'data-readonly', 'data-side', 'data-state' ]) const EIDOS_ONLY_PART_SUFFIXES = [ 'footer', 'handle', 'header', 'indicator', 'label', 'main', 'row', 'row-label', 'status' ] as const async function main() { const eidosDir = resolve('src/uix/eidos/components') const cssFiles = readdirSync(eidosDir, { withFileTypes: true }).flatMap((entry) => { if (entry.isFile() && entry.name.endsWith('.css')) { const name = entry.name.replace(/\.css$/, '') return [{ name, path: resolve(eidosDir, entry.name) }] } if (entry.isDirectory()) { const path = resolve(eidosDir, entry.name, `${entry.name}.css`) return existsSync(path) ? [{ name: entry.name, path }] : [] } return [] }) console.log('component morfo eidos invalid unused') console.log('─────────────────────────────────────────────────────') let totalInvalid = 0 let totalEidosOnly = 0 let totalUnused = 0 const driftHotspots: { name: string; eidosOnly: string[]; invalid: string[] }[] = [] for (const file of cssFiles) { const name = file.name const morfoPath = resolve(`src/uix/morfo/components/${name}.ts`) try { readFileSync(morfoPath) } catch { console.log(`${name.padEnd(22)}(no morfo file — skipped)`) continue } const mod = (await import(`../src/uix/morfo/components/${name}`)) as Record const exportName = `${name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())}Morfo` const morfo = mod[exportName] as Morfo | undefined if (!morfo) { console.log(`${name.padEnd(22)}(${exportName} not exported — skipped)`) continue } const compiled = compileMorfo(morfo) const partMarkers = new Set(Object.values(compiled.parts.attrs)) const css = readFileSync(file.path, 'utf-8') const report = lintEidosCss(css, compiled) console.log( `${name.padEnd(22)}${String(report.counts.morfoBacked).padEnd(7)}${String(report.counts.eidosOnly).padEnd(7)}${String(report.counts.invalid).padEnd(9)}${report.unusedFromContract.length}` ) totalInvalid += report.counts.invalid totalEidosOnly += report.counts.eidosOnly totalUnused += report.unusedFromContract.length // Filter eidos-only that look like drift (data-* on a part where the morfo // never declared the attr — vs design tokens like data-color/-size/-variant). const drift = report.findings.filter( (f) => f.status === 'eidos-only' && (f.eidosOnlyAttrs ?? f.attrs).some( (a) => a.attr.startsWith('data-') && !partMarkers.has(a.attr) && !isAllowedEidosOnlyAttr(a.attr) ) ) const invalid = report.findings.filter((f) => f.status === 'invalid') if (drift.length || invalid.length) { driftHotspots.push({ name, eidosOnly: drift.map((f) => f.rule), invalid: invalid.map((f) => `${f.rule} — ${f.reason}`) }) } } console.log('─────────────────────────────────────────────────────') console.log(`totals${' '.repeat(16)} ${totalEidosOnly} ${totalInvalid} ${totalUnused}`) if (driftHotspots.length) { console.log('\nDrift hotspots (eidos uses data-* attrs the morfo does not declare):') for (const h of driftHotspots) { if (!h.eidosOnly.length && !h.invalid.length) continue console.log(`\n ${h.name}`) for (const r of h.invalid) console.log(` ✗ ${r}`) for (const r of h.eidosOnly) console.log(` · ${r}`) } } else { console.log('\nNo drift hotspots beyond documented Eidos-only attrs/parts.') } if (totalInvalid > 0) process.exit(1) } function isAllowedEidosOnlyAttr(attr: string): boolean { if (EIDOS_ONLY_ATTRS.has(attr)) return true if (RUNTIME_SIGNAL_ATTRS.has(attr)) return true return EIDOS_ONLY_PART_SUFFIXES.some((suffix) => attr.endsWith(`-${suffix}`)) } main().catch((err) => { console.error(err) process.exit(2) })