/** * cli:scaffold-component — preflight.ts * * Fail-closed primitive preflight: every `@/components/ui/

` import the * generated output carries must resolve on disk (or be written by THIS * invocation). The historical hole: `ss upgrade` never re-runs * scaffold-ui-primitives, so a page importing a NEWER primitive (SectionCard) * compiled against nothing — and the imports-resolve gate then pushed * auto-heal into improvising the markup by hand (the « formulaire empilé sous * la fiche » shape DEV-UI-045 now flags). Refusing up front, with the * remediation named, is the only exit that keeps the read-first contract. */ export interface PreflightMiss { primitive: string expected: string } const UI_IMPORT_RE = /from '@\/components\/ui\/([A-Za-z0-9]+)'/g /** * Pure check — `exists` is injected so tests need no disk. `files` are the * generation output (path + content); a primitive written by this very * invocation never counts as missing. */ export function checkPrimitivePreflight( outdirAbs: string, files: ReadonlyArray<{ path: string; content: string }>, exists: (abs: string) => boolean, joinPath: (...parts: string[]) => string, ): PreflightMiss[] { const needed = new Set() for (const f of files) { for (const m of f.content.matchAll(UI_IMPORT_RE)) needed.add(m[1]) } const writtenBasenames = new Set(files.map(f => f.path.replace(/\\/g, '/').split('/').pop()!)) const misses: PreflightMiss[] = [] for (const primitive of [...needed].sort()) { if (writtenBasenames.has(`${primitive}.tsx`) || writtenBasenames.has(`${primitive}.ts`)) continue const candidates = [ joinPath(outdirAbs, 'src', 'components', 'ui', `${primitive}.tsx`), // useListState (hooks module) ships as .ts joinPath(outdirAbs, 'src', 'components', 'ui', `${primitive}.ts`), ] if (candidates.some(exists)) continue misses.push({ primitive, expected: `src/components/ui/${primitive}.tsx` }) } return misses }