@solidrt/cli 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/AGENTS.md +68 -38
  2. package/README.md +86 -15
  3. package/agents/assets.md +19 -5
  4. package/agents/debugging.md +173 -17
  5. package/dist/console.srtapp +73916 -51
  6. package/dist/demos/3d/assets/icon.svg +23 -0
  7. package/dist/demos/3d/package.json +9 -0
  8. package/dist/demos/3d/the-third-dimension/the-third-dimension.srt.js +7528 -0
  9. package/dist/demos/components/assets/icon.png +0 -0
  10. package/dist/demos/components/assets/icon.svg +23 -0
  11. package/dist/demos/components/gallery/gallery.srt.js +14087 -0
  12. package/dist/demos/components/package.json +9 -0
  13. package/dist/server.js +3936 -0
  14. package/package.json +10 -11
  15. package/src/android/docs.md +21 -0
  16. package/src/android/main.ts +286 -0
  17. package/src/{bundler.ts → bundle/bundler.ts} +70 -58
  18. package/src/bundle/docs.md +12 -0
  19. package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
  20. package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +3 -3
  21. package/src/check/docs.md +10 -0
  22. package/src/check/main.ts +85 -0
  23. package/src/{commands/check.ts → check/typecheck.ts} +11 -41
  24. package/src/client/docs.md +9 -0
  25. package/src/client/main.ts +33 -0
  26. package/src/console/docs.md +14 -0
  27. package/src/console/main.ts +21 -0
  28. package/src/demo/docs.md +33 -0
  29. package/src/demo/main.ts +69 -0
  30. package/src/init/docs.md +11 -0
  31. package/src/{commands/init.ts → init/main.ts} +21 -15
  32. package/src/init/scaffold/AGENTS.md +100 -0
  33. package/src/init/scaffold/package.json +23 -0
  34. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  35. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  36. package/src/lib/args.ts +194 -0
  37. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  38. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  39. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  40. package/src/lib/mode.ts +77 -0
  41. package/src/{project.ts → lib/project.ts} +109 -61
  42. package/src/lib/registry.ts +120 -0
  43. package/src/lib/server-bundle.ts +24 -0
  44. package/src/lib/usage.ts +117 -0
  45. package/src/lib/util.ts +36 -0
  46. package/src/main.ts +109 -31
  47. package/src/mcp/docs.md +22 -0
  48. package/src/mcp/main.ts +719 -0
  49. package/src/pack/docs.md +18 -0
  50. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  51. package/src/{commands/pack.ts → pack/main.ts} +28 -17
  52. package/src/{packer.ts → pack/trailer.ts} +32 -42
  53. package/src/render/docs.md +19 -0
  54. package/src/{commands/render.ts → render/main.ts} +17 -15
  55. package/src/server/args.ts +126 -0
  56. package/src/server/binaries.ts +47 -0
  57. package/src/server/config.ts +54 -0
  58. package/{server → src/server}/control.ts +239 -81
  59. package/src/server/docs.md +51 -0
  60. package/src/server/line-editor.ts +200 -0
  61. package/src/server/main.ts +476 -0
  62. package/src/server/mode.ts +92 -0
  63. package/src/server/rebuild.ts +90 -0
  64. package/src/server/registry.ts +138 -0
  65. package/src/server/repl.ts +223 -0
  66. package/src/server/state.ts +54 -0
  67. package/{server → src/server}/tsconfig.json +1 -1
  68. package/{server → src/server}/tunnel.ts +6 -6
  69. package/src/server/watcher.ts +121 -0
  70. package/src/tool/main.ts +70 -0
  71. package/src/types/bundle.d.ts +24 -0
  72. package/src/types/control.d.ts +90 -0
  73. package/src/types/registry.d.ts +16 -0
  74. package/scaffold/AGENTS.md +0 -185
  75. package/scaffold/package.json +0 -22
  76. package/scaffold/templates/components/icon.tsx +0 -48
  77. package/scaffold/templates/default/icon.tsx +0 -48
  78. package/server/main.ts +0 -308
  79. package/server/rebuild.ts +0 -76
  80. package/server/state.ts +0 -94
  81. package/src/args.ts +0 -210
  82. package/src/bundle-cli.ts +0 -13
  83. package/src/commands/client.ts +0 -34
  84. package/src/commands/mcp.ts +0 -617
  85. package/src/commands/server.ts +0 -73
  86. package/src/dev-android.ts +0 -176
  87. package/src/dev-client.ts +0 -29
  88. package/src/dev-server.ts +0 -302
  89. package/src/repl.ts +0 -249
  90. package/src/util.ts +0 -122
  91. package/src/watcher.ts +0 -73
  92. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  93. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  94. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  95. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  96. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  97. /package/{server → src/server}/cache.ts +0 -0
  98. /package/{server → src/server}/proxy.ts +0 -0
  99. /package/{server → src/server}/qr.ts +0 -0
  100. /package/{server → src/server}/remap.ts +0 -0
@@ -0,0 +1,121 @@
1
+ import { dir } from "flux:fs"
2
+ import { state } from "./state"
3
+ import { dirname } from "./mode"
4
+ import { rebuildAndBroadcast, showBuildFailure } from "./rebuild"
5
+
6
+ // Reload-on-save. Not a directory watch: the watch set is the bundle's own
7
+ // input list (BundleOutput.inputs: the app's modules, the dependency modules
8
+ // bundled in, inlined files, package.json, tsconfig.json), so a source file
9
+ // the running app does not import, or a dependency it does not use, never
10
+ // triggers anything, while an edit to a workspace package it does import
11
+ // does. Each input's parent directory is watched (never the file itself: an
12
+ // editor's atomic save replaces the inode, which a file watch would lose)
13
+ // and events are matched by name within that directory. On top of that, in
14
+ // project mode, the whole assets/ tree: the manifest lists the tree, so any
15
+ // change to it is a new version. While the last build failed the input list
16
+ // is gone, so the source directory is watched as a whole until a build
17
+ // succeeds again (the missing-import case: the file the build waits for is
18
+ // not in any list yet). Every hit debounces into the one
19
+ // rebuildAndBroadcast() path, which re-arms the watch from the new inputs.
20
+ // Paused by the MCP pause_watch tool while an agent edits (state.watchPaused):
21
+ // changes made meanwhile are not pushed; the agent's reload is.
22
+
23
+ const DEBOUNCE_MS = 100
24
+ const SOURCE_EXT = /\.(tsx?|jsx?)$/
25
+
26
+ let offs: (() => void)[] = []
27
+ let timer: ReturnType<typeof setTimeout> | null = null
28
+ let changed = new Set<string>()
29
+ let building = false
30
+ let dirty = false
31
+
32
+ function basename(path: string): string {
33
+ return path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1)
34
+ }
35
+
36
+ // A path with a dot component (below `root` when given) is never a source:
37
+ // .srt-data in particular is this server's own output, and rebuilding on it
38
+ // would rebuild forever. node_modules is the same in file mode, where the
39
+ // source directory may be a project root.
40
+ function isToolingPath(path: string, root: string): boolean {
41
+ let rel = path.startsWith(root) ? path.slice(root.length) : path
42
+ return rel.split(/[\\/]/).some((part) => part.startsWith(".") || part === "node_modules")
43
+ }
44
+
45
+ /** Arm the watch from a build's inputs; null (a failed build) watches the source tree as a whole. */
46
+ export function armWatcher(inputs: string[] | null) {
47
+ stopWatcher()
48
+ let config = state.config
49
+ let hit = (path: string) => onChange(path)
50
+ try {
51
+ if (inputs) {
52
+ let byDir = new Map<string, Set<string>>()
53
+ for (let input of inputs) {
54
+ let parent = dirname(input)
55
+ let names = byDir.get(parent)
56
+ if (!names) byDir.set(parent, (names = new Set()))
57
+ names.add(basename(input))
58
+ }
59
+ for (let [parent, names] of byDir) {
60
+ offs.push(dir(parent).watch((e) => names.has(basename(e.path)) && hit(e.path)))
61
+ }
62
+ } else {
63
+ let root = config.sourceDir
64
+ offs.push(
65
+ dir(root).watch((e) => SOURCE_EXT.test(e.path) && !isToolingPath(e.path, root) && hit(e.path), { recursive: true }),
66
+ )
67
+ }
68
+ if (config.projectDir) {
69
+ let assets = `${config.projectDir}/assets`
70
+ dir(assets)
71
+ .exists()
72
+ .then((exists) => {
73
+ if (exists && offs.length) offs.push(dir(assets).watch((e) => !isToolingPath(e.path, assets) && hit(e.path), { recursive: true }))
74
+ })
75
+ }
76
+ } catch (e) {
77
+ console.error(`[cli] Watch failed: ${e instanceof Error ? e.message : e}`)
78
+ stopWatcher()
79
+ }
80
+ }
81
+
82
+ /** Drop every watch; the engine loop can go idle. */
83
+ export function stopWatcher() {
84
+ for (let off of offs) off()
85
+ offs = []
86
+ if (timer !== null) clearTimeout(timer)
87
+ timer = null
88
+ changed.clear()
89
+ }
90
+
91
+ function onChange(path: string) {
92
+ if (state.watchPaused) return
93
+ changed.add(path)
94
+ if (building) {
95
+ dirty = true
96
+ return
97
+ }
98
+ if (timer !== null) clearTimeout(timer)
99
+ timer = setTimeout(() => void rebuild(), DEBOUNCE_MS)
100
+ }
101
+
102
+ // One rebuild per burst; a change that lands while the bundle runs queues
103
+ // exactly one more, since the running build cannot have seen it.
104
+ async function rebuild() {
105
+ timer = null
106
+ building = true
107
+ let root = state.config.projectDir ?? state.config.sourceDir
108
+ let names = [...changed].map((p) => (p.startsWith(root) ? p.slice(root.length + 1) : p))
109
+ changed.clear()
110
+ console.log(`[cli] Change detected: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ` (+${names.length - 3})` : ""}`)
111
+ let error = await rebuildAndBroadcast()
112
+ if (error) {
113
+ console.error(error)
114
+ showBuildFailure()
115
+ }
116
+ building = false
117
+ if (dirty) {
118
+ dirty = false
119
+ onChange(names[0] ?? root)
120
+ }
121
+ }
@@ -0,0 +1,70 @@
1
+ import { readdirSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { source, toolArgs } from "../lib/args"
4
+
5
+ // srt tool: the tools the installed @solidrt packages ship - build-time
6
+ // helpers that belong to an extension, not to core (a model converter in
7
+ // @solidrt/3d, say). Discovery is by convention, like demos: every
8
+ // `<package>/tools/<name>.ts` is a tool named `<package>/<name>`, run with
9
+ // bun in the caller's cwd with the arguments after the tool name passed
10
+ // through untouched. srt knows nothing about what a tool does; a tool
11
+ // prints its own usage.
12
+
13
+ type Tool = { name: string; script: string }
14
+
15
+ const SCOPE = join("node_modules", "@solidrt")
16
+
17
+ /** Every tool installed here, sorted so the listing is stable. The cwd and
18
+ * nothing above it - the same rule demos follow, so this lists what THIS
19
+ * project installed. */
20
+ function discover(): Tool[] {
21
+ let tools: Tool[] = []
22
+ for (let pkg of names(SCOPE)) {
23
+ let dir = join(SCOPE, pkg, "tools")
24
+ for (let file of names(dir)) {
25
+ if (!file.endsWith(".ts")) continue
26
+ tools.push({ name: `${pkg}/${file.slice(0, -".ts".length)}`, script: resolve(dir, file) })
27
+ }
28
+ }
29
+ return tools
30
+ }
31
+
32
+ // A missing folder is the normal case (most packages ship no tools), so it
33
+ // reads as an empty one rather than an error.
34
+ function names(dir: string): string[] {
35
+ try {
36
+ return readdirSync(dir).sort()
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ function list(tools: Tool[]) {
43
+ for (let tool of tools) console.log(` ${tool.name}`)
44
+ }
45
+
46
+ export async function main(): Promise<void> {
47
+ let tools = discover()
48
+ if (tools.length === 0) {
49
+ console.error(`No tools installed in ${process.cwd()} (looked in ${SCOPE}/*/tools/)`)
50
+ process.exit(1)
51
+ }
52
+
53
+ if (source === undefined) {
54
+ list(tools)
55
+ console.log("\nRun one with: srt tool <pkg>/<name> [arguments]")
56
+ return
57
+ }
58
+
59
+ let picked = tools.find((t) => t.name === source)
60
+ if (!picked) {
61
+ console.error(`No such tool: ${source}`)
62
+ list(tools)
63
+ process.exit(1)
64
+ }
65
+
66
+ let proc = Bun.spawn([process.execPath, picked.script, ...toolArgs], {
67
+ stdio: ["inherit", "inherit", "inherit"],
68
+ })
69
+ process.exit(await proc.exited)
70
+ }
@@ -0,0 +1,24 @@
1
+ // What `srt bundle --json` (src/bundle/main.ts, a bun subprocess of the
2
+ // dev server) writes to stdout as one JSON object and the dev server
3
+ // (src/server/rebuild.ts) reads back. src/types/ holds the type-only contracts
4
+ // between the two programs: each has its own tsconfig (bun types on one
5
+ // side, flux types on the other), both include this folder, so nothing here
6
+ // may reference either runtime.
7
+
8
+ export type BundleOutput = {
9
+ code: string
10
+ /** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
11
+ map: string | null
12
+ /** Version manifest JSON for this bundle; clients install pushes under its hash. */
13
+ manifest: string
14
+ /** The app's isolate bundles, one per "use isolate" module, in id order; maps dev builds only. */
15
+ isolates: { id: string; code: string; map: string | null }[]
16
+ /**
17
+ * Every file this bundle was built from, absolute and sorted: the app's
18
+ * modules, the dependency modules bundled in, files inlined by import
19
+ * attribute, and the project's package.json / tsconfig.json. The assets/
20
+ * tree is not listed (the manifest carries it). What the dev server
21
+ * watches for reload-on-save.
22
+ */
23
+ inputs: string[]
24
+ }
@@ -0,0 +1,90 @@
1
+ // The control API (/__control__/, server/control.ts) response shapes the
2
+ // MCP bridge (src/mcp/main.ts) reads fields from. Only the server-held
3
+ // answers are shaped here; queries forwarded to a client return whatever the
4
+ // client runtime answers. See bundle.d.ts for the folder.
5
+
6
+ export type ClientEntry = {
7
+ id: number
8
+ platform: string
9
+ /** The client runtime's version (git describe). */
10
+ version: string
11
+ /** Build profile: debug or release. */
12
+ profile: string
13
+ capabilities: string[]
14
+ /** Query kinds this client's runtime answers (empty on runtimes that predate
15
+ * the advertisement); dev tools plan their verification surface from it. */
16
+ queries: string[]
17
+ /** The client's storage tree on its own machine (`<data-root>/client<N>`
18
+ * for a dev client, the install folder for the launcher or a packed app),
19
+ * or null when it runs without writable storage or predates the field. */
20
+ clientDir: string | null
21
+ /** Whether its stats overlay is drawn (see POST /stats). */
22
+ stats: boolean
23
+ /** Its time scale as the client last reported it to POST /clock: 0 paused,
24
+ * 1 real time. Back to 1 on every push (a reload restarts the clock). */
25
+ timeScale: number
26
+ /** The client's process id on its own machine. */
27
+ pid: number | null
28
+ /** The runtime binary it runs. */
29
+ execPath: string | null
30
+ /** Its machine's hostname. */
31
+ host: string | null
32
+ /** The OS as a person names it ("Linux (Ubuntu 24.04)", "Android 15 on Pixel 9 Pro"). */
33
+ os: string | null
34
+ /** The kernel version. */
35
+ kernel: string | null
36
+ /** The SDL video driver ("wayland", "x11", "android", "offscreen", ...). */
37
+ videoDriver: string | null
38
+ /** The GPU strings as GL reports them; null on a client that connected
39
+ * before its GL context existed (a reconnect fills it in). */
40
+ gpu: GpuInfo | null
41
+ }
42
+
43
+ export type GpuInfo = { vendor: string; renderer: string; version: string }
44
+
45
+ /** GET /clients */
46
+ export type ClientsResponse = {
47
+ /** Identity of this server run: client ids and log cursors are only valid within one. */
48
+ generation: number
49
+ key: string
50
+ mode: "project" | "file"
51
+ entry: string
52
+ projectDir: string | null
53
+ /** Whether the user's own input is muted on every client (see /mute). */
54
+ userInputMuted: boolean
55
+ /** Whether reload-on-save is paused (see /watch). */
56
+ watchPaused: boolean
57
+ clients: ClientEntry[]
58
+ }
59
+
60
+ export type LogEntry = { seq: number; at: number; client: number; level: string; text: string }
61
+
62
+ /** GET /logs */
63
+ export type LogsResponse = {
64
+ /** Consecutive identical entries come back as one, with `repeats`. */
65
+ entries: (LogEntry & { repeats?: number })[]
66
+ /** The next `since` cursor. */
67
+ latest: number
68
+ generation: number
69
+ }
70
+
71
+ /** POST /reload */
72
+ export type ReloadResponse = { ok: true; clients: number }
73
+
74
+ /** POST /load: the canonical entry now served. */
75
+ export type LoadResponse = { ok: true; entry: string; clients: number }
76
+
77
+ /** POST /mute: the mute state now in force and the clients told. */
78
+ export type MuteResponse = { ok: true; active: boolean; clients: number }
79
+
80
+ /** POST /stats: whether the overlay is now on, and on how many clients. */
81
+ export type StatsResponse = { ok: true; active: boolean; clients: number }
82
+
83
+ /** POST /watch: whether reload-on-save is now active. */
84
+ export type WatchResponse = { ok: true; active: boolean }
85
+
86
+ /** GET /snapshot and /texture: png by default, RGBA8 bytes with format=raw. */
87
+ export type ImageResponse = { width: number; height: number; pngBase64?: string; rgbaBase64?: string }
88
+
89
+ /** Every non-2xx answer. */
90
+ export type ControlError = { error: string }
@@ -0,0 +1,16 @@
1
+ // The dev server registry record: ~/.solidrt/servers/<key hash>/live.json,
2
+ // written by the server (server/registry.ts) and read by srt client, srt mcp
3
+ // and the launcher (src/lib/registry.ts). See bundle.d.ts for the folder.
4
+
5
+ export type LiveRecord = {
6
+ pid: number
7
+ port: number
8
+ address: string
9
+ /** The canonical project root or file path (see src/lib/mode.ts). */
10
+ key: string
11
+ mode: "project" | "file"
12
+ entry: string
13
+ projectDir: string | null
14
+ /** ISO timestamp of the bind. */
15
+ started: string
16
+ }
@@ -1,185 +0,0 @@
1
- # SolidRT app - agent notes
2
-
3
- This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
4
- runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
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.
8
-
9
- ## Levels: core, and frameworks on top
10
-
11
- - @solidrt/core is the low-level foundation: host intrinsics (`<window>`,
12
- `<view>`, `<text>`, the detached `d-*` drawing primitives) with flat props
13
- that feed the layout and paint engine directly. An app can be written
14
- entirely at this level.
15
- - Higher-level component frameworks build on core. @solidrt/components is
16
- the first-party one: themed widgets (Window, View, Text, Button,
17
- ScrollView, SafeArea, ...) with the `layout={{...}}`/`style={{...}}` prop
18
- split. It is not privileged - a framework is just functions returning core
19
- JSX, and an app can use a third-party one or grow its own.
20
-
21
- Match the level the code you are editing already uses. package.json shows
22
- the choice this app made: if no component framework is among the
23
- dependencies, the app is core-only - do not add one for a change core
24
- covers.
25
-
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)
53
-
54
- <!-- Claude Code auto-imports these; other tools read the paths above. -->
55
- @./node_modules/solid-js/CHEATSHEET.md
56
- <!-- components:begin -->
57
- @./node_modules/@solidrt/components/AGENTS.md
58
- <!-- components:end -->
59
- @./node_modules/@solidrt/core/AGENTS.md
60
- @./node_modules/@solidrt/cli/AGENTS.md
61
-
62
- ## The things assistants get wrong (this is not React/DOM)
63
-
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>`
74
- (from @solidrt/components) or the core `<window>` - it throws otherwise.
75
- 3. Components' `Window`/`View` do not paint on their own; they only paint
76
- when you set `style.backgroundColor`/`borderColor` etc - there is no
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
111
- true even when you build almost entirely with @solidrt/components -
112
- components are plain functions returning core JSX). Solid peer deps are
113
- pinned betas - do not bump them casually.
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
117
- string or number value - and name those in ALL_CAPS.
118
- 11. Reading a signal/prop/store at the top level of a component body (not
119
- inside JSX, a `createMemo`, or an effect's compute phase) reads it
120
- untracked - it silently freezes at the initial value instead of updating
121
- on change. `createEffect` takes two arguments now: `(compute, apply)`.
122
- `compute` is the tracked read phase; `apply(value, prev)` runs untracked
123
- and is where side effects/DOM-equivalent writes belong. The old
124
- single-arg `createEffect(fn)` form is gone - using it is an error.
125
- 12. A scroll container (ScrollView, or anything on createScroll) needs an
126
- explicit main-axis size - a height, or flex inside a sized parent. With
127
- neither it resolves to 0 and its content silently vanishes; maxHeight
128
- alone does not size it (the auto size it would clamp is already 0). The
129
- runtime warns when this happens.
130
- 13. Text `lineHeight` is a MULTIPLIER of fontSize (the theme uses 1.3-1.6),
131
- not pixels. A CSS-reflex value like 22 makes each line box 22x the font
132
- size: the text becomes blank space and the parent balloons.
133
- 14. Signal writes flush on a microtask: a handler that sets a signal and
134
- immediately reads it back gets the OLD value. Read the new value in an
135
- effect, or call `flush()` (from @solidjs/signals) to force it through.
136
- 15. Portals cannot mount during the app's initial render: a Modal (or any
137
- createPortal content) that is visible at first mount throws "no mount
138
- target". Gate it behind a signal that starts false and open it after
139
- startup - overlay content is opened, not born open.
140
- 16. An element-valued prop (children, a content/icon slot) compiles to a
141
- getter that builds a fresh native subtree on EVERY read, and a subtree
142
- that is never inserted is never freed - native nodes are not garbage
143
- collected, so what is only wasted work in DOM Solid is a permanent
144
- memory leak here. Read such props exactly once, at the place they are
145
- mounted. To inspect children (a typeof probe, counting), resolve them
146
- first with the children() helper (re-exported from @solidrt/core) and
147
- probe the resolved memo - never `typeof props.children` on the raw prop.
148
- 17. Writing a signal or store from inside an owned scope - a component body, a
149
- `createMemo`, an effect's compute phase - throws
150
- `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Calling a loader/init function in
151
- the component body that sets state is the classic React / Solid 1.x
152
- reflex and hits this every time. Move the write into an event handler, an
153
- effect's apply phase, or `onSettled`; opt in narrowly with
154
- `createSignal(v, { ownedWrite: true })` for a signal that genuinely is
155
- internal state.
156
- 18. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
157
- "contain" | "none" | "scale-down", CSS object-fit semantics, centered)
158
- plus a box via `layout` in any form - numbers, pct(), flex. Without
159
- `fit`, only NUMERIC layout sizes reach the image; `width: pct(100)`
160
- alone draws at intrinsic size. `fit="cover"` is the answer for the
161
- ported-web hero-image/thumbnail pattern.
162
-
163
- ## Run / verify
164
-
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.
171
- - bunx srt run src/index.tsx - dev server + window (needs a display)
172
- - bunx srt check src/index.tsx - exit 0 means it compiles and the app's
173
- types hold (dependency-internal type errors are hidden). Builds in memory:
174
- writes nothing and never triggers a dev-server reload, so use this while
175
- iterating - `srt bundle` writes output files and reloads connected clients
176
- - bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
177
- render to PNG frames (proves it renders; see the cli AGENTS.md for where the
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.
@@ -1,22 +0,0 @@
1
- {
2
- "name": "solidrt-app",
3
- "type": "module",
4
- "private": true,
5
- "scripts": {
6
- "dev": "srt run src/index.tsx",
7
- "pack": "srt pack -o out src/index.tsx",
8
- "server": "srt server",
9
- "client": "srt client",
10
- "android": "srt client --android"
11
- },
12
- "dependencies": {
13
- "@solidrt/core": "0.0.51",
14
- "@solidrt/components": "0.0.51",
15
- "@solidrt/3d": "0.0.51"
16
- },
17
- "devDependencies": {
18
- "@solidrt/cli": "0.0.51",
19
- "@solidrt/flux-types": "0.0.51",
20
- "typescript": "^7"
21
- }
22
- }
@@ -1,48 +0,0 @@
1
- // The animated SolidRT logo: each segment fades in, then out, on its own delay.
2
- import { For, onFrame, createSignal, createLinearGradient } from "@solidrt/core"
3
-
4
- const FADE = 360
5
-
6
- let SEGMENTS = [
7
- { base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
8
- { base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
9
- { base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
10
- { base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
11
- { base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
12
- { base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
13
- { base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
14
- ]
15
-
16
- let LAST = SEGMENTS[SEGMENTS.length - 1]!.base
17
- let IN_DONE = LAST + FADE
18
- let CYCLE = IN_DONE + LAST + FADE
19
-
20
- let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
21
- let ease = (t: number) => 1 - (1 - t) * (1 - t)
22
- let byte = (x: number) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, "0")
23
-
24
- let fill = (seg: (typeof SEGMENTS)[number], a: number) =>
25
- createLinearGradient(0, 0, 1, 1, [
26
- { offset: 0, color: seg.light + byte(a * 255) },
27
- { offset: 1, color: seg.dark + byte(a * 255) },
28
- ])
29
-
30
- export function Icon() {
31
- let [clock, setClock] = createSignal(0)
32
- let start = -1
33
- onFrame((t) => (start < 0 && (start = t), setClock((t - start) % CYCLE)))
34
- return (
35
- <view width={300} height={300} viewBox={[100, 100]}>
36
- <For each={SEGMENTS}>
37
- {(seg) => {
38
- let a = () => {
39
- let t = clock(), end = IN_DONE + seg.base + FADE
40
- if (t < seg.base || t >= end) return 0
41
- return ease(Math.min(clamp((t - seg.base) / FADE), clamp((end - t) / FADE)))
42
- }
43
- return <d-path d={seg.d} color={fill(seg, a())} />
44
- }}
45
- </For>
46
- </view>
47
- )
48
- }
@@ -1,48 +0,0 @@
1
- // The animated SolidRT logo: each segment fades in, then out, on its own delay.
2
- import { For, onFrame, createSignal, createLinearGradient } from "@solidrt/core"
3
-
4
- const FADE = 360
5
-
6
- let SEGMENTS = [
7
- { base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
8
- { base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
9
- { base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
10
- { base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
11
- { base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
12
- { base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
13
- { base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
14
- ]
15
-
16
- let LAST = SEGMENTS[SEGMENTS.length - 1]!.base
17
- let IN_DONE = LAST + FADE
18
- let CYCLE = IN_DONE + LAST + FADE
19
-
20
- let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
21
- let ease = (t: number) => 1 - (1 - t) * (1 - t)
22
- let byte = (x: number) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, "0")
23
-
24
- let fill = (seg: (typeof SEGMENTS)[number], a: number) =>
25
- createLinearGradient(0, 0, 1, 1, [
26
- { offset: 0, color: seg.light + byte(a * 255) },
27
- { offset: 1, color: seg.dark + byte(a * 255) },
28
- ])
29
-
30
- export function Icon() {
31
- let [clock, setClock] = createSignal(0)
32
- let start = -1
33
- onFrame((t) => (start < 0 && (start = t), setClock((t - start) % CYCLE)))
34
- return (
35
- <view width={300} height={300} viewBox={[100, 100]}>
36
- <For each={SEGMENTS}>
37
- {(seg) => {
38
- let a = () => {
39
- let t = clock(), end = IN_DONE + seg.base + FADE
40
- if (t < seg.base || t >= end) return 0
41
- return ease(Math.min(clamp((t - seg.base) / FADE), clamp((end - t) / FADE)))
42
- }
43
- return <d-path d={seg.d} color={fill(seg, a())} />
44
- }}
45
- </For>
46
- </view>
47
- )
48
- }