Files
ujsx/src/core/reactive.ts
glm-5.1 3eb1f1d896 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.
2026-05-03 08:19:49 +00:00

93 lines
2.1 KiB
TypeScript

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 };