diff --git a/scripts/validate-json-ui-registry.cjs b/scripts/validate-json-ui-registry.cjs new file mode 100644 index 0000000..f7927dd --- /dev/null +++ b/scripts/validate-json-ui-registry.cjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +const registryPath = path.join(process.cwd(), 'json-components-registry.json') +const schemaPath = path.join(process.cwd(), 'src', 'schemas', 'registry-validation.json') + +if (!fs.existsSync(registryPath)) { + console.error('❌ Could not find json-components-registry.json') + process.exit(1) +} + +if (!fs.existsSync(schemaPath)) { + console.error('❌ Could not find src/schemas/registry-validation.json') + process.exit(1) +} + +const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')) +const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) + +const primitiveTypes = new Set([ + 'div', + 'span', + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'section', + 'article', + 'header', + 'footer', + 'main', + 'aside', + 'nav', +]) + +const registryTypes = new Set() + +for (const entry of registry.components || []) { + if (entry.source === 'atoms' || entry.source === 'molecules') { + const name = entry.export || entry.name || entry.type + if (name) { + registryTypes.add(name) + } + } +} + +const schemaTypes = new Set() + +const collectTypes = (components) => { + if (!components) return + if (Array.isArray(components)) { + components.forEach(collectTypes) + return + } + if (components.type) { + schemaTypes.add(components.type) + } + if (components.children) { + collectTypes(components.children) + } +} + +collectTypes(schema.components || []) + +const missing = [] +for (const type of schemaTypes) { + if (!primitiveTypes.has(type) && !registryTypes.has(type)) { + missing.push(type) + } +} + +if (missing.length) { + console.error(`❌ Missing registry entries for: ${missing.join(', ')}`) + process.exit(1) +} + +console.log('✅ JSON UI registry validation passed for primitives and atom/molecule components.') diff --git a/src/lib/json-ui/component-registry.ts b/src/lib/json-ui/component-registry.ts index a48105c..3e53dfe 100644 --- a/src/lib/json-ui/component-registry.ts +++ b/src/lib/json-ui/component-registry.ts @@ -17,40 +17,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { Heading } from '@/components/atoms/Heading' -import { Text } from '@/components/atoms/Text' -import { TextArea } from '@/components/atoms/TextArea' -import { List as ListComponent } from '@/components/atoms/List' -import { Grid } from '@/components/atoms/Grid' -import { Stack } from '@/components/atoms/Stack' -import { Flex } from '@/components/atoms/Flex' -import { Container } from '@/components/atoms/Container' -import { Link } from '@/components/atoms/Link' -import { Image } from '@/components/atoms/Image' -import { Avatar as AtomAvatar } from '@/components/atoms/Avatar' -import { Code } from '@/components/atoms/Code' -import { Tag } from '@/components/atoms/Tag' -import { Spinner } from '@/components/atoms/Spinner' -import { Skeleton as AtomSkeleton } from '@/components/atoms/Skeleton' -import { Slider } from '@/components/atoms/Slider' -import { NumberInput } from '@/components/atoms/NumberInput' -import { Radio } from '@/components/atoms/Radio' -import { Alert as AtomAlert } from '@/components/atoms/Alert' -import { InfoBox } from '@/components/atoms/InfoBox' -import { EmptyState } from '@/components/atoms/EmptyState' -import { Table as AtomTable } from '@/components/atoms/Table' -import { KeyValue } from '@/components/atoms/KeyValue' -import { StatCard } from '@/components/atoms/StatCard' -import { StatusBadge } from '@/components/atoms/StatusBadge' -import { DataCard } from '@/components/molecules/DataCard' -import { SearchInput } from '@/components/molecules/SearchInput' -import { ActionBar } from '@/components/molecules/ActionBar' -import { AppBranding } from '@/components/molecules/AppBranding' -import { LabelWithBadge } from '@/components/molecules/LabelWithBadge' -import { EmptyEditorState } from '@/components/molecules/EmptyEditorState' -import { LoadingFallback } from '@/components/molecules/LoadingFallback' -import { LoadingState } from '@/components/molecules/LoadingState' -import { NavigationGroupHeader } from '@/components/molecules/NavigationGroupHeader' +import * as AtomComponents from '@/components/atoms' +import * as MoleculeComponents from '@/components/molecules' +import jsonComponentsRegistry from '../../../json-components-registry.json' import { ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass, Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed, @@ -64,6 +33,42 @@ export interface UIComponentRegistry { [key: string]: ComponentType } +interface JsonRegistryEntry { + name?: string + type?: string + export?: string + source?: string +} + +interface JsonComponentRegistry { + components?: JsonRegistryEntry[] +} + +const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry + +const buildRegistryFromNames = ( + names: string[], + components: Record> +): UIComponentRegistry => { + return names.reduce((registry, name) => { + const component = components[name] + if (component) { + registry[name] = component + } + return registry + }, {}) +} + +const jsonRegistryEntries = jsonRegistry.components ?? [] +const atomRegistryNames = jsonRegistryEntries + .filter((entry) => entry.source === 'atoms') + .map((entry) => entry.export ?? entry.name ?? entry.type) + .filter((name): name is string => Boolean(name)) +const moleculeRegistryNames = jsonRegistryEntries + .filter((entry) => entry.source === 'molecules') + .map((entry) => entry.export ?? entry.name ?? entry.type) + .filter((name): name is string => Boolean(name)) + export const primitiveComponents: UIComponentRegistry = { div: 'div' as any, span: 'span' as any, @@ -131,45 +136,15 @@ export const shadcnComponents: UIComponentRegistry = { AvatarImage, } -export const atomComponents: UIComponentRegistry = { - Heading, - Text, - TextArea, - List: ListComponent, - Grid, - Stack, - Flex, - Container, - Link, - Image, - Avatar: AtomAvatar, - Code, - Tag, - Spinner, - Skeleton: AtomSkeleton, - Slider, - NumberInput, - Radio, - Alert: AtomAlert, - InfoBox, - EmptyState, - Table: AtomTable, - KeyValue, - StatCard, - StatusBadge, -} +export const atomComponents: UIComponentRegistry = buildRegistryFromNames( + atomRegistryNames, + AtomComponents as Record> +) -export const moleculeComponents: UIComponentRegistry = { - DataCard, - SearchInput, - ActionBar, - AppBranding, - LabelWithBadge, - EmptyEditorState, - LoadingFallback, - LoadingState, - NavigationGroupHeader, -} +export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames( + moleculeRegistryNames, + MoleculeComponents as Record> +) export const iconComponents: UIComponentRegistry = { ArrowLeft, diff --git a/src/schemas/registry-validation.json b/src/schemas/registry-validation.json new file mode 100644 index 0000000..ac591af --- /dev/null +++ b/src/schemas/registry-validation.json @@ -0,0 +1,74 @@ +{ + "id": "registry-validation", + "name": "Registry Validation", + "layout": { + "type": "single" + }, + "dataSources": [], + "components": [ + { + "id": "root", + "type": "div", + "props": { + "className": "p-6 space-y-6" + }, + "children": [ + { + "id": "branding", + "type": "AppBranding", + "props": { + "title": "Registry Validation", + "subtitle": "Atoms, molecules, and primitives" + } + }, + { + "id": "stack", + "type": "Stack", + "props": { + "direction": "horizontal", + "spacing": "lg", + "className": "items-stretch" + }, + "children": [ + { + "id": "stat-card", + "type": "StatCard", + "props": { + "title": "Active Users", + "value": "128", + "description": "Past 24 hours" + } + }, + { + "id": "flex", + "type": "Flex", + "props": { + "direction": "col", + "gap": "sm", + "className": "rounded-md border p-4" + }, + "children": [ + { + "id": "flex-title", + "type": "Heading", + "props": { + "level": 4, + "children": "Flex Container" + } + }, + { + "id": "flex-text", + "type": "Text", + "props": { + "children": "Ensures primitives and atom components resolve." + } + } + ] + } + ] + } + ] + } + ], + "globalActions": [] +}