- Add README.md with full API reference and usage examples for npm publishing - Add LICENSE-MIT and LICENSE-APACHE for dual licensing (MIT OR Apache-2.0) - Update AGENTS.md with project instructions for AI agents - Fix tsconfig.json: remove declaration, declarationMap, outDir, and sourceMap which conflicted with tsup's own DTS generation (TS5055 overwrite errors)
7.0 KiB
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
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<T>, selectNode(), setNode()
jsx-runtime.ts # Re-exports jsx/jsxs/jsxDEV/Fragment for jsxImportSource
host/
config.ts # HostConfig<TTag,Instance,RootCtx>, Root, createRoot(), mount/reconcile/unmount pipeline
fiber.ts # Fiber<I> type, Effect<I> 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:
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[] }
keyonUElementis extracted from props byh()and promoted to the element level — it is never inpropsURootis a transparent container: never has akey, its children mount directly into the parent- Function components are transparent: called with
{ ...props, children }, their output mounts in place
Rendering Pipeline
h()/ JSX createsUElementtrees (pure data)createHostRoot(host, container)creates aRootwith aHostConfigroot.render(node)mounts the tree viaHostConfigmethods- Re-renders use
reconcileChildren(key-based, LIS-optimized) andreconcileProps(TypeBoxValue.Diff/Value.Equal/Value.Hash) wireSignalToFiberbinds Preact signals to fibers for automaticscheduleUpdate→flushUpdates
Key Subsystems
- Reactivity: Built on
@preact/signals-core.Signal<UNode>flows throughcomputed→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:
TransformRegistrywith priority-sortedTransformRules. Rules match bydirectionandmatch()predicate. Thenext()callback enables recursive transformation. Directions use Unicode arrows (e.g."ujsx→mdast"). - Pointers:
ValuePointer<T>wrapssignal<T>with a path.selectNode/setNodeprovide 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/andsrc/transform/must not importfs,path,child_process, etc. - Dual format via tsup: Same source produces ESM and CJS; exports map always has matching
importandrequireentries - TypeBox is non-negotiable: Not optional, not replaceable
@preact/signals-coreis 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 guardselement-factory.md— h(), createRoot(), createComponent(), Fragmentreactive-layer.md— ReactiveRoot, reactiveComponent, reactiveElement, signal integrationhost-config.md— HostConfig interface, createRoot(), mount pipeline, reconciler gapreconciler.md— Fiber tree, reconciliation algorithm, update scheduling, TypeBox optimizationslifecycle.md— Mount, update, unmount/dispose lifecycle, signal cleanuptransforms.md— TransformRegistry, TransformRule, TransformContext, bi-directional transformsevents.md— EventEnvelope, PubSubLike, UjsxEventMappointers.md— ValuePointer, selectNode, setNode, tree navigationbuild-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:
keyas first-class field on UElement - 005: Signal-driven updates for props, reconciliation for structure