@xmachines/play-catalog
Documentation / @xmachines/play-catalog
Framework-agnostic UI schema with Zod validation for XMachines Play Architecture
Type-safe catalog defining component contracts without framework dependencies.
Overview
@xmachines/play-catalog provides defineCatalog() and defineComponents() for creating type-safe component catalogs with Zod schema validation. It enables business logic to define component props declaratively while maintaining zero framework dependencies — components can be React, Vue, Svelte, or any other framework.
Per RFC Play v1, this package implements:
- Strict Separation (INV-02): Business logic has zero framework imports
- Catalog validates prop types at state entry (build-time + runtime)
Installation
npm install zod@^4.3.6npm install @xmachines/play-catalogCurrent Exports
defineCatalogdefineComponents- types:
Catalog,InferComponentProps
Peer dependencies:
zod^4.3.6 — Schema validation (you control the version)
Quick Start
import { z } from "zod";import { defineCatalog } from "@xmachines/play-catalog";import type { InferComponentProps } from "@xmachines/play-catalog";
// 1. Define catalog with Zod schemas (business logic layer)const catalog = defineCatalog({ LoginForm: z.object({ error: z.string().optional(), }), Dashboard: z.object({ userId: z.string(), username: z.string(), stats: z.object({ logins: z.number(), lastLogin: z.date(), }), }),});
// 2. TypeScript infers prop types from schemastype DashboardProps = InferComponentProps<typeof catalog, "Dashboard">;// => { userId: string; username: string; stats: { logins: number; lastLogin: Date } }
// 3. Use in state machine meta.viewconst machine = setup({...}).createMachine({ states: { dashboard: { meta: { route: "/dashboard", view: { component: "Dashboard", // Type-checked against catalog props: { userId: "user123", username: "Alice", stats: { logins: 42, lastLogin: new Date(), }, }, }, }, }, },});API Reference
defineCatalog()
Create type-safe component catalog from Zod schemas:
const catalog = defineCatalog({ ComponentName: z.object({ /* props schema */ }),});Parameters:
schemas: Record<string, ZodObject>- Map of component names to Zod schemas
Returns: Frozen catalog object (immutable in development)
Example:
const catalog = defineCatalog({ UserProfile: z.object({ userId: z.string(), avatar: z.string().url().optional(), bio: z.string().max(500).optional(), }), SettingsForm: z.object({ theme: z.enum(["light", "dark"]), notifications: z.boolean(), }),});InferComponentProps<Catalog, Component>
Utility type extracting prop types from catalog:
type Props = InferComponentProps<typeof catalog, "ComponentName">;Example:
const catalog = defineCatalog({ Dashboard: z.object({ userId: z.string(), data: z.array(z.number()), }),});
type DashboardProps = InferComponentProps<typeof catalog, "Dashboard">;// => { userId: string; data: number[] }
// Use in component definitionfunction Dashboard(props: DashboardProps) { // props.userId is string // props.data is number[]}defineComponents()
Validate React component implementations match catalog (compile-time):
const components = defineComponents(catalog, { ComponentName: ComponentImplementation,});Compile-time validation:
- Component names must exist in catalog
- Component prop types must match schema-inferred types
Example:
import { defineComponents } from "@xmachines/play-catalog";
const catalog = defineCatalog({ LoginForm: z.object({ error: z.string().optional() }), Dashboard: z.object({ userId: z.string() }),});
const components = defineComponents(catalog, { LoginForm: ({ error }) => { // error is string | undefined (inferred from schema) return <form>{error && <p>{error}</p>}</form>; }, Dashboard: ({ userId }) => { // userId is string (inferred from schema) return <div>User: {userId}</div>; }, // @ts-expect-error - "InvalidComponent" not in catalog InvalidComponent: () => <div />,});Examples
Progressive Validation
import { z } from "zod";import { defineCatalog } from "@xmachines/play-catalog";
// Start simpleconst catalog = defineCatalog({ UserCard: z.object({ name: z.string(), }),});
// Add validation rulesconst enhancedCatalog = defineCatalog({ UserCard: z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().positive().max(150), }),});
// Complex nested schemasconst advancedCatalog = defineCatalog({ UserCard: z.object({ name: z.string().min(1), email: z.string().email(), profile: z.object({ avatar: z.string().url().optional(), bio: z.string().max(500).optional(), links: z.array(z.string().url()).max(5).optional(), }), preferences: z.object({ theme: z.enum(["light", "dark", "auto"]), language: z.string().length(2), // ISO 639-1 }), }),});Machine Integration
import { setup } from "xstate";import { defineCatalog } from "@xmachines/play-catalog";import { z } from "zod";
const catalog = defineCatalog({ LoginForm: z.object({ error: z.string().optional() }), Dashboard: z.object({ userId: z.string(), notifications: z.number(), }),});
const machine = setup({ types: { context: {} as { userId: string; errorMsg: string | null }, },}).createMachine({ initial: "login", context: { userId: "", errorMsg: null }, states: { login: { meta: { view: { component: "LoginForm", // Type-checked against catalog props: (context) => ({ error: context.errorMsg ?? undefined, }), }, }, on: { "auth.login": { target: "dashboard", actions: assign({ userId: ({ event }) => event.userId }), }, }, }, dashboard: { meta: { view: { component: "Dashboard", props: (context) => ({ userId: context.userId, notifications: 5, // Hardcoded for example }), }, }, }, },});Architecture
This package enforces Strict Separation (INV-02):
-
Zero Framework Dependencies:
- Catalog definition has no React/Vue/Svelte imports
- Business logic stays framework-agnostic
- Same catalog works with any renderer
-
Validation at State Entry:
- Zod validates props when actor enters state
- Invalid props trigger validation error (catchable)
- Build-time + runtime safety
-
Compile-Time Type Safety:
- TypeScript infers prop types from Zod schemas
- Component implementations type-checked at compile time
- Refactoring propagates through catalog and components
Benefits:
- Swap React for Vue without changing business logic
- Test catalog validation without framework overhead
- Framework updates don’t affect business logic
Related Packages
- @xmachines/play-xstate - XState adapter using catalogs
- @xmachines/play-react - React renderer consuming catalogs
- @xmachines/play - Protocol types
License
Copyright (c) 2016 Mikael Karon. All rights reserved.
This work is licensed under the terms of the MIT license.
For a copy, see https://opensource.org/licenses/MIT.