@roku-sdk/ai 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,157 @@
1
+ # Slideshow
2
+
3
+ ## Overview
4
+
5
+ `@roku-sdk/slideshow` shows **one thing at a time** and animates between them — a hero banner, a
6
+ full-screen image carousel, an onboarding or promo pager. You give it a slide count, a slide size
7
+ and a `renderSlide` callback; it owns the tween, which slide is current, and the arrows and dots
8
+ drawn around the strip. Three factories render the same handle in three different ways, and they
9
+ differ most in how focus works, not in what they draw.
10
+
11
+ ## Read the reference first for these tasks
12
+
13
+ | If the request is | Read now, before answering |
14
+ |---|---|
15
+ | Making a slide focusable, or a slide with buttons inside it | `references/focus-and-handoff.md` |
16
+ | Moving focus out of the slideshow at the first or last slide, or into it from a sibling region | `references/focus-and-handoff.md` |
17
+
18
+ The tables below name the callback and stop there. The three modes build focus in three different
19
+ ways, and the wiring each one needs — including which of them moves focus for you and which only
20
+ tells you it is time — is only in the reference. Answer a focus question from the reference and
21
+ quote it; an answer assembled from a table row here will describe the wrong mode.
22
+
23
+ ## When to Use
24
+
25
+ - one item on screen at a time, with a transition between items
26
+ - a hero banner or promo pager the viewer steps through with left and right
27
+ - a full-screen image carousel
28
+ - an onboarding flow of panels
29
+ - any of the above where the panels themselves contain buttons
30
+
31
+ ### When not to use
32
+
33
+ | Situation | Use instead | Because |
34
+ |---|---|---|
35
+ | A row, shelf, poster grid, or any screen showing many items at once | The `flex-grid` skill | It owns rows, columns and cell recycling. This package has no notion of a row — it advances one whole slide at a time |
36
+ | Which node draws the picture, the text or the button inside a slide | The `sdk-components` skill | A slide's contents are ordinary components. This skill owns the strip around them, not what you put in one |
37
+ | Moving between whole screens rather than between panels of one screen | The `screen-navigation` skill | A slideshow is one screen's content; pushing and popping screens is a different mechanism |
38
+ | A row of buttons or a menu you laid out yourself | `FocusBoundary` + `useFocusable` from `@roku-sdk/navigation` | Nothing transitions, so the tween and slide bookkeeping cost more than the screen needs |
39
+
40
+ ## Quick Reference
41
+
42
+ ### Choosing a factory
43
+
44
+ | Your situation | Use | Import from |
45
+ |---|---|---|
46
+ | A handful of slides, and/or slides containing focusable buttons | `createSlideshow` | `@roku-sdk/slideshow` |
47
+ | Many slides, each cheap to build, and you do not want them all mounted | `createVirtualSlideshow` | `@roku-sdk/slideshow/virtual` |
48
+ | Many slides of a few repeated shapes, where building SceneGraph nodes is the cost | `createPooledSlideshow` | `@roku-sdk/slideshow/pooled` |
49
+ | You would rather write it as JSX than call a factory | `defineSlideshow<T>()` or `Slideshow` | `@roku-sdk/slideshow` |
50
+
51
+ `defineSlideshow` wraps **eager mode only**. There is no `defineVirtualSlideshow` and no
52
+ `definePooledSlideshow` — for those two, call the factory.
53
+
54
+ Eager is the only mode whose individual slides can take focus in the ordinary way. Pooled is the
55
+ opposite extreme: its recycled template slides cannot be focused at all.
56
+
57
+ ### Config
58
+
59
+ | Field | Notes |
60
+ |---|---|
61
+ | `slideCount`, `slideWidth`, `slideHeight` | The three required fields |
62
+ | `items` | `(T \| undefined)[]`; `undefined` is a slide whose data has not arrived |
63
+ | `margin` | Gap **between** slides, default `0`. It is not padding around the strip |
64
+ | `translation` | `[x, y]` of the whole slideshow on screen |
65
+ | `renderSlide` | Optional; receives `index`, `item`, `focused`, `width`, `height`, `slideFocusId`, `goTo` |
66
+ | `initialIndex` | Default `0` |
67
+ | `animation` | `{ durationSec }` default `0.333`, `{ easeFunction }` default `"outCubic"` |
68
+ | `onSlideChange`, `onSelect` | `(index, item)` |
69
+ | `onEdgeReached` | `(edge)` where `edge` is `"first"` or `"last"` → `references/focus-and-handoff.md` |
70
+ | `focusId` | Identity the enclosing boundary's `initialFocus` or `setFocus` names |
71
+ | `chevrons`, `dots` | A component, or `false` to disable |
72
+ | `windowSize` | Virtual only; slides kept alive each side of current, default `1` |
73
+ | `templates`, `getTemplateId`, `poolSize`, `uniqueSlides` | Pooled only; `poolSize` defaults to `3` per template |
74
+
75
+ `onKeyEvent` also exists, but treat it as legacy: prefer letting keys reach the slideshow through
76
+ the focus chain, and use `onEdgeReached` for hand-off.
77
+
78
+ ### The handle
79
+
80
+ All three factories return the same shape: `element`, `currentIndex`, `currentItem`,
81
+ `goTo(index, animated?)`, `next()`, `prev()`, `getCurrentIndex()`,
82
+ `updateSlides(startIndex, items)`, `setSlideCount(n)`.
83
+
84
+ Render it as `{element()}` — always call it.
85
+
86
+ ### Chrome
87
+
88
+ | Component | Import from | Props |
89
+ |---|---|---|
90
+ | `DefaultChevrons` | `@roku-sdk/slideshow/chrome/chevrons` | `showLeft`, `showRight`, `onLeft?`, `onRight?`, `width`, `height` |
91
+ | `DefaultDotIndicator` | `@roku-sdk/slideshow/chrome/dots` | `slideCount`, `currentIndex`, `width`, `height` |
92
+
93
+ Pass the component itself — `chevrons: DefaultChevrons` — not an element. The factory binds the
94
+ props. Both are also re-exported from the package root for convenience.
95
+
96
+ ## Example
97
+
98
+ A five-slide hero banner with chrome, taking focus from the root boundary:
99
+
100
+ ```tsx rsg-skill=complete
101
+ import { FocusBoundary, useFocusable } from "@roku-sdk/navigation";
102
+ import { createSlideshow, DefaultChevrons, DefaultDotIndicator, FocusableSlide } from "@roku-sdk/slideshow";
103
+ import type { Component } from "@roku-sdk/solid";
104
+
105
+ const COLORS = ["0x1A1A2EFF", "0x0F3460FF", "0x533483FF", "0x2D6A4FFF", "0xE94560FF"];
106
+
107
+ const HeroBanner: Component = () => {
108
+ const slideshowId = {};
109
+
110
+ const { element } = createSlideshow<undefined>({
111
+ slideCount: 5,
112
+ slideWidth: 1720,
113
+ slideHeight: 940,
114
+ translation: [100, 40],
115
+ margin: 200,
116
+ focusId: slideshowId,
117
+ chevrons: DefaultChevrons,
118
+ dots: DefaultDotIndicator,
119
+ onEdgeReached: (edge: "first" | "last") => console.log(`at the ${edge} slide`),
120
+ renderSlide: ({ index, focused, width, height, slideFocusId, goTo }) => (
121
+ <FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
122
+ <rectangle width={width} height={height} color={COLORS[index]} />
123
+ <label text={`Slide ${index + 1}`} translation={[60, 60]} color="0xFFFFFFFF" />
124
+ <label text={focused() ? "[ FOCUSED ]" : ""} translation={[60, 130]} color="0x88FF88FF" />
125
+ </FocusableSlide>
126
+ ),
127
+ });
128
+
129
+ return (
130
+ <FocusBoundary focusId={{}} initialFocus={slideshowId}>
131
+ {element()}
132
+ </FocusBoundary>
133
+ );
134
+ };
135
+
136
+ // `useFocusable` is what FocusableSlide uses internally for a non-interactive slide.
137
+ void useFocusable;
138
+ ```
139
+
140
+ ## Common Mistakes
141
+
142
+ | Symptom | Cause | Fix |
143
+ |---|---|---|
144
+ | Slides render, but left and right do nothing and no slide is ever focused | `renderSlide` returns bare nodes, so nothing registers with the boundary | Wrap a non-interactive slide in `FocusableSlide` and hand it `slideFocusId` → `references/focus-and-handoff.md` |
145
+ | Nothing renders at all, or the strip appears only after a key press | `element` was rendered without calling it | It is a thunk: write `{element()}` |
146
+ | At the last slide, `onEdgeReached` fires but focus stays put | In eager and virtual modes the callback only *notifies* — it does not move focus | Call `setFocus` yourself in the callback → `references/focus-and-handoff.md` |
147
+ | A chevron is missing or half off the screen | Chevrons are drawn outside the slide box, roughly 80px beyond each edge | Give the slideshow enough `translation` room on both sides; `margin` will not help, it is the gap between slides |
148
+ | Chevrons or dots never appear | An element was passed instead of a component | Pass `chevrons: DefaultChevrons`, not `chevrons: <DefaultChevrons … />` |
149
+ | A pooled slide's buttons cannot be reached with the remote | Pooled templates render outside the focus context and can never be focused | List that index in `uniqueSlides`, which routes it through `renderSlide` instead of the pool |
150
+ | A virtual slideshow scrolls but focus lags a slide behind | `goTo` was wired into `onFocusGained` as eager mode requires | Virtual mode drives navigation itself — leave `goTo` for programmatic jumps such as an auto-advance timer |
151
+ | `slideRef` is not defined on the render props | It was renamed | It is `slideFocusId` |
152
+
153
+ ## Source of truth
154
+
155
+ Prefer the typed source over any prose, including this file: read
156
+ `node_modules/@roku-sdk/slideshow/src/types.ts` for the config, the render props and the handle,
157
+ and the factory's own destructure for a default this file does not list.
@@ -0,0 +1,187 @@
1
+ # Slideshow focus and hand-off
2
+
3
+ Everything focus-related imports from `@roku-sdk/navigation`, never from `@roku-sdk/components`.
4
+
5
+ ## The three modes build focus three different ways
6
+
7
+ This is the fact the body cannot compress, and it decides every answer below.
8
+
9
+ | Mode | What it renders | How left/right move | At the first or last slide |
10
+ |---|---|---|---|
11
+ | `createSlideshow` (eager) | A `FocusBoundary` with `navigation="horizontal"` | The boundary walks between slide positions | Fires `onEdgeReached`, and **keeps** the key |
12
+ | `createVirtualSlideshow` | A `FocusBoundary` with no `navigation` | The factory calls `prev()` / `next()` and moves focus itself | Fires `onEdgeReached`, and **keeps** the key |
13
+ | `createPooledSlideshow` | **No boundary** — a single leaf `useFocusable` | Its `onKey` handlers advance the index | Declines the key, so focus leaves on its own |
14
+
15
+ Two consequences worth stating outright:
16
+
17
+ - **In eager and virtual modes, `onEdgeReached` does not move focus.** It tells you the viewer
18
+ pushed past the end. Moving focus is your job, with `setFocus` from `useFocusActions()`.
19
+ - **In pooled mode you often need no hand-off code at all.** Its edge handler returns `bubble`, so
20
+ the key bubbles and the enclosing boundary's `navigationMap` decides where focus goes.
21
+
22
+ ## `slideFocusId`
23
+
24
+ `renderSlide` receives `slideFocusId: object` — an opaque identity created once per slide
25
+ **position** at factory init, not a node ref and not derived from the item. It stays with the
26
+ position when the data changes, and `setSlideCount` grows the list.
27
+
28
+ Also on the render props: `goTo()`, already bound to that slide's index, so you call it with no
29
+ arguments.
30
+
31
+ ## A non-interactive slide
32
+
33
+ A slide that is only a picture and a caption still has to register, or nothing in the slideshow
34
+ is focusable and no key does anything. `FocusableSlide` is the helper for exactly this:
35
+
36
+ ```tsx rsg-skill=complete
37
+ import { createSlideshow, FocusableSlide } from "@roku-sdk/slideshow";
38
+ import type { Component } from "@roku-sdk/solid";
39
+
40
+ const Banner: Component = () => {
41
+ const { element } = createSlideshow<undefined>({
42
+ slideCount: 3,
43
+ slideWidth: 1720,
44
+ slideHeight: 940,
45
+ focusId: {},
46
+ renderSlide: ({ index, width, height, slideFocusId, goTo }) => (
47
+ <FocusableSlide slideFocusId={slideFocusId} onFocused={goTo}>
48
+ <rectangle width={width} height={height} color="0x1A1A2EFF" />
49
+ <label text={`Panel ${index + 1}`} translation={[60, 60]} color="0xFFFFFFFF" />
50
+ </FocusableSlide>
51
+ ),
52
+ });
53
+ return <>{element()}</>;
54
+ };
55
+ ```
56
+
57
+ `onFocused={goTo}` is what keeps the strip in sync when focus lands on a slide. In **eager** mode
58
+ wire it. In **virtual** mode you may leave it off — that factory moves focus itself — and `goTo`
59
+ is then only for programmatic jumps such as an auto-advance timer.
60
+
61
+ ## A slide with buttons in it
62
+
63
+ An interactive slide is a nested boundary, and `slideFocusId` is its `focusId`. Wire each
64
+ button's `onFocusGained` to `goTo` so the strip follows focus into the slide:
65
+
66
+ ```tsx rsg-skill=complete
67
+ import { FocusBoundary, useFocusable } from "@roku-sdk/navigation";
68
+ import { createSlideshow } from "@roku-sdk/slideshow";
69
+ import type { Component } from "@roku-sdk/solid";
70
+
71
+ const Button: Component<{ label: string; x: number; focusId: object; onFocused: () => void }> = props => {
72
+ const { isFocused, ref } = useFocusable({
73
+ focusId: props.focusId,
74
+ onFocusGained: () => props.onFocused(),
75
+ onKey: { OK: (press: boolean) => { if (press) console.log(props.label); } },
76
+ });
77
+ return (
78
+ <group ref={ref} translation={[props.x, 0]}>
79
+ <rectangle width={200} height={70} color={isFocused() ? "0x0066FFFF" : "0x333333FF"} />
80
+ <label text={props.label} width={200} height={70} horizAlign="center" vertAlign="center" />
81
+ </group>
82
+ );
83
+ };
84
+
85
+ const Pager: Component = () => {
86
+ const { element } = createSlideshow<undefined>({
87
+ slideCount: 3,
88
+ slideWidth: 1720,
89
+ slideHeight: 940,
90
+ focusId: {},
91
+ renderSlide: ({ width, height, slideFocusId, goTo }) => {
92
+ const btnIds: object[] = [{}, {}];
93
+ return (
94
+ <group>
95
+ <rectangle width={width} height={height} color="0x0F3460FF" />
96
+ <FocusBoundary focusId={slideFocusId} initialFocus={btnIds[0]} navigation="horizontal">
97
+ <Button label="Play" x={0} focusId={btnIds[0]} onFocused={goTo} />
98
+ <Button label="More" x={220} focusId={btnIds[1]} onFocused={goTo} />
99
+ </FocusBoundary>
100
+ </group>
101
+ );
102
+ },
103
+ });
104
+ return <>{element()}</>;
105
+ };
106
+ ```
107
+
108
+ Only **eager** mode supports this. Pooled `templates` receive `PooledSlideRenderProps`, which
109
+ carries neither `slideFocusId` nor `goTo` — a pooled template can never be focusable. To make one
110
+ particular pooled slide interactive, name its index in `uniqueSlides`; those bypass the pool and
111
+ go through `renderSlide` with the full render props.
112
+
113
+ ## Taking focus in
114
+
115
+ Give the slideshow a `focusId` and name it from the enclosing boundary:
116
+
117
+ ```tsx rsg-skill=complete
118
+ import { FocusBoundary } from "@roku-sdk/navigation";
119
+ import { createSlideshow } from "@roku-sdk/slideshow";
120
+ import type { Component } from "@roku-sdk/solid";
121
+
122
+ const Screen: Component = () => {
123
+ const slideshowId = {};
124
+ const { element } = createSlideshow<undefined>({
125
+ slideCount: 4,
126
+ slideWidth: 1720,
127
+ slideHeight: 700,
128
+ focusId: slideshowId,
129
+ });
130
+ return (
131
+ <FocusBoundary focusId={{}} initialFocus={slideshowId}>
132
+ {element()}
133
+ </FocusBoundary>
134
+ );
135
+ };
136
+ ```
137
+
138
+ When `focusId` is set, the handle's `element()` **must** be rendered inside a `FocusBoundary`.
139
+
140
+ ## Handing focus back out
141
+
142
+ In eager and virtual modes, do it in `onEdgeReached`. The argument is `"first"` or `"last"` —
143
+ horizontal ends, not `"left"` / `"right"` and not a vertical edge:
144
+
145
+ ```tsx rsg-skill=complete
146
+ import { FocusBoundary, useFocusActions } from "@roku-sdk/navigation";
147
+ import { createSlideshow } from "@roku-sdk/slideshow";
148
+ import type { Component } from "@roku-sdk/solid";
149
+
150
+ const Screen: Component = () => {
151
+ const slideshowId = {};
152
+ const menuId = {};
153
+
154
+ const Inner: Component = () => {
155
+ const { setFocus } = useFocusActions();
156
+ const { element } = createSlideshow<undefined>({
157
+ slideCount: 4,
158
+ slideWidth: 1720,
159
+ slideHeight: 700,
160
+ focusId: slideshowId,
161
+ onEdgeReached: (edge: "first" | "last") => {
162
+ if (edge === "last") setFocus(menuId);
163
+ },
164
+ });
165
+ return <>{element()}</>;
166
+ };
167
+
168
+ return (
169
+ <FocusBoundary focusId={{}} initialFocus={slideshowId}>
170
+ <Inner />
171
+ </FocusBoundary>
172
+ );
173
+ };
174
+ ```
175
+
176
+ `useFocusActions()` must be called inside the boundary, which is why the slideshow is built in an
177
+ inner component here rather than beside the boundary that owns it.
178
+
179
+ For **pooled** mode this is usually unnecessary: its edge handler returns `bubble`, the key bubbles
180
+ to the enclosing boundary, and a `navigationMap` entry there moves focus. Add `onEdgeReached` only
181
+ when you need to run something else at the edge as well.
182
+
183
+ ## Things that do not exist
184
+
185
+ `slideRef` was renamed to `slideFocusId`, and the config field `focusRef` to `focusId`. There is
186
+ no `navigationMap` on a slideshow config, no per-slide `nextFocus*` property, and no
187
+ `onSlideExit`. Hand-off is `onEdgeReached` plus `setFocus`, or bubbling in pooled mode.
@@ -0,0 +1,181 @@
1
+ # Solid on Roku
2
+
3
+ ## Overview
4
+
5
+ An RSG SDK TypeScript app is a SolidJS app whose reactivity is unchanged and whose
6
+ *surroundings* are not: primitives that need a browser scheduler are not re-exported, the engine
7
+ running the code hoists `let` like `var`, a development build swaps three primitives for
8
+ instrumented ones, and the anchor node hands the app its input fields as signal getters rather
9
+ than as a plain object. This skill is the seam — what the platform does differently from what the
10
+ SolidJS documentation describes.
11
+
12
+ ## When to Use
13
+
14
+ Use this when Solid code is correct by the framework's rules and the platform disagrees:
15
+
16
+ - an import from `@roku-sdk/solid` that does not resolve, for a primitive `solid-js` documents
17
+ - an anchor node input field read once that never changes again, or an effect that re-runs on
18
+ fields it never named
19
+ - a `let` or `const` inside a loop behaving as if it were `var`
20
+ - a generator function the app cannot use
21
+ - a signal that behaves one way under a development build and another under a shipped one
22
+ - a conditional element with no fallback, and whether SceneGraph's lack of text nodes makes that
23
+ unsafe
24
+
25
+ ### When not to use
26
+
27
+ | Situation | Use instead | Because |
28
+ |---|---|---|
29
+ | Handling or recovering from something that threw | The `logging-and-errors` skill | This skill owns which primitives exist, not what to do when one of them throws |
30
+ | An assertion reading stale state in a test | The `testing` skill | When effects flush under a test harness is a testing concern, not a platform one |
31
+ | Which node draws a thing, or what to pass it | The `sdk-components` skill | Reactivity is the same question everywhere; the component surface is its own |
32
+ | Which item is focused, or what a key press does | The `focus-management` skill | Focus is a signal, but which signal and who owns it is that skill's whole subject |
33
+ | Which package a Solid import comes from, and declaring it | The `project-setup` skill | Import every primitive from `@roku-sdk/solid`, never `solid-js` — that rule and the `package.json` it needs live there |
34
+
35
+ ## Read the reference first for two of these tasks
36
+
37
+ | If the request is | Read now, before answering |
38
+ |---|---|
39
+ | The build rejected a module, or a transform did something to it that has to be explained | `references/hermes-and-exports.md` |
40
+ | Whether a specific `solid-js` symbol exists here, beyond the ones named below | `references/hermes-and-exports.md` — the full excluded table, with a reason per row |
41
+
42
+ The tables below name every fact; the reference carries the shapes and the complete lists. An
43
+ answer that needs one exact symbol should not open the file.
44
+
45
+ ## Quick Reference
46
+
47
+ | Question | Answer |
48
+ |---|---|
49
+ | Where do primitives come from | `@roku-sdk/solid`, always |
50
+ | What is missing, and why | `createDeferred` (needs `MessageChannel`), `lazy` (needs dynamic `import()`), `SuspenseList` and `enableScheduling` (need the concurrent scheduler), `enableHydration` (SSR), `onError` (deprecated upstream), plus thirteen `solid-js` internals |
51
+ | Reading an anchor node input | Name the field inside a reactive scope: `createMemo(() => inputs.title)` |
52
+ | Why a top-level read of `inputs` never updates | The component function is invoked inside `untrack`, so nothing read while it runs subscribes |
53
+ | Why an effect re-runs on fields it never named | Every field is an **enumerable** getter, so spreading, `Object.keys` + read, or stringifying `inputs` subscribes to all of them |
54
+ | Why `inputs.title` is `possibly 'undefined'` | The inputs type is `Partial`, though a declared field always holds its descriptor default at runtime — restate the default: `inputs.title ?? "Loading"` |
55
+ | `let` in a loop | Hermes hoists it to function scope; the SDK's block-scoping transform restores per-iteration capture |
56
+ | Generator functions | Not usable — Hermes does not support them, so no transform covers them |
57
+ | What a development build changes | `createSignal`, `createMemo` and `createEffect` are instrumented wrappers, and `__DEV__` is set |
58
+ | A conditional with no fallback | Safe. Between siblings the renderer's `createTextNode("")` returns an empty `group` to hold the position; as a sole child there is no node at all |
59
+ | Reading a prop | Inside JSX, `createMemo`, `createEffect` or a callback — never destructured, never once at the top |
60
+
61
+ ## Instructions
62
+
63
+ 1. **Import every primitive from `@roku-sdk/solid`.** It re-exports the part of `solid-js` that
64
+ works under the SceneGraph renderer, plus the SDK's own symbols. `project-setup` owns the
65
+ dependency rule; what matters here is that a primitive absent from this package is absent on
66
+ purpose, and the reference gives the reason for each one.
67
+
68
+ 2. **Do not reach for a missing primitive's `solid-js` original.** Installing `solid-js` beside
69
+ the SDK gives you a second reactive graph, not the missing feature — and the feature is missing
70
+ because the runtime cannot carry it. For a deferred value, throttle the *source* signal
71
+ yourself, with the SDK's own timing helpers; for code splitting, there is none to do, since the
72
+ module system is static. Do not substitute a browser scheduling or frame-timing API either —
73
+ this is not a browser, and none of them exist here.
74
+
75
+ 3. **Read anchor node inputs by naming one field, inside a reactive scope.** Each input field is
76
+ an enumerable getter over its own signal, so `inputs.title` inside a `createMemo` subscribes to
77
+ `title` alone. Two consequences follow, and they pull in opposite directions:
78
+ - A read at the top of the component body subscribes to **nothing** — the body is invoked
79
+ inside `untrack` — so the value is whatever it was at construction, forever.
80
+ - A read that touches the object *as a whole* inside a tracking scope subscribes to
81
+ **everything**. Spreading it, iterating its keys, or putting it in a log line does this.
82
+
83
+ The inputs type is `Partial`, so every field reads as possibly `undefined` even though a
84
+ declared field always holds its descriptor default. Restate that default at the read —
85
+ `inputs.title ?? "Loading"` — rather than asserting non-null.
86
+
87
+ 4. **Never destructure `props`, and never read `props.x` once at the top of a component.** The
88
+ compiler passes a component's props as an object of memoized getters, so a read is a call and
89
+ destructuring calls each one immediately and keeps the value. Read inside JSX, `createMemo`,
90
+ `createEffect` or a callback. `splitProps` and `mergeProps` are the sanctioned exceptions.
91
+
92
+ 5. **Write loops as you would anywhere, and know why they work.** Hermes hoists `let` and `const`
93
+ to function scope, which would make every closure created in a loop share one variable. The
94
+ SDK's block-scoping transform runs after `babel-preset-solid` and restores per-iteration
95
+ capture, so the ordinary shape is correct. Do not hand-write an immediately invoked wrapper to
96
+ "fix" it.
97
+
98
+ 6. **Do not use generator functions.** Hermes does not support them at all, so this is a language
99
+ limit rather than a transform gap — and it is why the block-scoping transform does not handle
100
+ `yield` inside a loop. Return an array, or expose a `next()` closure over an index.
101
+
102
+ 7. **Assume a development build is not the build you ship.** It sets `__DEV__` and replaces
103
+ `createSignal`, `createMemo` and `createEffect` with instrumented wrappers that record reactive
104
+ activity. Behaviour matches, but timing and anything you measure through them do not — so never
105
+ conclude a performance question from a development build, and never leave code that depends on
106
+ the wrappers being there.
107
+
108
+ 8. **Leave `fallback` off a conditional when there is nothing to show.** SceneGraph has no text
109
+ nodes, so the universal renderer's `createTextNode("")` is answered with an empty `group`
110
+ instead — childless, zero-size, and silent, because the renderer only warns for non-empty
111
+ text. One is created to hold the position when a conditional between siblings renders
112
+ nothing. Supplying a fallback purely to avoid it buys nothing.
113
+
114
+ ## Example
115
+
116
+ An anchor node whose two input fields are written by the host while the app runs. It reads each
117
+ field by name inside a reactive scope, so each subscription is to one field.
118
+
119
+ ```tsx rsg-skill=complete
120
+ import { Label, LayoutGroup, Rectangle } from "@roku-sdk/components";
121
+ import { type AnchorNodeImplementation, defineAnchorNode, FieldType } from "@roku-sdk/rsg-ts";
122
+ import { createEffect, createMemo, Show } from "@roku-sdk/solid";
123
+
124
+ const ProgressPanelDescription = {
125
+ componentName: "ProgressPanel",
126
+ extends: "Group" as const,
127
+ inputFields: {
128
+ title: FieldType.String("Loading"),
129
+ progress: FieldType.Float(0)
130
+ },
131
+ outputFields: {
132
+ settled: FieldType.Boolean()
133
+ }
134
+ };
135
+
136
+ const ProgressPanel: AnchorNodeImplementation<typeof ProgressPanelDescription> = (inputs, outputs) => {
137
+ // Naming one field subscribes to that field alone. Reading `inputs` as a whole
138
+ // here — spreading it, or putting it in a log line — would subscribe to both.
139
+ // Every field is typed optional, so restate the descriptor's default.
140
+ const heading = createMemo(() => (inputs.title ?? "Loading").toUpperCase());
141
+ const progress = createMemo(() => inputs.progress ?? 0);
142
+ const barWidth = createMemo(() => Math.round(progress() * 600));
143
+
144
+ createEffect(() => {
145
+ if (progress() >= 1) {
146
+ outputs.settled(true);
147
+ }
148
+ });
149
+
150
+ return (
151
+ <layoutGroup layoutDirection="vert">
152
+ <label text={heading()} width={600} height={48} color="0xFFFFFFFF" />
153
+ {/* No fallback: the renderer holds the position with one empty placeholder. */}
154
+ <Show when={barWidth() > 0}>
155
+ <rectangle width={barWidth()} height={8} color="0x0055FFFF" />
156
+ </Show>
157
+ </layoutGroup>
158
+ );
159
+ };
160
+
161
+ export default defineAnchorNode(ProgressPanelDescription, ProgressPanel);
162
+ ```
163
+
164
+ ## Common Mistakes
165
+
166
+ | Symptom | Cause | Fix |
167
+ |---|---|---|
168
+ | An import from `@roku-sdk/solid` does not resolve, though `solid-js` documents the symbol | It is one of the deliberately excluded exports | Read the reference's table for the reason and the alternative; do not install `solid-js` to get it |
169
+ | An anchor node input field holds its startup value forever | It was read at the top of the component body, which runs inside `untrack` | Read it inside `createMemo`, `createEffect` or JSX |
170
+ | An effect re-runs whenever any input field changes | The effect read `inputs` as a whole — a spread, a key iteration, or a log line — and every field is an enumerable getter | Name the one field the effect depends on |
171
+ | A component renders once with the right value and never updates | `props` was destructured, or read once into a local at the top | Read `props.x` inside a reactive scope; use `splitProps` when a subset is genuinely needed |
172
+ | `TS18048: 'inputs.x' is possibly 'undefined'` on a field the descriptor gives a default | The inputs type is `Partial`, so it is wider than the runtime | Restate the descriptor's default at the read — `inputs.x ?? <default>` — rather than `!` |
173
+ | A measurement taken around signals disappears from the shipped build | Those numbers came from the development build's instrumented `createSignal` / `createMemo` / `createEffect` | Measure the build you ship; treat the wrappers as a development aid only |
174
+ | A generator function fails on the device | Hermes has no generator support, and no transform adds it | Return an array, or expose a `next()` closure |
175
+ | A hand-written wrapper was added around a loop body to capture the index | The block-scoping transform already does this, after `babel-preset-solid` | Delete the wrapper and write the ordinary loop |
176
+ | A fallback was added to every conditional to avoid text nodes | `createTextNode("")` yields an empty `group` here, not a text node | Leave `fallback` off unless there is something to show |
177
+ | A node passed by reference, such as `effect={focused() ? <effect …/> : <effect …/>}`, is rebuilt on every change | The prop expression reads the signal itself, so each change builds a new node; a signal read by that node's own fields would update it in place | Hoist each node into a `const` and choose between them — the `effects` skill. This is about node-valued fields, not `fallback` or children, which are inline by design |
178
+
179
+ The `@roku-sdk/solid` export surface is enumerated in the package's own `EXPORTS.md`, including
180
+ the SDK-specific symbols this skill does not cover. Prefer it over any prose, including this file,
181
+ when the two disagree.
@@ -0,0 +1,130 @@
1
+ # Hermes and the Export Surface
2
+
3
+ Two things the SDK does to ordinary Solid code before it runs: it withholds primitives the
4
+ runtime cannot carry, and it rewrites loops the engine would otherwise get wrong. The body names
5
+ every fact here; this file is the complete lists and the shapes.
6
+
7
+ ## The compile pipeline, in order
8
+
9
+ ```
10
+ TypeScript → Babel (babel-preset-solid) → block-scoping transform → Hermes bytecode
11
+ ```
12
+
13
+ Order is the part worth holding on to. The block-scoping transform runs **after** Babel has
14
+ already turned components into reactive calls, so it operates on the emitted code rather than on
15
+ what was written. A closure Babel generated is treated exactly like one authored by hand.
16
+
17
+ ## Why the loop transform exists
18
+
19
+ Hermes does not implement block scoping. `let` and `const` are hoisted to function scope and
20
+ behave as `var`, so every closure created in a loop would capture the same binding and observe
21
+ the final value.
22
+
23
+ ```ts
24
+ // Authored — correct everywhere block scoping is real
25
+ for (let i = 0; i < 5; i += 1) {
26
+ schedule(() => report(i));
27
+ }
28
+ // Without the transform, on Hermes: five closures, all reporting 5.
29
+ ```
30
+
31
+ The transform restores per-iteration capture, so the ordinary shape is the correct one. Two
32
+ things follow:
33
+
34
+ - **Do not hand-write a wrapper** — an immediately invoked function around the loop body, or a
35
+ `.forEach` used purely to get a fresh binding. It is redundant, and it hides the intent.
36
+ - **The fix is compile-time.** Code that never goes through the SDK's build — a snippet evaluated
37
+ some other way — does not get it.
38
+
39
+ This is a workaround with an expiry: Hermes is gaining native block scoping, and the transform is
40
+ documented as removable once that lands. Treat the shape as permanent and the mechanism as not.
41
+
42
+ ## Generator functions
43
+
44
+ Hermes does not support `function*` at all. This is not a gap in the transform — there is nothing
45
+ for it to transform into, which is why the transform explicitly does not handle `yield` inside a
46
+ loop.
47
+
48
+ ```ts
49
+ // Not usable on the device
50
+ function* pages(): Generator<number> {
51
+ for (let i = 0; i < 5; i += 1) {
52
+ yield i;
53
+ }
54
+ }
55
+ ```
56
+
57
+ Replace with whichever fits:
58
+
59
+ - return the whole array when it is small enough to build eagerly;
60
+ - expose a closure over an index — `const next = () => items[cursor++]` — when it is not;
61
+ - use a signal for the cursor when the consumer is reactive.
62
+
63
+ ## Excluded exports, with the reason for each
64
+
65
+ These exist in `solid-js` or `solid-js/store` and are **not** re-exported by `@roku-sdk/solid`.
66
+ Reaching past the package for one does not work: the reason is the runtime, not the packaging.
67
+
68
+ ### Withheld because the platform cannot carry them
69
+
70
+ | Export | Reason | What to do instead |
71
+ |---|---|---|
72
+ | `createDeferred` | Solid's scheduler needs `MessageChannel`, which the Roku Hermes runtime does not have — the import fails with `ReferenceError: Property 'MessageChannel' doesn't exist` | Throttle the source signal, or derive from a signal you update on your own cadence |
73
+ | `lazy` | Dynamic `import()` against a static module system | Import statically; there is no split bundle to defer |
74
+ | `SuspenseList` | Coordinates several `<Suspense>` boundaries through the concurrent scheduler | Order the loading states yourself with ordinary signals |
75
+ | `enableScheduling` | Platform-specific scheduler wiring that has no Roku implementation | Nothing to enable |
76
+ | `enableHydration` | Server-side rendering, which does not apply here | Nothing to hydrate |
77
+ | `enableExternalSource` | A hook for bridging external reactive libraries; no current demand | Adapt the external source into a signal by hand |
78
+ | `onError` | Deprecated upstream | `catchError` — and for containing a throw at all, the `logging-and-errors` skill |
79
+
80
+ ### Withheld as internals
81
+
82
+ Not part of the public API in `solid-js` either. If one of these looks necessary, the design
83
+ usually wants revisiting rather than the symbol.
84
+
85
+ | Export | Source |
86
+ |---|---|
87
+ | `equalFn` | `solid-js` |
88
+ | `getListener` | `solid-js` |
89
+ | `requestCallback` | `solid-js` |
90
+ | `cancelCallback` | `solid-js` |
91
+ | `sharedConfig` | `solid-js` |
92
+ | `createComponent` | `solid-js` |
93
+ | `$PROXY`, `$TRACK`, `$DEVCOMP` | `solid-js` |
94
+ | `isWrappable` | `solid-js/store` |
95
+ | `setProperty` | `solid-js/store` |
96
+ | `updatePath` | `solid-js/store` |
97
+ | `$RAW` | `solid-js/store` |
98
+
99
+ ## What a development build substitutes
100
+
101
+ A development build sets `__DEV__`, and re-exports `createSignal`, `createMemo` and
102
+ `createEffect` from an instrumented module rather than from the reactive core. The wrappers record
103
+ reactive activity for tooling; semantics match, timing does not.
104
+
105
+ The consequences worth acting on:
106
+
107
+ - **Never measure performance on it.** Every signal read and write carries bookkeeping the
108
+ shipped build does not.
109
+ - **Never depend on the instrumentation existing.** Code that reads what the wrappers record
110
+ compiles fine and does nothing in the build you ship.
111
+ - **A behavioural difference between the two builds is worth reporting**, since the wrappers are
112
+ meant to be transparent.
113
+
114
+ ## The placeholder node behind a fallback-less conditional
115
+
116
+ SceneGraph has no text nodes, but the universal renderer asks for one to hold a position. The
117
+ SDK's renderer answers that by creating an empty `group`: childless, zero-size, no layout effect.
118
+ The warning path is only for *non-empty* text, so a positional placeholder is silent.
119
+
120
+ It appears in one situation: a conditional with no `fallback`, rendering falsy, as a direct child
121
+ of a real element **with siblings** — the compiler needs a marker to know where the content goes
122
+ when it comes back. Inside a fragment there is no marker and no placeholder, because a fragment
123
+ compiles to an array.
124
+
125
+ Each placeholder is its own node, and that is deliberate — the renderer tracks children by object
126
+ identity and a node has one parent, so a shared placeholder would corrupt positional tracking for
127
+ every conditional after the first. The cost is one node creation and one append, against the
128
+ hundreds a screen makes for real content.
129
+
130
+ So: leave `fallback` off when there is nothing to show. Add one when there is.