@solidrt/cli 0.0.49 → 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,212 +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
-
284
- ### Isolates: heavy work off the JS thread
285
-
286
- A long synchronous computation (a big parse, a simulation step, a blocking
287
- `flux:ffi`/`flux:wasm` call) freezes rendering and input for its duration.
288
- Move it into an isolate module: a file whose first statement is the
289
- `"use isolate"` directive runs in a second runtime on its own thread, and
290
- main calls its exports as async functions.
291
-
292
- ```ts
293
- // src/worker.ts
294
- "use isolate"
295
- export function crunch(data: Uint8Array): number { /* ... */ }
296
- ```
297
-
298
- ```ts
299
- // src/index.tsx
300
- import { isolate } from "flux:isolate"
301
- import type * as Worker from "./worker"
302
- let worker = isolate<typeof Worker>("worker") // id = path from src/, no extension
303
- let n = await worker.crunch(bytes) // main keeps rendering meanwhile
304
- ```
305
-
306
- The bundler builds each such module as its own bundle and ships it with the
307
- app (dev pushes and `srt pack` alike). Rules: main may only `import type`
308
- from an isolate module (a value import is a build error); arguments and
309
- results are copies (numbers, strings, byte buffers, arrays, plain objects -
310
- no functions, no class instances); the child has the non-gui `flux:*`
311
- modules only, so it never touches the render tree; module state persists
312
- between calls and each `isolate()` call is its own instance. An
313
- `async function*` export is a stream: `for await (let p of worker.progress())`
314
- pulls one item per step (progress, ticks, a subscription), `break` ends it in
315
- the isolate, and streams never block plain calls. Full contract:
316
- node_modules/@solidrt/flux-types/modules/isolate.d.ts.
317
-
318
- ### Where GPU work stops being free
319
-
320
- "GPU work is nearly free" is a property of the hardware, not of the engine, and
321
- the spread is wide enough to design against rather than discover late. The same
322
- app - two point-cloud pipelines, 233,600 vertices, one params write each per
323
- onFrame, i.e. exactly what rule 1 recommends - measured 16.7 ms/frame (60 fps,
324
- vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
325
- (8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
326
- indistinguishable from desktop. Measure on a target device if it matters; do
327
- not infer it from the desktop number.
328
-
329
- - **On a tiled GPU the budget is primitive count, not pixels.** Every point or
330
- triangle costs the tiler regardless of how few pixels it covers. On that TV,
331
- frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
332
- 35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
333
- the fill - measured within one vsync of 1.0, and rendering into a
334
- quarter-size target measured identical to full size. So for a heavy pass the
335
- lever is fewer primitives; shrinking the target or the splat usually is not,
336
- and coverage is far cheaper bought with point size than with more points.
337
- - **A device's compositor can set the frame budget outright**, in which case
338
- none of the above moves. That TV never presents faster than every 80 ms -
339
- four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
340
- scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
341
- content-independent floor: if a trivial scene and a heavy one present at
342
- nearly the same rate, you are compositor-bound and tuning the scene is
343
- wasted effort.
344
- - **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
345
- that costs more than a refresh period does not silently pile up. If
346
- `rasterQueue` sits persistently above 0 the raster thread is behind; if
347
- `fenceTimeouts` climbs, the GPU is over its pacing budget.
348
-
349
- Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
350
- rasterQueue and fenceTimeouts. When those disagree with what the screen is
351
- visibly doing, ground truth on Android is
352
- `adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
353
- timestamps - engine-reported phase timings can each be honest and still not add
354
- up to the frame period, because work outside the frame call is not in them.
355
-
356
- ## Assets and app identity
357
-
358
- - Everything under `assets/` ships with the app: the folder is collected
359
- wholesale into each build's version manifest (no bundler analysis, no
360
- registration step). Reference assets by path - `file("assets/sounds/x.ogg")`
361
- from `flux:fs` - and treat them as read-only at runtime; writes belong in
362
- plain relative paths, which land in the app's private data dir.
363
- - Small text-like assets (SVG documents, shaders) can instead be inlined via
364
- imports. An import attribute picks the form and works on any extension:
365
- `import src from "./effect.glsl" with { type: "text" }` yields the file's
366
- contents as a string, `with { type: "binary" }` yields a Uint8Array. `.svg`
367
- is text-loaded with no attribute needed. Shader sources (`.glsl`/`.vert`/
368
- `.frag`) are declared as text modules out of the box, so they typecheck
369
- without setup. Inlining trades update granularity for zero I/O - keep big or
370
- streamable files (audio, images) in `assets/`.
371
- - Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
372
- map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
373
- replace the built-in defaults, `false` drops one, other keys add fonts
374
- selectable via fontFamily). A newly added font shows after restarting the
375
- client.
376
- - The `solidrt` key in package.json is the app's identity: set a stable
377
- reverse-DNS `appId` before distributing - it keys the app's storage
378
- folder, defaults from the package name in dev, and `srt pack` warns
379
- while defaulted. `org` and `displayName` are optional display metadata
380
- (future launcher/window naming) with no storage meaning.
381
- - `bunx srt pack src/index.tsx` builds a single-file executable;
382
- `bunx srt pack --folder src/index.tsx` writes the flat app folder
383
- (runner + manifest.json + bundle + assets/, plus the runner's GL
384
- libraries on Windows and macOS) to `dist/`.
385
-
386
163
  ## Run / verify
387
164
 
388
- - FIRST check whether a dev server and a client are already running (MCP
389
- list_clients, see below) and build/test against those: `reload` pushes your
390
- edits to the live app, get_logs and get_snapshot verify them. Do not start a
391
- 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.
392
171
  - bunx srt run src/index.tsx - dev server + window (needs a display)
393
172
  - bunx srt check src/index.tsx - exit 0 means it compiles and the app's
394
173
  types hold (dependency-internal type errors are hidden). Builds in memory:
@@ -396,229 +175,11 @@ up to the frame period, because work outside the frame call is not in them.
396
175
  iterating - `srt bundle` writes output files and reloads connected clients
397
176
  - bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
398
177
  render to PNG frames (proves it renders; see the cli AGENTS.md for where the
399
- frames land). It is also the ONLY way to see the output of a window shader
400
- (the `shader` prop on `<window>`): that pass runs on the finished frame on
401
- its way to the screen, past the point every other capture reads, so `render`
402
- frames are the only programmatic view of what it produces
403
-
404
- ## MCP: inspect the running app
405
-
406
- The project ships an MCP server (.mcp.json, `srt mcp`) that talks to the dev
407
- server `bunx srt run` starts. When it is loaded in your environment, prefer
408
- its tools over guessing at runtime state:
409
-
410
- - list_clients: connected app clients, their platform and runtime
411
- capabilities, plus the server's `entry` (the app source it serves) and
412
- `projectDir` - check entry matches the app you think you are driving. Each
413
- tool call finds the dev server currently serving this project (by its
414
- project root), so a server restarted on another port/session is followed
415
- automatically; no server serving this project is an error, not a wrong
416
- server.
417
- Each client also lists `queries`, the dev-tool query kinds its runtime
418
- answers - check it before planning verification against a mixed-version
419
- fleet (no "input" = the client predates send_input)
420
- - get_logs: console output and runtime errors (seq cursor; `wait_ms` long-poll
421
- to catch output right after a reload; `level`/`contains` filters; repeated
422
- lines collapse into one entry with a `repeats` count)
423
- - get_render_tree: what the app actually rendered - node kinds, text, and
424
- window-relative boxes. Pass `props: true` for each node's current
425
- property values (JSX names, off-default only - "is rotate/color/overflow/d
426
- applied right now" is one call, not a probe entry) and, on transformed nodes, the
427
- painted `quad` (four corners after transforms; the box is just its
428
- axis-aligned bounds). Whole trees get large: `query` finds nodes by
429
- kind/text, then `root` + `depth` inspect just that region
430
- - client ids and log cursors die with the dev server: list_clients and
431
- get_logs responses carry `generation`, and a changed generation means
432
- re-fetch ids and restart cursors
433
- - get_stats: fps, CPU/memory, frame phase timings, setProperty rate, plus
434
- layout-activity counters for the last rebuild (nodes, measureCalls,
435
- paraShapes, dirtiedNodes, cacheGets/cacheHits) - when layoutMs looks
436
- wrong, these say whether the cost is text shaping, invalidation breadth,
437
- or a defeated layout cache (healthy incremental rebuilds show a near-100%
438
- cacheHits rate). reusedPerSec/skippedPerSec are the demand gate's visible
439
- signal: frames presented from the cached display list without a rebuild
440
- (texture content changed, no property writes - expect reusedPerSec near
441
- fps on texture-driven apps) and frames skipped entirely (nothing
442
- requested one)
443
- - get_snapshot: PNG capture of any render-tree node's pixels (get node ids
444
- from get_render_tree; the window node captures everything). A subtree
445
- capture renders with NO ancestor paint: pixels the subtree does not draw
446
- come back transparent, not the background behind the node. Captures
447
- re-rasterize the tree offscreen, so they are also PRE window shader: an app
448
- with a `shader` on its `<window>` snapshots as its unshaded content, window
449
- node included, and no MCP tool reads the shaded result (its layer is
450
- runtime-owned, so get_texture has no id for it; get_gpu_resources reports
451
- only that the pass exists). Use `srt render` for that one. Crop with
452
- x/y/width/height (captured-image pixels) and magnify with `scale` (1-8,
453
- nearest-neighbour) - a tight crop at 4-8x is how small geometry gets
454
- verified. Pass `save_to` on get_snapshot or get_texture to also write the
455
- PNG to a file - the image in the tool result cannot be saved afterwards,
456
- so decide before capturing (e.g. keep a before/after pair to diff)
457
- - set_time_scale / step_frames: the runtime clock. `set_time_scale 0`
458
- freezes app time (onFrame, requestAnimationFrame, timers, and
459
- performance.now all stop; Date.now stays wall time), so a snapshot can
460
- catch an exact frame of any animation instead of racing it; `step_frames
461
- n` then advances exactly n frames (one refresh period each). Pause,
462
- snapshot, step, snapshot again to see precisely what changed. ALWAYS set
463
- the scale back to 1 when done - a paused client looks wedged to the human
464
- watching - though reload/load also reset it
465
- - send_input: synthetic pointer/key/wheel/text events through the REAL
466
- input pipeline (hit testing, focus, bubbling) - the way to verify an
467
- interaction actually works, where call_debug would bypass it. A click is
468
- one call ({type: "pointer", action: "tap", x, y} - logical points, the
469
- same space get_render_tree reports); a key hold is {type: "key", action:
470
- "tap", key: "w", holdMs: 500}; text needs the field focused first (tap
471
- it), then {type: "text", text: "go"}; drags are down + moves (delayMs
472
- ~16 apiece) + up. Sequences run in order with per-event delayMs and the
473
- call returns after the last event is delivered, so a following snapshot
474
- sees the result. A synthetic mouse keeps hovering at its last position
475
- (like a real cursor at rest); use pointerType: "touch" for gestures that
476
- should end hover-free. Composes with the clock: pause, send_input,
477
- step_frames, snapshot = a deterministic interaction test
478
- - get_gpu_resources: inventory of GPU state - textures (size, render target
479
- or not), vertex buffers (byteLength), pipelines (draw count, attribute
480
- layout, bound textures, current uniform values - the most recent writes,
481
- which the next frame or readback draws with)
482
- - get_texture: any GPU texture read back as a PNG by id - atlases, data
483
- textures, and shader/pipeline render targets alike (a render target reads
484
- as its current output, pending writes included, with no frame or snapshot
485
- needed); crop with x/y/width/height, magnify with `scale`
486
- - get_buffer: a vertex-buffer range decoded to numbers (f32/u16/u8, 64 KiB
487
- per call) - verify geometry after a writeBuffer instead of inferring it
488
- from pixels
489
- - list_debug / call_debug: the app's own debug commands (registered with
490
- `registerDebug` from `srt:dev`) - list them, then invoke by name with a
491
- JSON argument. Per client, like get_snapshot
492
- - reload: rebuild from source and push to every client - THE dev loop is
493
- edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
494
- but not type errors; run `bunx srt check` for those.
495
- - load: bundle a given source file and push it to every client, replacing
496
- the running app; later reloads rebuild that entry. Use it when the dev
497
- server has no app loaded yet, or to switch apps without restarting srt.
498
- - watch: pause (enabled: false) or resume the automatic reload-on-save.
499
- Pause BEFORE creating or editing source files so half-finished work is
500
- not pushed to the user's screens mid-burst; a successful reload or load
501
- resumes it, so pause again before the next burst. Never leave it paused
502
- when you stop working - the user's own saves rely on it.
503
-
504
- The tools need a running app: if list_clients is empty, ask the user to start
505
- `bunx srt run src/index.tsx`. The bridge dials the dev server's default port
506
- (34884), so if the user started it with `--port N`, .mcp.json needs the same
507
- flag: `"args": [..., "mcp", "--port", "N"]`.
508
-
509
- - Permission prompts: agents typically ask approval per MCP tool. All of
510
- these tools only talk to the local dev server the user started with
511
- `bunx srt run` - nothing leaves the machine - so approving the server as
512
- a whole is a reasonable default. If repeated prompts get in the way, do
513
- not work around them; tell the user they can pre-approve the server in
514
- their agent's settings (most agents have a per-server trust or allowlist
515
- setting - in Claude Code, add "mcp__solidrt" to `permissions.allow` in
516
- ~/.claude/settings.json to cover every solidrt project). This is the
517
- user's call to make, once, in their own tooling.
518
- - Multiple clients: several clients may be attached (desktop window,
519
- phone, tablet) with different sizes, display scales, and safe areas.
520
- reload pushes to all of them, but call_debug / send_input / get_snapshot
521
- / log cursors are per client, and interactive state does NOT sync - a flow
522
- driven on one client leaves the others sitting on the initial screen,
523
- which reads as a crash to a human holding that device. So: when driving
524
- state via call_debug, send the same call to every client (or say which
525
- client you are using); and before calling a visual change done,
526
- snapshot each distinct form factor at least once - a layout that fits
527
- one window can clip or overflow another.
528
-
529
- ## Debugging a running app (lessons that cost real time)
530
-
531
- - console.log + get_logs is your primary probe into runtime state. For state
532
- you will want repeatedly (a pose, a mode, a counter), bind a debug key that
533
- logs it and read it back via get_logs.
534
- - Better than debug keys when driving the app over MCP: register debug
535
- COMMANDS - `registerDebug(name, fn)` from `srt:dev`, invoked via the
536
- list_debug/call_debug tools. Use them to SET UP state (jump to a level,
537
- force a mode, seed a scenario); then the runtime-level tools take over -
538
- set_time_scale 0 freezes the result for as many snapshots as you need,
539
- and step_frames walks it forward deterministically. Set state, pause,
540
- snapshot. Registrations reset on hot reload, so register at module init;
541
- sync return values only - and note a signal you just wrote flushes on a
542
- microtask, so returning a signal read straight after setting it returns
543
- the OLD value.
544
- - call_debug sets state directly, skipping focus, key routing, and
545
- TextInput - fine for SETUP, but "the interaction works" is only shown by
546
- the real pipeline: verify clicks, typing, and drags with send_input,
547
- which enters events where SDL input does.
548
- - Key events start at the focused node and bubble to the window root; with
549
- nothing focused they go to the window root alone. So a debug key bound via
550
- `<window onKeyDown>` always fires (unless a focused component consumes the
551
- key with stopPropagation, as TextInput does for editing keys). `key` and
552
- `code` are W3C KeyboardEvent values, so arrow keys arrive as "ArrowLeft"/
553
- "ArrowRight"/"ArrowUp"/"ArrowDown" (not "Left"), alongside "Enter",
554
- "Escape", "a".
555
- - Idle frames skip work: shaders/pipelines only re-render when an input
556
- changes - their own params/geometry, or a sampled texture (a data upload,
557
- or a sampled target re-rendering; chains propagate automatically). Measure
558
- performance while inputs are actually changing. get_snapshot works on an
559
- idle client (it requests its own frame); a timeout means the JS thread is
560
- busy or wedged. get_texture on a pipeline's render target reads the
561
- current output, pending writes included, without needing a new frame.
562
- - When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
563
- in it before investigating, so you agree on the symptom. If you cannot see
564
- the problem in the capture, say that instead of guessing.
565
- - Snapshots are downscaled by the time you see them, so a full-window capture
566
- cannot show you a defect a few pixels across. Whenever you hand-author
567
- geometry - a `d-path` from raw path math, a `radius` where two shapes meet,
568
- a stroke join - inspect it MAGNIFIED once, when you write it: get_snapshot
569
- with a tight crop at scale 4-8 shows the actual rendered pixels enlarged,
570
- in one call, on the real app. Verifying that a shape is in the right place
571
- is not the same check as verifying it is drawn right - and get_render_tree
572
- props answers the third question, whether the value you set is the value
573
- the renderer holds.
574
- - GPU/geometry bugs: inspect the actual GPU data FIRST - get_gpu_resources
575
- for draw counts/uniforms/sizes, get_texture for atlas or data-texture
576
- contents ("is this tile blank?" is a ten-second question), get_buffer for
577
- vertex data. The pixels only tell you THAT something is wrong; the
578
- resources tell you WHERE the data stops being right. In a one-big-pipeline
579
- app the render tree is a single <texture> leaf and tells you nothing -
580
- these tools are the visibility layer behind it. Only when the GPU data is
581
- all correct (so the bug is in producing it, or in the shader), reproduce
582
- the math CPU-side in a scratch bun script against the app's real data and
583
- print values.
584
- - Validate assets at load time and log anomalies (missing lumps/files,
585
- fully-transparent composites, zero-sized images). Silent fallbacks hide
586
- bugs for days; a one-line warning surfaces them the first run.
587
- - After every reload the app restarts from its initial state. If reaching
588
- the bug site takes navigation, add a dev shortcut (teleport key, noclip,
589
- initial-state override) before iterating - the round trips add up fast.
590
- - Clamp onFrame time deltas to [0, cap], not just capped: across a hot
591
- reload the runtime's tick counter resets AFTER the new instance's first
592
- frame, so the second frame computes a hugely NEGATIVE delta.
593
- Math.min(dt, cap) lets it through, and one bad frame can corrupt anything
594
- integrated from dt (positions fly off, accumulators go so negative they
595
- never recover). Math.max(0, Math.min(dt, cap)) costs nothing.
596
- - A registered onFrame is a standing request, not demand-gated: it re-requests
597
- the next frame every time it runs, so the runtime keeps calling it - and
598
- presents - every frame at the refresh rate until you deregister it (fps stays
599
- at the refresh rate on an idle screen; that is the 60fps burn called out in
600
- the performance notes above). The upside is that a self-running loop - a game
601
- clock, a shader driver, a stepped VM doing silent CPU work with no console
602
- output - keeps advancing on its own: it does NOT stall when the body changes
603
- nothing and needs no startup "prime" write. Deregister onFrame (return its
604
- cleanup, or let onCleanup fire) whenever there is nothing left to advance.
605
- - Layout is incremental: a change re-solves only the dirty path, and clean
606
- subtrees answer from a per-node cache, so long lists no longer cap layout
607
- (a thousand-node tree relays out in well under a millisecond). If layoutMs
608
- still grows with tree size, read the get_stats counters - a low
609
- cacheHits/cacheGets ratio means the layout cache is being defeated, high
610
- paraShapes means text is actually reshaping. Very long lists still pay
611
- for the initial mount and for memory, so windowing stays sensible at the
612
- thousands-of-rows scale.
613
- - Remote images: createImage (and Image) dedupes repeated URLs, caches the
614
- bytes on disk, and the runtime rate-limits concurrent asset fetches per
615
- host - do not build your own promise cache around it. Images are fetched
616
- with no freshness check (an already-cached URL is never re-checked), so
617
- use versioned URLs for content that changes. Use Image's `fallback` prop
618
- (an image source) for the broken-image case instead of catching errors
619
- yourself.
620
- - fetch() never caches by default and ignores server cache headers. Caching
621
- is explicit and per call: `fetch(url, { cache: "force-cache" })` for
622
- assets (serve from disk or fetch-and-store, no freshness),
623
- `{ cache: "reload" }` to refresh an entry. Image/createImage already do
624
- 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.