# AGENTS.md Instructions for AI agents working with the @alkdev/ujsx codebase. ## Project Overview `@alkdev/ujsx` is a runtime-agnostic reactive tree library. JSX serves as an intermediate representation — the same declarative tree can target different hosts (markdown, graph structures, DOM, workflow engines) through a `HostConfig` adapter. The core has zero platform-specific assumptions: no `onClick`, no `className`, no `style`. ## Build & Test Commands ```bash npm run build # tsup production build (ESM + CJS) → dist/ npm run build:tsc # Type checking only (tsc --noEmit) npm run lint # Same as build:tsc — tsc --noEmit npm run test # vitest run npm run test:watch # vitest in watch mode npm run test:coverage # vitest run --coverage ``` Always run `npm run lint` and `npm run test` after making changes. ## Architecture ### Source Layout ``` src/ mod.ts # Barrel export (all public API) core/ schema.ts # TypeBox Module (UJSX), UNode/UElement/URoot/UPrimitive types, type guards h.ts # Element factory: h(), createRoot(), createComponent(), Fragment, jsx/jsxs/jsxDEV reactive.ts # ReactiveRoot, reactiveComponent, reactiveElement; re-exports signal/computed/effect/batch context.ts # Context class (signal-backed), Density, Direction, RenderContext events.ts # EventEnvelope, PubSubLike, UjsxEventMap, createPubSubEmitter, proxyEventEmitter pointer.ts # ValuePointer, selectNode(), setNode() jsx-runtime.ts # Re-exports jsx/jsxs/jsxDEV/Fragment for jsxImportSource host/ config.ts # HostConfig, Root, createRoot(), mount/reconcile/unmount pipeline fiber.ts # Fiber type, Effect type, disposeFiber(), HostLike reconcile.ts # reconcileProps, reconcileChildren, commitMutations, scheduleUpdate, flushUpdates, LIS transform/ registry.ts # TransformRegistry, TransformRule, TransformContext, childCtx, matchesSchema, ctx ``` ### Core Data Model The tree is the truth. All nodes are `UNode`: ```typescript type UNode = UPrimitive | UElement | URoot; // UPrimitive = string | number | boolean | null // UElement = { type: string; props: UniversalProps; children: UNode[]; key?: string } // URoot = { type: "root"; props: UniversalProps; children: UNode[] } ``` - `key` on `UElement` is extracted from props by `h()` and promoted to the element level — it is never in `props` - `URoot` is a transparent container: never has a `key`, its children mount directly into the parent - Function components are transparent: called with `{ ...props, children }`, their output mounts in place ### Rendering Pipeline 1. `h()` / JSX creates `UElement` trees (pure data) 2. `createHostRoot(host, container)` creates a `Root` with a `HostConfig` 3. `root.render(node)` mounts the tree via `HostConfig` methods 4. Re-renders use `reconcileChildren` (key-based, LIS-optimized) and `reconcileProps` (TypeBox `Value.Diff`/`Value.Equal`/`Value.Hash`) 5. `wireSignalToFiber` binds Preact signals to fibers for automatic `scheduleUpdate` → `flushUpdates` ### Key Subsystems - **Reactivity**: Built on `@preact/signals-core`. `Signal` flows through `computed` → `effect` → reconciler. Do not introduce alternative reactive systems. - **HostConfig**: The sole integration point for platform-specific logic. Three type params: `TTag` (allowed tags), `Instance` (host instance type), `RootCtx` (root context type). Required methods: `createRootContext`, `createInstance`, `createTextInstance`, `appendChild`. Optional: `insertBefore`, `removeChild`, `prepareUpdate`, `commitUpdate`, `finalizeRoot`, `finalizeInstance`, `emit`. - **Transforms**: `TransformRegistry` with priority-sorted `TransformRule`s. Rules match by `direction` and `match()` predicate. The `next()` callback enables recursive transformation. Directions use Unicode arrows (e.g. `"ujsx→mdast"`). - **Pointers**: `ValuePointer` wraps `signal` with a path. `selectNode`/`setNode` provide immutable tree navigation and updates. ### TypeBox Usage `UJSX` is a `Type.Module` from `@alkdev/typebox`. Schema keys: `UPrimitive`, `PropValue`, `UniversalProps`, `UElement`, `URoot`, `UNode`. Runtime validation via `Value.Check(UJSX.Import("UElement"), node)`. The reconciler uses `Value.Diff`, `Value.Equal`, and `Value.Hash` for prop diffing with function-prop stripping. ## Code Conventions - **No comments in code** unless explicitly requested - **ESM primary**: `"type": "module"`, CJS is a distribution compatibility layer - **No Node-specific APIs in core**: `src/core/` and `src/transform/` must not import `fs`, `path`, `child_process`, etc. - **Dual format via tsup**: Same source produces ESM and CJS; exports map always has matching `import` and `require` entries - **TypeBox is non-negotiable**: Not optional, not replaceable - **`@preact/signals-core` is the only reactive primitive**: No RxJS, Solid signals, or Vue reactivity - **Platform agnostic**: No DOM APIs, Three.js APIs, or any platform-specific API calls in core ## Sub-path Exports Each sub-path maps to a single source file — no barrel re-exports within sub-paths: | Import | Source | |--------|--------| | `@alkdev/ujsx` | `src/mod.ts` | | `@alkdev/ujsx/schema` | `src/core/schema.ts` | | `@alkdev/ujsx/h` | `src/core/h.ts` | | `@alkdev/ujsx/reactive` | `src/core/reactive.ts` | | `@alkdev/ujsx/context` | `src/core/context.ts` | | `@alkdev/ujsx/events` | `src/core/events.ts` | | `@alkdev/ujsx/pointer` | `src/core/pointer.ts` | | `@alkdev/ujsx/host` | `src/host/config.ts` | | `@alkdev/ujsx/transform` | `src/transform/registry.ts` | | `@alkdev/ujsx/jsx-runtime` | `src/core/jsx-runtime.ts` | ## Testing Tests are in `test/` using Vitest. Each test file maps to a specific feature area. Tests import directly from `src/` (not from `dist/`). When adding features, add or update corresponding test files. ## Architecture Documentation Detailed architecture docs live in `docs/architecture/`: - `schema.md` — TypeBox Module, type definitions, type guards - `element-factory.md` — h(), createRoot(), createComponent(), Fragment - `reactive-layer.md` — ReactiveRoot, reactiveComponent, reactiveElement, signal integration - `host-config.md` — HostConfig interface, createRoot(), mount pipeline, reconciler gap - `reconciler.md` — Fiber tree, reconciliation algorithm, update scheduling, TypeBox optimizations - `lifecycle.md` — Mount, update, unmount/dispose lifecycle, signal cleanup - `transforms.md` — TransformRegistry, TransformRule, TransformContext, bi-directional transforms - `events.md` — EventEnvelope, PubSubLike, UjsxEventMap - `pointers.md` — ValuePointer, selectNode, setNode, tree navigation - `build-distribution.md` — Package structure, exports map, dependencies, runtime targets Design decisions (ADRs) are in `docs/architecture/decisions/`: - 001: HTML-agnostic core - 002: TypeBox Module as type registry - 003: Preact signals-core for reactivity - 004: `key` as first-class field on UElement - 005: Signal-driven updates for props, reconciliation for structure