@roku-sdk/solid 0.0.0-stage → 2.19.0
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/EXPORTS.md +234 -0
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/eslint.config.mjs +3 -0
- package/package.json +57 -3
- package/src/dev-wrappers.ts +108 -0
- package/src/dom-node.ts +150 -0
- package/src/focus.ts +142 -0
- package/src/index.dev.ts +6 -0
- package/src/index.ts +6 -0
- package/src/jsx-runtime.ts +5 -0
- package/src/solid.ts +899 -0
- package/tsconfig.json +10 -0
package/src/solid.ts
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { logger } from "@roku-sdk/common";
|
|
5
|
+
import { createSDHelper } from "@roku-sdk/common/sd-helper";
|
|
6
|
+
import { stringInterner } from "@roku-sdk/common/string-interner";
|
|
7
|
+
import { registerPreCommitHook, transactionManager } from "@roku-sdk/common/transaction";
|
|
8
|
+
import {
|
|
9
|
+
AnNodeRef,
|
|
10
|
+
DomNode,
|
|
11
|
+
NodeFieldChangeEvent,
|
|
12
|
+
PropValue,
|
|
13
|
+
StructuredDataApi,
|
|
14
|
+
TmArrayRef,
|
|
15
|
+
TmNodeRef,
|
|
16
|
+
TmObjectRef,
|
|
17
|
+
TmStringRef,
|
|
18
|
+
TmValue,
|
|
19
|
+
TreeManipulationApi
|
|
20
|
+
} from "@roku-sdk/interfaces";
|
|
21
|
+
import type { Component, JSX } from "solid-js";
|
|
22
|
+
import { createMemo, splitProps, untrack } from "solid-js";
|
|
23
|
+
import { createRenderer } from "solid-js/universal";
|
|
24
|
+
export {
|
|
25
|
+
// Store — immutable
|
|
26
|
+
createStore,
|
|
27
|
+
|
|
28
|
+
// Store — mutable
|
|
29
|
+
createMutable,
|
|
30
|
+
modifyMutable,
|
|
31
|
+
|
|
32
|
+
// Store — modifiers
|
|
33
|
+
produce,
|
|
34
|
+
reconcile,
|
|
35
|
+
unwrap,
|
|
36
|
+
|
|
37
|
+
// Types
|
|
38
|
+
type ArrayFilterFn,
|
|
39
|
+
type DeepMutable,
|
|
40
|
+
type DeepReadonly,
|
|
41
|
+
type NotWrappable,
|
|
42
|
+
type Part,
|
|
43
|
+
type ReconcileOptions,
|
|
44
|
+
type SetStoreFunction,
|
|
45
|
+
type SolidStore,
|
|
46
|
+
type Store,
|
|
47
|
+
type StoreNode,
|
|
48
|
+
type StorePathRange,
|
|
49
|
+
type StoreSetter
|
|
50
|
+
} from "solid-js/store";
|
|
51
|
+
|
|
52
|
+
// Re-export solid-js APIs that are functional within the solid-js/universal renderer model.
|
|
53
|
+
// Platform-incompatible features (SSR hydration, lazy dynamic imports, concurrent mode,
|
|
54
|
+
// internal symbols) are explicitly excluded — see EXPORTS.md for the full rationale.
|
|
55
|
+
export {
|
|
56
|
+
// Reactive primitives
|
|
57
|
+
createSignal,
|
|
58
|
+
createEffect,
|
|
59
|
+
createMemo,
|
|
60
|
+
createResource,
|
|
61
|
+
createComputed,
|
|
62
|
+
batch,
|
|
63
|
+
untrack,
|
|
64
|
+
|
|
65
|
+
// Secondary primitives
|
|
66
|
+
createSelector,
|
|
67
|
+
createReaction,
|
|
68
|
+
createRenderEffect,
|
|
69
|
+
|
|
70
|
+
// Transitions (deferred reactive commits; microtask-batched without a scheduler)
|
|
71
|
+
startTransition,
|
|
72
|
+
useTransition,
|
|
73
|
+
|
|
74
|
+
// Dependency helpers
|
|
75
|
+
on,
|
|
76
|
+
from,
|
|
77
|
+
observable,
|
|
78
|
+
|
|
79
|
+
// Array helpers (low-level primitives underlying <For> and <Index>)
|
|
80
|
+
mapArray,
|
|
81
|
+
indexArray,
|
|
82
|
+
|
|
83
|
+
// Lifecycle
|
|
84
|
+
onMount,
|
|
85
|
+
onCleanup,
|
|
86
|
+
|
|
87
|
+
// Error handling
|
|
88
|
+
catchError,
|
|
89
|
+
resetErrorBoundaries,
|
|
90
|
+
|
|
91
|
+
// Context
|
|
92
|
+
createContext,
|
|
93
|
+
useContext,
|
|
94
|
+
|
|
95
|
+
// Owner management
|
|
96
|
+
getOwner,
|
|
97
|
+
runWithOwner,
|
|
98
|
+
|
|
99
|
+
// Dev-mode API (undefined in production builds)
|
|
100
|
+
DEV,
|
|
101
|
+
|
|
102
|
+
// Props utilities
|
|
103
|
+
mergeProps,
|
|
104
|
+
splitProps,
|
|
105
|
+
|
|
106
|
+
// Components and JSX
|
|
107
|
+
Show,
|
|
108
|
+
For,
|
|
109
|
+
Index,
|
|
110
|
+
children,
|
|
111
|
+
ErrorBoundary,
|
|
112
|
+
Suspense,
|
|
113
|
+
Switch,
|
|
114
|
+
Match,
|
|
115
|
+
createRoot,
|
|
116
|
+
createUniqueId
|
|
117
|
+
} from "solid-js";
|
|
118
|
+
|
|
119
|
+
export type {
|
|
120
|
+
Accessor,
|
|
121
|
+
AccessorArray,
|
|
122
|
+
ChildrenReturn,
|
|
123
|
+
Component,
|
|
124
|
+
ComponentProps,
|
|
125
|
+
Context,
|
|
126
|
+
ContextProviderComponent,
|
|
127
|
+
EffectFunction,
|
|
128
|
+
EffectOptions,
|
|
129
|
+
FlowComponent,
|
|
130
|
+
FlowProps,
|
|
131
|
+
InitializedResource,
|
|
132
|
+
InitializedResourceOptions,
|
|
133
|
+
InitializedResourceReturn,
|
|
134
|
+
JSX,
|
|
135
|
+
JSXElement,
|
|
136
|
+
MemoOptions,
|
|
137
|
+
MergeProps,
|
|
138
|
+
NoInfer,
|
|
139
|
+
ObservableObserver,
|
|
140
|
+
OnOptions,
|
|
141
|
+
Owner,
|
|
142
|
+
ParentComponent,
|
|
143
|
+
ParentProps,
|
|
144
|
+
Ref,
|
|
145
|
+
ResolvedChildren,
|
|
146
|
+
ResolvedJSXElement,
|
|
147
|
+
Resource,
|
|
148
|
+
ResourceActions,
|
|
149
|
+
ResourceFetcher,
|
|
150
|
+
ResourceFetcherInfo,
|
|
151
|
+
ResourceOptions,
|
|
152
|
+
ResourceReturn,
|
|
153
|
+
ResourceSource,
|
|
154
|
+
Setter,
|
|
155
|
+
Signal,
|
|
156
|
+
SignalOptions,
|
|
157
|
+
SplitProps,
|
|
158
|
+
Transition,
|
|
159
|
+
ValidComponent,
|
|
160
|
+
VoidComponent,
|
|
161
|
+
VoidProps
|
|
162
|
+
} from "solid-js";
|
|
163
|
+
|
|
164
|
+
import { createDomNode, isDomNode, setBlurHandler, setFocusHandler, setSetPropertyHandler } from "./dom-node";
|
|
165
|
+
import { commitFocus, focusNodeRemoved, initFocus, releaseFocus, requestFocus, resetFocus } from "./focus";
|
|
166
|
+
const solidLogger = logger.getLogger("fwk");
|
|
167
|
+
|
|
168
|
+
type TmTransaction = {
|
|
169
|
+
createElement(tag: string): DomNode;
|
|
170
|
+
setProperty(node: DomNode, name: string, value: TmValue, prev?: TmValue): void;
|
|
171
|
+
insertNode(parent: DomNode, node: DomNode, anchor?: DomNode): void;
|
|
172
|
+
removeNode(parent: DomNode, child: DomNode): void;
|
|
173
|
+
preCommit(): void;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Called after `tmTransaction.setProperty` applies a property (including event handlers). */
|
|
177
|
+
export type SetPropertyObserver = (node: DomNode, name: string, value: TmValue) => void;
|
|
178
|
+
|
|
179
|
+
const setPropertyObservers: SetPropertyObserver[] = [];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Register a callback invoked after every successful `tmTransaction.setProperty`.
|
|
183
|
+
* Observers are skipped entirely when none are registered (empty-list fast path).
|
|
184
|
+
*
|
|
185
|
+
* Intended for `__DEV__` diagnostics that need to see intrinsic JSX props (e.g. Effect
|
|
186
|
+
* gradient validation). Production builds that never register pay only an array-length check.
|
|
187
|
+
*
|
|
188
|
+
* Returns an unsubscribe function.
|
|
189
|
+
*/
|
|
190
|
+
export const registerSetPropertyObserver = (observer: SetPropertyObserver): (() => void) => {
|
|
191
|
+
setPropertyObservers.push(observer);
|
|
192
|
+
return () => {
|
|
193
|
+
const index = setPropertyObservers.indexOf(observer);
|
|
194
|
+
if (index !== -1) {
|
|
195
|
+
setPropertyObservers.splice(index, 1);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const notifySetPropertyObservers = (node: DomNode, name: string, value: TmValue): void => {
|
|
201
|
+
if (setPropertyObservers.length === 0) return;
|
|
202
|
+
for (const observer of setPropertyObservers) {
|
|
203
|
+
// Observers are dev-only diagnostics; one that throws must not abort the
|
|
204
|
+
// transaction's setProperty call for every other observer and consumer.
|
|
205
|
+
try {
|
|
206
|
+
observer(node, name, value);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("[solid] setProperty observer threw:", error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export interface Renderer<NodeType> {
|
|
214
|
+
render(code: () => NodeType, node: NodeType): () => void;
|
|
215
|
+
effect<T>(fn: (prev?: T) => T, init?: T): void;
|
|
216
|
+
memo<T>(fn: () => T, equal: boolean): () => T;
|
|
217
|
+
createComponent<T>(Comp: (props: T) => NodeType, props: T): NodeType;
|
|
218
|
+
createElement(tag: string): NodeType;
|
|
219
|
+
createTextNode(value: string): NodeType;
|
|
220
|
+
insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void;
|
|
221
|
+
insert<T>(parent: NodeType, accessor: (() => T) | T, marker?: NodeType | null): NodeType;
|
|
222
|
+
spread<T>(node: NodeType, accessor: (() => T) | T, skipChildren?: boolean): void;
|
|
223
|
+
setProp<T>(node: NodeType, name: string, value: T, prev?: T): T;
|
|
224
|
+
use<A, T>(fn: (element: NodeType, arg: A) => T, element: NodeType, arg: A): T;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type EventCallback = (...args: unknown[]) => void;
|
|
228
|
+
export interface SolidRenderer {
|
|
229
|
+
renderer: Renderer<DomNode>;
|
|
230
|
+
tmFieldChangeHandlerApi: NodeFieldChangeEvent;
|
|
231
|
+
}
|
|
232
|
+
export interface MakeRenderer {
|
|
233
|
+
createRenderer: () => Renderer<DomNode>;
|
|
234
|
+
destroyRenderer: () => void;
|
|
235
|
+
tmFieldChangeHandlerApi: NodeFieldChangeEvent;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
type DynamicFnProps<T extends object> = {
|
|
239
|
+
component: Component<T> | null | undefined;
|
|
240
|
+
} & Omit<T, "component">;
|
|
241
|
+
|
|
242
|
+
type DynamicIntrinsicProps<K extends keyof JSX.IntrinsicElements> = {
|
|
243
|
+
component: K;
|
|
244
|
+
} & JSX.IntrinsicElements[K];
|
|
245
|
+
|
|
246
|
+
export function Dynamic<T extends object>(props: DynamicFnProps<T>): JSX.Element;
|
|
247
|
+
export function Dynamic<K extends keyof JSX.IntrinsicElements>(props: DynamicIntrinsicProps<K>): JSX.Element;
|
|
248
|
+
export function Dynamic(props: { component: unknown }): JSX.Element {
|
|
249
|
+
const [local, others] = splitProps(props, ["component"] as const);
|
|
250
|
+
|
|
251
|
+
return createMemo(() => {
|
|
252
|
+
const component = local.component;
|
|
253
|
+
|
|
254
|
+
if (typeof component === "function") {
|
|
255
|
+
return untrack(() =>
|
|
256
|
+
createComponent(component as unknown as (componentProps: typeof others) => DomNode, others)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (typeof component === "string") {
|
|
261
|
+
const node = createElement(component);
|
|
262
|
+
untrack(() => spread(node, others, false));
|
|
263
|
+
return node;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}) as unknown as JSX.Element;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let tmApi: TreeManipulationApi;
|
|
271
|
+
let createFullArray: (array: unknown[]) => TmArrayRef;
|
|
272
|
+
let createFullObject: (props: object) => TmObjectRef;
|
|
273
|
+
let sdCommit: () => void;
|
|
274
|
+
const observerCallbacks = new WeakMap<TmNodeRef, WeakMap<TmStringRef, EventCallback>>();
|
|
275
|
+
|
|
276
|
+
// Tree calls identify their target by node refs; anchor node ref is ignored.
|
|
277
|
+
const NO_ANCHOR: AnNodeRef = Object.freeze({});
|
|
278
|
+
|
|
279
|
+
// Local to the transaction.
|
|
280
|
+
const nodesToRemove = new Set<DomNode>();
|
|
281
|
+
|
|
282
|
+
// PanelSet orders children by append sequence and ignores the position passed to
|
|
283
|
+
// insertChild, so its children are attached natively in preCommit, in final order.
|
|
284
|
+
// Entries are never evicted early: a node that was removed or re-parented no longer
|
|
285
|
+
// has the pending parent as `parent`, and flushPendingAppends skips it.
|
|
286
|
+
// The tag is compared as a literal because the check runs on every insert.
|
|
287
|
+
const pendingAppendParents = new Set<DomNode>();
|
|
288
|
+
const pendingAppendChildren = new Set<DomNode>();
|
|
289
|
+
// Mirrors "pendingAppendParents is non-empty" without a Map/Set size getter on hot paths.
|
|
290
|
+
let appendsPending = false;
|
|
291
|
+
|
|
292
|
+
/** First child at or after `start` that is already attached natively. */
|
|
293
|
+
function nextNativeSibling(children: DomNode[], start: number): DomNode | undefined {
|
|
294
|
+
for (let i = start; i < children.length; i++) {
|
|
295
|
+
const child = children[i];
|
|
296
|
+
if (child && !pendingAppendChildren.has(child)) {
|
|
297
|
+
return child;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function warnUnorderedAppendOnlyInsert(parent: DomNode, node: DomNode, anchor: DomNode): void {
|
|
304
|
+
solidLogger.warn(
|
|
305
|
+
() =>
|
|
306
|
+
`${parent.t} only appends children: ${node.t} placed before ${anchor.t} may not keep that position in SceneGraph`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function insertIntoAppendOnlyParent(parent: DomNode, node: DomNode, anchor: DomNode | undefined): void {
|
|
311
|
+
const children = (parent.c = parent.c || []);
|
|
312
|
+
const nodeIdx = children.indexOf(node);
|
|
313
|
+
if (nodeIdx !== -1) {
|
|
314
|
+
children.splice(nodeIdx, 1);
|
|
315
|
+
}
|
|
316
|
+
node.parent = parent;
|
|
317
|
+
let anchorIdx = anchor ? children.indexOf(anchor) : -1;
|
|
318
|
+
if (anchorIdx === -1) {
|
|
319
|
+
anchorIdx = children.length;
|
|
320
|
+
}
|
|
321
|
+
children.splice(anchorIdx, 0, node);
|
|
322
|
+
|
|
323
|
+
if (nodeIdx === -1 || (appendsPending && pendingAppendChildren.has(node))) {
|
|
324
|
+
appendsPending = true;
|
|
325
|
+
pendingAppendChildren.add(node);
|
|
326
|
+
pendingAppendParents.add(parent);
|
|
327
|
+
solidLogger.trace(() => `insertNode: ${node.t} deferred to commit in parent: ${parent.t}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Moving an attached child: pending siblings are not in SceneGraph yet, so anchor on the next attached one.
|
|
332
|
+
const next = nextNativeSibling(children, anchorIdx + 1);
|
|
333
|
+
if (next) {
|
|
334
|
+
warnUnorderedAppendOnlyInsert(parent, node, next);
|
|
335
|
+
tmApi.insertNodeBefore(NO_ANCHOR, parent.r, node.r, next.r);
|
|
336
|
+
} else {
|
|
337
|
+
tmApi.appendNode(NO_ANCHOR, parent.r, node.r);
|
|
338
|
+
}
|
|
339
|
+
solidLogger.trace(() => `insertNode: moved ${node.t} in parent: ${parent.t}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function resetPendingAppends(): void {
|
|
343
|
+
pendingAppendParents.clear();
|
|
344
|
+
pendingAppendChildren.clear();
|
|
345
|
+
appendsPending = false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function flushPendingAppends(): void {
|
|
349
|
+
pendingAppendParents.forEach(parent => {
|
|
350
|
+
const children = parent.c ?? [];
|
|
351
|
+
for (let i = 0; i < children.length; i++) {
|
|
352
|
+
const child = children[i];
|
|
353
|
+
if (!child || child.parent !== parent || !pendingAppendChildren.delete(child)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const next = nextNativeSibling(children, i + 1);
|
|
357
|
+
if (next) {
|
|
358
|
+
warnUnorderedAppendOnlyInsert(parent, child, next);
|
|
359
|
+
tmApi.insertNodeBefore(NO_ANCHOR, parent.r, child.r, next.r);
|
|
360
|
+
} else {
|
|
361
|
+
tmApi.appendNode(NO_ANCHOR, parent.r, child.r);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
resetPendingAppends();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getParent(node: DomNode): DomNode | undefined {
|
|
369
|
+
return node.parent;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// TODO: Time this operation
|
|
373
|
+
const removeNodeRecursively = (node: DomNode) => {
|
|
374
|
+
const attachParent = node.parent;
|
|
375
|
+
const stack = [node];
|
|
376
|
+
while (stack.length > 0) {
|
|
377
|
+
const currentNode = stack.pop();
|
|
378
|
+
if (!currentNode) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (currentNode.c) {
|
|
382
|
+
stack.push(...currentNode.c);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
focusNodeRemoved(currentNode, attachParent);
|
|
386
|
+
|
|
387
|
+
if (currentNode.r) {
|
|
388
|
+
observerCallbacks.delete(currentNode.r);
|
|
389
|
+
// NOTE: We can call unregisterFieldHandler here.
|
|
390
|
+
// We are not doing it because removeNode must anyway unregister all the field handlers on native side
|
|
391
|
+
|
|
392
|
+
// clear parent pointer
|
|
393
|
+
currentNode.parent = undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Invoked only from unit tests
|
|
399
|
+
export function cleanUp(): void {
|
|
400
|
+
nodesToRemove.clear();
|
|
401
|
+
resetPendingAppends();
|
|
402
|
+
resetFocus();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Cache for event handler property name classification
|
|
406
|
+
const isEventHandlerCache = new Map<string, boolean>();
|
|
407
|
+
const eventNameTransformCache = new Map<string, string>();
|
|
408
|
+
|
|
409
|
+
function isEventHandler(key: string): boolean {
|
|
410
|
+
let result = isEventHandlerCache.get(key);
|
|
411
|
+
if (result === undefined) {
|
|
412
|
+
// Pattern: on[A-Z] - onStatus, onFocus, etc.
|
|
413
|
+
// Use charAt so noUncheckedIndexedAccess does not widen key[i] to string | undefined.
|
|
414
|
+
result =
|
|
415
|
+
key.length > 2 &&
|
|
416
|
+
key.charAt(0) === "o" &&
|
|
417
|
+
key.charAt(1) === "n" &&
|
|
418
|
+
key.charAt(2) >= "A" &&
|
|
419
|
+
key.charAt(2) <= "Z";
|
|
420
|
+
isEventHandlerCache.set(key, result);
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function transformEventName(eventName: string): string {
|
|
426
|
+
let transformed = eventNameTransformCache.get(eventName);
|
|
427
|
+
if (transformed === undefined) {
|
|
428
|
+
// onFocus -> focus, onLoadStatus -> loadStatus
|
|
429
|
+
transformed = eventName.slice(2).charAt(0).toLowerCase() + eventName.slice(3);
|
|
430
|
+
eventNameTransformCache.set(eventName, transformed);
|
|
431
|
+
}
|
|
432
|
+
return transformed;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const tagAliases = new Map<string, () => string>();
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Route a JSX tag to a different SceneGraph node type, decided when each node is created.
|
|
439
|
+
* `resolve` runs on every creation of `tag`, so it must be cheap; it may throw to refuse
|
|
440
|
+
* creation outright. A later registration for the same tag replaces the earlier one.
|
|
441
|
+
*
|
|
442
|
+
* The created node's `t` is the resolved type, not `tag`: a check such as `node.t === "tween"`
|
|
443
|
+
* misses a node created as its shim, so match every type the alias can resolve to.
|
|
444
|
+
*
|
|
445
|
+
* @param tag - The JSX tag as written in source, e.g. `"tween"`.
|
|
446
|
+
* @param resolve - Returns the SceneGraph node type to create for `tag`.
|
|
447
|
+
*/
|
|
448
|
+
export const registerTagAlias = (tag: string, resolve: () => string): void => {
|
|
449
|
+
tagAliases.set(tag, resolve);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/** Whether {@link registerTagAlias} has been called for `tag`. */
|
|
453
|
+
export const hasTagAlias = (tag: string): boolean => tagAliases.has(tag);
|
|
454
|
+
|
|
455
|
+
export const tmTransaction: TmTransaction = {
|
|
456
|
+
createElement: (tag: string): DomNode => {
|
|
457
|
+
const sgType = tagAliases.size === 0 ? tag : (tagAliases.get(tag)?.() ?? tag);
|
|
458
|
+
const tagRef: TmStringRef | undefined = stringInterner.internString(sgType);
|
|
459
|
+
if (tagRef === undefined) {
|
|
460
|
+
throw new Error(`Tag not interned: ${sgType}`);
|
|
461
|
+
}
|
|
462
|
+
const nodeRef: TmNodeRef = tmApi.createNode(NO_ANCHOR, tagRef);
|
|
463
|
+
const element: DomNode = createDomNode(sgType, nodeRef);
|
|
464
|
+
solidLogger.trace(() => `createElement: ${tag === sgType ? tag : `${tag} as ${sgType}`}`);
|
|
465
|
+
return element;
|
|
466
|
+
},
|
|
467
|
+
setProperty: (node: DomNode, name: string, value: TmValue, prev?: TmValue): void => {
|
|
468
|
+
const nodeRef = node.r;
|
|
469
|
+
let eventProp = false;
|
|
470
|
+
if (isEventHandler(name)) {
|
|
471
|
+
name = transformEventName(name);
|
|
472
|
+
eventProp = true;
|
|
473
|
+
}
|
|
474
|
+
const propRef: TmStringRef | undefined = stringInterner.internString(name);
|
|
475
|
+
if (propRef === undefined) {
|
|
476
|
+
throw new Error(`Property not interned: ${name}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
solidLogger.trace(() => `setProperty: node: ${node.t}, name: ${name}, value: ${value}, prev: ${prev}`);
|
|
480
|
+
if (eventProp) {
|
|
481
|
+
if (typeof prev === "function") {
|
|
482
|
+
tmApi.unregisterFieldHandler(NO_ANCHOR, node.r, propRef);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (typeof value === "function") {
|
|
486
|
+
tmApi.registerFieldChangeHandler(NO_ANCHOR, node.r, propRef);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let callbacks = observerCallbacks.get(nodeRef);
|
|
490
|
+
|
|
491
|
+
if (!callbacks) {
|
|
492
|
+
callbacks = new WeakMap<TmStringRef, EventCallback>();
|
|
493
|
+
observerCallbacks.set(nodeRef, callbacks);
|
|
494
|
+
}
|
|
495
|
+
callbacks.set(propRef, value as () => void);
|
|
496
|
+
notifySetPropertyObservers(node, name, value);
|
|
497
|
+
return;
|
|
498
|
+
} else {
|
|
499
|
+
switch (typeof value) {
|
|
500
|
+
case "boolean":
|
|
501
|
+
if (name === "gainFocus") {
|
|
502
|
+
if (value) {
|
|
503
|
+
requestFocus(node);
|
|
504
|
+
} else {
|
|
505
|
+
releaseFocus(node);
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
tmApi.setNodePropBool(NO_ANCHOR, nodeRef, propRef, value);
|
|
510
|
+
break;
|
|
511
|
+
case "number":
|
|
512
|
+
if (Number.isInteger(value)) {
|
|
513
|
+
tmApi.setNodePropInt(NO_ANCHOR, nodeRef, propRef, value);
|
|
514
|
+
} else {
|
|
515
|
+
tmApi.setNodePropFloat(NO_ANCHOR, nodeRef, propRef, value);
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
case "string":
|
|
519
|
+
tmApi.setNodePropString(NO_ANCHOR, nodeRef, propRef, value);
|
|
520
|
+
break;
|
|
521
|
+
case "object":
|
|
522
|
+
if (value === null) {
|
|
523
|
+
tmApi.setNodePropNull(NO_ANCHOR, nodeRef, propRef);
|
|
524
|
+
} else if (Array.isArray(value)) {
|
|
525
|
+
const arrayRef = createFullArray(value);
|
|
526
|
+
tmApi.setNodePropArray(NO_ANCHOR, nodeRef, propRef, arrayRef);
|
|
527
|
+
} else if (isDomNode(value as DomNode)) {
|
|
528
|
+
tmApi.setNodePropNodeRef(NO_ANCHOR, nodeRef, propRef, (value as DomNode).r);
|
|
529
|
+
} else {
|
|
530
|
+
const objectRef: TmObjectRef = createFullObject(value);
|
|
531
|
+
tmApi.setNodePropObject(NO_ANCHOR, nodeRef, propRef, objectRef);
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (typeof prev === "object" && isDomNode(prev as DomNode)) {
|
|
538
|
+
nodesToRemove.add(prev as DomNode);
|
|
539
|
+
}
|
|
540
|
+
notifySetPropertyObservers(node, name, value);
|
|
541
|
+
},
|
|
542
|
+
insertNode: (parent: DomNode, node: DomNode, anchor?: DomNode): void => {
|
|
543
|
+
// Do nothing if the node is anchor itself
|
|
544
|
+
if (node === anchor) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (parent.t === "panelSet") {
|
|
548
|
+
insertIntoAppendOnlyParent(parent, node, anchor);
|
|
549
|
+
nodesToRemove.delete(node);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Remove the node if its already in the parent
|
|
553
|
+
if (parent.c) {
|
|
554
|
+
const nodeIdx = parent.c.indexOf(node);
|
|
555
|
+
if (nodeIdx !== -1) {
|
|
556
|
+
parent.c.splice(nodeIdx, 1);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Plain assignment — `parent` slot is pre-defined as non-enumerable
|
|
561
|
+
// in createDomNode, so this hits the fast IC path.
|
|
562
|
+
node.parent = parent;
|
|
563
|
+
if (anchor && parent.c) {
|
|
564
|
+
const anchorIdx = parent.c.indexOf(anchor);
|
|
565
|
+
if (anchorIdx !== -1) {
|
|
566
|
+
parent.c.splice(anchorIdx, 0, node);
|
|
567
|
+
}
|
|
568
|
+
tmApi.insertNodeBefore(NO_ANCHOR, parent.r, node.r, anchor.r);
|
|
569
|
+
solidLogger.trace(() => `insertNode: ${node.t} before anchor: ${anchor.t} in parent: ${parent.t}`);
|
|
570
|
+
} else {
|
|
571
|
+
parent.c = parent.c || [];
|
|
572
|
+
parent.c.push(node);
|
|
573
|
+
tmApi.appendNode(NO_ANCHOR, parent.r, node.r);
|
|
574
|
+
solidLogger.trace(() => `appendNode: ${node.t} to parent: ${parent.t}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (nodesToRemove.has(node)) {
|
|
578
|
+
nodesToRemove.delete(node);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
removeNode: (parent: DomNode, child: DomNode): void => {
|
|
582
|
+
if (parent.c) {
|
|
583
|
+
const childIdx = parent.c.indexOf(child);
|
|
584
|
+
if (childIdx !== -1) {
|
|
585
|
+
parent.c.splice(childIdx, 1);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
nodesToRemove.add(child);
|
|
590
|
+
},
|
|
591
|
+
// Note: Precomit is used to remove nodes & set focus.
|
|
592
|
+
// Node removal is done later as solid.js can remove & attach nodes in the same transaction.
|
|
593
|
+
// Focus is set at the end of the transaction as focus changes are lost of setFocus is called in middle of the transaction.
|
|
594
|
+
preCommit: () => {
|
|
595
|
+
nodesToRemove.forEach(node => {
|
|
596
|
+
const parent = getParent(node);
|
|
597
|
+
// NOTE: Parent might be missing if the node is assigned as as prop
|
|
598
|
+
if (parent && !(appendsPending && parent.t === "panelSet" && pendingAppendChildren.has(node))) {
|
|
599
|
+
tmApi.removeNode(NO_ANCHOR, parent.r, node.r);
|
|
600
|
+
solidLogger.trace(() => `removeNode: ${node.t} from parent: ${parent.t}`);
|
|
601
|
+
}
|
|
602
|
+
removeNodeRecursively(node);
|
|
603
|
+
});
|
|
604
|
+
if (appendsPending) {
|
|
605
|
+
flushPendingAppends();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
commitFocus();
|
|
609
|
+
|
|
610
|
+
nodesToRemove.clear();
|
|
611
|
+
sdCommit();
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
registerPreCommitHook(tmTransaction.preCommit);
|
|
616
|
+
setSetPropertyHandler((node, name, value) => {
|
|
617
|
+
transactionManager.startTransaction();
|
|
618
|
+
tmTransaction.setProperty(node, name, value);
|
|
619
|
+
});
|
|
620
|
+
setFocusHandler((focusNode: DomNode) => {
|
|
621
|
+
transactionManager.startTransaction();
|
|
622
|
+
requestFocus(focusNode);
|
|
623
|
+
});
|
|
624
|
+
setBlurHandler((blurNode: DomNode) => {
|
|
625
|
+
transactionManager.startTransaction();
|
|
626
|
+
releaseFocus(blurNode);
|
|
627
|
+
solidLogger.trace(() => `setBlur: ${blurNode.t}`);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
function createSolidRenderer(): Renderer<DomNode> {
|
|
631
|
+
return createRenderer<DomNode>({
|
|
632
|
+
createElement(tag: string): DomNode {
|
|
633
|
+
transactionManager.startTransaction();
|
|
634
|
+
return tmTransaction.createElement(tag);
|
|
635
|
+
},
|
|
636
|
+
createTextNode(value: string): DomNode {
|
|
637
|
+
if (value !== "") {
|
|
638
|
+
solidLogger.warn(
|
|
639
|
+
() => `Text content "${value}" is not rendered on Roku SceneGraph. Use <label> for text display.`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
transactionManager.startTransaction();
|
|
643
|
+
return tmTransaction.createElement("group");
|
|
644
|
+
},
|
|
645
|
+
replaceText(textNode: DomNode, value: string): void {
|
|
646
|
+
solidLogger.trace(() => `replaceText: textNode: ${textNode} value: ${value}`);
|
|
647
|
+
return;
|
|
648
|
+
},
|
|
649
|
+
isTextNode(node: DomNode): boolean {
|
|
650
|
+
solidLogger.trace(() => `isTextNode node: ${JSON.stringify(node)}`);
|
|
651
|
+
return false;
|
|
652
|
+
},
|
|
653
|
+
setProperty<T>(node: DomNode, name: string, value: T, prev?: T): void {
|
|
654
|
+
transactionManager.startTransaction();
|
|
655
|
+
tmTransaction.setProperty(node, name, value as TmValue, prev as TmValue);
|
|
656
|
+
},
|
|
657
|
+
insertNode(parent: DomNode, node: DomNode, anchor?: DomNode): void {
|
|
658
|
+
transactionManager.startTransaction();
|
|
659
|
+
tmTransaction.insertNode(parent, node, anchor);
|
|
660
|
+
},
|
|
661
|
+
removeNode(parent: DomNode, child: DomNode): void {
|
|
662
|
+
transactionManager.startTransaction();
|
|
663
|
+
tmTransaction.removeNode(parent, child);
|
|
664
|
+
},
|
|
665
|
+
getParentNode(node: DomNode): DomNode | undefined {
|
|
666
|
+
return getParent(node);
|
|
667
|
+
},
|
|
668
|
+
getFirstChild(node: DomNode): DomNode | undefined {
|
|
669
|
+
return node?.c?.[0] ?? undefined;
|
|
670
|
+
},
|
|
671
|
+
getNextSibling(node: DomNode): DomNode | undefined {
|
|
672
|
+
const parent = getParent(node);
|
|
673
|
+
if (parent && parent.c) {
|
|
674
|
+
return parent.c[parent.c.indexOf(node) + 1] ?? undefined;
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function destroySolidRenderer(): void {
|
|
682
|
+
// no-op
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function makeRendererFactory(
|
|
686
|
+
type: string,
|
|
687
|
+
api: TreeManipulationApi,
|
|
688
|
+
incomingSDApi: StructuredDataApi,
|
|
689
|
+
outgoingSDApi: StructuredDataApi
|
|
690
|
+
): MakeRenderer {
|
|
691
|
+
tmApi = api;
|
|
692
|
+
initFocus(api, solidLogger, NO_ANCHOR);
|
|
693
|
+
({ createFullArray, createFullObject, commit: sdCommit } = createSDHelper(outgoingSDApi));
|
|
694
|
+
|
|
695
|
+
const fieldChangeHandler = (() => {
|
|
696
|
+
const invokeCallback = (_anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef, value: PropValue) => {
|
|
697
|
+
const callbacks = observerCallbacks.get(nodeRef);
|
|
698
|
+
if (callbacks) {
|
|
699
|
+
const callback = callbacks.get(fieldRef);
|
|
700
|
+
if (callback && typeof callback === "function") {
|
|
701
|
+
callback(value);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
onNodeNullFieldChange: (anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef) => {
|
|
708
|
+
invokeCallback(anRef, nodeRef, fieldRef, null);
|
|
709
|
+
},
|
|
710
|
+
onNodeBoolFieldChange: (anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef, value: boolean) => {
|
|
711
|
+
invokeCallback(anRef, nodeRef, fieldRef, value);
|
|
712
|
+
},
|
|
713
|
+
onNodeIntFieldChange: (anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef, value: number) => {
|
|
714
|
+
invokeCallback(anRef, nodeRef, fieldRef, value);
|
|
715
|
+
},
|
|
716
|
+
onNodeFloatFieldChange: (anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef, value: number) => {
|
|
717
|
+
invokeCallback(anRef, nodeRef, fieldRef, value);
|
|
718
|
+
},
|
|
719
|
+
onNodeStringFieldChange: (anRef: AnNodeRef, nodeRef: TmNodeRef, fieldRef: TmStringRef, value: string) => {
|
|
720
|
+
invokeCallback(anRef, nodeRef, fieldRef, value);
|
|
721
|
+
},
|
|
722
|
+
onNodeObjectFieldChange: (
|
|
723
|
+
anRef: AnNodeRef,
|
|
724
|
+
nodeRef: TmNodeRef,
|
|
725
|
+
fieldRef: TmStringRef,
|
|
726
|
+
valueRef: TmObjectRef
|
|
727
|
+
) => {
|
|
728
|
+
const data = incomingSDApi.getObject(valueRef);
|
|
729
|
+
if (!data) {
|
|
730
|
+
solidLogger.error(() => `Failed to get object from ref`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
invokeCallback(anRef, nodeRef, fieldRef, data as PropValue);
|
|
734
|
+
},
|
|
735
|
+
onNodeArrayFieldChange: (
|
|
736
|
+
anRef: AnNodeRef,
|
|
737
|
+
nodeRef: TmNodeRef,
|
|
738
|
+
fieldRef: TmStringRef,
|
|
739
|
+
valueRef: TmArrayRef
|
|
740
|
+
) => {
|
|
741
|
+
const data = incomingSDApi.getArray(valueRef);
|
|
742
|
+
if (!data) {
|
|
743
|
+
solidLogger.error(() => `Failed to get array from ref`);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
invokeCallback(anRef, nodeRef, fieldRef, data as PropValue);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
})();
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
createRenderer: createSolidRenderer,
|
|
753
|
+
destroyRenderer: destroySolidRenderer,
|
|
754
|
+
tmFieldChangeHandlerApi: fieldChangeHandler
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function throwUninitializedRendererHook(hookName: string): never {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`@roku-sdk/solid internal hook "${hookName}" was called before renderer initialization. ` +
|
|
761
|
+
`Ensure makeSolidRenderer() runs via @roku-sdk/rsg-ts setUp() before rendering JSX.`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
766
|
+
function uninitializedHook(name: string): (...args: any[]) => never {
|
|
767
|
+
return () => throwUninitializedRendererHook(name);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export let effect: <T>(fn: (prev?: T) => T, init?: T) => void,
|
|
771
|
+
memo: <T>(fn: () => T, equal: boolean) => () => T,
|
|
772
|
+
createComponent: <T>(Comp: (props: T) => DomNode, props: T) => DomNode,
|
|
773
|
+
createElement: (tag: string) => DomNode,
|
|
774
|
+
createTextNode: (value: string) => DomNode,
|
|
775
|
+
insertNode: (parent: DomNode, node: DomNode, anchor?: DomNode) => void,
|
|
776
|
+
insert: <T>(parent: DomNode, accessor: (() => T) | T, marker?: DomNode | null) => DomNode,
|
|
777
|
+
spread: <T>(node: DomNode, accessor: (() => T) | T, skipChildren?: boolean) => void,
|
|
778
|
+
setProp: <T>(node: DomNode, name: string, value: T, prev?: T) => T,
|
|
779
|
+
use: <A, T>(fn: (element: DomNode, arg: A) => T, element: DomNode, arg: A) => T;
|
|
780
|
+
|
|
781
|
+
// Guard all hooks against accidental use before makeSolidRenderer() wires them.
|
|
782
|
+
effect = uninitializedHook("effect") as typeof effect;
|
|
783
|
+
memo = uninitializedHook("memo") as typeof memo;
|
|
784
|
+
createComponent = uninitializedHook("createComponent") as typeof createComponent;
|
|
785
|
+
createElement = uninitializedHook("createElement") as typeof createElement;
|
|
786
|
+
createTextNode = uninitializedHook("createTextNode") as typeof createTextNode;
|
|
787
|
+
insertNode = uninitializedHook("insertNode") as typeof insertNode;
|
|
788
|
+
insert = uninitializedHook("insert") as typeof insert;
|
|
789
|
+
spread = uninitializedHook("spread") as typeof spread;
|
|
790
|
+
setProp = uninitializedHook("setProp") as typeof setProp;
|
|
791
|
+
use = uninitializedHook("use") as typeof use;
|
|
792
|
+
|
|
793
|
+
export function makeSolidRenderer(
|
|
794
|
+
api: TreeManipulationApi,
|
|
795
|
+
incomingSDApi: StructuredDataApi,
|
|
796
|
+
outgoingSDApi: StructuredDataApi
|
|
797
|
+
): SolidRenderer {
|
|
798
|
+
// No reason to destroy the renderer. Dispose function need to be called to clean up the resources
|
|
799
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
800
|
+
const { createRenderer, destroyRenderer, tmFieldChangeHandlerApi } = makeRendererFactory(
|
|
801
|
+
"solid",
|
|
802
|
+
api,
|
|
803
|
+
incomingSDApi,
|
|
804
|
+
outgoingSDApi
|
|
805
|
+
);
|
|
806
|
+
const renderer = createRenderer();
|
|
807
|
+
({ effect, memo, createComponent, createElement, createTextNode, insertNode, insert, spread, setProp, use } =
|
|
808
|
+
renderer);
|
|
809
|
+
|
|
810
|
+
// Wrap spread to ensure event handlers are registered before regular props
|
|
811
|
+
const originalSpread = spread;
|
|
812
|
+
spread = <T>(node: DomNode, accessor: (() => T) | T, skipChildren?: boolean) => {
|
|
813
|
+
// Helper to create reordered proxy from props object
|
|
814
|
+
const createReorderedProxy = (propsObj: Record<string, unknown>) => {
|
|
815
|
+
const keys = Object.keys(propsObj);
|
|
816
|
+
|
|
817
|
+
// If empty object or only one property, no need to reorder
|
|
818
|
+
if (keys.length <= 1) {
|
|
819
|
+
return propsObj;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const eventHandlers: string[] = [];
|
|
823
|
+
const regularProps: string[] = [];
|
|
824
|
+
let needsReorder = false;
|
|
825
|
+
let seenRegularProp = false;
|
|
826
|
+
|
|
827
|
+
// Partition using cached results and check if already ordered
|
|
828
|
+
for (let i = 0; i < keys.length; i++) {
|
|
829
|
+
const key = keys[i];
|
|
830
|
+
if (key === undefined) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (isEventHandler(key)) {
|
|
834
|
+
// If we've already seen a regular prop, order is wrong
|
|
835
|
+
if (seenRegularProp) {
|
|
836
|
+
needsReorder = true;
|
|
837
|
+
}
|
|
838
|
+
eventHandlers.push(key);
|
|
839
|
+
} else {
|
|
840
|
+
seenRegularProp = true;
|
|
841
|
+
regularProps.push(key);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// If all same type, no need to reorder
|
|
846
|
+
if (eventHandlers.length === 0 || regularProps.length === 0) {
|
|
847
|
+
return propsObj;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// If already in correct order, return original object
|
|
851
|
+
if (!needsReorder) {
|
|
852
|
+
return propsObj;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Create ordered keys array: event handlers first, then regular props
|
|
856
|
+
const orderedKeys = [...eventHandlers, ...regularProps];
|
|
857
|
+
|
|
858
|
+
// Create a Proxy that preserves reactive getters but controls key iteration order
|
|
859
|
+
return new Proxy(propsObj, {
|
|
860
|
+
ownKeys: () => orderedKeys,
|
|
861
|
+
get: (target, prop) => target[prop as keyof typeof target],
|
|
862
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
863
|
+
// Return original descriptor to preserve references and avoid breaking equality checks
|
|
864
|
+
if (orderedKeys.includes(prop as string)) {
|
|
865
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
866
|
+
}
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// Handle function accessors - defer evaluation to preserve reactive tracking
|
|
873
|
+
if (typeof accessor === "function") {
|
|
874
|
+
const wrappedAccessor = () => {
|
|
875
|
+
const props = (accessor as () => T)();
|
|
876
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
877
|
+
return createReorderedProxy(props as Record<string, unknown>);
|
|
878
|
+
}
|
|
879
|
+
return props;
|
|
880
|
+
};
|
|
881
|
+
originalSpread(node, wrappedAccessor, skipChildren);
|
|
882
|
+
} else {
|
|
883
|
+
// Handle static objects
|
|
884
|
+
if (accessor && typeof accessor === "object" && !Array.isArray(accessor)) {
|
|
885
|
+
const reordered = createReorderedProxy(accessor as Record<string, unknown>);
|
|
886
|
+
originalSpread(node, reordered, skipChildren);
|
|
887
|
+
} else {
|
|
888
|
+
originalSpread(node, accessor, skipChildren);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// Update the renderer object with the wrapped spread
|
|
894
|
+
renderer.spread = spread;
|
|
895
|
+
return {
|
|
896
|
+
renderer,
|
|
897
|
+
tmFieldChangeHandlerApi
|
|
898
|
+
};
|
|
899
|
+
}
|