npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alkdev/ujsx

v0.1.0

Published

Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas

Readme

@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

npm install @alkdev/ujsx

Works 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.

Quick Start

Element Construction

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

import { createRoot as createHostRoot } from "@alkdev/ujsx/host";
import type { HostConfig } from "@alkdev/ujsx/host";
import { h } from "@alkdev/ujsx/h";

const host: HostConfig<string, MyInstance, MyRootCtx> = {
  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

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:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@alkdev/ujsx"
  }
}

Then write JSX that produces UElement trees:

const tree = <div class="container"><span>hello</span></div>;

Bi-directional Transforms

import { TransformRegistry, childCtx, ctx as transformCtx } from "@alkdev/ujsx/transform";
import type { TransformRule, Direction } from "@alkdev/ujsx/transform";

const registry = new TransformRegistry<UNode, unknown, unknown>();
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:

type UPrimitive = string | number | boolean | null;
type UElement = {
  type: string;          // tag name or component function
  props: UniversalProps; // Record<string, PropValue | undefined>
  children: UNode[];
  key?: string;          // extracted from props, not in props
};
type URoot = {
  type: "root";
  props: UniversalProps;
  children: UNode[];
};
type UNode = UPrimitive | UElement | URoot;

PropValue

type PropValue = string | number | boolean | null | unknown[] | UNode | Record<string, unknown> | ((...args: unknown[]) => unknown);

ComponentFn & UComponent

type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode;

interface UComponent<P extends UniversalProps = UniversalProps> {
  (props: P & { children?: UNode[] }): UNode;
  displayName?: string;
  targets?: string[];
}

Type Guards

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<TTag, Instance, RootCtx> interface defines how UJSX interacts with a target platform:

interface HostConfig<TTag extends string, Instance, RootCtx> {
  name: string;
  createRootContext(container: unknown, options?: Record<string, unknown>, context?: Context): RootCtx;
  finalizeRoot?(ctx: RootCtx): void;
  createInstance(tag: TTag, props: Record<string, unknown>, 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<string, unknown>, nextProps: Record<string, unknown>, ctx: RootCtx): unknown | null;
  commitUpdate?(instance: Instance, payload: unknown, tag: TTag, prevProps: Record<string, unknown>, nextProps: Record<string, unknown>, 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

interface Fiber<I> {
  instance: I;
  tag: string;
  props: Record<string, unknown>;
  key: string | undefined;
  children: Fiber<I>[];
  parent: Fiber<I> | null;
  effect: Effect<I> | null;
  signalDisposers: (() => void)[];
  prevProps: Record<string, unknown> | null;
  disposed: boolean;
  cachedNode: UNode | null;
  hash: bigint | null;
}

type Effect<I> =
  | { type: "update"; payload: unknown }
  | { type: "insert"; before: Fiber<I> | null }
  | { type: "move"; before: Fiber<I> | 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

class ReactiveRoot {
  constructor(initial: UNode);
  get value(): ReadonlySignal<UNode>;
  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

class Context {
  constructor(initial?: Partial<ContextValue>);
  get(): ContextValue;
  get signal(): ReadonlySignal<ContextValue>;
  set(partial: Partial<ContextValue>): void;
  subscribe(fn: (value: ContextValue) => void): () => void;
  fork(overrides: Partial<ContextValue>): 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<string, unknown>; }

Events

interface EventEnvelope<TType extends string = string, TPayload = unknown> {
  readonly type: TType;
  readonly id: string;
  readonly payload: TPayload;
}

interface PubSubLike<TEventMap> {
  publish<TType>(type: TType, id: string, payload: TEventMap[TType]): void;
  subscribe<TType>(type: TType, id: string): AsyncIterable<EventEnvelope<TType, TEventMap[TType]>>;
}

type UjsxEventMap = {
  "root.render": { childCount: number };
  "root.unmount": Record<string, unknown>;
  "instance.create": { kind: "text" | "element"; tag?: string; value?: string; props?: Record<string, unknown> };
  "component.invoke": { type: string };
  "type.call": { objectName: string; methodName: string; args: unknown[] };
  "transform.apply": { ruleName: string; direction: string };
};

Tree Pointers

class ValuePointer<T> {
  constructor(initial: T, path?: string[]);
  get value(): T;
  set value(v: T);
  get reactive(): ReadonlySignal<T>;
  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:

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 or Apache 2.0 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.