import type { FragmentDefinition, FragmentDefinitionV2, CompiledFragment, FragmentComponent, BlockDefinition, CompiledBlock, AIMetadata, FragmentGenerated, } from "./types.js"; import type { GovernedFragmentDefinition, ResolvedGovernedFragmentDefinition, } from "./governance.js"; import { fragmentDefinitionSchema, fragmentDefinitionV2Schema, governedFragmentDefinitionSchema, blockDefinitionSchema, } from "./schema.js"; import { resolveComponentGovernance } from "./governance.js"; function isGovernedDefinition( def: FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition ): def is GovernedFragmentDefinition { return "govern" in def || "governance" in def; } /** * Check if a definition uses v2 field names. * Detects `guidance` or `examples` (v2) vs `usage` or `variants` (v1). */ function isV2Definition( def: FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition ): def is FragmentDefinitionV2 { return !isGovernedDefinition(def) && ("guidance" in def || "examples" in def); } /** * Normalize a v2 definition to v1 shape for downstream compatibility. * The build pipeline, compiler, and validators all work with v1 internally. */ export function normalizeToV1(def: FragmentDefinitionV2): FragmentDefinition { // Map composition → ai (rename fields) let ai: AIMetadata | undefined; if (def.composition) { ai = { compositionPattern: def.composition.pattern, subComponents: def.composition.subComponents, requiredChildren: def.composition.requiredChildren, commonPatterns: def.composition.commonPatterns, }; } // Map _provenance → _generated (narrow source type, drop extended fields) let generated: FragmentGenerated | undefined; if (def._provenance) { generated = { source: def._provenance.source === "scan" ? "ai" : def._provenance.source, sourceFile: def._provenance.sourceFile, confidence: def._provenance.confidence, timestamp: def._provenance.timestamp, }; } return { component: def.component, meta: def.meta, usage: def.guidance, props: def.props, relations: def.relations, variants: def.examples, contract: def.contract, ai, _generated: generated, }; } /** * Define a fragment for a component. * * This is the main API for creating fragment documentation. * It provides runtime validation and type safety. * * Accepts both v1 and v2 shapes: * - v1: `{ usage, variants, ai, _generated }` * - v2: `{ guidance, examples, composition, _provenance }` * * @example v1 * ```tsx * import { defineFragment } from '@fragments-sdk/core'; * import { Button } from './Button'; * * export default defineFragment({ * component: Button, * meta: { * name: 'Button', * description: 'Primary action trigger', * category: 'actions', * }, * usage: { * when: ['User needs to trigger an action'], * whenNot: ['Navigation without side effects'], * }, * props: { * variant: { * type: 'enum', * values: ['primary', 'secondary'], * default: 'primary', * description: 'Visual style', * }, * }, * variants: [ * { * name: 'Default', * description: 'Default button', * render: () => , * }, * ], * }); * ``` * * @example v2 * ```tsx * import { defineFragment } from '@fragments-sdk/core'; * import { Card } from './Card'; * * export default defineFragment({ * component: Card, * meta: { name: 'Card', description: 'Content container', category: 'layout' }, * guidance: { * when: ['Grouping related content'], * whenNot: ['Full-page layouts'], * }, * props: { ... }, * examples: [{ name: 'Default', description: 'Basic card', render: () => ... }], * composition: { pattern: 'compound', subComponents: ['Header', 'Body', 'Footer'] }, * }); * ``` */ export function defineFragment( definition: GovernedFragmentDefinition ): ResolvedGovernedFragmentDefinition; export function defineFragment( definition: FragmentDefinition ): FragmentDefinition; export function defineFragment( definition: FragmentDefinitionV2 ): FragmentDefinitionV2; export function defineFragment( definition: | FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition ): | FragmentDefinition | FragmentDefinitionV2 | ResolvedGovernedFragmentDefinition { // Validate at runtime in development if (process.env.NODE_ENV !== "production") { const governed = isGovernedDefinition(definition); const v2 = isV2Definition(definition); const schema = governed ? governedFragmentDefinitionSchema : v2 ? fragmentDefinitionV2Schema : fragmentDefinitionSchema; const result = schema.safeParse(definition); if (!result.success) { const name = definition.meta?.name || "unknown"; const errors = result.error.errors .map((e) => ` - ${e.path.join(".")}: ${e.message}`) .join("\n"); throw new Error(`Invalid fragment definition for "${name}":\n${errors}`); } } if (isGovernedDefinition(definition)) { const governance = resolveComponentGovernance(definition); return { ...definition, governance, }; } return definition; } /** * Compile a fragment definition to JSON-serializable format. * Used for generating fragments.json for AI consumption. * * Accepts both v1 and v2 shapes. V2 is normalized to v1 internally. */ export function compileFragment( definition: FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition, filePath: string ): CompiledFragment { // Normalize v2 → v1 if needed const v1 = isGovernedDefinition(definition) ? { component: definition.component, meta: definition.meta, usage: definition.guidance, props: definition.props ?? {}, relations: definition.relations, variants: definition.examples ?? [], contract: definition.contract, _generated: definition._provenance ? { source: definition._provenance.source === "scan" ? "ai" : definition._provenance.source, sourceFile: definition._provenance.sourceFile, confidence: definition._provenance.confidence, timestamp: definition._provenance.timestamp, } : undefined, } : isV2Definition(definition) ? normalizeToV1(definition) : definition; return { filePath, meta: v1.meta, ...(isGovernedDefinition(definition) && { guidance: definition.guidance }), usage: v1.usage, props: v1.props, ...(isGovernedDefinition(definition) && { governance: resolveComponentGovernance(definition), }), relations: v1.relations, variants: v1.variants.map((v) => ({ name: v.name, description: v.description, code: v.code, figma: v.figma, })), contract: v1.contract, ai: v1.ai, _generated: v1._generated, }; } /** * Define a composition block. * * Blocks are pure data describing how design system components * wire together for common use cases. */ export function defineBlock(definition: BlockDefinition): BlockDefinition { if (process.env.NODE_ENV !== "production") { const result = blockDefinitionSchema.safeParse(definition); if (!result.success) { const errors = result.error.errors .map((e) => ` - ${e.path.join(".")}: ${e.message}`) .join("\n"); throw new Error(`Invalid block definition for "${definition.name || "unknown"}":\n${errors}`); } } return definition; } /** * @deprecated Use defineBlock instead */ export const defineRecipe = defineBlock; /** * Compile a block definition to JSON-serializable format. */ export function compileBlock(definition: BlockDefinition, filePath: string): CompiledBlock { return { filePath, name: definition.name, description: definition.description, category: definition.category, components: definition.components, code: definition.code, tags: definition.tags, }; } /** * @deprecated Use compileBlock instead */ export const compileRecipe = compileBlock; /** * Type helper for extracting props type from a component */ export type InferProps = T extends FragmentComponent ? P : never;