@vsreact/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Bundles a VSReacT app entry into a single QuickJS-ready IIFE.
2
+ // bun run build.ts [entry] [outfile]
3
+ export {};
4
+
5
+ const entry = process.argv[2] ?? "demo/main.tsx";
6
+ const outfile = process.argv[3] ?? "demo/build/main.js";
7
+
8
+ const result = await Bun.build({
9
+ entrypoints: [entry],
10
+ target: "browser",
11
+ format: "iife",
12
+ minify: false,
13
+ define: {
14
+ "process.env.NODE_ENV": '"production"',
15
+ },
16
+ });
17
+
18
+ if (!result.success) {
19
+ for (const log of result.logs) console.error(log);
20
+ process.exit(1);
21
+ }
22
+
23
+ await Bun.write(outfile, await result.outputs[0].text());
24
+ console.log(`built ${entry} -> ${outfile}`);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@vsreact/core",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/N9RecordsTechnologiesIL/VSReacT.git",
9
+ "directory": "vsreact/js"
10
+ },
11
+ "main": "src/index.ts",
12
+ "files": [
13
+ "src",
14
+ "build.ts"
15
+ ],
16
+ "scripts": {
17
+ "test": "bun test",
18
+ "typecheck": "tsc --noEmit",
19
+ "build": "bun run build.ts"
20
+ },
21
+ "dependencies": {
22
+ "react": "18.3.1",
23
+ "react-reconciler": "0.29.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^18.3.3",
27
+ "@types/react-reconciler": "0.28.8",
28
+ "bun-types": "^1.3.14",
29
+ "typescript": "^5.5.0"
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Easing, lerp } from "./animation";
3
+
4
+ describe("animation", () => {
5
+ test("easing curves start at 0 and end at 1", () => {
6
+ for (const fn of [Easing.linear, Easing.outCubic, Easing.inOutCubic, Easing.outExpo, Easing.outBack, Easing.outQuint]) {
7
+ expect(fn(0)).toBeCloseTo(0, 5);
8
+ expect(fn(1)).toBeCloseTo(1, 5);
9
+ }
10
+ });
11
+
12
+ test("outBack overshoots past 1 mid-curve", () => {
13
+ const peak = Math.max(...Array.from({ length: 99 }, (_, i) => Easing.outBack((i + 1) / 100)));
14
+ expect(peak).toBeGreaterThan(1);
15
+ });
16
+
17
+ test("lerp interpolates", () => {
18
+ expect(lerp(0, 10, 0.5)).toBe(5);
19
+ expect(lerp(-20, 20, 0.75)).toBe(10);
20
+ expect(lerp(4, 4, 0.3)).toBe(4);
21
+ });
22
+ });
@@ -0,0 +1,70 @@
1
+ // Frame-driven animation for VSReacT. Tweens run on the host timer (16ms
2
+ // ticks through the C++ Scheduler) and re-render by setting React state, so
3
+ // every animated style flows through the normal setProps → repaint path.
4
+
5
+ import { useEffect, useRef, useState } from "react";
6
+
7
+ export type EasingFn = (t: number) => number;
8
+
9
+ export const Easing = {
10
+ linear: ((t) => t) as EasingFn,
11
+ outCubic: ((t) => 1 - Math.pow(1 - t, 3)) as EasingFn,
12
+ inOutCubic: ((t) =>
13
+ t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2) as EasingFn,
14
+ outExpo: ((t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))) as EasingFn,
15
+ outBack: ((t) => {
16
+ const c1 = 1.70158;
17
+ const c3 = c1 + 1;
18
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
19
+ }) as EasingFn,
20
+ outQuint: ((t) => 1 - Math.pow(1 - t, 5)) as EasingFn,
21
+ };
22
+
23
+ export interface TweenOptions {
24
+ /** Milliseconds the tween runs for (after the delay). */
25
+ duration: number;
26
+ delay?: number;
27
+ easing?: EasingFn;
28
+ onComplete?: () => void;
29
+ }
30
+
31
+ const FRAME_MS = 16;
32
+
33
+ /** Eased progress 0→1 that starts when the component mounts. */
34
+ export function useTween({ duration, delay = 0, easing = Easing.outCubic, onComplete }: TweenOptions): number {
35
+ const [progress, setProgress] = useState(0);
36
+ const doneRef = useRef(false);
37
+ const completeRef = useRef(onComplete);
38
+ completeRef.current = onComplete;
39
+
40
+ useEffect(() => {
41
+ let elapsed = -delay;
42
+
43
+ const id = setInterval(() => {
44
+ elapsed += FRAME_MS;
45
+
46
+ if (elapsed <= 0) return;
47
+
48
+ const t = Math.min(1, elapsed / duration);
49
+ setProgress(easing(t));
50
+
51
+ if (t >= 1) {
52
+ clearInterval(id);
53
+
54
+ if (!doneRef.current) {
55
+ doneRef.current = true;
56
+ completeRef.current?.();
57
+ }
58
+ }
59
+ }, FRAME_MS);
60
+
61
+ return () => clearInterval(id);
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, []);
64
+
65
+ return progress;
66
+ }
67
+
68
+ export function lerp(from: number, to: number, t: number): number {
69
+ return from + (to - from) * t;
70
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,66 @@
1
+ // JS side of the mutation/event bridge. Ops queue up during a React commit
2
+ // and flush to C++ as one JSON batch; C++ pushes events back through the
3
+ // __vsreact_dispatch global registered here.
4
+
5
+ import { fireTimer } from "./runtime";
6
+
7
+ export type Op = unknown[];
8
+
9
+ let queue: Op[] = [];
10
+
11
+ export function queueOp(op: Op): void {
12
+ queue.push(op);
13
+ }
14
+
15
+ export function flushOps(): void {
16
+ if (queue.length === 0) return;
17
+ const json = JSON.stringify(queue);
18
+ queue = [];
19
+ (globalThis as Record<string, any>).__vsreact_flush?.(json);
20
+ }
21
+
22
+ export type EventHandler = (payload: unknown) => void;
23
+
24
+ const eventHandlers = new Map<number, Map<string, EventHandler>>();
25
+
26
+ export function setHandlers(nodeId: number, handlers: Map<string, EventHandler>): void {
27
+ if (handlers.size === 0) eventHandlers.delete(nodeId);
28
+ else eventHandlers.set(nodeId, handlers);
29
+ }
30
+
31
+ export function removeHandlers(nodeId: number): void {
32
+ eventHandlers.delete(nodeId);
33
+ }
34
+
35
+ const nativeListeners = new Map<string, Set<EventHandler>>();
36
+
37
+ export function addNativeListener(name: string, cb: EventHandler): () => void {
38
+ let set = nativeListeners.get(name);
39
+ if (!set) nativeListeners.set(name, (set = new Set()));
40
+ set.add(cb);
41
+ return () => {
42
+ set.delete(cb);
43
+ if (set.size === 0) nativeListeners.delete(name);
44
+ };
45
+ }
46
+
47
+ interface DispatchMessage {
48
+ kind: "event" | "native" | "timer";
49
+ nodeId?: number;
50
+ type?: string;
51
+ name?: string;
52
+ id?: number;
53
+ payload?: unknown;
54
+ }
55
+
56
+ (globalThis as Record<string, any>).__vsreact_dispatch = (json: string) => {
57
+ const msg = JSON.parse(json) as DispatchMessage;
58
+
59
+ if (msg.kind === "timer" && msg.id !== undefined) {
60
+ fireTimer(msg.id);
61
+ } else if (msg.kind === "event" && msg.nodeId !== undefined && msg.type) {
62
+ eventHandlers.get(msg.nodeId)?.get(msg.type)?.(msg.payload);
63
+ } else if (msg.kind === "native" && msg.name) {
64
+ nativeListeners.get(msg.name)?.forEach((cb) => cb(msg.payload));
65
+ }
66
+ };
@@ -0,0 +1,194 @@
1
+ // Knob and Slider — the classic VST controls, drawn by the VSReacT painter
2
+ // (arc styles) and driven by drag gestures.
3
+
4
+ import { useRef } from "react";
5
+ import { View, Text } from "./primitives";
6
+ import { useParameter } from "./parameters";
7
+
8
+ /** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
9
+ export function dragToValue(startValue: number, dy: number, sensitivity = 0.005): number {
10
+ return Math.min(1, Math.max(0, startValue - dy * sensitivity));
11
+ }
12
+
13
+ const ARC_START = -135;
14
+ const ARC_END = 135;
15
+
16
+ export interface KnobProps {
17
+ value: number; // normalized 0..1
18
+ text?: string;
19
+ label?: string;
20
+ size?: number;
21
+ disabled?: boolean;
22
+ trackColor?: string;
23
+ valueColor?: string;
24
+ onChange: (value: number) => void;
25
+ onBegin?: () => void;
26
+ onEnd?: () => void;
27
+ }
28
+
29
+ export function Knob({
30
+ value,
31
+ text,
32
+ label,
33
+ size = 64,
34
+ disabled,
35
+ trackColor = "#2A2F27",
36
+ valueColor = "#C6F135",
37
+ onChange,
38
+ onBegin,
39
+ onEnd,
40
+ }: KnobProps) {
41
+ const startValue = useRef(0);
42
+
43
+ return (
44
+ <View className="items-center gap-2">
45
+ <View
46
+ className={`items-center justify-center ${disabled ? "opacity-40" : "cursor-pointer"}`}
47
+ style={{
48
+ width: size,
49
+ height: size,
50
+ arcTrackColor: trackColor,
51
+ arcColor: valueColor,
52
+ arcStart: ARC_START,
53
+ arcEnd: ARC_END,
54
+ arcValueEnd: ARC_START + (ARC_END - ARC_START) * Math.min(1, Math.max(0, value)),
55
+ arcThickness: Math.max(3, size * 0.08),
56
+ }}
57
+ onDragStart={
58
+ disabled
59
+ ? undefined
60
+ : () => {
61
+ startValue.current = value;
62
+ onBegin?.();
63
+ }
64
+ }
65
+ onDrag={disabled ? undefined : (e) => onChange(dragToValue(startValue.current, e.dy))}
66
+ onDragEnd={disabled ? undefined : () => onEnd?.()}
67
+ >
68
+ {text !== undefined ? (
69
+ <Text
70
+ className="text-text font-bold text-center"
71
+ style={{ fontSize: Math.max(10, size * 0.16) }}
72
+ >
73
+ {text}
74
+ </Text>
75
+ ) : null}
76
+ </View>
77
+ {label !== undefined ? (
78
+ <Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
79
+ ) : null}
80
+ </View>
81
+ );
82
+ }
83
+
84
+ export interface ParamKnobProps {
85
+ paramId: string;
86
+ label?: string;
87
+ size?: number;
88
+ trackColor?: string;
89
+ valueColor?: string;
90
+ }
91
+
92
+ /** A Knob bound to an APVTS parameter (via ParameterBridge). */
93
+ export function ParamKnob({ paramId, label, size, trackColor, valueColor }: ParamKnobProps) {
94
+ const param = useParameter(paramId);
95
+
96
+ return (
97
+ <Knob
98
+ value={param.value}
99
+ text={param.text}
100
+ label={label ?? param.name.toUpperCase()}
101
+ size={size}
102
+ trackColor={trackColor}
103
+ valueColor={valueColor}
104
+ onChange={param.set}
105
+ onBegin={param.begin}
106
+ onEnd={param.end}
107
+ />
108
+ );
109
+ }
110
+
111
+ export interface SliderProps {
112
+ value: number; // normalized 0..1
113
+ width?: number;
114
+ label?: string;
115
+ disabled?: boolean;
116
+ trackColor?: string;
117
+ valueColor?: string;
118
+ onChange: (value: number) => void;
119
+ onBegin?: () => void;
120
+ onEnd?: () => void;
121
+ }
122
+
123
+ export function Slider({
124
+ value,
125
+ width = 160,
126
+ label,
127
+ disabled,
128
+ trackColor = "#2A2F27",
129
+ valueColor = "#C6F135",
130
+ onChange,
131
+ onBegin,
132
+ onEnd,
133
+ }: SliderProps) {
134
+ const startValue = useRef(0);
135
+ const clamped = Math.min(1, Math.max(0, value));
136
+
137
+ return (
138
+ <View className="gap-2">
139
+ <View
140
+ className={`h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`}
141
+ style={{ width }}
142
+ onDragStart={
143
+ disabled
144
+ ? undefined
145
+ : () => {
146
+ startValue.current = clamped;
147
+ onBegin?.();
148
+ }
149
+ }
150
+ onDrag={
151
+ disabled
152
+ ? undefined
153
+ : (e) => onChange(Math.min(1, Math.max(0, startValue.current + e.dx / width)))
154
+ }
155
+ onDragEnd={disabled ? undefined : () => onEnd?.()}
156
+ >
157
+ <View className="h-[4] rounded-full" style={{ backgroundColor: trackColor }} />
158
+ <View
159
+ className="absolute h-[4] rounded-full top-[7]"
160
+ style={{ width: clamped * width, backgroundColor: valueColor }}
161
+ />
162
+ <View
163
+ className="absolute w-[12] h-[12] rounded-full top-[3]"
164
+ style={{ left: clamped * (width - 12), backgroundColor: valueColor }}
165
+ />
166
+ </View>
167
+ {label !== undefined ? (
168
+ <Text className="text-faint text-[10] font-bold tracking-widest">{label}</Text>
169
+ ) : null}
170
+ </View>
171
+ );
172
+ }
173
+
174
+ export interface ParamSliderProps {
175
+ paramId: string;
176
+ label?: string;
177
+ width?: number;
178
+ }
179
+
180
+ /** A Slider bound to an APVTS parameter (via ParameterBridge). */
181
+ export function ParamSlider({ paramId, label, width }: ParamSliderProps) {
182
+ const param = useParameter(paramId);
183
+
184
+ return (
185
+ <Slider
186
+ value={param.value}
187
+ label={label ?? param.name.toUpperCase()}
188
+ width={width}
189
+ onChange={param.set}
190
+ onBegin={param.begin}
191
+ onEnd={param.end}
192
+ />
193
+ );
194
+ }
@@ -0,0 +1,120 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { useState } from "react";
3
+
4
+ const batches: unknown[][][] = [];
5
+ (globalThis as Record<string, any>).__vsreact_flush = (json: string) => {
6
+ batches.push(JSON.parse(json));
7
+ };
8
+
9
+ import { render, unmount, View, Text, TextInput } from "./index";
10
+
11
+ const allOps = () => batches.flat();
12
+ const opsNamed = (name: string) => allOps().filter((op: any) => op[0] === name);
13
+ const dispatch = (msg: unknown) =>
14
+ (globalThis as Record<string, any>).__vsreact_dispatch(JSON.stringify(msg));
15
+
16
+ beforeEach(() => {
17
+ unmount();
18
+ batches.length = 0;
19
+ });
20
+
21
+ describe("reconciler host config", () => {
22
+ test("initial mount emits create/setProps/append ops ending at the root", () => {
23
+ render(
24
+ <View className="p-4">
25
+ <Text>hi</Text>
26
+ </View>,
27
+ );
28
+
29
+ const ops = allOps();
30
+ const creates = opsNamed("create");
31
+
32
+ // view + text + rawtext
33
+ expect(creates.length).toBe(3);
34
+ const viewId = (creates.find((op: any) => op[2] === "view") as any)[1];
35
+ const textId = (creates.find((op: any) => op[2] === "text") as any)[1];
36
+ const rawId = (creates.find((op: any) => op[2] === "rawtext") as any)[1];
37
+
38
+ const viewProps: any = opsNamed("setProps").find((op: any) => op[1] === viewId);
39
+ expect(viewProps[2].style.padding).toBe(16);
40
+
41
+ expect(opsNamed("setText")).toContainEqual(["setText", rawId, "hi"]);
42
+ expect(opsNamed("appendChild")).toContainEqual(["appendChild", textId, rawId]);
43
+ expect(opsNamed("appendChild")).toContainEqual(["appendChild", viewId, textId]);
44
+ expect(opsNamed("appendChild")).toContainEqual(["appendChild", 0, viewId]);
45
+
46
+ // root attach comes after the subtree is assembled
47
+ const opNames = ops.map((op: any) => op.join(":"));
48
+ expect(opNames.indexOf(`appendChild:0:${viewId}`)).toBeGreaterThan(
49
+ opNames.indexOf(`appendChild:${viewId}:${textId}`),
50
+ );
51
+ });
52
+
53
+ test("click handlers register as listeners and receive dispatches", () => {
54
+ let clicks = 0;
55
+
56
+ render(<View onClick={() => clicks++} className="w-4" />);
57
+
58
+ const create: any = opsNamed("create")[0];
59
+ const props: any = opsNamed("setProps").find((op: any) => op[1] === create[1]);
60
+ expect(props[2].listeners).toEqual(["click"]);
61
+
62
+ dispatch({ kind: "event", nodeId: create[1], type: "click" });
63
+ expect(clicks).toBe(1);
64
+ });
65
+
66
+ test("state updates emit new setProps and setText", () => {
67
+ function Counter() {
68
+ const [count, setCount] = useState(0);
69
+ return (
70
+ <View onClick={() => setCount((c) => c + 1)}>
71
+ <Text>{`count:${count}`}</Text>
72
+ </View>
73
+ );
74
+ }
75
+
76
+ render(<Counter />);
77
+
78
+ const viewId = (opsNamed("create").find((op: any) => op[2] === "view") as any)[1];
79
+ batches.length = 0;
80
+
81
+ dispatch({ kind: "event", nodeId: viewId, type: "click" });
82
+
83
+ expect(opsNamed("setText").some((op: any) => op[2] === "count:1")).toBe(true);
84
+ });
85
+
86
+ test("TextInput onChange receives the plain string value", () => {
87
+ const seen: string[] = [];
88
+
89
+ render(<TextInput onChange={(v) => seen.push(v)} />);
90
+
91
+ const inputId = (opsNamed("create").find((op: any) => op[2] === "textinput") as any)[1];
92
+ dispatch({ kind: "event", nodeId: inputId, type: "change", payload: { value: "abc" } });
93
+
94
+ expect(seen).toEqual(["abc"]);
95
+ });
96
+
97
+ test("conditional removal emits removeChild", () => {
98
+ function Toggle({ on }: { on: boolean }) {
99
+ return <View>{on ? <View className="w-2" /> : null}</View>;
100
+ }
101
+
102
+ render(<Toggle on={true} />);
103
+ // React creates instances bottom-up: the inner view is created first.
104
+ const childId = (opsNamed("create")[0] as any)[1];
105
+ batches.length = 0;
106
+
107
+ render(<Toggle on={false} />);
108
+ expect(opsNamed("removeChild").some((op: any) => op[2] === childId)).toBe(true);
109
+ });
110
+
111
+ test("unmount clears the container", () => {
112
+ render(<View />);
113
+ batches.length = 0;
114
+ unmount();
115
+ const ops = allOps();
116
+ expect(ops.some((op: any) => op[0] === "removeChild" || op[0] === "clearContainer")).toBe(
117
+ true,
118
+ );
119
+ });
120
+ });
@@ -0,0 +1,161 @@
1
+ // react-reconciler host config (mutation mode). React commits become batched
2
+ // mutation ops; event props become handler-registry entries + listener lists.
3
+
4
+ import { DefaultEventPriority } from "react-reconciler/constants";
5
+ import { queueOp, flushOps, setHandlers, removeHandlers, type EventHandler } from "./bridge";
6
+ import { tw, type Style } from "./tw";
7
+
8
+ export interface Instance {
9
+ id: number;
10
+ type: string;
11
+ }
12
+
13
+ export type TextInstance = Instance;
14
+
15
+ let nextId = 1;
16
+
17
+ const hostTypes: Record<string, string> = {
18
+ "vs-view": "view",
19
+ "vs-text": "text",
20
+ "vs-image": "image",
21
+ "vs-textinput": "textinput",
22
+ "vs-native": "native",
23
+ };
24
+
25
+ const eventPropNames: Record<string, string> = {
26
+ onClick: "click",
27
+ onMouseEnter: "mouseenter",
28
+ onMouseLeave: "mouseleave",
29
+ onMouseDown: "mousedown",
30
+ onMouseUp: "mouseup",
31
+ onDragStart: "dragstart",
32
+ onDrag: "drag",
33
+ onDragEnd: "dragend",
34
+ onChange: "change",
35
+ onSubmit: "submit",
36
+ onFocus: "focus",
37
+ onBlur: "blur",
38
+ };
39
+
40
+ function normalizeProps(props: Record<string, unknown>): {
41
+ payload: Record<string, unknown>;
42
+ handlers: Map<string, EventHandler>;
43
+ } {
44
+ const payload: Record<string, unknown> = {};
45
+ const handlers = new Map<string, EventHandler>();
46
+ const listeners: string[] = [];
47
+
48
+ const resolved = typeof props.className === "string" ? tw(props.className) : { style: {} };
49
+ payload.style = { ...resolved.style, ...((props.style as Style) ?? {}) };
50
+ if (resolved.hoverStyle) payload.hoverStyle = resolved.hoverStyle;
51
+ if (resolved.activeStyle) payload.activeStyle = resolved.activeStyle;
52
+ if (resolved.focusStyle) payload.focusStyle = resolved.focusStyle;
53
+
54
+ for (const [key, value] of Object.entries(props)) {
55
+ if (value === undefined || key === "children" || key === "className" || key === "style")
56
+ continue;
57
+
58
+ const eventType = eventPropNames[key];
59
+
60
+ if (eventType && typeof value === "function") {
61
+ handlers.set(eventType, value as EventHandler);
62
+ listeners.push(eventType);
63
+ continue;
64
+ }
65
+
66
+ if (typeof value !== "function" && typeof value !== "object") payload[key] = value;
67
+ }
68
+
69
+ if (listeners.length > 0) payload.listeners = listeners;
70
+
71
+ return { payload, handlers };
72
+ }
73
+
74
+ function applyProps(instance: Instance, props: Record<string, unknown>): void {
75
+ const { payload, handlers } = normalizeProps(props);
76
+ setHandlers(instance.id, handlers);
77
+ queueOp(["setProps", instance.id, payload]);
78
+ }
79
+
80
+ export const hostConfig = {
81
+ supportsMutation: true,
82
+ supportsPersistence: false,
83
+ supportsHydration: false,
84
+ isPrimaryRenderer: true,
85
+ supportsMicrotasks: true,
86
+ scheduleMicrotask: (fn: () => void) => queueMicrotask(fn),
87
+ scheduleTimeout: (fn: (...args: unknown[]) => void, delay?: number) => setTimeout(fn, delay),
88
+ cancelTimeout: (id: ReturnType<typeof setTimeout>) => clearTimeout(id),
89
+ noTimeout: -1 as const,
90
+
91
+ getRootHostContext: () => ({}),
92
+ getChildHostContext: (parent: object) => parent,
93
+ getPublicInstance: (instance: Instance) => instance,
94
+ prepareForCommit: () => null,
95
+ resetAfterCommit: () => flushOps(),
96
+ shouldSetTextContent: () => false,
97
+
98
+ createInstance(type: string, props: Record<string, unknown>): Instance {
99
+ const hostType = hostTypes[type];
100
+ if (!hostType) throw new Error(`Unknown VSReacT element <${type}>`);
101
+
102
+ const instance: Instance = { id: nextId++, type: hostType };
103
+ queueOp(["create", instance.id, hostType]);
104
+ applyProps(instance, props);
105
+ return instance;
106
+ },
107
+
108
+ createTextInstance(text: string): TextInstance {
109
+ const instance: TextInstance = { id: nextId++, type: "rawtext" };
110
+ queueOp(["create", instance.id, "rawtext"]);
111
+ queueOp(["setText", instance.id, text]);
112
+ return instance;
113
+ },
114
+
115
+ appendInitialChild: (parent: Instance, child: Instance) =>
116
+ queueOp(["appendChild", parent.id, child.id]),
117
+ appendChild: (parent: Instance, child: Instance) =>
118
+ queueOp(["appendChild", parent.id, child.id]),
119
+ appendChildToContainer: (_container: unknown, child: Instance) =>
120
+ queueOp(["appendChild", 0, child.id]),
121
+ insertBefore: (parent: Instance, child: Instance, before: Instance) =>
122
+ queueOp(["insertBefore", parent.id, child.id, before.id]),
123
+ insertInContainerBefore: (_container: unknown, child: Instance, before: Instance) =>
124
+ queueOp(["insertBefore", 0, child.id, before.id]),
125
+ removeChild: (parent: Instance, child: Instance) =>
126
+ queueOp(["removeChild", parent.id, child.id]),
127
+ removeChildFromContainer: (_container: unknown, child: Instance) =>
128
+ queueOp(["removeChild", 0, child.id]),
129
+
130
+ finalizeInitialChildren: () => false,
131
+
132
+ prepareUpdate: () => true,
133
+
134
+ commitUpdate(
135
+ instance: Instance,
136
+ _updatePayload: unknown,
137
+ _type: string,
138
+ _prevProps: Record<string, unknown>,
139
+ nextProps: Record<string, unknown>,
140
+ ): void {
141
+ applyProps(instance, nextProps);
142
+ },
143
+
144
+ commitTextUpdate(instance: TextInstance, _oldText: string, newText: string): void {
145
+ queueOp(["setText", instance.id, newText]);
146
+ },
147
+
148
+ clearContainer: () => queueOp(["clearContainer"]),
149
+
150
+ detachDeletedInstance(instance: Instance): void {
151
+ removeHandlers(instance.id);
152
+ },
153
+
154
+ getCurrentEventPriority: () => DefaultEventPriority,
155
+ getInstanceFromNode: () => null,
156
+ getInstanceFromScope: () => null,
157
+ beforeActiveInstanceBlur: () => {},
158
+ afterActiveInstanceBlur: () => {},
159
+ prepareScopeUpdate: () => {},
160
+ preparePortalMount: () => {},
161
+ };