@solidrt/cli 0.0.39 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/scaffold/AGENTS.md +95 -19
- package/scaffold/package.json +4 -4
- package/src/args.ts +11 -0
- package/src/bundler.ts +28 -20
- package/src/commands/check.ts +13 -9
- package/src/commands/mcp.ts +3 -3
- package/src/dev-server.ts +32 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"zod": "^4.4.3"
|
|
29
29
|
},
|
|
30
30
|
"optionalDependencies": {
|
|
31
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
32
|
-
"@solidrt/linux-arm64-gnu": "0.0.
|
|
33
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
34
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
31
|
+
"@solidrt/darwin-arm64": "0.0.40",
|
|
32
|
+
"@solidrt/linux-arm64-gnu": "0.0.40",
|
|
33
|
+
"@solidrt/linux-x64-gnu": "0.0.40",
|
|
34
|
+
"@solidrt/win32-x64-msvc": "0.0.40"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@solidrt/core": "0.0.
|
|
37
|
+
"@solidrt/core": "0.0.40",
|
|
38
38
|
"typescript": "^7"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@solidrt/flux-types": "0.0.
|
|
41
|
+
"@solidrt/flux-types": "0.0.40",
|
|
42
42
|
"@types/babel__core": "^7.20.5",
|
|
43
43
|
"@types/bun": "latest"
|
|
44
44
|
}
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -117,7 +117,15 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
117
117
|
mounted. To inspect children (a typeof probe, counting), resolve them
|
|
118
118
|
first with the children() helper (re-exported from @solidrt/core) and
|
|
119
119
|
probe the resolved memo - never `typeof props.children` on the raw prop.
|
|
120
|
-
18.
|
|
120
|
+
18. Writing a signal or store from inside an owned scope - a component body, a
|
|
121
|
+
`createMemo`, an effect's compute phase - throws
|
|
122
|
+
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Calling a loader/init function in
|
|
123
|
+
the component body that sets state is the classic React / Solid 1.x
|
|
124
|
+
reflex and hits this every time. Move the write into an event handler, an
|
|
125
|
+
effect's apply phase, or `onSettled`; opt in narrowly with
|
|
126
|
+
`createSignal(v, { ownedWrite: true })` for a signal that genuinely is
|
|
127
|
+
internal state.
|
|
128
|
+
19. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
|
|
121
129
|
"contain" | "none" | "scale-down", CSS object-fit semantics, centered)
|
|
122
130
|
plus a box via `layout` in any form - numbers, pct(), flex. Without
|
|
123
131
|
`fit`, only NUMERIC layout sizes reach the image; `width: pct(100)`
|
|
@@ -128,14 +136,34 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
128
136
|
|
|
129
137
|
The JS engine is interpreted and every property write crosses an FFI boundary
|
|
130
138
|
into the runtime, so per-frame JS work is the expensive path while GPU work is
|
|
131
|
-
nearly free.
|
|
139
|
+
nearly free. That holds on desktop and on current mobile hardware; "Where GPU
|
|
140
|
+
work stops being free" below is where it does not. Rules, in order of leverage:
|
|
132
141
|
|
|
133
142
|
1. Continuous effects (snow, particles, animated backgrounds) belong in a
|
|
134
143
|
fragment shader: createShader (from @solidrt/core/gpu) + `<texture
|
|
135
144
|
params={{ iTime }}>`. The whole effect then costs one setProperty per
|
|
136
145
|
frame - the iTime write - regardless of visual complexity. Shader output
|
|
137
146
|
must be premultiplied alpha (white flakes are `vec4(vec3(a), a)`);
|
|
138
|
-
straight alpha (`vec4(1,1,1,a)`) composites as opaque white.
|
|
147
|
+
straight alpha (`vec4(1,1,1,a)`) composites as opaque white. A source that
|
|
148
|
+
starts with `#version 300 es` is compiled exactly as written - no preamble
|
|
149
|
+
is injected, though the built-in vertex stage still supplies `vUV` - so a
|
|
150
|
+
shader ported from elsewhere keeps its own uniform names without dropping
|
|
151
|
+
to compileShader/linkProgram. Params drive any uniform type: a number
|
|
152
|
+
fills a `float`/`int` scalar, a flat number array fills `vec2`/`vec3`/
|
|
153
|
+
`vec4` (2/3/4 numbers) or `mat4` (16, column-major), dispatched by the
|
|
154
|
+
shader's own declaration - a ported shader's `vec2 uCenter` or Shadertoy's
|
|
155
|
+
`vec3 iResolution` needs no splitting into scalars. To combine several
|
|
156
|
+
GPU passes, stack `<texture>` elements and set `blendMode` (e.g. a base
|
|
157
|
+
pass plus an additive `blendMode="plus"` pass) rather than writing a
|
|
158
|
+
compositing shader. Within one pipeline draw, createPipeline's
|
|
159
|
+
`blend: "add"` accumulates overlapping geometry additively (soft point
|
|
160
|
+
splats, glow) - pair it with `depthWrite: false` when depth-tested;
|
|
161
|
+
neither option implies the other. Sampling is a create-time option on
|
|
162
|
+
every texture: `{ filter: "nearest" }` for hard-pixel upscaling (render a
|
|
163
|
+
small target, display it big - the retro/pixel-art path) and
|
|
164
|
+
`{ wrap: "repeat" }` to tile outside 0..1 in shaders; the defaults are
|
|
165
|
+
linear and clamp, and the choice applies both on screen and to shaders
|
|
166
|
+
sampling the texture.
|
|
139
167
|
2. Reduce setProperty calls wherever possible: one path string rebuilt per
|
|
140
168
|
frame beats N elements with N animated positions; a shader beats the path
|
|
141
169
|
string. get_stats' setPropsPerFrame is the counter to watch.
|
|
@@ -159,6 +187,44 @@ nearly free. Rules, in order of leverage:
|
|
|
159
187
|
creating many at once (dealing a board of 64 sprites) is a visible
|
|
160
188
|
one-frame hiccup - pool or pre-warm if that moment matters.
|
|
161
189
|
|
|
190
|
+
### Where GPU work stops being free
|
|
191
|
+
|
|
192
|
+
"GPU work is nearly free" is a property of the hardware, not of the engine, and
|
|
193
|
+
the spread is wide enough to design against rather than discover late. The same
|
|
194
|
+
app - two point-cloud pipelines, 233,600 vertices, one params write each per
|
|
195
|
+
onFrame, i.e. exactly what rule 1 recommends - measured 16.7 ms/frame (60 fps,
|
|
196
|
+
vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
|
|
197
|
+
(8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
|
|
198
|
+
indistinguishable from desktop. Measure on a target device if it matters; do
|
|
199
|
+
not infer it from the desktop number.
|
|
200
|
+
|
|
201
|
+
- **On a tiled GPU the budget is primitive count, not pixels.** Every point or
|
|
202
|
+
triangle costs the tiler regardless of how few pixels it covers. On that TV,
|
|
203
|
+
frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
|
|
204
|
+
35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
|
|
205
|
+
the fill - measured within one vsync of 1.0, and rendering into a
|
|
206
|
+
quarter-size target measured identical to full size. So for a heavy pass the
|
|
207
|
+
lever is fewer primitives; shrinking the target or the splat usually is not,
|
|
208
|
+
and coverage is far cheaper bought with point size than with more points.
|
|
209
|
+
- **A device's compositor can set the frame budget outright**, in which case
|
|
210
|
+
none of the above moves. That TV never presents faster than every 80 ms -
|
|
211
|
+
four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
|
|
212
|
+
scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
|
|
213
|
+
content-independent floor: if a trivial scene and a heavy one present at
|
|
214
|
+
nearly the same rate, you are compositor-bound and tuning the scene is
|
|
215
|
+
wasted effort.
|
|
216
|
+
- **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
|
|
217
|
+
that costs more than a refresh period does not silently pile up. If
|
|
218
|
+
`rasterQueue` sits persistently above 0 the raster thread is behind; if
|
|
219
|
+
`fenceTimeouts` climbs, the GPU is over its pacing budget.
|
|
220
|
+
|
|
221
|
+
Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
|
|
222
|
+
rasterQueue and fenceTimeouts. When those disagree with what the screen is
|
|
223
|
+
visibly doing, ground truth on Android is
|
|
224
|
+
`adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
|
|
225
|
+
timestamps - engine-reported phase timings can each be honest and still not add
|
|
226
|
+
up to the frame period, because work outside the frame call is not in them.
|
|
227
|
+
|
|
162
228
|
## Assets and app identity
|
|
163
229
|
|
|
164
230
|
- Everything under `assets/` ships with the app: the folder is collected
|
|
@@ -167,10 +233,13 @@ nearly free. Rules, in order of leverage:
|
|
|
167
233
|
from `flux:fs` - and treat them as read-only at runtime; writes belong in
|
|
168
234
|
plain relative paths, which land in the app's private data dir.
|
|
169
235
|
- Small text-like assets (SVG documents, shaders) can instead be inlined via
|
|
170
|
-
imports
|
|
171
|
-
`with { type: "
|
|
172
|
-
|
|
173
|
-
|
|
236
|
+
imports. An import attribute picks the form and works on any extension:
|
|
237
|
+
`import src from "./effect.glsl" with { type: "text" }` yields the file's
|
|
238
|
+
contents as a string, `with { type: "binary" }` yields a Uint8Array. `.svg`
|
|
239
|
+
is text-loaded with no attribute needed. Shader sources (`.glsl`/`.vert`/
|
|
240
|
+
`.frag`) are declared as text modules out of the box, so they typecheck
|
|
241
|
+
without setup. Inlining trades update granularity for zero I/O - keep big or
|
|
242
|
+
streamable files (audio, images) in `assets/`.
|
|
174
243
|
- Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
|
|
175
244
|
map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
|
|
176
245
|
replace the built-in defaults, `false` drops one, other keys add fonts
|
|
@@ -225,11 +294,12 @@ its tools over guessing at runtime state:
|
|
|
225
294
|
(e.g. keep a before/after pair to diff)
|
|
226
295
|
- get_gpu_resources: inventory of GPU state - textures (size, render target
|
|
227
296
|
or not), vertex buffers (byteLength), pipelines (draw count, attribute
|
|
228
|
-
layout, bound textures,
|
|
297
|
+
layout, bound textures, current uniform values - the most recent writes,
|
|
298
|
+
which the next frame or readback draws with)
|
|
229
299
|
- get_texture: any GPU texture read back as a PNG by id - atlases, data
|
|
230
|
-
textures, and shader/pipeline render targets alike (a render target
|
|
231
|
-
|
|
232
|
-
x/y/width/height
|
|
300
|
+
textures, and shader/pipeline render targets alike (a render target reads
|
|
301
|
+
as its current output, pending writes included, with no frame or snapshot
|
|
302
|
+
needed); crop with x/y/width/height
|
|
233
303
|
- get_buffer: a vertex-buffer range decoded to numbers (f32/u16/u8, 64 KiB
|
|
234
304
|
per call) - verify geometry after a writeBuffer instead of inferring it
|
|
235
305
|
from pixels
|
|
@@ -246,7 +316,9 @@ its tools over guessing at runtime state:
|
|
|
246
316
|
when you stop working - the user's own saves rely on it.
|
|
247
317
|
|
|
248
318
|
The tools need a running app: if list_clients is empty, ask the user to start
|
|
249
|
-
`bunx srt run src/index.tsx`.
|
|
319
|
+
`bunx srt run src/index.tsx`. The bridge dials the dev server's default port
|
|
320
|
+
(34884), so if the user started it with `--port N`, .mcp.json needs the same
|
|
321
|
+
flag: `"args": [..., "mcp", "--port", "N"]`.
|
|
250
322
|
|
|
251
323
|
- Permission prompts: agents typically ask approval per MCP tool. All of
|
|
252
324
|
these tools only talk to the local dev server the user started with
|
|
@@ -274,13 +346,17 @@ The tools need a running app: if list_clients is empty, ask the user to start
|
|
|
274
346
|
you will want repeatedly (a pose, a mode, a counter), bind a debug key that
|
|
275
347
|
logs it and read it back via get_logs.
|
|
276
348
|
- Key events are delivered ONLY to the focused node (no bubbling): call
|
|
277
|
-
setFocus(node.id) from the window's ref or onKeyDown never fires.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
349
|
+
setFocus(node.id) from the window's ref or onKeyDown never fires. `key` and
|
|
350
|
+
`code` are W3C KeyboardEvent values, so arrow keys arrive as "ArrowLeft"/
|
|
351
|
+
"ArrowRight"/"ArrowUp"/"ArrowDown" (not "Left"), alongside "Enter",
|
|
352
|
+
"Escape", "a".
|
|
353
|
+
- Idle frames skip work: shaders/pipelines only re-render when an input
|
|
354
|
+
changes - their own params/geometry, or a sampled texture (a data upload,
|
|
355
|
+
or a sampled target re-rendering; chains propagate automatically). Measure
|
|
356
|
+
performance while inputs are actually changing. get_snapshot works on an
|
|
357
|
+
idle client (it requests its own frame); a timeout means the JS thread is
|
|
358
|
+
busy or wedged. get_texture on a pipeline's render target reads the
|
|
359
|
+
current output, pending writes included, without needing a new frame.
|
|
284
360
|
- When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
|
|
285
361
|
in it before investigating, so you agree on the symptom. If you cannot see
|
|
286
362
|
the problem in the capture, say that instead of guessing.
|
package/scaffold/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"android": "srt client --android"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@solidrt/core": "0.0.
|
|
13
|
-
"@solidrt/components": "0.0.
|
|
12
|
+
"@solidrt/core": "0.0.40",
|
|
13
|
+
"@solidrt/components": "0.0.40"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
16
|
+
"@solidrt/cli": "0.0.40",
|
|
17
|
+
"@solidrt/flux-types": "0.0.40",
|
|
18
18
|
"typescript": "^7"
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/args.ts
CHANGED
|
@@ -21,6 +21,7 @@ export let { values, positionals } = parseArgs({
|
|
|
21
21
|
"data-root": { type: "string" },
|
|
22
22
|
client: { type: "string" },
|
|
23
23
|
server: { type: "string" },
|
|
24
|
+
port: { type: "string" },
|
|
24
25
|
android: { type: "boolean", default: false },
|
|
25
26
|
device: { type: "string" },
|
|
26
27
|
template: { type: "string", short: "t" },
|
|
@@ -95,6 +96,12 @@ export function validateArgs() {
|
|
|
95
96
|
if (values.server && command !== "client") {
|
|
96
97
|
usage("srt client --server <host[:port]> (--server is only valid with the client command)")
|
|
97
98
|
}
|
|
99
|
+
// --port moves the dev server off its default port, so it belongs to the
|
|
100
|
+
// commands that start one (`run`, `server`) or attach to one (`mcp`). A
|
|
101
|
+
// standalone client carries the port in --server <host:port> instead.
|
|
102
|
+
if (values.port !== undefined && command !== "run" && command !== "server" && command !== "mcp") {
|
|
103
|
+
usage("srt <run|server|mcp> --port <N> (--port is only valid with the run, server and mcp commands)")
|
|
104
|
+
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
export function printUsage() {
|
|
@@ -115,6 +122,7 @@ init options:
|
|
|
115
122
|
-t, --template <name> Start from a named template (skips the interactive picker)
|
|
116
123
|
|
|
117
124
|
run/server options:
|
|
125
|
+
--port <N> Dev server port (default: 34884)
|
|
118
126
|
--proxy-http Route fetch calls through the dev server (HTTP cache enabled)
|
|
119
127
|
--capture <file> Record connected clients' key events to a script file
|
|
120
128
|
--tunnel Accept ticket-paired clients through the p2p tunnel
|
|
@@ -130,6 +138,9 @@ client options:
|
|
|
130
138
|
--android Install and launch the client on a connected Android device
|
|
131
139
|
--device <serial> Target a specific adb device by serial or unique prefix
|
|
132
140
|
|
|
141
|
+
mcp options:
|
|
142
|
+
--port <N> Port of the dev server to attach to (default: 34884)
|
|
143
|
+
|
|
133
144
|
bundle options:
|
|
134
145
|
-f, --flux Bundle for the bare Flux runtime, without SolidJS (entry must be .ts|.js)
|
|
135
146
|
-d, --dev Use development build of SolidJS (default: production)
|
package/src/bundler.ts
CHANGED
|
@@ -11,40 +11,48 @@ import { state, print, requireBinary } from "./util"
|
|
|
11
11
|
import { buildManifest } from "./project"
|
|
12
12
|
|
|
13
13
|
// Babel plugin: rewrite `import data from "./x" with { type: "binary" }` into an
|
|
14
|
-
// inline Uint8Array of the file's bytes
|
|
14
|
+
// inline Uint8Array of the file's bytes, and `with { type: "text" }` into an
|
|
15
|
+
// inline string of its UTF-8 contents. The import attribute is invisible to
|
|
15
16
|
// Bun's bundler and its plugins in this Bun version, so we handle it here in the
|
|
16
17
|
// transform where the AST still carries it. Inlining (rather than emitting a
|
|
17
18
|
// separate asset) keeps a single bundle output and hands JS a Uint8Array, which
|
|
18
|
-
// is what createImage and friends expect.
|
|
19
|
-
// atob; for ASCII-extension files (.jpg/.png/...) we may add an
|
|
20
|
-
// path later.
|
|
21
|
-
|
|
19
|
+
// is what createImage and friends expect. Binary is decoded at runtime via the
|
|
20
|
+
// global atob; for ASCII-extension files (.jpg/.png/...) we may add an
|
|
21
|
+
// attribute-free path later. Both attributes work on any extension, so shader
|
|
22
|
+
// and other text sources inline by attribute the same way bytes do; `.svg` is
|
|
23
|
+
// additionally text-loaded without an attribute (see Bun's `loader` below).
|
|
24
|
+
function inlineImport({ types: t }: { types: any }) {
|
|
22
25
|
return {
|
|
23
26
|
visitor: {
|
|
24
27
|
ImportDeclaration(path: any, pluginState: any) {
|
|
25
28
|
let attrs = path.node.attributes ?? path.node.assertions
|
|
26
|
-
let
|
|
27
|
-
if (
|
|
29
|
+
let kind = attrs?.find((a: any) => a.key.name === "type")?.value.value
|
|
30
|
+
if (kind !== "binary" && kind !== "text") return
|
|
28
31
|
|
|
29
32
|
let def = path.node.specifiers.find((s: any) => s.type === "ImportDefaultSpecifier")
|
|
30
33
|
if (!def) {
|
|
31
34
|
throw path.buildCodeFrameError(
|
|
32
|
-
|
|
35
|
+
`A ${kind} import needs a default import: import data from "./file" with { type: "${kind}" }`,
|
|
33
36
|
)
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
let importer = pluginState.file.opts.filename as string
|
|
37
40
|
let abs = resolvePath(dirname(importer), path.node.source.value)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// var <local> = Uint8Array.from(atob("<b64>"), c => c.charCodeAt(0))
|
|
41
|
-
let expr =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
|
|
42
|
+
// text: var <local> = "<contents>"
|
|
43
|
+
// binary: var <local> = Uint8Array.from(atob("<b64>"), c => c.charCodeAt(0))
|
|
44
|
+
let expr =
|
|
45
|
+
kind === "text"
|
|
46
|
+
? t.stringLiteral(readFileSync(abs, "utf8"))
|
|
47
|
+
: t.callExpression(t.memberExpression(t.identifier("Uint8Array"), t.identifier("from")), [
|
|
48
|
+
t.callExpression(t.identifier("atob"), [t.stringLiteral(readFileSync(abs).toString("base64"))]),
|
|
49
|
+
t.arrowFunctionExpression(
|
|
50
|
+
[t.identifier("c")],
|
|
51
|
+
t.callExpression(t.memberExpression(t.identifier("c"), t.identifier("charCodeAt")), [
|
|
52
|
+
t.numericLiteral(0),
|
|
53
|
+
]),
|
|
54
|
+
),
|
|
55
|
+
])
|
|
48
56
|
path.replaceWith(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(def.local.name), expr)]))
|
|
49
57
|
},
|
|
50
58
|
},
|
|
@@ -65,7 +73,7 @@ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
|
|
|
65
73
|
|
|
66
74
|
// Bun build plugin that runs JSX/TSX through babel-preset-solid (universal
|
|
67
75
|
// generate, targeting @solidrt/core) plus the TS preset. Plain .js/.ts app
|
|
68
|
-
// modules take the same path (solid is a no-op without JSX) so
|
|
76
|
+
// modules take the same path (solid is a no-op without JSX) so inlineImport
|
|
69
77
|
// can rewrite their `with { type: "binary" }` imports too; dependency code
|
|
70
78
|
// (node_modules) skips the babel detour and keeps Bun's native loaders.
|
|
71
79
|
// With `babelMaps`, each file's transform map (original -> babel output) is
|
|
@@ -82,7 +90,7 @@ function solidPlugin(babelMaps?: Map<string, object>): BunPlugin {
|
|
|
82
90
|
filename: args.path,
|
|
83
91
|
sourceMaps: !!babelMaps,
|
|
84
92
|
presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
|
|
85
|
-
plugins: [jsx,
|
|
93
|
+
plugins: [jsx, inlineImport],
|
|
86
94
|
})
|
|
87
95
|
if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
|
|
88
96
|
return { contents: transforms?.code ?? "", loader: "js" }
|
package/src/commands/check.ts
CHANGED
|
@@ -60,11 +60,11 @@ function findTsc(fromDir: string): string | null {
|
|
|
60
60
|
|
|
61
61
|
// Typecheck the entry's program, not the enclosing project: a transient
|
|
62
62
|
// config extends the project's tsconfig and roots the program at the entry
|
|
63
|
-
// alone, so tsc checks exactly the
|
|
64
|
-
// are excluded by construction. The
|
|
65
|
-
// .srt-data (the dev-artifact dir; absolute
|
|
66
|
-
// only matters for type-package resolution,
|
|
67
|
-
// node_modules from there).
|
|
63
|
+
// alone (plus the project's ambient declarations), so tsc checks exactly the
|
|
64
|
+
// entry's import closure - unrelated files are excluded by construction. The
|
|
65
|
+
// config lives in the project-local .srt-data (the dev-artifact dir; absolute
|
|
66
|
+
// paths inside, so its location only matters for type-package resolution,
|
|
67
|
+
// which walks up to the project's node_modules from there).
|
|
68
68
|
export async function typecheck(root: string, entry: string): Promise<{ app: Diagnostic[]; hidden: number } | null> {
|
|
69
69
|
let tsconfig = join(root, "tsconfig.json")
|
|
70
70
|
if (!existsSync(tsconfig)) {
|
|
@@ -79,10 +79,14 @@ export async function typecheck(root: string, entry: string): Promise<{ app: Dia
|
|
|
79
79
|
let dataDir = join(root, ".srt-data")
|
|
80
80
|
mkdirSync(dataDir, { recursive: true })
|
|
81
81
|
let config = join(dataDir, `typecheck-${process.pid}.tsconfig.json`)
|
|
82
|
-
// include
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
82
|
+
// The include narrows the inherited one (files and include are unioned, so
|
|
83
|
+
// without it a base config's include would drag the whole project back into
|
|
84
|
+
// the program) down to declaration files only. Those are the one thing the
|
|
85
|
+
// entry's import closure cannot reach: an ambient `declare module "*.glsl"`
|
|
86
|
+
// applies precisely because nothing imports it, so entry-only rooting would
|
|
87
|
+
// silently drop it and every asset import would fail with TS2307. The
|
|
88
|
+
// pattern is relative to this config, which sits one level under the root.
|
|
89
|
+
await Bun.write(config, JSON.stringify({ extends: tsconfig, include: ["../**/*.d.ts"], files: [resolve(entry)] }))
|
|
86
90
|
try {
|
|
87
91
|
let proc = Bun.spawn([tsc, "-p", config, "--noEmit", "--pretty", "false"], {
|
|
88
92
|
cwd: root,
|
package/src/commands/mcp.ts
CHANGED
|
@@ -137,7 +137,7 @@ let TOOLS: {
|
|
|
137
137
|
name: "get_snapshot",
|
|
138
138
|
readOnly: true,
|
|
139
139
|
description:
|
|
140
|
-
"Capture a PNG image of any node in a running app client's render tree, by node id (get ids from get_render_tree). Returns the rendered pixels of that node's subtree, so you can see what the app actually drew. Capture the smallest node that contains what you are checking (e.g. the <texture> leaf itself) - that is exactly the content at its own pixel size; the window root is mostly empty layout around it and orders of magnitude more pixels. Reserve root captures for when layout/positioning itself is the question. The node must be currently mounted and
|
|
140
|
+
"Capture a PNG image of any node in a running app client's render tree, by node id (get ids from get_render_tree). Returns the rendered pixels of that node's subtree, so you can see what the app actually drew. Capture the smallest node that contains what you are checking (e.g. the <texture> leaf itself) - that is exactly the content at its own pixel size; the window root is mostly empty layout around it and orders of magnitude more pixels. Reserve root captures for when layout/positioning itself is the question. The node must be currently mounted and paint a non-zero box. Detached (`d-*`) nodes capture their painted box: their own `w`/`h` when set, else the box inherited from the nearest laid-out ancestor (the same box get_render_tree reports for them). Works on an idle client (the capture requests its own frame); a timeout means the client's JS thread is busy or wedged, not that the app is idle.",
|
|
141
141
|
inputSchema: {
|
|
142
142
|
nodeId: z
|
|
143
143
|
.number()
|
|
@@ -151,14 +151,14 @@ let TOOLS: {
|
|
|
151
151
|
name: "get_gpu_resources",
|
|
152
152
|
readOnly: true,
|
|
153
153
|
description:
|
|
154
|
-
"Inventory of a running app client's GPU resources: textures (id, size, whether a shader renders into it), vertex buffers (id, byteLength), and shader/pipeline targets (output textureId, kind, bufferId, topology, drawCount, depth, attribute layout, bound sampler texture ids,
|
|
154
|
+
"Inventory of a running app client's GPU resources: textures (id, size, whether a shader renders into it), vertex buffers (id, byteLength), and shader/pipeline targets (output textureId, kind, bufferId, topology, drawCount, depth, attribute layout, bound sampler texture ids, current uniform values - the most recent writes, which the next frame or readback draws with). Use it when the render tree is just a <texture> leaf and the interesting state lives behind it; follow up with get_texture or get_buffer to see contents.",
|
|
155
155
|
inputSchema: { client: CLIENT_ARG },
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
name: "get_texture",
|
|
159
159
|
readOnly: true,
|
|
160
160
|
description:
|
|
161
|
-
"Read back any GPU texture from a running app client as a PNG, by texture id (from get_gpu_resources, or the id returned by createImage/createShader/createPipeline in app code). Works on sampled textures (atlases, data textures) and shader/pipeline render targets alike, without needing a frame. Pass x/y/width/height to crop, e.g. one tile of an atlas.",
|
|
161
|
+
"Read back any GPU texture from a running app client as a PNG, by texture id (from get_gpu_resources, or the id returned by createImage/createShader/createPipeline in app code). Works on sampled textures (atlases, data textures) and shader/pipeline render targets alike, without needing a frame: a render target reads as its current output, with any pending params, geometry or sampled-input changes resolved first. Pass x/y/width/height to crop, e.g. one tile of an atlas.",
|
|
162
162
|
inputSchema: {
|
|
163
163
|
id: z.number().int().describe("Texture id, from get_gpu_resources"),
|
|
164
164
|
x: z.number().int().describe("Crop rect left edge in texture pixels (requires y, width, height)").optional(),
|
package/src/dev-server.ts
CHANGED
|
@@ -5,7 +5,23 @@ import { state, print, printErr, requireBinary, pipeAbovePrompt, shutdown } from
|
|
|
5
5
|
import { values } from "./args"
|
|
6
6
|
|
|
7
7
|
export const DEV_HOST = "127.0.0.1"
|
|
8
|
-
export const
|
|
8
|
+
export const DEFAULT_DEV_PORT = 0x8844
|
|
9
|
+
|
|
10
|
+
// The port every dev-server consumer dials: the spawned server, the local and
|
|
11
|
+
// Android clients' --dev-server address, and the MCP bridge's control base.
|
|
12
|
+
// Resolved once here, so --port needs no threading through those call sites.
|
|
13
|
+
function resolveDevPort(): number {
|
|
14
|
+
let raw = values.port
|
|
15
|
+
if (raw === undefined) return DEFAULT_DEV_PORT
|
|
16
|
+
let port = Number(raw)
|
|
17
|
+
if (!/^\d+$/.test(raw) || port < 1 || port > 65535) {
|
|
18
|
+
console.error(`Invalid --port value "${raw}": expected a port number between 1 and 65535`)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
return port
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export let DEV_PORT = resolveDevPort()
|
|
9
25
|
|
|
10
26
|
// The dev server itself is a flux script (packages/cli/server/), spawned by
|
|
11
27
|
// srt: bundling, file watching, and the repl stay here and drive the server
|
|
@@ -157,8 +173,23 @@ async function bundleServer(): Promise<string> {
|
|
|
157
173
|
return outfile
|
|
158
174
|
}
|
|
159
175
|
|
|
176
|
+
// The server runs as a separate flux process, so a port clash surfaces there as
|
|
177
|
+
// a bare non-zero exit ("Dev server exited unexpectedly (1)"). Claim the port
|
|
178
|
+
// here first to turn that into the actual reason, and into the fix.
|
|
179
|
+
function requireFreePort(port: number) {
|
|
180
|
+
try {
|
|
181
|
+
// Default hostname: the server binds every interface, so probe the same way.
|
|
182
|
+
let probe = Bun.serve({ port, fetch: () => new Response() })
|
|
183
|
+
probe.stop(true)
|
|
184
|
+
} catch {
|
|
185
|
+
printErr(`[cli] Port ${port} is already in use; start on another port with --port <N>`)
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
160
190
|
export async function startServer() {
|
|
161
191
|
let flux = requireBinary("flux")
|
|
192
|
+
requireFreePort(DEV_PORT)
|
|
162
193
|
let script = await bundleServer()
|
|
163
194
|
|
|
164
195
|
let lanAddress = Object.values(networkInterfaces())
|