Files
ujsx/AGENTS.md
glm-5.1 743265c34a docs: add README, dual license files, AGENTS.md; fix tsup DTS build
- 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)
2026-05-19 07:06:56 +00:00

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[] }
  • 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 scheduleUpdateflushUpdates

Key Subsystems

  • Reactivity: Built on @preact/signals-core. Signal<UNode> flows through computedeffect → 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 TransformRules. Rules match by direction and match() predicate. The next() callback enables recursive transformation. Directions use Unicode arrows (e.g. "ujsx→mdast").
  • Pointers: ValuePointer<T> wraps signal<T> 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