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:
110
src/host/config.ts
Normal file
110
src/host/config.ts
Normal 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()}`, {});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user