@vsreact/core 0.0.11 → 0.0.12

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.
@@ -0,0 +1,188 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ const batches: unknown[][][] = [];
4
+
5
+ (globalThis as Record<string, any>).__vsreact_flush = (json: string) => {
6
+ batches.push(JSON.parse(json));
7
+ };
8
+ (globalThis as Record<string, any>).__vsreact_nativeCall = () => "null";
9
+
10
+ import {
11
+ render,
12
+ unmount,
13
+ PianoKeyboard,
14
+ StepSequencer,
15
+ ProgressBar,
16
+ mapRange,
17
+ formatDb,
18
+ formatHz,
19
+ formatMs,
20
+ formatPercent,
21
+ formatSemitones,
22
+ midiNoteName,
23
+ } from "./index";
24
+
25
+ const allOps = () => batches.flat();
26
+ const opsNamed = (name: string) => allOps().filter((op: any) => op[0] === name);
27
+ const dispatch = (msg: unknown) =>
28
+ (globalThis as Record<string, any>).__vsreact_dispatch(JSON.stringify(msg));
29
+
30
+ /** Node ids whose registered listeners include `type`, in creation order. */
31
+ const nodesWithListener = (type: string): number[] => {
32
+ const seen = new Set<number>();
33
+ for (const op of opsNamed("setProps") as any[]) {
34
+ if (op[2]?.listeners?.includes(type)) seen.add(op[1]);
35
+ }
36
+ return [...seen];
37
+ };
38
+
39
+ beforeEach(() => {
40
+ unmount();
41
+ batches.length = 0;
42
+ });
43
+
44
+ describe("format", () => {
45
+ test("formatDb signs and floors", () => {
46
+ expect(formatDb(6)).toBe("+6.0 dB");
47
+ expect(formatDb(0)).toBe("0.0 dB");
48
+ expect(formatDb(-12.53)).toBe("-12.5 dB");
49
+ expect(formatDb(Number.NEGATIVE_INFINITY)).toBe("-inf dB");
50
+ });
51
+
52
+ test("formatHz switches to kHz at 1000", () => {
53
+ expect(formatHz(440)).toBe("440 Hz");
54
+ expect(formatHz(1200)).toBe("1.2 kHz");
55
+ expect(formatHz(18500, 2)).toBe("18.5 kHz");
56
+ });
57
+
58
+ test("formatMs switches to seconds at 1000", () => {
59
+ expect(formatMs(350)).toBe("350 ms");
60
+ expect(formatMs(1250)).toBe("1.25 s");
61
+ });
62
+
63
+ test("formatPercent and formatSemitones", () => {
64
+ expect(formatPercent(0.42)).toBe("42%");
65
+ expect(formatSemitones(7)).toBe("+7 st");
66
+ expect(formatSemitones(-12)).toBe("-12 st");
67
+ expect(formatSemitones(0)).toBe("0 st");
68
+ });
69
+
70
+ test("midiNoteName uses scientific pitch", () => {
71
+ expect(midiNoteName(60)).toBe("C4");
72
+ expect(midiNoteName(69)).toBe("A4");
73
+ expect(midiNoteName(48)).toBe("C3");
74
+ expect(midiNoteName(61)).toBe("C#4");
75
+ });
76
+
77
+ test("mapRange maps and optionally clamps", () => {
78
+ expect(mapRange(0.5, 0, 1, 0, 100)).toBe(50);
79
+ expect(mapRange(2, 0, 1, 0, 100)).toBe(200);
80
+ expect(mapRange(2, 0, 1, 0, 100, true)).toBe(100);
81
+ });
82
+ });
83
+
84
+ describe("PianoKeyboard", () => {
85
+ test("one octave renders 8 whites + 5 blacks, all pressable", () => {
86
+ render(<PianoKeyboard startNote={48} octaves={1} onNoteOn={() => {}} />);
87
+ expect(nodesWithListener("mousedown")).toHaveLength(13);
88
+ });
89
+
90
+ test("press fires note-on, release fires note-off", () => {
91
+ const on: number[] = [];
92
+ const off: number[] = [];
93
+ render(
94
+ <PianoKeyboard startNote={48} octaves={1} onNoteOn={(n) => on.push(n)} onNoteOff={(n) => off.push(n)} />,
95
+ );
96
+
97
+ // Whites render first: the first pressable node is the low C (48).
98
+ const keys = nodesWithListener("mousedown");
99
+ dispatch({ kind: "event", nodeId: keys[0], type: "mousedown" });
100
+ expect(on).toEqual([48]);
101
+ dispatch({ kind: "event", nodeId: keys[0], type: "mouseup" });
102
+ expect(off).toEqual([48]);
103
+ });
104
+
105
+ test("glissando: dragging one key-width retargets to the next white", () => {
106
+ const on: number[] = [];
107
+ const off: number[] = [];
108
+ render(
109
+ <PianoKeyboard
110
+ startNote={48}
111
+ octaves={1}
112
+ whiteKeyWidth={24}
113
+ onNoteOn={(n) => on.push(n)}
114
+ onNoteOff={(n) => off.push(n)}
115
+ />,
116
+ );
117
+
118
+ const keys = nodesWithListener("mousedown");
119
+ dispatch({ kind: "event", nodeId: keys[0], type: "mousedown" });
120
+ dispatch({ kind: "event", nodeId: keys[0], type: "drag", payload: { dx: 24, dy: 0 } });
121
+ expect(on).toEqual([48, 50]);
122
+ expect(off).toEqual([48]);
123
+
124
+ dispatch({ kind: "event", nodeId: keys[0], type: "dragend", payload: { dx: 24, dy: 0 } });
125
+ expect(off).toEqual([48, 50]);
126
+ });
127
+
128
+ test("blacks sit above whites: dragging into the black zone hits C#", () => {
129
+ const on: number[] = [];
130
+ render(<PianoKeyboard startNote={48} octaves={1} whiteKeyWidth={24} onNoteOn={(n) => on.push(n)} />);
131
+
132
+ const keys = nodesWithListener("mousedown");
133
+ // From the low C's center (x=12, y in the white zone) up-right into
134
+ // the C# head: C# spans x 16.56..31.44, y 0..57.6.
135
+ dispatch({ kind: "event", nodeId: keys[0], type: "mousedown" });
136
+ dispatch({ kind: "event", nodeId: keys[0], type: "drag", payload: { dx: 8, dy: -60 } });
137
+ expect(on).toEqual([48, 49]);
138
+ });
139
+
140
+ test("disabled keyboard registers no handlers", () => {
141
+ render(<PianoKeyboard disabled onNoteOn={() => {}} />);
142
+ expect(nodesWithListener("mousedown")).toHaveLength(0);
143
+ });
144
+ });
145
+
146
+ describe("StepSequencer", () => {
147
+ test("clicking a cell reports row, step, and the flipped state", () => {
148
+ const seen: Array<[number, number, boolean]> = [];
149
+ const pattern = [
150
+ [true, false, false, false],
151
+ [false, false, true, false],
152
+ ];
153
+ render(<StepSequencer pattern={pattern} onToggle={(r, s, next) => seen.push([r, s, next])} />);
154
+
155
+ const cells = nodesWithListener("click");
156
+ expect(cells).toHaveLength(8);
157
+
158
+ dispatch({ kind: "event", nodeId: cells[2], type: "click" }); // row 0, step 2 — off → on
159
+ dispatch({ kind: "event", nodeId: cells[6], type: "click" }); // row 1, step 2 — on → off
160
+ expect(seen).toEqual([
161
+ [0, 2, true],
162
+ [1, 2, false],
163
+ ]);
164
+ });
165
+
166
+ test("disabled grid registers no click handlers", () => {
167
+ render(<StepSequencer pattern={[[false, false]]} disabled onToggle={() => {}} />);
168
+ expect(nodesWithListener("click")).toHaveLength(0);
169
+ });
170
+ });
171
+
172
+ describe("ProgressBar indeterminate", () => {
173
+ test("renders a sweeping segment that moves over time", async () => {
174
+ render(<ProgressBar value={0} indeterminate width={200} />);
175
+
176
+ const lefts = () =>
177
+ (opsNamed("setProps") as any[])
178
+ .map((op) => op[2]?.style?.left)
179
+ .filter((left) => typeof left === "number");
180
+
181
+ const before = lefts().length;
182
+ expect(before).toBeGreaterThan(0);
183
+
184
+ await new Promise((r) => setTimeout(r, 60));
185
+ const all = lefts();
186
+ expect(new Set(all).size).toBeGreaterThan(1); // the segment moved
187
+ });
188
+ });
@@ -1,95 +1,95 @@
1
- import { createElement, type ReactNode } from "react";
2
- import type { Style } from "./tw";
3
-
4
- export interface DragEventPayload {
5
- /** Delta from the drag start, in root coordinates. */
6
- dx: number;
7
- dy: number;
8
- /** Current pointer position in root coordinates. */
9
- x: number;
10
- y: number;
11
- }
12
-
13
- /** A node's laid-out rect in root coordinates (scroll-adjusted). */
14
- export interface LayoutRect {
15
- x: number;
16
- y: number;
17
- width: number;
18
- height: number;
19
- }
20
-
21
- /** Mouse-wheel payload. dy is JUCE's notch fraction (~0.1 per notch,
22
- positive = wheel up). */
23
- export interface WheelEventPayload {
24
- dy: number;
25
- }
26
-
27
- export interface CommonProps {
28
- className?: string;
29
- style?: Style;
30
- children?: ReactNode;
31
- /** Resets the scroll offset of an overflow-y-scroll container. */
32
- scrollTop?: number;
33
- onClick?: () => void;
34
- onDoubleClick?: () => void;
35
- /** Wheel over this node (controls win the wheel over scroll containers). */
36
- onWheel?: (e: WheelEventPayload) => void;
37
- onMouseEnter?: () => void;
38
- onMouseLeave?: () => void;
39
- onMouseDown?: () => void;
40
- onMouseUp?: () => void;
41
- onDragStart?: (e: DragEventPayload) => void;
42
- onDrag?: (e: DragEventPayload) => void;
43
- onDragEnd?: (e: DragEventPayload) => void;
44
- /** Fires after layout whenever this node's root-space rect changes —
45
- the foundation for popovers, menus, and tooltips. */
46
- onLayout?: (rect: LayoutRect) => void;
47
- }
48
-
49
- export function View(props: CommonProps) {
50
- return createElement("vs-view", props);
51
- }
52
-
53
- export interface TextProps extends Omit<CommonProps, "children"> {
54
- children?: ReactNode; // strings/numbers (and fragments of them)
55
- }
56
-
57
- export function Text(props: TextProps) {
58
- return createElement("vs-text", props);
59
- }
60
-
61
- export interface ImageProps extends CommonProps {
62
- src: string;
63
- }
64
-
65
- export function Image(props: ImageProps) {
66
- return createElement("vs-image", props);
67
- }
68
-
69
- export interface TextInputProps extends Omit<CommonProps, "children" | "onChange" | "onSubmit"> {
70
- value?: string;
71
- defaultValue?: string;
72
- placeholder?: string;
73
- disabled?: boolean;
74
- onChange?: (value: string) => void;
75
- onSubmit?: (value: string) => void;
76
- onFocus?: () => void;
77
- onBlur?: () => void;
78
- }
79
-
80
- export function TextInput({ onChange, onSubmit, ...rest }: TextInputProps) {
81
- return createElement("vs-textinput", {
82
- ...rest,
83
- onChange: onChange ? (payload: any) => onChange(String(payload?.value ?? "")) : undefined,
84
- onSubmit: onSubmit ? (payload: any) => onSubmit(String(payload?.value ?? "")) : undefined,
85
- });
86
- }
87
-
88
- export interface NativeViewProps extends CommonProps {
89
- /** Id of a component factory registered in the C++ NativeRegistry. */
90
- nativeId: string;
91
- }
92
-
93
- export function NativeView(props: NativeViewProps) {
94
- return createElement("vs-native", props);
95
- }
1
+ import { createElement, type ReactNode } from "react";
2
+ import type { Style } from "./tw";
3
+
4
+ export interface DragEventPayload {
5
+ /** Delta from the drag start, in root coordinates. */
6
+ dx: number;
7
+ dy: number;
8
+ /** Current pointer position in root coordinates. */
9
+ x: number;
10
+ y: number;
11
+ }
12
+
13
+ /** A node's laid-out rect in root coordinates (scroll-adjusted). */
14
+ export interface LayoutRect {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ /** Mouse-wheel payload. dy is JUCE's notch fraction (~0.1 per notch,
22
+ positive = wheel up). */
23
+ export interface WheelEventPayload {
24
+ dy: number;
25
+ }
26
+
27
+ export interface CommonProps {
28
+ className?: string;
29
+ style?: Style;
30
+ children?: ReactNode;
31
+ /** Resets the scroll offset of an overflow-y-scroll container. */
32
+ scrollTop?: number;
33
+ onClick?: () => void;
34
+ onDoubleClick?: () => void;
35
+ /** Wheel over this node (controls win the wheel over scroll containers). */
36
+ onWheel?: (e: WheelEventPayload) => void;
37
+ onMouseEnter?: () => void;
38
+ onMouseLeave?: () => void;
39
+ onMouseDown?: () => void;
40
+ onMouseUp?: () => void;
41
+ onDragStart?: (e: DragEventPayload) => void;
42
+ onDrag?: (e: DragEventPayload) => void;
43
+ onDragEnd?: (e: DragEventPayload) => void;
44
+ /** Fires after layout whenever this node's root-space rect changes —
45
+ the foundation for popovers, menus, and tooltips. */
46
+ onLayout?: (rect: LayoutRect) => void;
47
+ }
48
+
49
+ export function View(props: CommonProps) {
50
+ return createElement("vs-view", props);
51
+ }
52
+
53
+ export interface TextProps extends Omit<CommonProps, "children"> {
54
+ children?: ReactNode; // strings/numbers (and fragments of them)
55
+ }
56
+
57
+ export function Text(props: TextProps) {
58
+ return createElement("vs-text", props);
59
+ }
60
+
61
+ export interface ImageProps extends CommonProps {
62
+ src: string;
63
+ }
64
+
65
+ export function Image(props: ImageProps) {
66
+ return createElement("vs-image", props);
67
+ }
68
+
69
+ export interface TextInputProps extends Omit<CommonProps, "children" | "onChange" | "onSubmit"> {
70
+ value?: string;
71
+ defaultValue?: string;
72
+ placeholder?: string;
73
+ disabled?: boolean;
74
+ onChange?: (value: string) => void;
75
+ onSubmit?: (value: string) => void;
76
+ onFocus?: () => void;
77
+ onBlur?: () => void;
78
+ }
79
+
80
+ export function TextInput({ onChange, onSubmit, ...rest }: TextInputProps) {
81
+ return createElement("vs-textinput", {
82
+ ...rest,
83
+ onChange: onChange ? (payload: any) => onChange(String(payload?.value ?? "")) : undefined,
84
+ onSubmit: onSubmit ? (payload: any) => onSubmit(String(payload?.value ?? "")) : undefined,
85
+ });
86
+ }
87
+
88
+ export interface NativeViewProps extends CommonProps {
89
+ /** Id of a component factory registered in the C++ NativeRegistry. */
90
+ nativeId: string;
91
+ }
92
+
93
+ export function NativeView(props: NativeViewProps) {
94
+ return createElement("vs-native", props);
95
+ }
package/src/render.tsx CHANGED
@@ -1,47 +1,47 @@
1
- import Reconciler from "react-reconciler";
2
- import { LegacyRoot } from "react-reconciler/constants";
3
- import type { ReactNode } from "react";
4
- import { hostConfig } from "./hostConfig";
5
- import { flushOps } from "./bridge";
6
- import { OverlayLayer } from "./overlay";
7
-
8
- const reconciler = Reconciler(hostConfig as any);
9
-
10
- let container: ReturnType<typeof reconciler.createContainer> | null = null;
11
-
12
- /** Mounts (or re-renders) the app into the plugin window. The overlay
13
- layer (menus, tooltips) mounts after the app so it paints on top. */
14
- export function render(element: ReactNode): void {
15
- if (!container) {
16
- container = reconciler.createContainer(
17
- {},
18
- LegacyRoot,
19
- null,
20
- false,
21
- null,
22
- "vsreact",
23
- (error: unknown) => console.error("[vsreact] recoverable error:", error),
24
- null,
25
- );
26
- }
27
-
28
- reconciler.updateContainer(
29
- <>
30
- {element}
31
- <OverlayLayer />
32
- </>,
33
- container,
34
- null,
35
- null,
36
- );
37
- flushOps();
38
- }
39
-
40
- /** Unmounts the current app (used by hot reload teardown). */
41
- export function unmount(): void {
42
- if (container) {
43
- reconciler.updateContainer(null, container, null, null);
44
- flushOps();
45
- container = null;
46
- }
47
- }
1
+ import Reconciler from "react-reconciler";
2
+ import { LegacyRoot } from "react-reconciler/constants";
3
+ import type { ReactNode } from "react";
4
+ import { hostConfig } from "./hostConfig";
5
+ import { flushOps } from "./bridge";
6
+ import { OverlayLayer } from "./overlay";
7
+
8
+ const reconciler = Reconciler(hostConfig as any);
9
+
10
+ let container: ReturnType<typeof reconciler.createContainer> | null = null;
11
+
12
+ /** Mounts (or re-renders) the app into the plugin window. The overlay
13
+ layer (menus, tooltips) mounts after the app so it paints on top. */
14
+ export function render(element: ReactNode): void {
15
+ if (!container) {
16
+ container = reconciler.createContainer(
17
+ {},
18
+ LegacyRoot,
19
+ null,
20
+ false,
21
+ null,
22
+ "vsreact",
23
+ (error: unknown) => console.error("[vsreact] recoverable error:", error),
24
+ null,
25
+ );
26
+ }
27
+
28
+ reconciler.updateContainer(
29
+ <>
30
+ {element}
31
+ <OverlayLayer />
32
+ </>,
33
+ container,
34
+ null,
35
+ null,
36
+ );
37
+ flushOps();
38
+ }
39
+
40
+ /** Unmounts the current app (used by hot reload teardown). */
41
+ export function unmount(): void {
42
+ if (container) {
43
+ reconciler.updateContainer(null, container, null, null);
44
+ flushOps();
45
+ container = null;
46
+ }
47
+ }
package/src/runtime.ts CHANGED
@@ -1,92 +1,92 @@
1
- // Environment shims for QuickJS. This module MUST be imported before react —
2
- // it provides the timer/console/microtask facilities react and the scheduler
3
- // expect. The __vsreact_* natives are registered by the C++ host; when absent
4
- // (e.g. under `bun test`) the shims fall back to the platform implementations.
5
-
6
- export type NativeFns = {
7
- __vsreact_flush(json: string): void;
8
- __vsreact_nativeCall(name: string, argsJson: string): string;
9
- __vsreact_log(level: string, msg: string): void;
10
- __vsreact_setTimer(id: number, ms: number): void;
11
- __vsreact_clearTimer(id: number): void;
12
- };
13
-
14
- const g = globalThis as typeof globalThis & Partial<NativeFns> & Record<string, unknown>;
15
-
16
- export const isHosted = typeof g.__vsreact_setTimer === "function";
17
-
18
- interface TimerEntry {
19
- cb: (...args: unknown[]) => void;
20
- args: unknown[];
21
- intervalMs?: number;
22
- }
23
-
24
- let nextTimerId = 1;
25
- const timers = new Map<number, TimerEntry>();
26
-
27
- /** Called by bridge.ts when C++ dispatches {kind:"timer",id}. */
28
- export function fireTimer(id: number): void {
29
- const entry = timers.get(id);
30
- if (!entry) return;
31
-
32
- if (entry.intervalMs !== undefined) g.__vsreact_setTimer!(id, entry.intervalMs);
33
- else timers.delete(id);
34
-
35
- entry.cb(...entry.args);
36
- }
37
-
38
- function stringify(value: unknown): string {
39
- if (typeof value === "string") return value;
40
- if (value instanceof Error) return `${value.message}\n${value.stack ?? ""}`;
41
- try {
42
- return JSON.stringify(value) ?? String(value);
43
- } catch {
44
- return String(value);
45
- }
46
- }
47
-
48
- if (isHosted) {
49
- g.setTimeout = ((cb: TimerEntry["cb"], ms = 0, ...args: unknown[]) => {
50
- const id = nextTimerId++;
51
- timers.set(id, { cb, args });
52
- g.__vsreact_setTimer!(id, Math.max(0, ms | 0));
53
- return id;
54
- }) as typeof setTimeout;
55
-
56
- g.setInterval = ((cb: TimerEntry["cb"], ms = 0, ...args: unknown[]) => {
57
- const id = nextTimerId++;
58
- const intervalMs = Math.max(1, ms | 0);
59
- timers.set(id, { cb, args, intervalMs });
60
- g.__vsreact_setTimer!(id, intervalMs);
61
- return id;
62
- }) as typeof setInterval;
63
-
64
- g.clearTimeout = g.clearInterval = ((id?: number) => {
65
- if (id !== undefined && timers.delete(id)) g.__vsreact_clearTimer!(id);
66
- }) as typeof clearTimeout;
67
-
68
- const makeLog =
69
- (level: string) =>
70
- (...args: unknown[]) =>
71
- g.__vsreact_log!(level, args.map(stringify).join(" "));
72
-
73
- g.console = {
74
- log: makeLog("log"),
75
- info: makeLog("info"),
76
- debug: makeLog("debug"),
77
- warn: makeLog("warn"),
78
- error: makeLog("error"),
79
- } as Console;
80
-
81
- g.queueMicrotask ??= (cb: VoidFunction) => {
82
- Promise.resolve().then(cb);
83
- };
84
-
85
- g.performance ??= { now: () => Date.now() } as Performance;
86
- // Typed loosely on purpose: the dist build has no Node/Bun types, and the
87
- // shim only exists for libraries that probe NODE_ENV.
88
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
- g.process ??= { env: { NODE_ENV: "production" } } as any;
90
- g.self ??= g as unknown as Window & typeof globalThis;
91
- g.global ??= g;
92
- }
1
+ // Environment shims for QuickJS. This module MUST be imported before react —
2
+ // it provides the timer/console/microtask facilities react and the scheduler
3
+ // expect. The __vsreact_* natives are registered by the C++ host; when absent
4
+ // (e.g. under `bun test`) the shims fall back to the platform implementations.
5
+
6
+ export type NativeFns = {
7
+ __vsreact_flush(json: string): void;
8
+ __vsreact_nativeCall(name: string, argsJson: string): string;
9
+ __vsreact_log(level: string, msg: string): void;
10
+ __vsreact_setTimer(id: number, ms: number): void;
11
+ __vsreact_clearTimer(id: number): void;
12
+ };
13
+
14
+ const g = globalThis as typeof globalThis & Partial<NativeFns> & Record<string, unknown>;
15
+
16
+ export const isHosted = typeof g.__vsreact_setTimer === "function";
17
+
18
+ interface TimerEntry {
19
+ cb: (...args: unknown[]) => void;
20
+ args: unknown[];
21
+ intervalMs?: number;
22
+ }
23
+
24
+ let nextTimerId = 1;
25
+ const timers = new Map<number, TimerEntry>();
26
+
27
+ /** Called by bridge.ts when C++ dispatches {kind:"timer",id}. */
28
+ export function fireTimer(id: number): void {
29
+ const entry = timers.get(id);
30
+ if (!entry) return;
31
+
32
+ if (entry.intervalMs !== undefined) g.__vsreact_setTimer!(id, entry.intervalMs);
33
+ else timers.delete(id);
34
+
35
+ entry.cb(...entry.args);
36
+ }
37
+
38
+ function stringify(value: unknown): string {
39
+ if (typeof value === "string") return value;
40
+ if (value instanceof Error) return `${value.message}\n${value.stack ?? ""}`;
41
+ try {
42
+ return JSON.stringify(value) ?? String(value);
43
+ } catch {
44
+ return String(value);
45
+ }
46
+ }
47
+
48
+ if (isHosted) {
49
+ g.setTimeout = ((cb: TimerEntry["cb"], ms = 0, ...args: unknown[]) => {
50
+ const id = nextTimerId++;
51
+ timers.set(id, { cb, args });
52
+ g.__vsreact_setTimer!(id, Math.max(0, ms | 0));
53
+ return id;
54
+ }) as typeof setTimeout;
55
+
56
+ g.setInterval = ((cb: TimerEntry["cb"], ms = 0, ...args: unknown[]) => {
57
+ const id = nextTimerId++;
58
+ const intervalMs = Math.max(1, ms | 0);
59
+ timers.set(id, { cb, args, intervalMs });
60
+ g.__vsreact_setTimer!(id, intervalMs);
61
+ return id;
62
+ }) as typeof setInterval;
63
+
64
+ g.clearTimeout = g.clearInterval = ((id?: number) => {
65
+ if (id !== undefined && timers.delete(id)) g.__vsreact_clearTimer!(id);
66
+ }) as typeof clearTimeout;
67
+
68
+ const makeLog =
69
+ (level: string) =>
70
+ (...args: unknown[]) =>
71
+ g.__vsreact_log!(level, args.map(stringify).join(" "));
72
+
73
+ g.console = {
74
+ log: makeLog("log"),
75
+ info: makeLog("info"),
76
+ debug: makeLog("debug"),
77
+ warn: makeLog("warn"),
78
+ error: makeLog("error"),
79
+ } as Console;
80
+
81
+ g.queueMicrotask ??= (cb: VoidFunction) => {
82
+ Promise.resolve().then(cb);
83
+ };
84
+
85
+ g.performance ??= { now: () => Date.now() } as Performance;
86
+ // Typed loosely on purpose: the dist build has no Node/Bun types, and the
87
+ // shim only exists for libraries that probe NODE_ENV.
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ g.process ??= { env: { NODE_ENV: "production" } } as any;
90
+ g.self ??= g as unknown as Window & typeof globalThis;
91
+ g.global ??= g;
92
+ }