# @alkdev/ujsx Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas. UJSX treats JSX as an intermediate representation for multi-target rendering. The same declarative tree can target different hosts (markdown, graph structures, DOM, workflow engines) through a `HostConfig` adapter. No `onClick`, no `className`, no `style` — the tree is a pure data structure, and hosts are interpreters. ## Install ```bash npm install @alkdev/ujsx ``` Requires Node.js 18+. Dual ESM/CJS output with TypeScript declarations. ## Quick Start ### Element Construction ```typescript import { h, createRoot, createComponent } from "@alkdev/ujsx"; import type { UNode, UElement, URoot } from "@alkdev/ujsx"; const el: UElement = h("div", { class: "container" }, "hello", h("span", null, "world")); const root: URoot = createRoot("app", h("h1", null, "Title")); const MyComp = createComponent("MyComp", (props) => h("div", null, props.text as string)); ``` ### HostConfig Rendering ```typescript import { createRoot as createHostRoot } from "@alkdev/ujsx/host"; import type { HostConfig } from "@alkdev/ujsx/host"; import { h } from "@alkdev/ujsx/h"; const host: HostConfig = { name: "my-host", createRootContext: (container) => ({ container }), createInstance: (tag, props, ctx) => /* create your instance */, createTextInstance: (text, ctx) => /* create text instance */, appendChild: (parent, child, ctx) => /* attach child to parent */, }; const root = createHostRoot(host, container); root.render(h("div", { color: "red" }, "hello")); root.unmount(); ``` ### Reactive Trees ```typescript import { ReactiveRoot, signal, reactiveComponent } from "@alkdev/ujsx/reactive"; import { h, createComponent } from "@alkdev/ujsx/h"; const r = new ReactiveRoot(h("div", null, "initial")); r.update((prev) => h("div", null, "updated")); const unsub = r.subscribe((node) => console.log(node)); r.render((event) => console.log(event)); unsub(); r.dispose(); ``` ### JSX Configuration Set `jsxImportSource` in `tsconfig.json` to use JSX syntax directly: ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@alkdev/ujsx" } } ``` Then write JSX that produces `UElement` trees: ```tsx const tree =
hello
; ``` ### Bi-directional Transforms ```typescript import { TransformRegistry, childCtx, ctx as transformCtx } from "@alkdev/ujsx/transform"; import type { TransformRule, Direction } from "@alkdev/ujsx/transform"; const registry = new TransformRegistry(); registry.register({ name: "div-to-paragraph", direction: "ujsx→mdast" as Direction, match: (n) => isUElement(n) && n.type === "div", transform: (n, ctx, next) => ({ type: "paragraph", children: (n as UElement).children.map((c, i) => next(c, childCtx(n, ctx, i))), }), priority: 1, }); const result = registry.transform(myNode, transformCtx("ujsx→mdast" as Direction)); ``` ## Sub-path Exports Tree-shakeable imports — only pull in what you use: | Sub-path | Source | Key Exports | |----------|--------|-------------| | `@alkdev/ujsx` | `src/mod.ts` | All exports (barrel) | | `@alkdev/ujsx/schema` | `src/core/schema.ts` | `UJSX`, `UElement`, `URoot`, `UNode`, `UPrimitive`, `isUElement`, `isURoot`, `isUPrimitive` | | `@alkdev/ujsx/h` | `src/core/h.ts` | `h`, `createRoot`, `createComponent`, `Fragment`, `jsx`, `jsxs`, `jsxDEV` | | `@alkdev/ujsx/reactive` | `src/core/reactive.ts` | `ReactiveRoot`, `reactiveComponent`, `reactiveElement`, `signal`, `computed`, `effect`, `batch` | | `@alkdev/ujsx/context` | `src/core/context.ts` | `Context`, `Density`, `Direction`, `RenderContext` | | `@alkdev/ujsx/events` | `src/core/events.ts` | `EventEnvelope`, `PubSubLike`, `UjsxEventMap`, `createPubSubEmitter`, `proxyEventEmitter` | | `@alkdev/ujsx/pointer` | `src/core/pointer.ts` | `ValuePointer`, `selectNode`, `setNode` | | `@alkdev/ujsx/host` | `src/host/config.ts` | `HostConfig`, `Root`, `createRoot` | | `@alkdev/ujsx/transform` | `src/transform/registry.ts` | `TransformRegistry`, `TransformRule`, `TransformContext`, `TransformFn`, `childCtx`, `matchesSchema`, `ctx` | | `@alkdev/ujsx/jsx-runtime` | `src/core/jsx-runtime.ts` | `jsx`, `jsxs`, `jsxDEV`, `Fragment` | ## Core Types ### UNode (union type) The fundamental tree node type. Every value in a UJSX tree is a `UNode`: ```typescript type UPrimitive = string | number | boolean | null; type UElement = { type: string; // tag name or component function props: UniversalProps; // Record children: UNode[]; key?: string; // extracted from props, not in props }; type URoot = { type: "root"; props: UniversalProps; children: UNode[]; }; type UNode = UPrimitive | UElement | URoot; ``` ### PropValue ```typescript type PropValue = string | number | boolean | null | unknown[] | UNode | Record | ((...args: unknown[]) => unknown); ``` ### ComponentFn & UComponent ```typescript type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode; interface UComponent

{ (props: P & { children?: UNode[] }): UNode; displayName?: string; targets?: string[]; } ``` ### Type Guards ```typescript function isUElement(node: UNode): node is UElement; function isURoot(node: UNode): node is URoot; function isUPrimitive(node: UNode): node is UPrimitive; ``` ## HostConfig Interface The `HostConfig` interface defines how UJSX interacts with a target platform: ```typescript interface HostConfig { name: string; createRootContext(container: unknown, options?: Record, context?: Context): RootCtx; finalizeRoot?(ctx: RootCtx): void; createInstance(tag: TTag, props: Record, ctx: RootCtx, parent?: Instance): Instance; createTextInstance(text: string, ctx: RootCtx, parent?: Instance): Instance; appendChild(parent: Instance, child: Instance, ctx: RootCtx): void; insertBefore?(parent: Instance, child: Instance, before: Instance, ctx: RootCtx): void; removeChild?(parent: Instance, child: Instance, ctx: RootCtx): void; prepareUpdate?(instance: Instance, tag: TTag, prevProps: Record, nextProps: Record, ctx: RootCtx): unknown | null; commitUpdate?(instance: Instance, payload: unknown, tag: TTag, prevProps: Record, nextProps: Record, ctx: RootCtx): void; emit?(type: string, id: string, payload: unknown): void; finalizeInstance?(instance: Instance, ctx: RootCtx): void; } ``` **Type parameters:** - `TTag` — string literal union constraining allowed element types - `Instance` — host-specific instance type (e.g. `HTMLElement`, `Object3D`) - `RootCtx` — host-specific root context (carries refs, handles, etc.) **Required methods:** `name`, `createRootContext`, `createInstance`, `createTextInstance`, `appendChild` **Optional methods:** `finalizeRoot`, `insertBefore`, `removeChild`, `prepareUpdate`, `commitUpdate`, `emit`, `finalizeInstance` ## Reconciler The reconciler manages fiber tree diffing, key-based children reconciliation, and signal-driven updates: | Export | Purpose | |--------|---------| | `scheduleUpdate(fiber, nextNode, host, ctx)` | Queue a fiber update (via `queueMicrotask`) | | `flushUpdates(host, ctx)` | Process all pending updates | | `reconcileProps(fiber, nextNode, host, ctx)` | Diff props using `Value.Diff` / `Value.Equal` / `Value.Hash` | | `reconcileChildren(oldFibers, newChildren)` | Key-based classification into matched/added/removed/moves | | `commitMutations(parentFiber, classification, commitCtx)` | Apply insertions, moves, removals to host instances | | `commitEffects(fiber, host, ctx)` | Walk fiber tree and call `commitUpdate` for pending effects | | `wireSignalToFiber(fiber, signalGetter, host, ctx)` | Bind a Preact signal to a fiber for automatic updates | | `longestIncreasingSubsequence(arr)` | LIS algorithm for minimum-move reorder detection | ### Fiber Type ```typescript interface Fiber { instance: I; tag: string; props: Record; key: string | undefined; children: Fiber[]; parent: Fiber | null; effect: Effect | null; signalDisposers: (() => void)[]; prevProps: Record | null; disposed: boolean; cachedNode: UNode | null; hash: bigint | null; } type Effect = | { type: "update"; payload: unknown } | { type: "insert"; before: Fiber | null } | { type: "move"; before: Fiber | null } | { type: "remove" }; ``` ### Children Reconciliation `reconcileChildren` uses key-based matching with LIS (Longest Increasing Subsequence) to minimize DOM moves: - **Keyed children** are matched by `key` across old and new lists - **Unkeyed children** are matched positionally (left-to-right, first-available) - The LIS of matched indices identifies children that don't need moving - Non-LIS matched children are marked as moves - Unmatched old children are removed; unmatched new children are added ## Reactive Root ```typescript class ReactiveRoot { constructor(initial: UNode); get value(): ReadonlySignal; update(fn: (current: UNode) => UNode): void; subscribe(listener: (node: UNode) => void): () => void; render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void; dispose(): void; } ``` ## Context ```typescript class Context { constructor(initial?: Partial); get(): ContextValue; get signal(): ReadonlySignal; set(partial: Partial): void; subscribe(fn: (value: ContextValue) => void): () => void; fork(overrides: Partial): Context; } type Density = "full" | "compact" | "minimal"; type Direction = "ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx"; interface ContextValue { density: Density; target: string; metadata: Record; } ``` ## Events ```typescript interface EventEnvelope { readonly type: TType; readonly id: string; readonly payload: TPayload; } interface PubSubLike { publish(type: TType, id: string, payload: TEventMap[TType]): void; subscribe(type: TType, id: string): AsyncIterable>; } type UjsxEventMap = { "root.render": { childCount: number }; "root.unmount": Record; "instance.create": { kind: "text" | "element"; tag?: string; value?: string; props?: Record }; "component.invoke": { type: string }; "type.call": { objectName: string; methodName: string; args: unknown[] }; "transform.apply": { ruleName: string; direction: string }; }; ``` ## Tree Pointers ```typescript class ValuePointer { constructor(initial: T, path?: string[]); get value(): T; set value(v: T); get reactive(): ReadonlySignal; get path(): string[]; } function selectNode(root: UNode, path: string[]): UNode | undefined; function setNode(root: UNode, path: string[], value: UNode): UNode; ``` ## TypeBox Runtime Validation The `UJSX` export is a `Type.Module` from `@alkdev/typebox`. Use it with `Value.Check` for runtime validation: ```typescript import { UJSX } from "@alkdev/ujsx/schema"; import { Value } from "@alkdev/typebox/value"; const UElementSchema = UJSX.Import("UElement"); Value.Check(UElementSchema, myElement); // true | false ``` Available schema keys: `UPrimitive`, `PropValue`, `UniversalProps`, `UElement`, `URoot`, `UNode`. ## Design Principles 1. **The tree is the truth. Hosts are interpreters.** UJSX defines what a tree looks like, not what it means. 2. **HTML-agnostic core.** No DOM-specific props. `onClick`, `className`, `style` are not special. 3. **TypeBox Module IS the type registry.** Runtime validation via `Value.Check`, compile-time types via TypeScript. 4. **Preact signals for reactivity.** Signal-driven updates for props, reconciliation for structure. 5. **`key` as first-class field.** Extracted from props, promoted to `UElement.key` — not stored in `props`. ## Dependencies | Package | Version | Role | |---------|---------|------| | `@alkdev/typebox` | `^0.34.49` | Schema definition and runtime validation | | `@preact/signals-core` | `^1.14.1` | Reactive primitives (`signal`, `effect`, `computed`, `batch`) | | `@alkdev/pubsub` | `^0.1.0` | `PubSubLike` interface for event system | ## Scripts | Command | Description | |---------|-------------| | `npm run build` | tsup production build (ESM + CJS) | | `npm run build:tsc` | Type checking only (`tsc --noEmit`) | | `npm run lint` | Type checking (`tsc --noEmit`) | | `npm run test` | Run tests with Vitest | | `npm run test:watch` | Vitest in watch mode | | `npm run test:coverage` | Vitest with V8 coverage | ## License Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.