{"_id":"@alkdev/ujsx","name":"@alkdev/ujsx","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@alkdev/ujsx","version":"0.1.0","description":"Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas","type":"module","main":"./dist/mod.cjs","module":"./dist/mod.js","types":"./dist/mod.d.ts","exports":{".":{"import":{"types":"./dist/mod.d.ts","default":"./dist/mod.js"},"require":{"types":"./dist/mod.d.cts","default":"./dist/mod.cjs"}},"./schema":{"import":{"types":"./dist/core/schema.d.ts","default":"./dist/core/schema.js"},"require":{"types":"./dist/core/schema.d.cts","default":"./dist/core/schema.cjs"}},"./h":{"import":{"types":"./dist/core/h.d.ts","default":"./dist/core/h.js"},"require":{"types":"./dist/core/h.d.cts","default":"./dist/core/h.cjs"}},"./reactive":{"import":{"types":"./dist/core/reactive.d.ts","default":"./dist/core/reactive.js"},"require":{"types":"./dist/core/reactive.d.cts","default":"./dist/core/reactive.cjs"}},"./context":{"import":{"types":"./dist/core/context.d.ts","default":"./dist/core/context.js"},"require":{"types":"./dist/core/context.d.cts","default":"./dist/core/context.cjs"}},"./events":{"import":{"types":"./dist/core/events.d.ts","default":"./dist/core/events.js"},"require":{"types":"./dist/core/events.d.cts","default":"./dist/core/events.cjs"}},"./pointer":{"import":{"types":"./dist/core/pointer.d.ts","default":"./dist/core/pointer.js"},"require":{"types":"./dist/core/pointer.d.cts","default":"./dist/core/pointer.cjs"}},"./host":{"import":{"types":"./dist/host/config.d.ts","default":"./dist/host/config.js"},"require":{"types":"./dist/host/config.d.cts","default":"./dist/host/config.cjs"}},"./transform":{"import":{"types":"./dist/transform/registry.d.ts","default":"./dist/transform/registry.js"},"require":{"types":"./dist/transform/registry.d.cts","default":"./dist/transform/registry.cjs"}},"./jsx-runtime":{"import":{"types":"./dist/core/jsx-runtime.d.ts","default":"./dist/core/jsx-runtime.js"},"require":{"types":"./dist/core/jsx-runtime.d.cts","default":"./dist/core/jsx-runtime.cjs"}}},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","build:tsc":"tsc","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"tsc --noEmit","prepublishOnly":"npm run build"},"keywords":["ujsx","jsx","reactive","signals","typebox","universal-jsx"],"license":"MIT OR Apache-2.0","dependencies":{"@alkdev/pubsub":"^0.1.0","@alkdev/typebox":"^0.34.49","@preact/signals-core":"^1.14.1"},"devDependencies":{"@types/node":"^22.0.0","@vitest/coverage-v8":"^3.2.4","tsup":"^8.5.1","typescript":"^5.7.0","vitest":"^3.1.0"},"engines":{"node":">=18.0.0"},"deno":true,"gitHead":"fbf13ed44462a0537aba65e107196c9538e1f4d2","_id":"@alkdev/ujsx@0.1.0","_nodeVersion":"25.8.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-GNLpghJZA+dPNA4Umvh+oTBG99xjRKm6odG4FcKk7UadjtZ/Chjo1TWnJUleMNlJjqgx0dwzJ5ODxmddex6GqQ==","shasum":"6cb32dfdbeee6632dc4e7074c9fd6cec0edb5b66","tarball":"https://registry.npmjs.org/@alkdev/ujsx/-/ujsx-0.1.0.tgz","fileCount":80,"unpackedSize":372084,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIChQkInz05URJrrVTP/qyy35MFFk1heSI+50pUoMEqMXAiEAstCq6llBGoSZHsq4P1irGtB11hm/0JbxEJfOBLqiCQU="}]},"_npmUser":{"name":"alkdev","email":"admin@alk.dev"},"directories":{},"maintainers":[{"name":"alkdev","email":"admin@alk.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ujsx_0.1.0_1779175223190_0.38206449179546187"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T07:20:23.076Z","0.1.0":"2026-05-19T07:20:23.411Z","modified":"2026-05-19T07:20:23.680Z"},"maintainers":[{"name":"alkdev","email":"admin@alk.dev"}],"description":"Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas","keywords":["ujsx","jsx","reactive","signals","typebox","universal-jsx"],"license":"MIT OR Apache-2.0","readme":"# @alkdev/ujsx\n\nUniversal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas.\n\nUJSX 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.\n\n## Install\n\n```bash\nnpm install @alkdev/ujsx\n```\n\nWorks in Node.js 18+, Deno, and Bun. Ships dual ESM/CJS with TypeScript declarations. No Node-specific APIs in core — runs anywhere `@preact/signals-core` and `@alkdev/typebox` do.\n\n## Quick Start\n\n### Element Construction\n\n```typescript\nimport { h, createRoot, createComponent } from \"@alkdev/ujsx\";\nimport type { UNode, UElement, URoot } from \"@alkdev/ujsx\";\n\nconst el: UElement = h(\"div\", { class: \"container\" }, \"hello\", h(\"span\", null, \"world\"));\nconst root: URoot = createRoot(\"app\", h(\"h1\", null, \"Title\"));\nconst MyComp = createComponent(\"MyComp\", (props) => h(\"div\", null, props.text as string));\n```\n\n### HostConfig Rendering\n\n```typescript\nimport { createRoot as createHostRoot } from \"@alkdev/ujsx/host\";\nimport type { HostConfig } from \"@alkdev/ujsx/host\";\nimport { h } from \"@alkdev/ujsx/h\";\n\nconst host: HostConfig<string, MyInstance, MyRootCtx> = {\n  name: \"my-host\",\n  createRootContext: (container) => ({ container }),\n  createInstance: (tag, props, ctx) => /* create your instance */,\n  createTextInstance: (text, ctx) => /* create text instance */,\n  appendChild: (parent, child, ctx) => /* attach child to parent */,\n};\n\nconst root = createHostRoot(host, container);\nroot.render(h(\"div\", { color: \"red\" }, \"hello\"));\nroot.unmount();\n```\n\n### Reactive Trees\n\n```typescript\nimport { ReactiveRoot, signal, reactiveComponent } from \"@alkdev/ujsx/reactive\";\nimport { h, createComponent } from \"@alkdev/ujsx/h\";\n\nconst r = new ReactiveRoot(h(\"div\", null, \"initial\"));\nr.update((prev) => h(\"div\", null, \"updated\"));\nconst unsub = r.subscribe((node) => console.log(node));\nr.render((event) => console.log(event));\nunsub();\nr.dispose();\n```\n\n### JSX Configuration\n\nSet `jsxImportSource` in `tsconfig.json` to use JSX syntax directly:\n\n```json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@alkdev/ujsx\"\n  }\n}\n```\n\nThen write JSX that produces `UElement` trees:\n\n```tsx\nconst tree = <div class=\"container\"><span>hello</span></div>;\n```\n\n### Bi-directional Transforms\n\n```typescript\nimport { TransformRegistry, childCtx, ctx as transformCtx } from \"@alkdev/ujsx/transform\";\nimport type { TransformRule, Direction } from \"@alkdev/ujsx/transform\";\n\nconst registry = new TransformRegistry<UNode, unknown, unknown>();\nregistry.register({\n  name: \"div-to-paragraph\",\n  direction: \"ujsx→mdast\" as Direction,\n  match: (n) => isUElement(n) && n.type === \"div\",\n  transform: (n, ctx, next) => ({\n    type: \"paragraph\",\n    children: (n as UElement).children.map((c, i) => next(c, childCtx(n, ctx, i))),\n  }),\n  priority: 1,\n});\n\nconst result = registry.transform(myNode, transformCtx(\"ujsx→mdast\" as Direction));\n```\n\n## Sub-path Exports\n\nTree-shakeable imports — only pull in what you use:\n\n| Sub-path | Source | Key Exports |\n|----------|--------|-------------|\n| `@alkdev/ujsx` | `src/mod.ts` | All exports (barrel) |\n| `@alkdev/ujsx/schema` | `src/core/schema.ts` | `UJSX`, `UElement`, `URoot`, `UNode`, `UPrimitive`, `isUElement`, `isURoot`, `isUPrimitive` |\n| `@alkdev/ujsx/h` | `src/core/h.ts` | `h`, `createRoot`, `createComponent`, `Fragment`, `jsx`, `jsxs`, `jsxDEV` |\n| `@alkdev/ujsx/reactive` | `src/core/reactive.ts` | `ReactiveRoot`, `reactiveComponent`, `reactiveElement`, `signal`, `computed`, `effect`, `batch` |\n| `@alkdev/ujsx/context` | `src/core/context.ts` | `Context`, `Density`, `Direction`, `RenderContext` |\n| `@alkdev/ujsx/events` | `src/core/events.ts` | `EventEnvelope`, `PubSubLike`, `UjsxEventMap`, `createPubSubEmitter`, `proxyEventEmitter` |\n| `@alkdev/ujsx/pointer` | `src/core/pointer.ts` | `ValuePointer`, `selectNode`, `setNode` |\n| `@alkdev/ujsx/host` | `src/host/config.ts` | `HostConfig`, `Root`, `createRoot` |\n| `@alkdev/ujsx/transform` | `src/transform/registry.ts` | `TransformRegistry`, `TransformRule`, `TransformContext`, `TransformFn`, `childCtx`, `matchesSchema`, `ctx` |\n| `@alkdev/ujsx/jsx-runtime` | `src/core/jsx-runtime.ts` | `jsx`, `jsxs`, `jsxDEV`, `Fragment` |\n\n## Core Types\n\n### UNode (union type)\n\nThe fundamental tree node type. Every value in a UJSX tree is a `UNode`:\n\n```typescript\ntype UPrimitive = string | number | boolean | null;\ntype UElement = {\n  type: string;          // tag name or component function\n  props: UniversalProps; // Record<string, PropValue | undefined>\n  children: UNode[];\n  key?: string;          // extracted from props, not in props\n};\ntype URoot = {\n  type: \"root\";\n  props: UniversalProps;\n  children: UNode[];\n};\ntype UNode = UPrimitive | UElement | URoot;\n```\n\n### PropValue\n\n```typescript\ntype PropValue = string | number | boolean | null | unknown[] | UNode | Record<string, unknown> | ((...args: unknown[]) => unknown);\n```\n\n### ComponentFn & UComponent\n\n```typescript\ntype ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode;\n\ninterface UComponent<P extends UniversalProps = UniversalProps> {\n  (props: P & { children?: UNode[] }): UNode;\n  displayName?: string;\n  targets?: string[];\n}\n```\n\n### Type Guards\n\n```typescript\nfunction isUElement(node: UNode): node is UElement;\nfunction isURoot(node: UNode): node is URoot;\nfunction isUPrimitive(node: UNode): node is UPrimitive;\n```\n\n## HostConfig Interface\n\nThe `HostConfig<TTag, Instance, RootCtx>` interface defines how UJSX interacts with a target platform:\n\n```typescript\ninterface HostConfig<TTag extends string, Instance, RootCtx> {\n  name: string;\n  createRootContext(container: unknown, options?: Record<string, unknown>, context?: Context): RootCtx;\n  finalizeRoot?(ctx: RootCtx): void;\n  createInstance(tag: TTag, props: Record<string, unknown>, ctx: RootCtx, parent?: Instance): Instance;\n  createTextInstance(text: string, ctx: RootCtx, parent?: Instance): Instance;\n  appendChild(parent: Instance, child: Instance, ctx: RootCtx): void;\n  insertBefore?(parent: Instance, child: Instance, before: Instance, ctx: RootCtx): void;\n  removeChild?(parent: Instance, child: Instance, ctx: RootCtx): void;\n  prepareUpdate?(instance: Instance, tag: TTag, prevProps: Record<string, unknown>, nextProps: Record<string, unknown>, ctx: RootCtx): unknown | null;\n  commitUpdate?(instance: Instance, payload: unknown, tag: TTag, prevProps: Record<string, unknown>, nextProps: Record<string, unknown>, ctx: RootCtx): void;\n  emit?(type: string, id: string, payload: unknown): void;\n  finalizeInstance?(instance: Instance, ctx: RootCtx): void;\n}\n```\n\n**Type parameters:**\n- `TTag` — string literal union constraining allowed element types\n- `Instance` — host-specific instance type (e.g. `HTMLElement`, `Object3D`)\n- `RootCtx` — host-specific root context (carries refs, handles, etc.)\n\n**Required methods:** `name`, `createRootContext`, `createInstance`, `createTextInstance`, `appendChild`\n**Optional methods:** `finalizeRoot`, `insertBefore`, `removeChild`, `prepareUpdate`, `commitUpdate`, `emit`, `finalizeInstance`\n\n## Reconciler\n\nThe reconciler manages fiber tree diffing, key-based children reconciliation, and signal-driven updates:\n\n| Export | Purpose |\n|--------|---------|\n| `scheduleUpdate(fiber, nextNode, host, ctx)` | Queue a fiber update (via `queueMicrotask`) |\n| `flushUpdates(host, ctx)` | Process all pending updates |\n| `reconcileProps(fiber, nextNode, host, ctx)` | Diff props using `Value.Diff` / `Value.Equal` / `Value.Hash` |\n| `reconcileChildren(oldFibers, newChildren)` | Key-based classification into matched/added/removed/moves |\n| `commitMutations(parentFiber, classification, commitCtx)` | Apply insertions, moves, removals to host instances |\n| `commitEffects(fiber, host, ctx)` | Walk fiber tree and call `commitUpdate` for pending effects |\n| `wireSignalToFiber(fiber, signalGetter, host, ctx)` | Bind a Preact signal to a fiber for automatic updates |\n| `longestIncreasingSubsequence(arr)` | LIS algorithm for minimum-move reorder detection |\n\n### Fiber Type\n\n```typescript\ninterface Fiber<I> {\n  instance: I;\n  tag: string;\n  props: Record<string, unknown>;\n  key: string | undefined;\n  children: Fiber<I>[];\n  parent: Fiber<I> | null;\n  effect: Effect<I> | null;\n  signalDisposers: (() => void)[];\n  prevProps: Record<string, unknown> | null;\n  disposed: boolean;\n  cachedNode: UNode | null;\n  hash: bigint | null;\n}\n\ntype Effect<I> =\n  | { type: \"update\"; payload: unknown }\n  | { type: \"insert\"; before: Fiber<I> | null }\n  | { type: \"move\"; before: Fiber<I> | null }\n  | { type: \"remove\" };\n```\n\n### Children Reconciliation\n\n`reconcileChildren` uses key-based matching with LIS (Longest Increasing Subsequence) to minimize DOM moves:\n\n- **Keyed children** are matched by `key` across old and new lists\n- **Unkeyed children** are matched positionally (left-to-right, first-available)\n- The LIS of matched indices identifies children that don't need moving\n- Non-LIS matched children are marked as moves\n- Unmatched old children are removed; unmatched new children are added\n\n## Reactive Root\n\n```typescript\nclass ReactiveRoot {\n  constructor(initial: UNode);\n  get value(): ReadonlySignal<UNode>;\n  update(fn: (current: UNode) => UNode): void;\n  subscribe(listener: (node: UNode) => void): () => void;\n  render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void;\n  dispose(): void;\n}\n```\n\n## Context\n\n```typescript\nclass Context {\n  constructor(initial?: Partial<ContextValue>);\n  get(): ContextValue;\n  get signal(): ReadonlySignal<ContextValue>;\n  set(partial: Partial<ContextValue>): void;\n  subscribe(fn: (value: ContextValue) => void): () => void;\n  fork(overrides: Partial<ContextValue>): Context;\n}\n\ntype Density = \"full\" | \"compact\" | \"minimal\";\ntype Direction = \"ujsx→mdast\" | \"mdast→ujsx\" | \"ujsx→jpath\" | \"jpath→ujsx\" | \"ujsx→hast\" | \"hast→ujsx\";\ninterface ContextValue { density: Density; target: string; metadata: Record<string, unknown>; }\n```\n\n## Events\n\n```typescript\ninterface EventEnvelope<TType extends string = string, TPayload = unknown> {\n  readonly type: TType;\n  readonly id: string;\n  readonly payload: TPayload;\n}\n\ninterface PubSubLike<TEventMap> {\n  publish<TType>(type: TType, id: string, payload: TEventMap[TType]): void;\n  subscribe<TType>(type: TType, id: string): AsyncIterable<EventEnvelope<TType, TEventMap[TType]>>;\n}\n\ntype UjsxEventMap = {\n  \"root.render\": { childCount: number };\n  \"root.unmount\": Record<string, unknown>;\n  \"instance.create\": { kind: \"text\" | \"element\"; tag?: string; value?: string; props?: Record<string, unknown> };\n  \"component.invoke\": { type: string };\n  \"type.call\": { objectName: string; methodName: string; args: unknown[] };\n  \"transform.apply\": { ruleName: string; direction: string };\n};\n```\n\n## Tree Pointers\n\n```typescript\nclass ValuePointer<T> {\n  constructor(initial: T, path?: string[]);\n  get value(): T;\n  set value(v: T);\n  get reactive(): ReadonlySignal<T>;\n  get path(): string[];\n}\n\nfunction selectNode(root: UNode, path: string[]): UNode | undefined;\nfunction setNode(root: UNode, path: string[], value: UNode): UNode;\n```\n\n## TypeBox Runtime Validation\n\nThe `UJSX` export is a `Type.Module` from `@alkdev/typebox`. Use it with `Value.Check` for runtime validation:\n\n```typescript\nimport { UJSX } from \"@alkdev/ujsx/schema\";\nimport { Value } from \"@alkdev/typebox/value\";\n\nconst UElementSchema = UJSX.Import(\"UElement\");\nValue.Check(UElementSchema, myElement); // true | false\n```\n\nAvailable schema keys: `UPrimitive`, `PropValue`, `UniversalProps`, `UElement`, `URoot`, `UNode`.\n\n## Design Principles\n\n1. **The tree is the truth. Hosts are interpreters.** UJSX defines what a tree looks like, not what it means.\n2. **HTML-agnostic core.** No DOM-specific props. `onClick`, `className`, `style` are not special.\n3. **TypeBox Module IS the type registry.** Runtime validation via `Value.Check`, compile-time types via TypeScript.\n4. **Preact signals for reactivity.** Signal-driven updates for props, reconciliation for structure.\n5. **`key` as first-class field.** Extracted from props, promoted to `UElement.key` — not stored in `props`.\n\n## Dependencies\n\n| Package | Version | Role |\n|---------|---------|------|\n| `@alkdev/typebox` | `^0.34.49` | Schema definition and runtime validation |\n| `@preact/signals-core` | `^1.14.1` | Reactive primitives (`signal`, `effect`, `computed`, `batch`) |\n| `@alkdev/pubsub` | `^0.1.0` | `PubSubLike` interface for event system |\n\n## Scripts\n\n| Command | Description |\n|---------|-------------|\n| `npm run build` | tsup production build (ESM + CJS) |\n| `npm run build:tsc` | Type checking only (`tsc --noEmit`) |\n| `npm run lint` | Type checking (`tsc --noEmit`) |\n| `npm run test` | Run tests with Vitest |\n| `npm run test:watch` | Vitest in watch mode |\n| `npm run test:coverage` | Vitest with V8 coverage |\n\n## License\n\nDual-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.","readmeFilename":"README.md","_rev":"1-9fb4553f5709595891ab6bd4511b8099"}