@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/EXPORTS.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @roku-sdk/solid — Export Reference
|
|
2
|
+
|
|
3
|
+
This document is the single source of truth for the **supported** public API surface of `@roku-sdk/solid`.
|
|
4
|
+
The package uses `export *` for backward compatibility, so additional internal symbols are
|
|
5
|
+
technically accessible — but only the symbols listed in this document are part of the supported API.
|
|
6
|
+
Importing unlisted symbols is unsupported and may break without notice.
|
|
7
|
+
|
|
8
|
+
## Inclusion Strategy
|
|
9
|
+
|
|
10
|
+
The package provides two categories of exports:
|
|
11
|
+
|
|
12
|
+
1. **SolidJS re-exports** — All `solid-js` and `solid-js/store` APIs that are functional within the `solid-js/universal` renderer model. Platform-incompatible features (SSR hydration, lazy dynamic imports, concurrent mode scheduling, internal symbols) are excluded.
|
|
13
|
+
2. **SDK-specific exports** — Renderer initialization and runtime hooks consumed by the JSX transform.
|
|
14
|
+
|
|
15
|
+
**Always import from `@roku-sdk/solid`, never directly from `solid-js` or `solid-js/store`.**
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Correct
|
|
19
|
+
import { createSignal, createStore, mergeProps } from "@roku-sdk/solid";
|
|
20
|
+
|
|
21
|
+
// Incorrect — bypasses the SDK abstraction layer
|
|
22
|
+
import { createSignal } from "solid-js";
|
|
23
|
+
import { createStore } from "solid-js/store";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## SolidJS Re-exports
|
|
27
|
+
|
|
28
|
+
### solid-js (core)
|
|
29
|
+
|
|
30
|
+
#### Functions
|
|
31
|
+
|
|
32
|
+
| Export | Category | Description |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `createSignal` | Reactive primitive | Create a reactive signal (getter + setter) |
|
|
35
|
+
| `createEffect` | Reactive primitive | Run a side effect that tracks reactive dependencies |
|
|
36
|
+
| `createMemo` | Reactive primitive | Create a derived/cached reactive computation |
|
|
37
|
+
| `createResource` | Reactive primitive | Create an async resource tied to a signal source |
|
|
38
|
+
| `createComputed` | Reactive primitive | Eager/synchronous variant of `createEffect` |
|
|
39
|
+
| `batch` | Reactive primitive | Batch multiple signal writes into a single update |
|
|
40
|
+
| `untrack` | Reactive primitive | Read signals without tracking dependencies |
|
|
41
|
+
| `startTransition` | Reactive primitive | Defer signal commits; without `enableScheduling` it falls through to a microtask-deferred batch (no web/scheduler APIs — verified on-device) |
|
|
42
|
+
| `useTransition` | Reactive primitive | Returns `[pending, startTransition]`; thin wrapper over `startTransition` (verified on-device) |
|
|
43
|
+
| `createSelector` | Secondary primitive | Optimized selector for list selection patterns |
|
|
44
|
+
| `createReaction` | Secondary primitive | Create a tracked reaction with explicit tracking/running phases |
|
|
45
|
+
| `createRenderEffect` | Secondary primitive | Like `createEffect` but runs during render |
|
|
46
|
+
| `on` | Dependency helper | Explicit dependency declaration wrapper for effects |
|
|
47
|
+
| `from` | Dependency helper | Create a signal from an observable or producer |
|
|
48
|
+
| `observable` | Dependency helper | Convert a signal to an observable (verified on-device) |
|
|
49
|
+
| `mapArray` | Array helper | Low-level mapped array helper (underlies `<For>`) |
|
|
50
|
+
| `indexArray` | Array helper | Low-level indexed array helper (underlies `<Index>`) |
|
|
51
|
+
| `onMount` | Lifecycle | Register a callback to run once after first render |
|
|
52
|
+
| `onCleanup` | Lifecycle | Register a cleanup callback for the current scope |
|
|
53
|
+
| `catchError` | Error handling | Catch errors within a reactive scope |
|
|
54
|
+
| `resetErrorBoundaries` | Error handling | Programmatically reset `<ErrorBoundary>` state |
|
|
55
|
+
| `createContext` | Context | Create a context for dependency injection |
|
|
56
|
+
| `useContext` | Context | Consume a context value |
|
|
57
|
+
| `getOwner` | Owner management | Get the current reactive owner |
|
|
58
|
+
| `runWithOwner` | Owner management | Run code under a specific owner |
|
|
59
|
+
| `DEV` | Dev-mode API | Solid's dev-mode namespace (hooks/registry); `undefined` in production builds |
|
|
60
|
+
| `mergeProps` | Props utility | Merge multiple props objects with reactive tracking |
|
|
61
|
+
| `splitProps` | Props utility | Split a props object into groups for forwarding |
|
|
62
|
+
| `Show` | Component | Conditional rendering |
|
|
63
|
+
| `For` | Component | Keyed list rendering |
|
|
64
|
+
| `Index` | Component | Indexed (non-keyed) list rendering |
|
|
65
|
+
| `Switch` | Component | Multi-branch conditional rendering |
|
|
66
|
+
| `Match` | Component | Branch for `<Switch>` |
|
|
67
|
+
| `ErrorBoundary` | Component | Catch and recover from errors in children |
|
|
68
|
+
| `Suspense` | Component | Render a fallback while descendant `createResource` reads are loading; context-based loading counter with no DOM/SSR dependency (verified on-device) |
|
|
69
|
+
| `children` | Component | Resolve and track children |
|
|
70
|
+
| `createRoot` | Component | Create a new reactive root |
|
|
71
|
+
| `createUniqueId` | Utility | Generate a stable unique ID per reactive scope |
|
|
72
|
+
|
|
73
|
+
#### Types
|
|
74
|
+
|
|
75
|
+
| Export | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `Accessor` | Read-only signal getter type |
|
|
78
|
+
| `AccessorArray` | Tuple of accessors for `on()` |
|
|
79
|
+
| `ChildrenReturn` | Return type of `children()` |
|
|
80
|
+
| `Component` | Standard component type `(props) => JSXElement` |
|
|
81
|
+
| `ComponentProps` | Infer props type from a component or intrinsic element |
|
|
82
|
+
| `Context` | Context object type returned by `createContext` |
|
|
83
|
+
| `ContextProviderComponent` | Provider component type returned inside a `Context` |
|
|
84
|
+
| `EffectFunction` | Callback type for effects |
|
|
85
|
+
| `EffectOptions` | Options for `createEffect` / `createComputed` |
|
|
86
|
+
| `FlowComponent` | Component type for flow control (e.g., `<Show>`) |
|
|
87
|
+
| `FlowProps` | Props type for flow components |
|
|
88
|
+
| `InitializedResource` | Resource type that is guaranteed to have a value |
|
|
89
|
+
| `InitializedResourceOptions` | Options for `createResource` with an initial value |
|
|
90
|
+
| `InitializedResourceReturn` | Return tuple of `createResource` with an initial value |
|
|
91
|
+
| `JSX` | JSX namespace with element/attribute types |
|
|
92
|
+
| `JSXElement` | Union of all valid JSX return types |
|
|
93
|
+
| `MemoOptions` | Options for `createMemo` |
|
|
94
|
+
| `MergeProps` | Inferred return type of `mergeProps()` |
|
|
95
|
+
| `NoInfer` | Utility type to prevent inference |
|
|
96
|
+
| `ObservableObserver` | Observer type for `observable()` |
|
|
97
|
+
| `OnOptions` | Options for `on()` |
|
|
98
|
+
| `Owner` | Reactive owner type |
|
|
99
|
+
| `ParentComponent` | Component type that accepts children |
|
|
100
|
+
| `ParentProps` | Props type that includes `children` |
|
|
101
|
+
| `Ref` | Ref callback or value type for element references |
|
|
102
|
+
| `ResolvedChildren` | Flattened children after resolution |
|
|
103
|
+
| `ResolvedJSXElement` | Single resolved JSX element type |
|
|
104
|
+
| `Resource` | Async resource accessor type |
|
|
105
|
+
| `ResourceActions` | Action methods (`mutate`, `refetch`) for a resource |
|
|
106
|
+
| `ResourceFetcher` | Fetcher function type for `createResource` |
|
|
107
|
+
| `ResourceFetcherInfo` | Info object passed to a resource fetcher |
|
|
108
|
+
| `ResourceOptions` | Options for `createResource` |
|
|
109
|
+
| `ResourceReturn` | Full return tuple type of `createResource` |
|
|
110
|
+
| `ResourceSource` | Source signal type for `createResource` |
|
|
111
|
+
| `Setter` | Signal setter type |
|
|
112
|
+
| `Signal` | Tuple of `[Accessor, Setter]` |
|
|
113
|
+
| `SignalOptions` | Options for `createSignal` |
|
|
114
|
+
| `SplitProps` | Inferred return type of `splitProps()` |
|
|
115
|
+
| `Transition` | Return tuple type of `useTransition()` — `[Accessor<boolean>, (fn: () => void) => Promise<void>]` |
|
|
116
|
+
| `ValidComponent` | Union of all valid component types (intrinsic + functional) |
|
|
117
|
+
| `VoidComponent` | Component type with no props |
|
|
118
|
+
| `VoidProps` | Empty props type |
|
|
119
|
+
|
|
120
|
+
### solid-js/store
|
|
121
|
+
|
|
122
|
+
#### Functions
|
|
123
|
+
|
|
124
|
+
| Export | Description |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `createStore` | Create an immutable reactive store (getter + setter) |
|
|
127
|
+
| `createMutable` | Create a mutable reactive store proxy |
|
|
128
|
+
| `modifyMutable` | Apply batched mutations to a mutable store |
|
|
129
|
+
| `produce` | Immer-style mutation helper for immutable stores |
|
|
130
|
+
| `reconcile` | Diff and patch a store with new data (useful for API responses) |
|
|
131
|
+
| `unwrap` | Retrieve the raw JS object from a store proxy |
|
|
132
|
+
|
|
133
|
+
#### Types
|
|
134
|
+
|
|
135
|
+
| Export | Description |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `ArrayFilterFn` | Filter function type for store paths |
|
|
138
|
+
| `DeepMutable` | Recursively mutable version of a type |
|
|
139
|
+
| `DeepReadonly` | Recursively readonly version of a type |
|
|
140
|
+
| `NotWrappable` | Types that cannot be wrapped in a store proxy |
|
|
141
|
+
| `Part` | Store path segment type |
|
|
142
|
+
| `ReconcileOptions` | Options for `reconcile()` |
|
|
143
|
+
| `SetStoreFunction` | Setter function type returned by `createStore` |
|
|
144
|
+
| `SolidStore` | Internal store representation type |
|
|
145
|
+
| `Store` | Store proxy type |
|
|
146
|
+
| `StoreNode` | Base store node type |
|
|
147
|
+
| `StorePathRange` | Range specification for store paths |
|
|
148
|
+
| `StoreSetter` | Store setter value type |
|
|
149
|
+
|
|
150
|
+
## Excluded Exports
|
|
151
|
+
|
|
152
|
+
These exist in `solid-js` or `solid-js/store` but are **not** re-exported because they are not functional on the Roku platform or are internal implementation details.
|
|
153
|
+
|
|
154
|
+
| Export | Source | Reason |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `onError` | `solid-js` | Deprecated in solid-js — use `catchError` instead |
|
|
157
|
+
| `SuspenseList` | `solid-js` | Coordination of multiple `<Suspense>` boundaries — depends on the concurrent scheduler (`enableScheduling`), which is unavailable on Roku |
|
|
158
|
+
| `lazy` | `solid-js` | Dynamic `import()` — incompatible with Roku's static module system |
|
|
159
|
+
| `enableHydration` | `solid-js` | SSR hydration, not applicable to Roku |
|
|
160
|
+
| `createDeferred` | `solid-js` | Uses Solid scheduler (`requestCallback`) that requires `MessageChannel`; on current Roku Hermes runtime `MessageChannel` is unavailable (`ReferenceError: Property 'MessageChannel' doesn't exist`) |
|
|
161
|
+
| `enableScheduling` | `solid-js` | Platform-specific scheduler wiring |
|
|
162
|
+
| `enableExternalSource` | `solid-js` | Power-user hook for external reactive libraries (RxJS, MobX) — no current demand |
|
|
163
|
+
| `equalFn` | `solid-js` | Internal default signal equality comparator |
|
|
164
|
+
| `getListener` | `solid-js` | Internal reactive graph introspection |
|
|
165
|
+
| `requestCallback` | `solid-js` | Internal scheduler primitive |
|
|
166
|
+
| `cancelCallback` | `solid-js` | Internal scheduler primitive |
|
|
167
|
+
| `$PROXY` | `solid-js` | Internal symbol, not part of the public API |
|
|
168
|
+
| `$TRACK` | `solid-js` | Internal symbol, not part of the public API |
|
|
169
|
+
| `$DEVCOMP` | `solid-js` | Internal symbol, not part of the public API |
|
|
170
|
+
| `sharedConfig` | `solid-js` | Internal runtime state |
|
|
171
|
+
| `createComponent` | `solid-js` | Internal JSX transform helper, not for direct use |
|
|
172
|
+
| `isWrappable` | `solid-js/store` | Internal utility to check if a value can be wrapped in a store proxy |
|
|
173
|
+
| `setProperty` | `solid-js/store` | Internal low-level store property setter |
|
|
174
|
+
| `updatePath` | `solid-js/store` | Internal low-level store path updater |
|
|
175
|
+
| `$RAW` | `solid-js/store` | Internal symbol, not part of the public API |
|
|
176
|
+
|
|
177
|
+
## SDK-Specific Exports
|
|
178
|
+
|
|
179
|
+
These are SDK-owned symbols — not re-exports from `solid-js`. They power the Roku renderer and JSX runtime.
|
|
180
|
+
|
|
181
|
+
### Renderer Initialization
|
|
182
|
+
|
|
183
|
+
| Export | Kind | Description |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `makeSolidRenderer` | Function | Create and wire the Roku SolidJS renderer. Must be called (via `@roku-sdk/rsg-ts` `setUp()`) before any JSX is rendered. |
|
|
186
|
+
| `Renderer` | Type | Generic renderer interface returned by the universal renderer factory. |
|
|
187
|
+
| `SolidRenderer` | Type | Return type of `makeSolidRenderer` — contains the `renderer` instance and `tmFieldChangeHandlerApi`. |
|
|
188
|
+
|
|
189
|
+
### Tag Aliases
|
|
190
|
+
|
|
191
|
+
| Export | Kind | Description |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `registerTagAlias` | Function | `registerTagAlias(tag, resolve)` — create the SceneGraph node type `resolve()` returns whenever `tag` is created. Used by `@roku-sdk/runtime` and `@roku-sdk/compat` to choose between a firmware node and its shim; apps do not normally call it. |
|
|
194
|
+
| `hasTagAlias` | Function | `hasTagAlias(tag)` — whether `tag` already has an alias, so a default can be registered without replacing one installed earlier. |
|
|
195
|
+
| `registerSetPropertyObserver` | Function | `registerSetPropertyObserver(observer)` — invoke `observer(node, name, value)` after every `tmTransaction.setProperty`. Used by `__DEV__` diagnostics in `@roku-sdk/components` (Effect gradient validation); apps do not normally call it. Empty-list fast path when nothing is registered. |
|
|
196
|
+
|
|
197
|
+
### Renderer Runtime Hooks
|
|
198
|
+
|
|
199
|
+
These mutable bindings are wired when `makeSolidRenderer()` runs. They are consumed by the compiled JSX transform and **must not be called directly by application code**. Calling any of them before renderer initialization throws a descriptive error.
|
|
200
|
+
|
|
201
|
+
| Export | Description |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `effect` | Schedule a reactive effect (renderer-level) |
|
|
204
|
+
| `memo` | Create a renderer-level memo |
|
|
205
|
+
| `createComponent` | Instantiate a component from the JSX transform (name-collides with the excluded `solid-js` internal — see below) |
|
|
206
|
+
| `createElement` | Create a Roku DOM node for a given tag |
|
|
207
|
+
| `createTextNode` | Create a text node (throws — text nodes are not supported on Roku) |
|
|
208
|
+
| `insertNode` | Insert a child node into a parent |
|
|
209
|
+
| `insert` | Insert a reactive accessor into a parent |
|
|
210
|
+
| `spread` | Spread props onto a node (wraps the underlying renderer to ensure event handlers register before regular props) |
|
|
211
|
+
| `setProp` | Set a single property on a node |
|
|
212
|
+
| `use` | Invoke a `use:` directive on a node |
|
|
213
|
+
|
|
214
|
+
## Documented Name-Collision Exception
|
|
215
|
+
|
|
216
|
+
`@roku-sdk/solid` exposes a renderer runtime hook named `createComponent` that collides with the excluded `solid-js` internal of the same name. This is **not** a `solid-js` re-export — it is the SDK's own JSX-transform hook, wired at renderer initialization.
|
|
217
|
+
|
|
218
|
+
If called before `makeSolidRenderer()` runs, `createComponent` throws:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
@roku-sdk/solid internal hook "createComponent" was called before renderer initialization.
|
|
222
|
+
Ensure makeSolidRenderer() runs via @roku-sdk/rsg-ts setUp() before rendering JSX.
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Unsupported Internal Symbols
|
|
226
|
+
|
|
227
|
+
The following symbols are accessible via `export *` but are **not** part of the supported public API. They are implementation details that may be removed, renamed, or changed without notice in any release. Do not import them in application code.
|
|
228
|
+
|
|
229
|
+
| Symbol | Source | Reason |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `tmTransaction` | `solid.ts` | Internal transaction object — raw renderer plumbing |
|
|
232
|
+
| `cleanUp` | `solid.ts` | Test-only utility that resets internal state |
|
|
233
|
+
| `forgetAnchor` | `focus.ts` | Called by `@roku-sdk/ts-core` when an anchor node is removed, to drop focus state that points into its tree |
|
|
234
|
+
| `MakeRenderer` | `solid.ts` | Internal factory interface used only by `makeRendererFactory` |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roku, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@roku-sdk/solid`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SolidJS for RSG SDK apps. It re-exports the parts of `solid-js` and `solid-js/store` that work with the SDK's SceneGraph renderer, together with the renderer hooks the JSX transform calls. Apps import every Solid primitive from this package and never from `solid-js` directly, so that the app and the SDK share one reactive runtime.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @roku-sdk/solid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { createSignal, createStore, mergeProps } from "@roku-sdk/solid";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Some Solid features are left out on purpose, such as SSR hydration and lazy dynamic imports. [`EXPORTS.md`](EXPORTS.md) lists what is supported and what is excluded.
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
|
|
21
|
+
- [`EXPORTS.md`](EXPORTS.md) — the supported export list
|
|
22
|
+
- [SolidJS idiosyncrasies on Roku](../../documentation/knowledge-base/solid-idiosyncrasies.md)
|
|
23
|
+
- [Full SDK documentation](../../documentation/README.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roku-sdk/solid",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.19.0",
|
|
4
|
+
"description": "Roku SceneGraph modules",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"development": {
|
|
10
|
+
"import": "./src/index.dev.ts"
|
|
11
|
+
},
|
|
12
|
+
"import": "./src/index.ts",
|
|
13
|
+
"default": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./src/dom-node": "./src/dom-node.ts",
|
|
16
|
+
"./jsx-runtime": "./src/jsx-runtime.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src/",
|
|
20
|
+
"dist/",
|
|
21
|
+
"eslint.config.mjs",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"README.md",
|
|
24
|
+
"EXPORTS.md"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"npm": ">=8.5.0",
|
|
28
|
+
"node": ">=24.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"solid-js": "^1.9.17",
|
|
32
|
+
"@roku-sdk/common": "2.19.0",
|
|
33
|
+
"@roku-sdk/interfaces": "2.19.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"@roku-sdk/dev-tools": "2.19.0"
|
|
38
|
+
},
|
|
39
|
+
"author": "Roku Inc",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://gitlab.com/rokudev/rsg-sdk.git",
|
|
44
|
+
"directory": "packages/solid"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://gitlab.com/rokudev/rsg-sdk/-/tree/main/packages/solid#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://gitlab.com/rokudev/rsg-sdk/-/issues"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"type-check": "tsc --noEmit --project tsconfig.typecheck.json",
|
|
55
|
+
"test": "pnpm run type-check && jest --passWithNoTests",
|
|
56
|
+
"coverage": "jest --coverage --runInBand --passWithNoTests --testTimeout=60000 --forceExit",
|
|
57
|
+
"clean": "rm -rf dist",
|
|
58
|
+
"lint": "eslint --no-error-on-unmatched-pattern \"src/**/*.{ts,tsx,mjs,cjs,js}\""
|
|
59
|
+
}
|
|
6
60
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import {
|
|
4
|
+
type Accessor,
|
|
5
|
+
createEffect as _createEffect,
|
|
6
|
+
createMemo as _createMemo,
|
|
7
|
+
createSignal as _createSignal,
|
|
8
|
+
type EffectFunction,
|
|
9
|
+
type EffectOptions,
|
|
10
|
+
type MemoOptions,
|
|
11
|
+
type NoInfer,
|
|
12
|
+
type Signal,
|
|
13
|
+
type SignalOptions
|
|
14
|
+
} from "solid-js";
|
|
15
|
+
|
|
16
|
+
// `perfetto` was moved under the global `roku` namespace.
|
|
17
|
+
declare const roku: {
|
|
18
|
+
perfetto: {
|
|
19
|
+
beginEvent: (label: string, args?: Record<string, string>) => void;
|
|
20
|
+
endEvent: () => void;
|
|
21
|
+
instantEvent: (label: string, args?: Record<string, string>) => void;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
declare const HermesInternal: {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
26
|
+
getFunctionLocation: (fn: Function) => { lineNumber: number; columnNumber: number };
|
|
27
|
+
getCallStack: () => string;
|
|
28
|
+
};
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const perfScope = function (label: string, value: string | undefined, fn: () => any) {
|
|
31
|
+
try {
|
|
32
|
+
if (value) {
|
|
33
|
+
roku.perfetto?.beginEvent(label, { value: value ?? "" });
|
|
34
|
+
} else {
|
|
35
|
+
roku.perfetto?.beginEvent(label);
|
|
36
|
+
}
|
|
37
|
+
return fn();
|
|
38
|
+
} finally {
|
|
39
|
+
roku.perfetto?.endEvent();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function createSignal<T>(): Signal<T | undefined>;
|
|
44
|
+
export function createSignal<T>(value: T, options?: SignalOptions<T | undefined>): Signal<T>;
|
|
45
|
+
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> {
|
|
46
|
+
const name = (options as { name?: string } | undefined)?.name ?? "unnamed";
|
|
47
|
+
const [get, set] = _createSignal(value as T, options as SignalOptions<T | undefined>);
|
|
48
|
+
const src = HermesInternal.getCallStack().split("\n")[2];
|
|
49
|
+
|
|
50
|
+
const wrappedGet: typeof get = () => {
|
|
51
|
+
return perfScope(`readSignal:${name}`, src, get);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const wrappedSet: typeof set = (...args) => {
|
|
55
|
+
return perfScope(`writeSignal:${name}`, src, () => set(...(args as Parameters<typeof set>)));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return [wrappedGet, wrappedSet] as Signal<T | undefined>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createMemo<Next extends Prev, Prev = Next>(
|
|
62
|
+
fn: EffectFunction<Prev | Next, Next>,
|
|
63
|
+
value?: undefined,
|
|
64
|
+
options?: MemoOptions<Next>
|
|
65
|
+
): Accessor<Next>;
|
|
66
|
+
export function createMemo<Next extends Prev, Init = Next, Prev = Next>(
|
|
67
|
+
fn: EffectFunction<Init | Prev | Next, Next>,
|
|
68
|
+
value: Init,
|
|
69
|
+
options?: MemoOptions<Next>
|
|
70
|
+
): Accessor<Next>;
|
|
71
|
+
|
|
72
|
+
export function createMemo(
|
|
73
|
+
fn: EffectFunction<unknown, unknown>,
|
|
74
|
+
value?: unknown,
|
|
75
|
+
options?: MemoOptions<unknown>
|
|
76
|
+
): Accessor<unknown> {
|
|
77
|
+
const name = options?.name ?? "";
|
|
78
|
+
const info = HermesInternal.getFunctionLocation(fn);
|
|
79
|
+
|
|
80
|
+
let resolvedStr: string | undefined;
|
|
81
|
+
const fnWrapper = (prev: unknown) => {
|
|
82
|
+
if (resolvedStr === undefined) {
|
|
83
|
+
resolvedStr = `${info.lineNumber}:${info.columnNumber}`;
|
|
84
|
+
}
|
|
85
|
+
return perfScope(`memo:${name}`, resolvedStr, () => fn(prev));
|
|
86
|
+
};
|
|
87
|
+
return _createMemo(fnWrapper, value, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createEffect<Next>(fn: EffectFunction<undefined | NoInfer<Next>, Next>): void;
|
|
91
|
+
export function createEffect<Next, Init = Next>(
|
|
92
|
+
fn: EffectFunction<Init | Next, Next>,
|
|
93
|
+
value: Init,
|
|
94
|
+
options?: EffectOptions
|
|
95
|
+
): void;
|
|
96
|
+
|
|
97
|
+
export function createEffect(fn: EffectFunction<unknown, unknown>, value?: unknown, options?: EffectOptions): void {
|
|
98
|
+
const name = (options as { name?: string } | undefined)?.name ?? "";
|
|
99
|
+
const info = HermesInternal.getFunctionLocation(fn);
|
|
100
|
+
let resolvedStr: string | undefined;
|
|
101
|
+
const fnWrapper = (prev: unknown) => {
|
|
102
|
+
if (resolvedStr === undefined) {
|
|
103
|
+
resolvedStr = `${info.lineNumber}:${info.columnNumber}`;
|
|
104
|
+
}
|
|
105
|
+
return perfScope(`effect:${name}`, resolvedStr, () => fn(prev));
|
|
106
|
+
};
|
|
107
|
+
_createEffect(fnWrapper, value, options);
|
|
108
|
+
}
|
package/src/dom-node.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { DomNode, domNodeSymbol, TmNodeRef, TmValue } from "@roku-sdk/interfaces";
|
|
5
|
+
|
|
6
|
+
type SetPropertyHandler = (node: DomNode, name: string, value: TmValue) => void;
|
|
7
|
+
|
|
8
|
+
const defaultProto = { _s: domNodeSymbol };
|
|
9
|
+
const tagProtoMap = new Map<string, object>();
|
|
10
|
+
const imperativeMethodsRegistry = new Map<string, PropertyDescriptorMap>();
|
|
11
|
+
let focusHandler: ((focusNode: DomNode) => void) | undefined;
|
|
12
|
+
let blurHandler: ((blurNode: DomNode) => void) | undefined;
|
|
13
|
+
let setPropertyHandler: SetPropertyHandler | undefined;
|
|
14
|
+
|
|
15
|
+
function setPropertyValue(node: DomNode, name: string, value: TmValue) {
|
|
16
|
+
setPropertyHandler?.(node, name, value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a data PropertyDescriptor for an imperative method that sets a property on the node.
|
|
21
|
+
* The returned descriptor is always a non-writable, non-enumerable, non-configurable data descriptor.
|
|
22
|
+
*
|
|
23
|
+
* @param propName - The property name to set on the underlying SceneGraph node.
|
|
24
|
+
* @param fixedValue - If provided, the method takes no arguments and always sets this value.
|
|
25
|
+
* If omitted, the method takes one argument and sets it as the value.
|
|
26
|
+
* @returns A data PropertyDescriptor whose `value` is the imperative method function.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Zero-arg method: sets control = "play"
|
|
30
|
+
* setPropMethod("control", "play")
|
|
31
|
+
*
|
|
32
|
+
* // One-arg method: sets seek = <user-provided value>
|
|
33
|
+
* setPropMethod("seek")
|
|
34
|
+
*/
|
|
35
|
+
export function setPropMethod(propName: string, fixedValue?: TmValue): PropertyDescriptor {
|
|
36
|
+
if (fixedValue !== undefined) {
|
|
37
|
+
return {
|
|
38
|
+
value: function (this: DomNode) {
|
|
39
|
+
setPropertyValue(this, propName, fixedValue);
|
|
40
|
+
},
|
|
41
|
+
writable: false,
|
|
42
|
+
enumerable: false,
|
|
43
|
+
configurable: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
value: function (this: DomNode, value: TmValue) {
|
|
48
|
+
setPropertyValue(this, propName, value);
|
|
49
|
+
},
|
|
50
|
+
writable: false,
|
|
51
|
+
enumerable: false,
|
|
52
|
+
configurable: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Registers imperative methods for a given tag. Methods are attached to the node prototype
|
|
58
|
+
* when the first node of that tag is created.
|
|
59
|
+
*
|
|
60
|
+
* Must be called before any node of the given tag is created (i.e., at module load time
|
|
61
|
+
* in the component file that defines the tag). Late registration has no effect on
|
|
62
|
+
* already-created prototypes because `getOrCreatePrototype` caches in `tagProtoMap`.
|
|
63
|
+
*/
|
|
64
|
+
export function registerImperativeMethods(tag: string, methods: PropertyDescriptorMap): void {
|
|
65
|
+
if (imperativeMethodsRegistry.has(tag)) {
|
|
66
|
+
console.warn(`[solid] Duplicate imperative methods registration for tag "${tag}"`);
|
|
67
|
+
}
|
|
68
|
+
if (tagProtoMap.has(tag)) {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[solid] Late imperative methods registration for tag "${tag}": nodes already created with this tag will not receive the new methods`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
imperativeMethodsRegistry.set(tag, methods);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addImperativeMethods(tag: string, proto: object): void {
|
|
77
|
+
const methods = imperativeMethodsRegistry.get(tag);
|
|
78
|
+
if (methods) {
|
|
79
|
+
Object.defineProperties(proto, methods);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createDomNode(tag: string, nodeRef: TmNodeRef): DomNode {
|
|
84
|
+
if (!tagProtoMap.has(tag)) {
|
|
85
|
+
const objByTag = Object.create(defaultProto, {
|
|
86
|
+
t: {
|
|
87
|
+
// Not storing interned string ref since this object is never sent to other domain.
|
|
88
|
+
// Hermes does intern all string literals.
|
|
89
|
+
// Do use tagRef if we use js engine that does not support string interning.
|
|
90
|
+
value: tag,
|
|
91
|
+
writable: false,
|
|
92
|
+
enumerable: true,
|
|
93
|
+
configurable: false
|
|
94
|
+
},
|
|
95
|
+
focus: {
|
|
96
|
+
value: function () {
|
|
97
|
+
focusHandler?.(this);
|
|
98
|
+
},
|
|
99
|
+
writable: false,
|
|
100
|
+
enumerable: false,
|
|
101
|
+
configurable: false
|
|
102
|
+
},
|
|
103
|
+
blur: {
|
|
104
|
+
value: function () {
|
|
105
|
+
blurHandler?.(this);
|
|
106
|
+
},
|
|
107
|
+
writable: false,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: false
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
addImperativeMethods(tag, objByTag);
|
|
114
|
+
|
|
115
|
+
tagProtoMap.set(tag, objByTag);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const node: DomNode = Object.create(tagProtoMap.get(tag)!, {
|
|
119
|
+
r: {
|
|
120
|
+
value: nodeRef,
|
|
121
|
+
writable: false,
|
|
122
|
+
enumerable: true,
|
|
123
|
+
configurable: false
|
|
124
|
+
},
|
|
125
|
+
parent: {
|
|
126
|
+
value: undefined,
|
|
127
|
+
writable: true,
|
|
128
|
+
enumerable: false,
|
|
129
|
+
configurable: false
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return node;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isDomNode(node: DomNode): node is DomNode {
|
|
137
|
+
return node?._s === domNodeSymbol;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function setFocusHandler(handler: (focusNode: DomNode) => void): void {
|
|
141
|
+
focusHandler = handler;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function setBlurHandler(handler: (blurNode: DomNode) => void): void {
|
|
145
|
+
blurHandler = handler;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function setSetPropertyHandler(handler: SetPropertyHandler): void {
|
|
149
|
+
setPropertyHandler = handler;
|
|
150
|
+
}
|