@solidrt/cli 0.0.50 → 0.0.51

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.
@@ -2,7 +2,9 @@
2
2
 
3
3
  This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
4
4
  runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
5
- before writing or editing code here.
5
+ whole file before writing or editing code here - it is short on purpose. The
6
+ depth lives in the topic files listed under "Read before you", one of which you
7
+ should open whenever the work matches its trigger.
6
8
 
7
9
  ## Levels: core, and frameworks on top
8
10
 
@@ -21,153 +23,121 @@ the choice this app made: if no component framework is among the
21
23
  dependencies, the app is core-only - do not add one for a change core
22
24
  covers.
23
25
 
24
- Authoritative references ship inside the installed packages - read them:
25
- - node_modules/solid-js/CHEATSHEET.md - SolidJS 2.0 reactivity/control-flow model
26
- - node_modules/@solidrt/components/AGENTS.md - the component vocabulary; build UI from these
27
- - node_modules/@solidrt/components/README.md - full prop tables for every component
28
- - node_modules/@solidrt/components/examples/ - single-concept usage patterns to copy (see its README.md index)
29
- - node_modules/@solidrt/core/AGENTS.md - the underlying element/prop/reactivity model
30
- - node_modules/@solidrt/core/examples/ - single-concept usage patterns to copy (see its README.md index)
31
- - node_modules/@solidrt/cli/AGENTS.md - running, bundling, headless verify
32
- - node_modules/@solidrt/core/src/types.d.ts and jsx-runtime.d.ts - source of truth
26
+ ## Read before you
27
+
28
+ The authoritative references ship inside the installed packages. Open the one
29
+ that matches the work; do not work from memory of what a web framework does.
30
+
31
+ - write any reactive code (signals, effects, control flow) ->
32
+ node_modules/solid-js/CHEATSHEET.md - the SolidJS 2.0 model
33
+ - touch elements, props, events, gestures or text ->
34
+ node_modules/@solidrt/core/AGENTS.md, and
35
+ node_modules/@solidrt/core/src/types.d.ts + jsx-runtime.d.ts (source of truth)
36
+ <!-- components:begin -->
37
+ - build UI from the component vocabulary ->
38
+ node_modules/@solidrt/components/AGENTS.md, with full prop tables in its
39
+ README.md and single-concept usage in its examples/ (see that README index)
40
+ <!-- components:end -->
41
+ - style a screen: a background, a gradient, a shadow, an effect, vector art,
42
+ a chart -> node_modules/@solidrt/core/agents/painting.md
43
+ - write per-frame code, an animation, or anything writing properties in a
44
+ loop -> node_modules/@solidrt/core/agents/performance.md
45
+ - debug a running app, or drive it over MCP to verify a change ->
46
+ node_modules/@solidrt/cli/agents/debugging.md
47
+ - add an asset or font, set the app's identity, or build for distribution ->
48
+ node_modules/@solidrt/cli/agents/assets.md
49
+ - run, bundle, typecheck or render headlessly ->
50
+ node_modules/@solidrt/cli/AGENTS.md
51
+ - copy a working pattern -> node_modules/@solidrt/core/examples/ (see its
52
+ README.md index)
33
53
 
34
54
  <!-- Claude Code auto-imports these; other tools read the paths above. -->
35
55
  @./node_modules/solid-js/CHEATSHEET.md
56
+ <!-- components:begin -->
36
57
  @./node_modules/@solidrt/components/AGENTS.md
58
+ <!-- components:end -->
37
59
  @./node_modules/@solidrt/core/AGENTS.md
38
60
  @./node_modules/@solidrt/cli/AGENTS.md
39
61
 
40
- ## What you paint with (there is no CSS layer)
41
-
42
- Layout and props are half the model. There is no stylesheet: no filters, no
43
- box-shadow, no keyframes, no canvas. The visual range a web app gets from CSS
44
- comes from the tiers below instead, and reaching past tier 1 is ordinary
45
- app-building here, not optimization - a screen built only from view
46
- backgrounds and text is using a fraction of the runtime. Pick the tier the
47
- CONTENT calls for, not the one that looks safest.
48
-
49
- 1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
50
- `<d-rect>` child) for background, border, radius. The structure of a
51
- screen, not its finish.
52
- 2. Vector art, detached from layout - `d-path`/`d-rect`/`d-oval`/`d-line`,
53
- whose `color` takes a gradient (createLinearGradient /
54
- createRadialGradient) and which honour `blendMode`, plus `parseSvg` to
55
- draw a whole SVG document as one subtree. Free-form shapes, decoration,
56
- diagrams, charts, anything positioned rather than flowed. Examples:
57
- parse-svg, detached-positioning, text-paint-styling.
58
- 3. GPU textures - `createShaderTexture` puts a fragment shader in a
59
- `<texture>` (moving gradients, noise, glow, dissolves, a background that
60
- is alive), `createPipelineTexture` draws geometry you generate yourself
61
- (particles, point clouds, splats), and the `shader` prop post-processes
62
- content that already exists: on a `<view>` it grades, warps or dissolves
63
- that subtree, on `<window>` the whole frame. Stack `<texture>` elements
64
- with `blendMode` to combine passes. Examples: gpu-shader, gpu-particles,
65
- gpu-pipeline, gpu-instancing, gpu-texture-blend, view-shader,
66
- window-shader.
67
- 4. 3D scenes - add `@solidrt/3d` (not a scaffold dependency): meshes,
68
- materials and a camera declared as Solid components, rendered into a
69
- texture that sits in the UI tree like any other element.
70
-
71
- Tier 3 is cheaper than it looks. A shader costs one property write per frame
72
- no matter how complex the effect, which is why the performance notes below
73
- reach for it first rather than as a last resort.
74
-
75
- Web reflexes and what replaces them:
76
- - gradient background -> a gradient `color` on a `d-rect` (gradients are
77
- paint values, usable anywhere a color is)
78
- - `filter: blur/grayscale/hue-rotate`, and any "make this look processed" ->
79
- a `shader` on the view (requires repaintBoundary="snapshot"), or on
80
- `<window>` for the whole frame
81
- - `box-shadow` / `text-shadow` / glow -> no shadow prop exists: draw an
82
- offset `d-*` shape under the content, or a view shader with `outset` (the
83
- transparent margin an effect bleeds into)
84
- - `backdrop-filter` -> no equivalent. A view shader sees only its own
85
- subtree's pixels, never what is behind it. Frost the whole frame with a
86
- window shader, or fake the layer with your own content
87
- - `@keyframes` / transitions -> `onFrame` writing a signal for discrete
88
- motion; a `uTime` uniform when the animation is continuous and visual
89
- - `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
90
- rather than animating N elements)
91
- - `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
92
- - video background, animated hero, particle field -> a shader texture; this
93
- is the case the runtime is built for
94
-
95
62
  ## The things assistants get wrong (this is not React/DOM)
96
63
 
97
- 1. This is SolidJS 2.0 (see CHEATSHEET.md for the reactivity/control-flow
98
- model), rendering through a custom Rust runtime instead of the DOM. Build
99
- UI at the level the app uses (see "Levels" above): core intrinsics
100
- directly, or a component framework such as @solidrt/components (Window,
101
- View, Text, Image, TextInput, ScrollView, Pressable, Button, SafeArea,
102
- theme/setTheme) - the first-party, batteries-included one.
103
- 2. Most components split their props into two objects: `layout={{...}}` for
104
- anything that feeds the layout engine (flex/grid, sizing, padding/margin,
105
- position; font fields for Text) and `style={{...}}` for paint-only
106
- properties that never relayout (`backgroundColor`, `borderColor`,
107
- `borderWidth`, `borderRadius`, `color`, and the transform `x`/`y`/
108
- `rotate`/`scale`). Event handlers (`onPointerDown`, `onKeyDown`, ...) are
109
- top-level props, not inside `layout`/`style`.
110
- 3. `render(() => <App/>)` once, top level. The root MUST be a `<Window>`
64
+ 1. In a components-based app, most components split their props into two
65
+ objects: `layout={{...}}` for anything that feeds the layout engine
66
+ (flex/grid, sizing, padding/margin, position; font fields for Text), and
67
+ `style={{...}}` for paint-only properties that never relayout
68
+ (`backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, `color`,
69
+ and the transform `x`/`y`/`rotate`/`scale`). Event handlers
70
+ (`onPointerDown`, `onKeyDown`, ...) are top-level props, not inside
71
+ `layout`/`style`. Core intrinsics take these props flat instead - there
72
+ are no `layout`/`style` objects at that level.
73
+ 2. `render(() => <App/>)` once, top level. The root MUST be a `<Window>`
111
74
  (from @solidrt/components) or the core `<window>` - it throws otherwise.
112
- 4. Components' `Window`/`View` do not paint on their own; they only paint
75
+ 3. Components' `Window`/`View` do not paint on their own; they only paint
113
76
  when you set `style.backgroundColor`/`borderColor` etc - there is no
114
- separate background element to place by hand. That is the components level
115
- only: the core `<view>`/`<window>` have no background prop at all - in a
116
- core-only app the background is a draw-primitive child
117
- (`<d-rect color={...} />`) behind the content.
118
- 5. There is no onClick/onPress on host elements. Use `Pressable`/`Button`
119
- from components (`onPress`), or `onPointerDown` on a `View` for anything
120
- custom.
121
- 6. Reactive window state: prefer the accessors windowSize(), safeArea(),
122
- displayScale(), windowFocused(), keyboardHeight() (re-exported from
123
- @solidrt/core) over onResize/onLayout callbacks for reading layout and
124
- window state. SafeArea (the component) is usually the simpler fix for
125
- avoiding notches/system UI.
126
- 7. Per-frame animation: onFrame((tick, frame) => {}) is the native hook
127
- (runtime-paced, auto-cleans), re-exported from @solidrt/core.
128
- requestAnimationFrame(t => {}) exists as a web-standard one-shot but is
129
- not the preferred animation driver.
130
- 8. In a components-based app, reach for @solidrt/core directly only for
131
- what components doesn't wrap:
132
- raw host intrinsics and the `d-` (detached, non-layout) primitives like
133
- `d-rect`/`d-path`/`d-oval` for vector art or perf-sensitive positioned
134
- drawing, device/GPU subpath imports (@solidrt/core/camera, /microphone,
135
- /gpu), gradients (createLinearGradient/createRadialGradient), and
136
- createImage/decodeImage for images below the `Image` component's level.
137
- Components and core primitives compose freely in the same tree - a
138
- components-based app can drop to a `<d-path>` for one custom shape without
139
- giving up `View`/`Text` everywhere else.
140
- 9. tsconfig needs jsx:"preserve" + jsxImportSource:"@solidrt/core" (still
77
+ separate background element to place by hand. The core `<view>`/`<window>`
78
+ have no background prop at all: in a core-only app the background is a
79
+ draw-primitive child (`<d-rect color={...} />`) behind the content.
80
+ 4. There is no onClick/onPress on host elements: `onPointerDown` is how you
81
+ make something tappable. A components-based app gets `onPress` from
82
+ `Pressable`/`Button` on top of it.
83
+ 5. Reactive window state: prefer the accessors windowSize(), safeArea(),
84
+ displayScale(), windowFocused(), keyboardHeight(), pointerLocked()
85
+ (re-exported from @solidrt/core) over onResize/onLayout callbacks for
86
+ reading layout and window state. SafeArea (the component) is usually the
87
+ simpler fix for avoiding notches and system UI. For mouse look,
88
+ lockPointer(true) enters relative mouse mode (cursor hidden and confined,
89
+ positions freeze) and pointer events keep reporting motion through
90
+ movementX/movementY.
91
+ 6. Animation is target-shaped first: declare `transition` on the element and
92
+ write targets, and the runtime animates natively with no per-frame JS.
93
+ Reach for per-frame work only for genuinely procedural motion, where
94
+ onFrame((tick, frame) => {}) is the native hook (runtime-paced,
95
+ auto-cleans, re-exported from @solidrt/core); requestAnimationFrame
96
+ exists as a web-standard one-shot but is not the preferred driver. A JS
97
+ tween loop or an animation library pushing interpolated values through
98
+ signals is the single most expensive mistake available here - read
99
+ @solidrt/core/agents/performance.md before writing either.
100
+ 7. In a components-based app, reach for @solidrt/core directly only for what
101
+ components doesn't wrap: raw host intrinsics and the `d-` (detached,
102
+ non-layout) primitives like `d-rect`/`d-path`/`d-oval` for vector art or
103
+ perf-sensitive positioned drawing, device/GPU subpath imports
104
+ (@solidrt/core/camera, /microphone, /gpu), gradients
105
+ (createLinearGradient/createRadialGradient), and createImage/decodeImage
106
+ for images below the `Image` component's level. Components and core
107
+ primitives compose freely in the same tree - a components-based app can
108
+ drop to a `<d-path>` for one custom shape without giving up `View`/`Text`
109
+ everywhere else.
110
+ 8. tsconfig needs jsx:"preserve" + jsxImportSource:"@solidrt/core" (still
141
111
  true even when you build almost entirely with @solidrt/components -
142
112
  components are plain functions returning core JSX). Solid peer deps are
143
113
  pinned betas - do not bump them casually.
144
- 10. Use ASCII characters whenever possible in code and text - for example, no
145
- em-dashes (use a hyphen), no smart/curly quotes, no unicode symbols.
146
- 11. Prefer let over const. Use const only for real constants - a single fixed
114
+ 9. Use ASCII characters whenever possible in code and text - for example, no
115
+ em-dashes (use a hyphen), no smart/curly quotes, no unicode symbols.
116
+ 10. Prefer let over const. Use const only for real constants - a single fixed
147
117
  string or number value - and name those in ALL_CAPS.
148
- 12. Reading a signal/prop/store at the top level of a component body (not
118
+ 11. Reading a signal/prop/store at the top level of a component body (not
149
119
  inside JSX, a `createMemo`, or an effect's compute phase) reads it
150
120
  untracked - it silently freezes at the initial value instead of updating
151
121
  on change. `createEffect` takes two arguments now: `(compute, apply)`.
152
122
  `compute` is the tracked read phase; `apply(value, prev)` runs untracked
153
123
  and is where side effects/DOM-equivalent writes belong. The old
154
124
  single-arg `createEffect(fn)` form is gone - using it is an error.
155
- 13. A scroll container (ScrollView, or anything on createScroll) needs an
125
+ 12. A scroll container (ScrollView, or anything on createScroll) needs an
156
126
  explicit main-axis size - a height, or flex inside a sized parent. With
157
127
  neither it resolves to 0 and its content silently vanishes; maxHeight
158
128
  alone does not size it (the auto size it would clamp is already 0). The
159
129
  runtime warns when this happens.
160
- 14. Text `lineHeight` is a MULTIPLIER of fontSize (the theme uses 1.3-1.6),
130
+ 13. Text `lineHeight` is a MULTIPLIER of fontSize (the theme uses 1.3-1.6),
161
131
  not pixels. A CSS-reflex value like 22 makes each line box 22x the font
162
132
  size: the text becomes blank space and the parent balloons.
163
- 15. Signal writes flush on a microtask: a handler that sets a signal and
133
+ 14. Signal writes flush on a microtask: a handler that sets a signal and
164
134
  immediately reads it back gets the OLD value. Read the new value in an
165
135
  effect, or call `flush()` (from @solidjs/signals) to force it through.
166
- 16. Portals cannot mount during the app's initial render: a Modal (or any
136
+ 15. Portals cannot mount during the app's initial render: a Modal (or any
167
137
  createPortal content) that is visible at first mount throws "no mount
168
138
  target". Gate it behind a signal that starts false and open it after
169
139
  startup - overlay content is opened, not born open.
170
- 17. An element-valued prop (children, a content/icon slot) compiles to a
140
+ 16. An element-valued prop (children, a content/icon slot) compiles to a
171
141
  getter that builds a fresh native subtree on EVERY read, and a subtree
172
142
  that is never inserted is never freed - native nodes are not garbage
173
143
  collected, so what is only wasted work in DOM Solid is a permanent
@@ -175,7 +145,7 @@ Web reflexes and what replaces them:
175
145
  mounted. To inspect children (a typeof probe, counting), resolve them
176
146
  first with the children() helper (re-exported from @solidrt/core) and
177
147
  probe the resolved memo - never `typeof props.children` on the raw prop.
178
- 18. Writing a signal or store from inside an owned scope - a component body, a
148
+ 17. Writing a signal or store from inside an owned scope - a component body, a
179
149
  `createMemo`, an effect's compute phase - throws
180
150
  `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Calling a loader/init function in
181
151
  the component body that sets state is the classic React / Solid 1.x
@@ -183,218 +153,21 @@ Web reflexes and what replaces them:
183
153
  effect's apply phase, or `onSettled`; opt in narrowly with
184
154
  `createSignal(v, { ownedWrite: true })` for a signal that genuinely is
185
155
  internal state.
186
- 19. Transform origin on a `d-view`: unset `originX`/`originY` pivots
187
- scale/rotate at the view's local (0,0), the point its children's
188
- coordinates are drawn against (a laid-out view pivots at its own box
189
- center; a d-view has no box). To pivot a detached group around its
190
- content's center, set the origin explicitly in pixels
191
- (`originX={100} originY={50}` for content drawn in a 200x100 local
192
- space). Avoid pct()/keyword origins on a d-view - they resolve against
193
- the box inherited from the nearest laid-out ancestor.
194
- 20. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
156
+ 18. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
195
157
  "contain" | "none" | "scale-down", CSS object-fit semantics, centered)
196
158
  plus a box via `layout` in any form - numbers, pct(), flex. Without
197
159
  `fit`, only NUMERIC layout sizes reach the image; `width: pct(100)`
198
160
  alone draws at intrinsic size. `fit="cover"` is the answer for the
199
161
  ported-web hero-image/thumbnail pattern.
200
162
 
201
- ## Performance model (JS is the slow lane)
202
-
203
- The JS engine is interpreted and every property write crosses an FFI boundary
204
- into the runtime, so per-frame JS work is the expensive path while GPU work is
205
- nearly free. That holds on desktop and on current mobile hardware; "Where GPU
206
- work stops being free" below is where it does not. Rules, in order of leverage:
207
-
208
- 1. Continuous effects (snow, particles, animated backgrounds) belong in a
209
- fragment shader: createShaderTexture (from @solidrt/core/gpu) + `<texture
210
- params={{ uTime }}>` (the shader declares `uniform float uTime;` itself -
211
- the preamble declares only what the runtime fills). The whole effect then
212
- costs one setProperty per frame - the uTime write - regardless of visual
213
- complexity. Shader output
214
- must be premultiplied alpha (white flakes are `vec4(vec3(a), a)`);
215
- straight alpha (`vec4(1,1,1,a)`) composites as opaque white. A source that
216
- starts with `#version 300 es` is compiled exactly as written - no preamble
217
- is injected, though the built-in vertex stage still supplies `vUV` - so a
218
- shader ported from elsewhere keeps its own uniform names without dropping
219
- to compileShader/linkProgram. Params drive any uniform type: a number
220
- fills a `float`/`int` scalar, a flat number array fills `vec2`/`vec3`/
221
- `vec4` (2/3/4 numbers) or `mat4` (16, column-major), dispatched by the
222
- shader's own declaration - a ported shader's `vec2 uCenter` or
223
- `vec3 iResolution` needs no splitting into scalars. To combine several
224
- GPU passes, stack `<texture>` elements and set `blendMode` (e.g. a base
225
- pass plus an additive `blendMode="plus"` pass) rather than writing a
226
- compositing shader. Within one pipeline draw, createPipelineTexture's
227
- `blend: "add"` accumulates overlapping geometry additively (soft point
228
- splats, glow) - pair it with `depthWrite: false` when depth-tested;
229
- neither option implies the other. A pipeline's own vertex stage writes
230
- into a y-down clip space: `gl_Position` y = -1 is the top row of the
231
- target and +1 the bottom, so camera-up geometry must negate y (or fold
232
- the flip into its projection) or it draws upside down. Sampling is a
233
- create-time option on every texture: `{ filter: "nearest" }` for
234
- hard-pixel upscaling (render a small target, display it big - the
235
- retro/pixel-art path) and `{ wrap: "repeat" }` to tile outside 0..1 in
236
- shaders; the defaults are linear and clamp, and the choice applies both
237
- on screen and to shaders sampling the texture.
238
- 2. Reduce setProperty calls wherever possible: one path string rebuilt per
239
- frame beats N elements with N animated positions; a shader beats the path
240
- string. get_stats' setPropsPerFrame is the counter to watch. Compiled JSX
241
- attribute expressions diff before writing, so a per-frame expression that
242
- returns an unchanged value costs no property write - setPropsPerFrame
243
- counts values that actually changed, not expressions re-run.
244
- 3. Never leave onFrame registered while nothing animates: a pending onFrame
245
- is a standing frame request, so the runtime renders and presents every
246
- vsync even when the callback body does nothing - an invisible 60fps GPU
247
- burn that also drags the OS compositor along with it. For an on-demand
248
- animation pump (tweens), use a self-rechaining one-shot
249
- requestAnimationFrame that stops re-requesting when its work list
250
- empties. (Registering onFrame outside a component body also warns
251
- NO_OWNER_CLEANUP - it assumes a reactive owner.)
252
- 4. repaintBoundary works like Flutter's: transforms and opacity on the
253
- boundary node itself (or any ancestor) are hoisted out of the cache and
254
- applied at composite time, so animating x/y/scale/rotate/opacity of a
255
- boundary does NOT re-raster it (verified by A/B measurement - the damage
256
- system classifies these as Transform and keeps the node's own cache).
257
- What DOES invalidate the cache is any paint or content change inside the
258
- subtree - colors, path data, text, a Show toggling - so drive animation
259
- with transforms and keep the cached content itself static. Off a boundary,
260
- `opacity` on a view is NOT cheap: it wraps the subtree in a compositing
261
- layer (save_layer) for as long as it is below 1. To fade a single
262
- primitive, put the alpha in its `color` (`rgba(...)`) - paint alpha is
263
- free; reserve view `opacity` for fading a genuine group as a whole.
264
- 5. "snapshot" boundaries pay first-frame texture allocation + raster:
265
- creating many at once (dealing a board of 64 sprites) is a visible
266
- one-frame hiccup - pool or pre-warm if that moment matters.
267
- 6. Shading pixels the app already drew is a different mechanism from rule 1's
268
- generated textures, and both forms are a `shader` prop taking a linked
269
- program from compileShader/linkProgram (@solidrt/core/gpu), not a
270
- createShaderTexture source. On `<window>`, `shader={{ program, params }}`
271
- runs the finished frame through the program as the last step before it
272
- reaches the screen: the frame binds as `uniform sampler2D uSource`,
273
- `iResolution` fills by name, and `previous: true` retains the last frame as
274
- `uPrevious` for motion echo or frame differencing. On a `<view>` the same
275
- prop shades that subtree in place and REQUIRES repaintBoundary="snapshot"
276
- (without it the shader is ignored with a warning); the pass sees only the
277
- subtree's own pixels - grading, warping or dissolving the panel works,
278
- anything needing what is behind it does not - and is split from content
279
- invalidation, so a params-only change re-runs the pass against the cached
280
- snapshot instead of re-rasterizing. A window shader's output is invisible
281
- to get_snapshot and every other MCP tool; `srt render` is the only way to
282
- see it (Run / verify below).
283
- 7. `flux:wasm` is not the fast lane. It runs a pure interpreter (wasmi, no
284
- JIT), so tight typed compute gains a small constant factor over the same
285
- loop in JavaScript, nowhere near browser wasm speed, and every host call
286
- costs marshalling. Use it to ship one compiled module across every target
287
- without native binaries, not to speed up per-frame work; for that, rules
288
- 1-2 (move it to the GPU, cut property writes) are the leverage.
289
-
290
- ### Isolates: heavy work off the JS thread
291
-
292
- A long synchronous computation (a big parse, a simulation step, a blocking
293
- `flux:ffi`/`flux:wasm` call) freezes rendering and input for its duration.
294
- Move it into an isolate module: a file whose first statement is the
295
- `"use isolate"` directive runs in a second runtime on its own thread, and
296
- main calls its exports as async functions.
297
-
298
- ```ts
299
- // src/worker.ts
300
- "use isolate"
301
- export function crunch(data: Uint8Array): number { /* ... */ }
302
- ```
303
-
304
- ```ts
305
- // src/index.tsx
306
- import { isolate } from "flux:isolate"
307
- import type * as Worker from "./worker"
308
- let worker = isolate<typeof Worker>("worker") // id = path from src/, no extension
309
- let n = await worker.crunch(bytes) // main keeps rendering meanwhile
310
- ```
311
-
312
- The bundler builds each such module as its own bundle and ships it with the
313
- app (dev pushes and `srt pack` alike). Rules: main may only `import type`
314
- from an isolate module (a value import is a build error); arguments and
315
- results are copies (numbers, strings, byte buffers, arrays, plain objects -
316
- no functions, no class instances); the child has the non-gui `flux:*`
317
- modules only, so it never touches the render tree; module state persists
318
- between calls and each `isolate()` call is its own instance. An
319
- `async function*` export is a stream: `for await (let p of worker.progress())`
320
- pulls one item per step (progress, ticks, a subscription), `break` ends it in
321
- the isolate, and streams never block plain calls. Full contract:
322
- node_modules/@solidrt/flux-types/modules/isolate.d.ts.
323
-
324
- ### Where GPU work stops being free
325
-
326
- "GPU work is nearly free" is a property of the hardware, not of the engine, and
327
- the spread is wide enough to design against rather than discover late. The same
328
- app - two point-cloud pipelines, 233,600 vertices, one params write each per
329
- onFrame, i.e. exactly what rule 1 recommends - measured 16.7 ms/frame (60 fps,
330
- vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
331
- (8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
332
- indistinguishable from desktop. Measure on a target device if it matters; do
333
- not infer it from the desktop number.
334
-
335
- - **On a tiled GPU the budget is primitive count, not pixels.** Every point or
336
- triangle costs the tiler regardless of how few pixels it covers. On that TV,
337
- frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
338
- 35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
339
- the fill - measured within one vsync of 1.0, and rendering into a
340
- quarter-size target measured identical to full size. So for a heavy pass the
341
- lever is fewer primitives; shrinking the target or the splat usually is not,
342
- and coverage is far cheaper bought with point size than with more points.
343
- - **A device's compositor can set the frame budget outright**, in which case
344
- none of the above moves. That TV never presents faster than every 80 ms -
345
- four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
346
- scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
347
- content-independent floor: if a trivial scene and a heavy one present at
348
- nearly the same rate, you are compositor-bound and tuning the scene is
349
- wasted effort.
350
- - **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
351
- that costs more than a refresh period does not silently pile up. If
352
- `rasterQueue` sits persistently above 0 the raster thread is behind; if
353
- `fenceTimeouts` climbs, the GPU is over its pacing budget.
354
-
355
- Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
356
- rasterQueue and fenceTimeouts. When those disagree with what the screen is
357
- visibly doing, ground truth on Android is
358
- `adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
359
- timestamps - engine-reported phase timings can each be honest and still not add
360
- up to the frame period, because work outside the frame call is not in them.
361
-
362
- ## Assets and app identity
363
-
364
- - Everything under `assets/` ships with the app: the folder is collected
365
- wholesale into each build's version manifest (no bundler analysis, no
366
- registration step). Reference assets by path - `file("assets/sounds/x.ogg")`
367
- from `flux:fs` - and treat them as read-only at runtime; writes belong in
368
- plain relative paths, which land in the app's private data dir.
369
- - Small text-like assets (SVG documents, shaders) can instead be inlined via
370
- imports. An import attribute picks the form and works on any extension:
371
- `import src from "./effect.glsl" with { type: "text" }` yields the file's
372
- contents as a string, `with { type: "binary" }` yields a Uint8Array. `.svg`
373
- is text-loaded with no attribute needed. Shader sources (`.glsl`/`.vert`/
374
- `.frag`) are declared as text modules out of the box, so they typecheck
375
- without setup. Inlining trades update granularity for zero I/O - keep big or
376
- streamable files (audio, images) in `assets/`.
377
- - Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
378
- map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
379
- replace the built-in defaults, `false` drops one, other keys add fonts
380
- selectable via fontFamily). A newly added font shows after restarting the
381
- client.
382
- - The `solidrt` key in package.json is the app's identity: set a stable
383
- reverse-DNS `appId` before distributing - it keys the app's storage
384
- folder, defaults from the package name in dev, and `srt pack` warns
385
- while defaulted. `org` and `displayName` are optional display metadata
386
- (future launcher/window naming) with no storage meaning.
387
- - `bunx srt pack src/index.tsx` builds a single-file executable;
388
- `bunx srt pack --folder src/index.tsx` writes the flat app folder
389
- (runner + manifest.json + bundle + assets/, plus the runner's GL
390
- libraries on Windows and macOS) to `dist/`.
391
-
392
163
  ## Run / verify
393
164
 
394
- - FIRST check whether a dev server and a client are already running (MCP
395
- list_clients, see below) and build/test against those: `reload` pushes your
396
- edits to the live app, get_logs and get_snapshot verify them. Do not start a
397
- second `srt run` when one is already up.
165
+ - FIRST check whether a dev server and a client are already running (the MCP
166
+ list_clients tool) and build against those: `reload` pushes your edits to
167
+ the live app, get_logs and get_snapshot verify them. Do not start a second
168
+ `srt run` when one is already up.
169
+ - The dev loop is edit -> reload -> get_logs -> get_snapshot. `reload`
170
+ surfaces build errors but not type errors; run `bunx srt check` for those.
398
171
  - bunx srt run src/index.tsx - dev server + window (needs a display)
399
172
  - bunx srt check src/index.tsx - exit 0 means it compiles and the app's
400
173
  types hold (dependency-internal type errors are hidden). Builds in memory:
@@ -402,229 +175,11 @@ up to the frame period, because work outside the frame call is not in them.
402
175
  iterating - `srt bundle` writes output files and reloads connected clients
403
176
  - bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
404
177
  render to PNG frames (proves it renders; see the cli AGENTS.md for where the
405
- frames land). It is also the ONLY way to see the output of a window shader
406
- (the `shader` prop on `<window>`): that pass runs on the finished frame on
407
- its way to the screen, past the point every other capture reads, so `render`
408
- frames are the only programmatic view of what it produces
409
-
410
- ## MCP: inspect the running app
411
-
412
- The project ships an MCP server (.mcp.json, `srt mcp`) that talks to the dev
413
- server `bunx srt run` starts. When it is loaded in your environment, prefer
414
- its tools over guessing at runtime state:
415
-
416
- - list_clients: connected app clients, their platform and runtime
417
- capabilities, plus the server's `entry` (the app source it serves) and
418
- `projectDir` - check entry matches the app you think you are driving. Each
419
- tool call finds the dev server currently serving this project (by its
420
- project root), so a server restarted on another port/session is followed
421
- automatically; no server serving this project is an error, not a wrong
422
- server.
423
- Each client also lists `queries`, the dev-tool query kinds its runtime
424
- answers - check it before planning verification against a mixed-version
425
- fleet (no "input" = the client predates send_input)
426
- - get_logs: console output and runtime errors (seq cursor; `wait_ms` long-poll
427
- to catch output right after a reload; `level`/`contains` filters; repeated
428
- lines collapse into one entry with a `repeats` count)
429
- - get_render_tree: what the app actually rendered - node kinds, text, and
430
- window-relative boxes. Pass `props: true` for each node's current
431
- property values (JSX names, off-default only - "is rotate/color/overflow/d
432
- applied right now" is one call, not a probe entry) and, on transformed nodes, the
433
- painted `quad` (four corners after transforms; the box is just its
434
- axis-aligned bounds). Whole trees get large: `query` finds nodes by
435
- kind/text, then `root` + `depth` inspect just that region
436
- - client ids and log cursors die with the dev server: list_clients and
437
- get_logs responses carry `generation`, and a changed generation means
438
- re-fetch ids and restart cursors
439
- - get_stats: fps, CPU/memory, frame phase timings, setProperty rate, plus
440
- layout-activity counters for the last rebuild (nodes, measureCalls,
441
- paraShapes/wordHits, dirtiedNodes, cacheGets/cacheHits) - when layoutMs looks
442
- wrong, these say whether the cost is text shaping, invalidation breadth,
443
- or a defeated layout cache (healthy incremental rebuilds show a near-100%
444
- cacheHits rate). reusedPerSec/skippedPerSec are the demand gate's visible
445
- signal: frames presented from the cached display list without a rebuild
446
- (texture content changed, no property writes - expect reusedPerSec near
447
- fps on texture-driven apps) and frames skipped entirely (nothing
448
- requested one)
449
- - get_snapshot: PNG capture of any render-tree node's pixels (get node ids
450
- from get_render_tree; the window node captures everything). A subtree
451
- capture renders with NO ancestor paint: pixels the subtree does not draw
452
- come back transparent, not the background behind the node. Captures
453
- re-rasterize the tree offscreen, so they are also PRE window shader: an app
454
- with a `shader` on its `<window>` snapshots as its unshaded content, window
455
- node included, and no MCP tool reads the shaded result (its layer is
456
- runtime-owned, so get_texture has no id for it; get_gpu_resources reports
457
- only that the pass exists). Use `srt render` for that one. Crop with
458
- x/y/width/height (captured-image pixels) and magnify with `scale` (1-8,
459
- nearest-neighbour) - a tight crop at 4-8x is how small geometry gets
460
- verified. Pass `save_to` on get_snapshot or get_texture to also write the
461
- PNG to a file - the image in the tool result cannot be saved afterwards,
462
- so decide before capturing (e.g. keep a before/after pair to diff)
463
- - set_time_scale / step_frames: the runtime clock. `set_time_scale 0`
464
- freezes app time (onFrame, requestAnimationFrame, timers, and
465
- performance.now all stop; Date.now stays wall time), so a snapshot can
466
- catch an exact frame of any animation instead of racing it; `step_frames
467
- n` then advances exactly n frames (one refresh period each). Pause,
468
- snapshot, step, snapshot again to see precisely what changed. ALWAYS set
469
- the scale back to 1 when done - a paused client looks wedged to the human
470
- watching - though reload/load also reset it
471
- - send_input: synthetic pointer/key/wheel/text events through the REAL
472
- input pipeline (hit testing, focus, bubbling) - the way to verify an
473
- interaction actually works, where call_debug would bypass it. A click is
474
- one call ({type: "pointer", action: "tap", x, y} - logical points, the
475
- same space get_render_tree reports); a key hold is {type: "key", action:
476
- "tap", key: "w", holdMs: 500}; text needs the field focused first (tap
477
- it), then {type: "text", text: "go"}; drags are down + moves (delayMs
478
- ~16 apiece) + up. Sequences run in order with per-event delayMs and the
479
- call returns after the last event is delivered, so a following snapshot
480
- sees the result. A synthetic mouse keeps hovering at its last position
481
- (like a real cursor at rest); use pointerType: "touch" for gestures that
482
- should end hover-free. Composes with the clock: pause, send_input,
483
- step_frames, snapshot = a deterministic interaction test
484
- - get_gpu_resources: inventory of GPU state - textures (size, render target
485
- or not), vertex buffers (byteLength), pipelines (draw count, attribute
486
- layout, bound textures, current uniform values - the most recent writes,
487
- which the next frame or readback draws with)
488
- - get_texture: any GPU texture read back as a PNG by id - atlases, data
489
- textures, and shader/pipeline render targets alike (a render target reads
490
- as its current output, pending writes included, with no frame or snapshot
491
- needed); crop with x/y/width/height, magnify with `scale`
492
- - get_buffer: a vertex-buffer range decoded to numbers (f32/u16/u8, 64 KiB
493
- per call) - verify geometry after a writeBuffer instead of inferring it
494
- from pixels
495
- - list_debug / call_debug: the app's own debug commands (registered with
496
- `registerDebug` from `srt:dev`) - list them, then invoke by name with a
497
- JSON argument. Per client, like get_snapshot
498
- - reload: rebuild from source and push to every client - THE dev loop is
499
- edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
500
- but not type errors; run `bunx srt check` for those.
501
- - load: bundle a given source file and push it to every client, replacing
502
- the running app; later reloads rebuild that entry. Use it when the dev
503
- server has no app loaded yet, or to switch apps without restarting srt.
504
- - watch: pause (enabled: false) or resume the automatic reload-on-save.
505
- Pause BEFORE creating or editing source files so half-finished work is
506
- not pushed to the user's screens mid-burst; a successful reload or load
507
- resumes it, so pause again before the next burst. Never leave it paused
508
- when you stop working - the user's own saves rely on it.
509
-
510
- The tools need a running app: if list_clients is empty, ask the user to start
511
- `bunx srt run src/index.tsx`. The bridge dials the dev server's default port
512
- (34884), so if the user started it with `--port N`, .mcp.json needs the same
513
- flag: `"args": [..., "mcp", "--port", "N"]`.
514
-
515
- - Permission prompts: agents typically ask approval per MCP tool. All of
516
- these tools only talk to the local dev server the user started with
517
- `bunx srt run` - nothing leaves the machine - so approving the server as
518
- a whole is a reasonable default. If repeated prompts get in the way, do
519
- not work around them; tell the user they can pre-approve the server in
520
- their agent's settings (most agents have a per-server trust or allowlist
521
- setting - in Claude Code, add "mcp__solidrt" to `permissions.allow` in
522
- ~/.claude/settings.json to cover every solidrt project). This is the
523
- user's call to make, once, in their own tooling.
524
- - Multiple clients: several clients may be attached (desktop window,
525
- phone, tablet) with different sizes, display scales, and safe areas.
526
- reload pushes to all of them, but call_debug / send_input / get_snapshot
527
- / log cursors are per client, and interactive state does NOT sync - a flow
528
- driven on one client leaves the others sitting on the initial screen,
529
- which reads as a crash to a human holding that device. So: when driving
530
- state via call_debug, send the same call to every client (or say which
531
- client you are using); and before calling a visual change done,
532
- snapshot each distinct form factor at least once - a layout that fits
533
- one window can clip or overflow another.
534
-
535
- ## Debugging a running app (lessons that cost real time)
536
-
537
- - console.log + get_logs is your primary probe into runtime state. For state
538
- you will want repeatedly (a pose, a mode, a counter), bind a debug key that
539
- logs it and read it back via get_logs.
540
- - Better than debug keys when driving the app over MCP: register debug
541
- COMMANDS - `registerDebug(name, fn)` from `srt:dev`, invoked via the
542
- list_debug/call_debug tools. Use them to SET UP state (jump to a level,
543
- force a mode, seed a scenario); then the runtime-level tools take over -
544
- set_time_scale 0 freezes the result for as many snapshots as you need,
545
- and step_frames walks it forward deterministically. Set state, pause,
546
- snapshot. Registrations reset on hot reload, so register at module init;
547
- sync return values only - and note a signal you just wrote flushes on a
548
- microtask, so returning a signal read straight after setting it returns
549
- the OLD value.
550
- - call_debug sets state directly, skipping focus, key routing, and
551
- TextInput - fine for SETUP, but "the interaction works" is only shown by
552
- the real pipeline: verify clicks, typing, and drags with send_input,
553
- which enters events where SDL input does.
554
- - Key events start at the focused node and bubble to the window root; with
555
- nothing focused they go to the window root alone. So a debug key bound via
556
- `<window onKeyDown>` always fires (unless a focused component consumes the
557
- key with stopPropagation, as TextInput does for editing keys). `key` and
558
- `code` are W3C KeyboardEvent values, so arrow keys arrive as "ArrowLeft"/
559
- "ArrowRight"/"ArrowUp"/"ArrowDown" (not "Left"), alongside "Enter",
560
- "Escape", "a".
561
- - Idle frames skip work: shaders/pipelines only re-render when an input
562
- changes - their own params/geometry, or a sampled texture (a data upload,
563
- or a sampled target re-rendering; chains propagate automatically). Measure
564
- performance while inputs are actually changing. get_snapshot works on an
565
- idle client (it requests its own frame); a timeout means the JS thread is
566
- busy or wedged. get_texture on a pipeline's render target reads the
567
- current output, pending writes included, without needing a new frame.
568
- - When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
569
- in it before investigating, so you agree on the symptom. If you cannot see
570
- the problem in the capture, say that instead of guessing.
571
- - Snapshots are downscaled by the time you see them, so a full-window capture
572
- cannot show you a defect a few pixels across. Whenever you hand-author
573
- geometry - a `d-path` from raw path math, a `radius` where two shapes meet,
574
- a stroke join - inspect it MAGNIFIED once, when you write it: get_snapshot
575
- with a tight crop at scale 4-8 shows the actual rendered pixels enlarged,
576
- in one call, on the real app. Verifying that a shape is in the right place
577
- is not the same check as verifying it is drawn right - and get_render_tree
578
- props answers the third question, whether the value you set is the value
579
- the renderer holds.
580
- - GPU/geometry bugs: inspect the actual GPU data FIRST - get_gpu_resources
581
- for draw counts/uniforms/sizes, get_texture for atlas or data-texture
582
- contents ("is this tile blank?" is a ten-second question), get_buffer for
583
- vertex data. The pixels only tell you THAT something is wrong; the
584
- resources tell you WHERE the data stops being right. In a one-big-pipeline
585
- app the render tree is a single <texture> leaf and tells you nothing -
586
- these tools are the visibility layer behind it. Only when the GPU data is
587
- all correct (so the bug is in producing it, or in the shader), reproduce
588
- the math CPU-side in a scratch bun script against the app's real data and
589
- print values.
590
- - Validate assets at load time and log anomalies (missing lumps/files,
591
- fully-transparent composites, zero-sized images). Silent fallbacks hide
592
- bugs for days; a one-line warning surfaces them the first run.
593
- - After every reload the app restarts from its initial state. If reaching
594
- the bug site takes navigation, add a dev shortcut (teleport key, noclip,
595
- initial-state override) before iterating - the round trips add up fast.
596
- - Clamp onFrame time deltas to [0, cap], not just capped: across a hot
597
- reload the runtime's tick counter resets AFTER the new instance's first
598
- frame, so the second frame computes a hugely NEGATIVE delta.
599
- Math.min(dt, cap) lets it through, and one bad frame can corrupt anything
600
- integrated from dt (positions fly off, accumulators go so negative they
601
- never recover). Math.max(0, Math.min(dt, cap)) costs nothing.
602
- - A registered onFrame is a standing request, not demand-gated: it re-requests
603
- the next frame every time it runs, so the runtime keeps calling it - and
604
- presents - every frame at the refresh rate until you deregister it (fps stays
605
- at the refresh rate on an idle screen; that is the 60fps burn called out in
606
- the performance notes above). The upside is that a self-running loop - a game
607
- clock, a shader driver, a stepped VM doing silent CPU work with no console
608
- output - keeps advancing on its own: it does NOT stall when the body changes
609
- nothing and needs no startup "prime" write. Deregister onFrame (return its
610
- cleanup, or let onCleanup fire) whenever there is nothing left to advance.
611
- - Layout is incremental: a change re-solves only the dirty path, and clean
612
- subtrees answer from a per-node cache, so long lists no longer cap layout
613
- (a thousand-node tree relays out in well under a millisecond). If layoutMs
614
- still grows with tree size, read the get_stats counters - a low
615
- cacheHits/cacheGets ratio means the layout cache is being defeated, high
616
- paraShapes means text is actually reshaping. Very long lists still pay
617
- for the initial mount and for memory, so windowing stays sensible at the
618
- thousands-of-rows scale.
619
- - Remote images: createImage (and Image) dedupes repeated URLs, caches the
620
- bytes on disk, and the runtime rate-limits concurrent asset fetches per
621
- host - do not build your own promise cache around it. Images are fetched
622
- with no freshness check (an already-cached URL is never re-checked), so
623
- use versioned URLs for content that changes. Use Image's `fallback` prop
624
- (an image source) for the broken-image case instead of catching errors
625
- yourself.
626
- - fetch() never caches by default and ignores server cache headers. Caching
627
- is explicit and per call: `fetch(url, { cache: "force-cache" })` for
628
- assets (serve from disk or fetch-and-store, no freshness),
629
- `{ cache: "reload" }` to refresh an entry. Image/createImage already do
630
- this for you.
178
+ frames land). The project's assets/ resolve exactly as under `srt run`, so
179
+ asset-dependent apps render headlessly too. It is also the ONLY way to see
180
+ the output of a window shader, which every MCP capture is blind to.
181
+ - The project ships an MCP server (.mcp.json, `srt mcp`) that inspects and
182
+ drives the running app: logs, render tree, snapshots, GPU resources, stats,
183
+ synthetic input, a controllable clock. Each tool documents itself in full -
184
+ prefer them over guessing at runtime state, and read
185
+ node_modules/@solidrt/cli/agents/debugging.md before an investigation.