diff --git a/src/host/fiber.ts b/src/host/fiber.ts new file mode 100644 index 0000000..87bacf2 --- /dev/null +++ b/src/host/fiber.ts @@ -0,0 +1,17 @@ +export interface Fiber { + instance: I; + tag: string; + props: Record; + key: string | undefined; + children: Fiber[]; + parent: Fiber | null; + effect: Effect | null; + signalDisposers: (() => void)[]; + prevProps: Record | null; +} + +export type Effect = + | { type: "update"; payload: unknown } + | { type: "insert"; before: Fiber | null } + | { type: "move"; before: Fiber | null } + | { type: "remove" }; \ No newline at end of file diff --git a/src/mod.ts b/src/mod.ts index 2044767..2af5285 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -19,5 +19,7 @@ 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 type { Fiber, Effect } from "./host/fiber.js"; + export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js"; export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js"; \ No newline at end of file diff --git a/test/fiber.test.ts b/test/fiber.test.ts new file mode 100644 index 0000000..a2ca9e6 --- /dev/null +++ b/test/fiber.test.ts @@ -0,0 +1,192 @@ +import { describe, it, expect, expectTypeOf } from "vitest"; +import type { Fiber, Effect } from "../src/host/fiber.js"; + +describe("Fiber interface", () => { + it("has all required fields", () => { + const fiber: Fiber = { + instance: "inst-1", + tag: "div", + props: { class: "test" }, + key: "a", + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + expect(fiber.instance).toBe("inst-1"); + expect(fiber.tag).toBe("div"); + expect(fiber.props.class).toBe("test"); + expect(fiber.key).toBe("a"); + expect(fiber.children).toEqual([]); + expect(fiber.parent).toBeNull(); + expect(fiber.effect).toBeNull(); + expect(fiber.signalDisposers).toEqual([]); + expect(fiber.prevProps).toBeNull(); + }); + + it("supports key as undefined", () => { + const fiber: Fiber = { + instance: "inst-2", + tag: "span", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + expect(fiber.key).toBeUndefined(); + }); + + it("supports parent reference", () => { + const parent: Fiber = { + instance: "parent-inst", + tag: "div", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + const child: Fiber = { + instance: "child-inst", + tag: "span", + props: {}, + key: "child-1", + children: [], + parent, + effect: null, + signalDisposers: [], + prevProps: null, + }; + parent.children.push(child); + expect(child.parent).toBe(parent); + expect(parent.children[0]).toBe(child); + }); + + it("supports signalDisposers array", () => { + const disposed: string[] = []; + const fiber: Fiber = { + instance: "inst", + tag: "div", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [() => disposed.push("a"), () => disposed.push("b")], + prevProps: null, + }; + fiber.signalDisposers.forEach((d) => d()); + expect(disposed).toEqual(["a", "b"]); + }); +}); + +describe("Effect union type", () => { + it("update effect", () => { + const effect: Effect = { type: "update", payload: { class: "new" } }; + expect(effect.type).toBe("update"); + expect((effect as { type: "update"; payload: unknown }).payload).toEqual({ class: "new" }); + }); + + it("insert effect with before=null (append)", () => { + const effect: Effect = { type: "insert", before: null }; + expect(effect.type).toBe("insert"); + }); + + it("insert effect with before=fiber", () => { + const before: Fiber = { + instance: "existing", + tag: "span", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + const effect: Effect = { type: "insert", before }; + expect(effect.type).toBe("insert"); + expect(effect.before).toBe(before); + }); + + it("move effect with before=fiber", () => { + const before: Fiber = { + instance: "target", + tag: "span", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + const effect: Effect = { type: "move", before }; + expect(effect.type).toBe("move"); + expect(effect.before).toBe(before); + }); + + it("move effect with before=null (append at end)", () => { + const effect: Effect = { type: "move", before: null }; + expect(effect.type).toBe("move"); + }); + + it("remove effect", () => { + const effect: Effect = { type: "remove" }; + expect(effect.type).toBe("remove"); + }); + + it("effect field on Fiber accepts all variants", () => { + const fiber: Fiber = { + instance: 1, + tag: "div", + props: {}, + key: undefined, + children: [], + parent: null, + effect: { type: "update", payload: "x" }, + signalDisposers: [], + prevProps: {}, + }; + expect(fiber.effect!.type).toBe("update"); + + fiber.effect = { type: "remove" }; + expect(fiber.effect.type).toBe("remove"); + + fiber.effect = { type: "insert", before: null }; + expect(fiber.effect.type).toBe("insert"); + + fiber.effect = { type: "move", before: null }; + expect(fiber.effect.type).toBe("move"); + + fiber.effect = null; + expect(fiber.effect).toBeNull(); + }); +}); + +describe("Fiber re-export from barrel", () => { + it("Fiber and Effect types are importable from src/mod.ts", async () => { + type _FiberCheck = import("../src/mod.js").Fiber; + type _EffectCheck = import("../src/mod.js").Effect; + const _fiber: _FiberCheck = { + instance: "inst", + tag: "div", + props: {}, + key: undefined, + children: [], + parent: null, + effect: null, + signalDisposers: [], + prevProps: null, + }; + const _effect: _EffectCheck = { type: "remove" }; + expect(_fiber.instance).toBe("inst"); + expect(_effect.type).toBe("remove"); + }); +}); \ No newline at end of file