port ujsx from Deno-only to cross-platform (Node/Bun/Deno)

Add npm project configuration (package.json, tsconfig.json, tsup, vitest)
matching the taskgraph_ts conventions. All source imports changed from .ts
to .js extensions for Node16 module resolution. Tests migrated from Deno.test
to vitest. Fixed strict type errors (noUncheckedIndexedAccess). Preserved
deno.json with sloppy-imports for dual Deno/Node compatibility.

Subpath exports: schema, h, reactive, context, events, pointer, host,
transform, jsx-runtime — plus barrel export at root.

Build: ESM + CJS dual output via tsup. 22 tests passing.
This commit is contained in:
2026-05-03 08:19:49 +00:00
parent b256fc7eb5
commit 3eb1f1d896
19 changed files with 4137 additions and 0 deletions

54
src/core/context.ts Normal file
View File

@@ -0,0 +1,54 @@
import { signal, effect, batch, type Signal, type ReadonlySignal } from "@preact/signals-core";
export type Density = "full" | "compact" | "minimal";
export interface ContextValue {
density: Density;
target: string;
metadata: Record<string, unknown>;
}
const defaultContext: ContextValue = {
density: "full",
target: "markdown",
metadata: {},
};
export class Context {
private value: Signal<ContextValue>;
constructor(initial?: Partial<ContextValue>) {
this.value = signal<ContextValue>({ ...defaultContext, ...initial });
}
get(): ContextValue {
return this.value.value;
}
get signal(): ReadonlySignal<ContextValue> {
return this.value;
}
set(partial: Partial<ContextValue>): void {
batch(() => {
this.value.value = { ...this.value.value, ...partial };
});
}
subscribe(fn: (value: ContextValue) => void): () => void {
return effect(() => {
fn(this.value.value);
});
}
fork(overrides: Partial<ContextValue>): Context {
const forked = new Context({ ...this.value.value, ...overrides });
return forked;
}
}
export type Direction = "ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx";
export interface RenderContext extends ContextValue {
direction: Direction;
}

51
src/core/events.ts Normal file
View File

@@ -0,0 +1,51 @@
export interface EventEnvelope<TType extends string = string, TPayload = unknown> {
readonly type: TType;
readonly id: string;
readonly payload: TPayload;
}
export interface PubSubLike<TEventMap extends Record<string, unknown> = Record<string, unknown>> {
publish<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
payload: TEventMap[TType],
): void;
subscribe<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
): AsyncIterable<EventEnvelope<TType, TEventMap[TType]>>;
}
export 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 };
};
export type UjsxEnvelope<TType extends keyof UjsxEventMap = keyof UjsxEventMap> = EventEnvelope<
TType,
UjsxEventMap[TType]
>;
export function createPubSubEmitter<TEventMap extends Record<string, unknown>>(
pubsub: PubSubLike<TEventMap>,
) {
return function emit<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
payload: TEventMap[TType],
): void {
pubsub.publish(type, id, payload);
};
}
export function proxyEventEmitter(pubsub: PubSubLike<UjsxEventMap>) {
return {
onTypeCall(objectName: string, methodName: string, args: unknown[]): void {
pubsub.publish("type.call", `call_${Date.now()}`, { objectName, methodName, args });
},
};
}

49
src/core/h.ts Normal file
View File

@@ -0,0 +1,49 @@
import type { UNode, UElement, URoot, UType, UComponent, UniversalProps } from "./schema.js";
let _idCounter = 0;
export function h(type: UType, props?: UniversalProps | null, ...children: UNode[]): UElement | URoot {
const resolvedProps: UniversalProps = props ? { ...props } : {};
const flatChildren = children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
if (type === "root") {
return {
type: "root",
props: resolvedProps,
children: flatChildren,
} as URoot;
}
return {
type: type as string,
props: resolvedProps,
children: flatChildren,
} as UElement;
}
export function createRoot(id: string | undefined, ...children: UNode[]): URoot {
return {
type: "root",
props: { id: id ?? `root_${++_idCounter}` },
children: children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[],
};
}
export function createComponent<P extends UniversalProps>(
name: string,
render: (props: P) => UNode,
targets?: string[],
): UComponent<P> {
const c = render as unknown as UComponent<P>;
c.displayName = name;
c.targets = targets;
return c;
}
export function Fragment(props: { children?: UNode[] }): UNode[] {
return ((props.children ?? []) as UNode[]).flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
}
export const jsx = h;
export const jsxs = h;
export const jsxDEV = h;

2
src/core/jsx-runtime.ts Normal file
View File

@@ -0,0 +1,2 @@
export { jsx, jsxs, jsxDEV, Fragment } from "./h.js";
export type {} from "./schema.js";

70
src/core/pointer.ts Normal file
View File

@@ -0,0 +1,70 @@
import { signal, type Signal, type ReadonlySignal } from "@preact/signals-core";
import type { UNode, UElement } from "./schema.js";
import { isUElement } from "./schema.js";
export class ValuePointer<T> {
private _signal: Signal<T>;
private _path: string[];
constructor(initial: T, path: string[] = []) {
this._signal = signal(initial);
this._path = path;
}
get value(): T {
return this._signal.value;
}
set value(v: T) {
this._signal.value = v;
}
get reactive(): ReadonlySignal<T> {
return this._signal;
}
get path(): string[] {
return this._path;
}
}
export function selectNode(root: UNode, path: string[]): UNode | undefined {
let current: UNode | undefined = root;
for (const segment of path) {
if (current === undefined || !isUElement(current)) return undefined;
const el = current as UElement;
const index = parseInt(segment, 10);
if (!isNaN(index) && index >= 0 && index < el.children.length) {
current = el.children[index];
} else {
const propVal = el.props[segment];
if (propVal !== undefined && typeof propVal === "object" && propVal !== null) {
current = propVal as UNode;
} else {
return undefined;
}
}
}
return current;
}
export function setNode(root: UNode, path: string[], value: UNode): UNode {
if (path.length === 0) return value;
const [head, ...tail] = path;
if (!isUElement(root)) return root;
const rootEl = root as UElement;
const index = parseInt(head!, 10);
if (!isNaN(index)) {
const children = [...rootEl.children];
if (index >= 0 && index < children.length) {
children[index] = setNode(children[index]!, tail, value);
}
return { ...rootEl, children } as UElement;
}
const props = { ...rootEl.props, [head as string]: value };
return { ...rootEl, props } as UElement;
}

93
src/core/reactive.ts Normal file
View File

@@ -0,0 +1,93 @@
import { signal, computed, effect, batch, type Signal, type ReadonlySignal } from "@preact/signals-core";
import type { UNode, UElement, UniversalProps, UComponent } from "./schema.js";
export interface ReactiveNode {
readonly type: string;
readonly signal: ReadonlySignal<UNode>;
readonly dispose: () => void;
}
export function reactiveComponent<P extends UniversalProps>(
component: UComponent<P>,
propsSignal: Signal<P>,
): ReactiveNode {
const nodeSignal = computed(() => {
const props = propsSignal.value;
return component(props);
});
return {
get type() {
return component.displayName ?? "anonymous";
},
signal: nodeSignal,
dispose: () => {},
};
}
export function reactiveElement(
type: string,
propsSignal: Signal<UniversalProps>,
childrenSignals: ReadonlySignal<UNode>[],
): ReactiveNode {
const nodeSignal = computed(() => {
const children = childrenSignals.map((s) => s.value);
return {
type,
props: propsSignal.value,
children,
} as UElement;
});
return {
type,
signal: nodeSignal,
dispose: () => {},
};
}
export class ReactiveRoot {
private root: Signal<UNode>;
private renderDisposer: (() => void) | null = null;
constructor(initial: UNode) {
this.root = signal(initial);
}
get value(): ReadonlySignal<UNode> {
return this.root;
}
update(fn: (current: UNode) => UNode): void {
batch(() => {
this.root.value = fn(this.root.value);
});
}
subscribe(listener: (node: UNode) => void): () => void {
return effect(() => {
listener(this.root.value);
});
}
render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void {
this.renderDisposer = effect(() => {
const node = this.root.value;
emit({
type: "root.render",
id: `root_${Date.now()}`,
payload: node,
});
});
return () => {
if (this.renderDisposer) {
this.renderDisposer();
this.renderDisposer = null;
}
};
}
}
export { signal, computed, effect, batch };
export type { Signal, ReadonlySignal };

79
src/core/schema.ts Normal file
View File

@@ -0,0 +1,79 @@
import { Type } from "@alkdev/typebox";
export const UJSX = Type.Module({
UPrimitive: Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]),
PropValue: Type.Union([
Type.String(),
Type.Number(),
Type.Boolean(),
Type.Null(),
Type.Array(Type.Unknown()),
Type.Ref("UNode"),
Type.Record(Type.String(), Type.Unknown()),
Type.Function([...Type.Rest(Type.Array(Type.Unknown()))], Type.Unknown()),
]),
UniversalProps: Type.Object(
{},
{ additionalProperties: Type.Union([Type.Ref("PropValue"), Type.Undefined()]) },
),
UElement: Type.Object({
type: Type.String(),
props: Type.Ref("UniversalProps"),
children: Type.Array(Type.Ref("UNode")),
}),
URoot: Type.Object({
type: Type.Literal("root"),
props: Type.Ref("UniversalProps"),
children: Type.Array(Type.Ref("UNode")),
}),
UNode: Type.Union([Type.Ref("UPrimitive"), Type.Ref("UElement"), Type.Ref("URoot")]),
});
// TypeScript types — the Module is for runtime validation;
// TS types are defined directly for clean inference.
// ComponentFn types in the schema are runtime-only (not serializable).
export type UPrimitive = string | number | boolean | null;
export type PropValue = string | number | boolean | null | unknown[] | UNode | Record<string, unknown> | ((...args: unknown[]) => unknown);
export type UniversalProps = Record<string, PropValue | undefined>;
export type UElement = {
type: string;
props: UniversalProps;
children: UNode[];
};
export type URoot = {
type: "root";
props: UniversalProps;
children: UNode[];
};
export type UNode = UPrimitive | UElement | URoot;
export type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode;
export type UType = string | ComponentFn;
export interface UComponent<P extends UniversalProps = UniversalProps> {
(props: P & { children?: UNode[] }): UNode;
displayName?: string;
targets?: string[];
}
export function isUElement(node: UNode): node is UElement {
return typeof node === "object" && node !== null && "type" in node && "props" in node && "children" in node && node.type !== "root";
}
export function isURoot(node: UNode): node is URoot {
return typeof node === "object" && node !== null && "type" in node && node.type === "root";
}
export function isUPrimitive(node: UNode): node is UPrimitive {
return typeof node === "string" || typeof node === "number" || typeof node === "boolean" || node === null;
}
// Runtime validation using TypeBox schemas
// Use UJSX.Import("UElement") etc. at call sites with Value.Check
export { UJSX as schema };

110
src/host/config.ts Normal file
View File

@@ -0,0 +1,110 @@
import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/schema.js";
import { isURoot, isUPrimitive } from "../core/schema.js";
import { Context } from "../core/context.js";
export 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;
}
export interface Root<TTag extends string, Instance, RootCtx> {
host: HostConfig<TTag, Instance, RootCtx>;
ctx: RootCtx;
container: unknown;
context: Context;
render(node: UNode): void;
unmount(): void;
}
export function createRoot<TTag extends string, Instance, RootCtx>(
host: HostConfig<TTag, Instance, RootCtx>,
container: unknown,
options?: Record<string, unknown>,
context?: Context,
): Root<TTag, Instance, RootCtx> {
const ctx = host.createRootContext(container, options, context);
const rootContext = context ?? new Context();
function mountNode(node: UNode, parentInst?: Instance): Instance | undefined {
if (node == null || node === false) return undefined;
if (isUPrimitive(node)) {
const text = node === null ? "" : String(node);
const t = host.createTextInstance(text, ctx, parentInst);
if (parentInst) host.appendChild(parentInst, t, ctx);
host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text });
return t;
}
if (isURoot(node)) {
for (const child of node.children) {
mountNode(child, parentInst);
}
return parentInst;
}
// node must be a UElement here
const el = node as UElement;
// Function component — type is a function (runtime-only, before resolution)
if (typeof el.type === "function") {
const component = el.type as ComponentFn;
const out = component({ ...el.props, children: el.children });
host.emit?.("component.invoke", `comp_${Date.now()}`, { type: (component as unknown as UComponent).displayName ?? "anonymous" });
return mountNode(out, parentInst);
}
// Intrinsic element
const tag = el.type as TTag;
const inst = host.createInstance(tag, el.props as Record<string, unknown>, ctx, parentInst);
host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props });
for (const child of el.children) {
mountNode(child, inst);
}
if (parentInst) host.appendChild(parentInst, inst, ctx);
return inst;
}
return {
host,
ctx,
container,
context: rootContext,
render(node: UNode) {
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (const child of payloadChildren) {
mountNode(child, undefined);
}
host.finalizeRoot?.(ctx);
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
},
unmount() {
host.finalizeRoot?.(ctx);
host.emit?.("root.unmount", `root_${Date.now()}`, {});
},
};
}

23
src/mod.ts Normal file
View File

@@ -0,0 +1,23 @@
export { UJSX as schema } from "./core/schema.js";
export type { UPrimitive, PropValue, UniversalProps, UElement, URoot, UNode, ComponentFn, UType, UComponent } from "./core/schema.js";
export { isUElement, isURoot, isUPrimitive } from "./core/schema.js";
export { h, createRoot, createComponent, Fragment, jsx, jsxs, jsxDEV } from "./core/h.js";
export { reactiveComponent, reactiveElement, ReactiveRoot } from "./core/reactive.js";
export { signal, computed, effect, batch } from "./core/reactive.js";
export type { Signal, ReadonlySignal, ReactiveNode } from "./core/reactive.js";
export { Context } from "./core/context.js";
export type { Density, Direction, RenderContext } from "./core/context.js";
export { createPubSubEmitter, proxyEventEmitter } from "./core/events.js";
export type { EventEnvelope, PubSubLike, UjsxEventMap, UjsxEnvelope } from "./core/events.js";
export { ValuePointer, selectNode, setNode } from "./core/pointer.js";
export { createRoot as createHostRoot } from "./host/config.js";
export type { HostConfig, Root } from "./host/config.js";
export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js";
export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js";

63
src/transform/registry.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { TSchema } from "@alkdev/typebox";
import { Value } from "@alkdev/typebox/value";
import type { Direction } from "../core/context.js";
export interface TransformContext<A = unknown> {
ancestors: A[];
index: number;
direction: Direction;
metadata: Record<string, unknown>;
}
export type TransformFn<T, U, A> = (node: T, ctx: TransformContext<A>) => U;
export interface TransformRule<TInput, TOutput, A = unknown> {
name: string;
direction: Direction;
schema?: TSchema;
match: (node: TInput) => boolean;
transform: (node: TInput, ctx: TransformContext<A>, next: TransformFn<TInput, TOutput, A>) => TOutput;
priority?: number;
}
export class TransformRegistry<TInput, TOutput, A = unknown> {
private rules: TransformRule<TInput, TOutput, A>[] = [];
register(rule: TransformRule<TInput, TOutput, A>): void {
this.rules.push(rule);
this.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
transform(node: TInput, ctx: TransformContext<A>): TOutput {
const rule = this.rules.find((r) => r.direction === ctx.direction && r.match(node));
if (!rule) {
throw new Error(`No transform rule found for node in direction '${ctx.direction}'`);
}
return rule.transform(node, ctx, (n, c) => this.transform(n, c));
}
transformAll(nodes: TInput[], ctx: TransformContext<A>): TOutput[] {
return nodes.map((node, i) => this.transform(node, { ...ctx, index: i, ancestors: ctx.ancestors }));
}
}
export function childCtx<A>(parent: A, ctx: TransformContext<A>, index: number): TransformContext<A> {
return {
...ctx,
ancestors: [...ctx.ancestors, parent],
index,
};
}
export function matchesSchema(schema: TSchema, node: unknown): boolean {
return Value.Check(schema, node);
}
export function ctx<A>(
direction: Direction,
ancestors: A[] = [],
index = 0,
metadata: Record<string, unknown> = {},
): TransformContext<A> {
return { ancestors, index, direction, metadata };
}