/** * 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' 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}`) } } } function isAllowedEidosOnlyAttr(attr: string): boolean { return ( attr === 'data-event' || attr === 'data-event-id' || attr === 'data-event-phase' || attr === 'data-event-family' || attr === 'data-event-intent' || attr === 'data-archetype' || attr.endsWith('-color') || attr.endsWith('-size') || attr.endsWith('-variant') || attr.endsWith('-position') || attr.endsWith('-block') || attr.endsWith('-icon-only') ) } main().catch((err) => { console.error(err) process.exit(2) })