@solidrt/cli 0.0.50 → 0.0.52
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/AGENTS.md +79 -36
- package/README.md +86 -15
- package/agents/assets.md +46 -0
- package/agents/debugging.md +294 -0
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +11 -10
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +163 -76
- package/src/bundle/docs.md +12 -0
- package/src/bundle/main.ts +203 -0
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -34
- package/src/client/docs.md +9 -0
- package/src/client/main.ts +33 -0
- package/src/console/docs.md +14 -0
- package/src/console/main.ts +21 -0
- package/src/demo/docs.md +27 -0
- package/src/demo/main.ts +67 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +31 -25
- package/src/init/scaffold/AGENTS.md +98 -0
- package/src/init/scaffold/package.json +23 -0
- package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
- package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
- package/src/lib/args.ts +194 -0
- package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
- package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
- package/src/{fonts.ts → lib/fonts.ts} +12 -26
- package/src/lib/mode.ts +77 -0
- package/src/{project.ts → lib/project.ts} +109 -61
- package/src/lib/registry.ts +120 -0
- package/src/lib/server-bundle.ts +24 -0
- package/src/lib/usage.ts +117 -0
- package/src/lib/util.ts +36 -0
- package/src/main.ts +109 -31
- package/src/mcp/docs.md +22 -0
- package/src/mcp/main.ts +719 -0
- package/src/pack/docs.md +18 -0
- package/src/{pack-folder.ts → pack/layout.ts} +13 -22
- package/src/pack/main.ts +86 -0
- package/src/pack/trailer.ts +97 -0
- package/src/render/docs.md +19 -0
- package/src/render/main.ts +49 -0
- package/src/server/args.ts +126 -0
- package/src/server/binaries.ts +47 -0
- package/src/server/config.ts +54 -0
- package/{server → src/server}/control.ts +246 -78
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +473 -0
- package/src/server/mode.ts +92 -0
- package/src/server/rebuild.ts +90 -0
- package/src/server/registry.ts +138 -0
- package/src/server/remap.ts +60 -0
- package/src/server/repl.ts +223 -0
- package/src/server/state.ts +54 -0
- package/{server → src/server}/tsconfig.json +1 -1
- package/{server → src/server}/tunnel.ts +6 -6
- package/src/server/watcher.ts +121 -0
- package/src/tool/main.ts +70 -0
- package/src/types/bundle.d.ts +24 -0
- package/src/types/control.d.ts +90 -0
- package/src/types/registry.d.ts +16 -0
- package/scaffold/AGENTS.md +0 -630
- package/scaffold/package.json +0 -22
- package/scaffold/templates/components/icon.tsx +0 -48
- package/scaffold/templates/default/icon.tsx +0 -48
- package/server/main.ts +0 -308
- package/server/rebuild.ts +0 -68
- package/server/remap.ts +0 -47
- package/server/state.ts +0 -93
- package/src/args.ts +0 -212
- package/src/bundle-cli.ts +0 -13
- package/src/commands/bundle.ts +0 -76
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -603
- package/src/commands/pack.ts +0 -65
- package/src/commands/render.ts +0 -24
- package/src/commands/server.ts +0 -73
- package/src/dev-android.ts +0 -176
- package/src/dev-client.ts +0 -29
- package/src/dev-server.ts +0 -302
- package/src/packer.ts +0 -103
- package/src/repl.ts +0 -233
- package/src/util.ts +0 -121
- package/src/watcher.ts +0 -69
- /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
- /package/src/{prompt.ts → init/prompt.ts} +0 -0
- /package/{scaffold → src/init/scaffold}/gitignore +0 -0
- /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
- /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
- /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
- /package/{server → src/server}/cache.ts +0 -0
- /package/{server → src/server}/proxy.ts +0 -0
- /package/{server → src/server}/qr.ts +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs"
|
|
2
|
+
import { Glob } from "bun"
|
|
3
|
+
import { join, resolve } from "node:path"
|
|
4
|
+
import { source } from "../lib/args"
|
|
5
|
+
import { bundleWith } from "../bundle/bundler"
|
|
6
|
+
import { findProject } from "../lib/project"
|
|
7
|
+
import { findProjectRoot, reportTypes, typecheck } from "./typecheck"
|
|
8
|
+
|
|
9
|
+
// srt check: verify the app without side effects. Bundles in memory (nothing
|
|
10
|
+
// written, so no dev-server reload fires and no build outputs land in the
|
|
11
|
+
// project) and typechecks with the project's own tsc (typecheck.ts).
|
|
12
|
+
|
|
13
|
+
// Check one entry: bundle in memory, then typecheck. Returns whether it passed.
|
|
14
|
+
async function checkEntry(entry: string): Promise<boolean> {
|
|
15
|
+
let failed = false
|
|
16
|
+
// check verifies trees of entries from one cwd, so it is the one command
|
|
17
|
+
// that walks up from each entry to its project.
|
|
18
|
+
let result = await bundleWith({ entry, dev: true, minify: false, project: findProject(entry)?.dir ?? null })
|
|
19
|
+
if (!result) {
|
|
20
|
+
// bundleWith already printed the compile errors.
|
|
21
|
+
failed = true
|
|
22
|
+
}
|
|
23
|
+
let root = findProjectRoot(entry)
|
|
24
|
+
if (!root) {
|
|
25
|
+
console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
|
|
26
|
+
} else {
|
|
27
|
+
let types = await typecheck(root, entry)
|
|
28
|
+
if (types && reportTypes(types)) failed = true
|
|
29
|
+
}
|
|
30
|
+
return !failed
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The entries `srt check <folder>` covers, relative to the folder (a bare
|
|
34
|
+
// `srt check` is `srt check .`): the app itself, its own examples, and in
|
|
35
|
+
// a monorepo every example app, package example and package demo. The same
|
|
36
|
+
// set CI gates, so one call at the repo root answers "did I break any
|
|
37
|
+
// example" before pushing. Entries, not files: a source no entry imports is
|
|
38
|
+
// not checked.
|
|
39
|
+
const CHECK_ALL_GLOBS = [
|
|
40
|
+
"src/index.tsx",
|
|
41
|
+
"examples/*.tsx",
|
|
42
|
+
"examples/*/src/index.tsx",
|
|
43
|
+
"packages/*/examples/*.tsx",
|
|
44
|
+
"packages/*/demos/src/*.tsx",
|
|
45
|
+
]
|
|
46
|
+
function discoverEntries(root: string): string[] {
|
|
47
|
+
let entries: string[] = []
|
|
48
|
+
for (let pattern of CHECK_ALL_GLOBS) {
|
|
49
|
+
entries.push(...[...new Glob(pattern).scanSync({ cwd: root })].map((e) => join(root, e)))
|
|
50
|
+
}
|
|
51
|
+
return entries.sort()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function main() {
|
|
55
|
+
let target = source ?? "."
|
|
56
|
+
if (!existsSync(target)) {
|
|
57
|
+
// Without this, the missing file surfaces later as an internal ENOENT
|
|
58
|
+
// stack trace (scandir/Bun.build), which reads as a CLI bug - the common
|
|
59
|
+
// cause is just running from the wrong directory.
|
|
60
|
+
console.error(`No such entry: ${target} (resolved from ${process.cwd()})`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
if (!statSync(target).isDirectory()) {
|
|
64
|
+
if (!(await checkEntry(target))) process.exit(1)
|
|
65
|
+
console.log("Check passed")
|
|
66
|
+
process.exit(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let entries = discoverEntries(target)
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
console.error(`No entries found under ${resolve(target)} (looked for ${CHECK_ALL_GLOBS.join(", ")})`)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
let failures: string[] = []
|
|
75
|
+
for (let entry of entries) {
|
|
76
|
+
console.log(`== ${entry}`)
|
|
77
|
+
if (!(await checkEntry(entry))) failures.push(entry)
|
|
78
|
+
}
|
|
79
|
+
if (failures.length > 0) {
|
|
80
|
+
console.error(`${failures.length} of ${entries.length} entries failed:\n ${failures.join("\n ")}`)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
console.log(`Check passed (${entries.length} entries)`)
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync } from "node:fs"
|
|
2
2
|
import { dirname, join, resolve } from "node:path"
|
|
3
|
-
import {
|
|
4
|
-
import { bundleWith, findIsolateModules } from "../bundler"
|
|
3
|
+
import { findIsolateModules } from "../bundle/bundler"
|
|
5
4
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// hidden, not the caller's problem to wade through.
|
|
5
|
+
// Typechecking an app entry with the project's own tsc, reporting only
|
|
6
|
+
// diagnostics in app code. Used by `srt check` (the hard gate) and by the dev
|
|
7
|
+
// server's startup check (it spawns `srt check <entry>`). @solidrt packages ship
|
|
8
|
+
// raw .ts sources, so a strict consumer config surfaces their internal errors
|
|
9
|
+
// too; those are counted and hidden, not the caller's problem to wade through.
|
|
12
10
|
|
|
13
11
|
// Walk up from the entry to the enclosing project (tsconfig.json or, failing
|
|
14
12
|
// that, package.json).
|
|
@@ -27,7 +25,7 @@ export function findProjectRoot(entry: string): string | null {
|
|
|
27
25
|
|
|
28
26
|
// One tsc --pretty false diagnostic: the "path(line,col): error TS...: ..."
|
|
29
27
|
// head line plus any indented continuation lines.
|
|
30
|
-
type Diagnostic = { head: string; lines: string[]; inDependencies: boolean }
|
|
28
|
+
export type Diagnostic = { head: string; lines: string[]; inDependencies: boolean }
|
|
31
29
|
|
|
32
30
|
function parseDiagnostics(output: string): Diagnostic[] {
|
|
33
31
|
let diagnostics: Diagnostic[] = []
|
|
@@ -58,6 +56,8 @@ function findTsc(fromDir: string): string | null {
|
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
59
|
+
export type TypecheckResult = { app: Diagnostic[]; hidden: number }
|
|
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
63
|
// alone (plus the project's ambient declarations), so tsc checks exactly the
|
|
@@ -65,7 +65,7 @@ function findTsc(fromDir: string): string | null {
|
|
|
65
65
|
// config lives in the project-local .srt-data (the dev-artifact dir; absolute
|
|
66
66
|
// paths inside, so its location only matters for type-package resolution,
|
|
67
67
|
// which walks up to the project's node_modules from there).
|
|
68
|
-
export async function typecheck(root: string, entry: string): Promise<
|
|
68
|
+
export async function typecheck(root: string, entry: string): Promise<TypecheckResult | null> {
|
|
69
69
|
let tsconfig = join(root, "tsconfig.json")
|
|
70
70
|
if (!existsSync(tsconfig)) {
|
|
71
71
|
console.warn("Typecheck skipped: no tsconfig.json above the entry")
|
|
@@ -111,7 +111,7 @@ export async function typecheck(root: string, entry: string): Promise<{ app: Dia
|
|
|
111
111
|
// whether app-code errors were found. Callers pass repl-aware printers when
|
|
112
112
|
// the output lands over a live prompt (the dev-server startup check).
|
|
113
113
|
export function reportTypes(
|
|
114
|
-
types:
|
|
114
|
+
types: TypecheckResult,
|
|
115
115
|
log: (...args: any[]) => void = console.log,
|
|
116
116
|
error: (...args: any[]) => void = console.error,
|
|
117
117
|
): boolean {
|
|
@@ -125,26 +125,3 @@ export function reportTypes(
|
|
|
125
125
|
else log("Types OK")
|
|
126
126
|
return false
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
export async function runCheckCommand() {
|
|
130
|
-
let entry = source!
|
|
131
|
-
let failed = false
|
|
132
|
-
|
|
133
|
-
let result = await bundleWith({ entry, dev: true, minify: false })
|
|
134
|
-
if (!result) {
|
|
135
|
-
// bundleWith already printed the compile errors.
|
|
136
|
-
failed = true
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let root = findProjectRoot(entry)
|
|
140
|
-
if (!root) {
|
|
141
|
-
console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
|
|
142
|
-
} else {
|
|
143
|
-
let types = await typecheck(root, entry)
|
|
144
|
-
if (types && reportTypes(types)) failed = true
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (failed) process.exit(1)
|
|
148
|
-
console.log("Check passed")
|
|
149
|
-
process.exit(0)
|
|
150
|
-
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# srt client
|
|
2
|
+
|
|
3
|
+
{{ usage client }}
|
|
4
|
+
|
|
5
|
+
The client half of [srt run](../server/docs.md), on its own. Without flags
|
|
6
|
+
it attaches to the dev server of the project (or file) in the current
|
|
7
|
+
directory; `--port` picks a local server by port and `--server` names any
|
|
8
|
+
address, which is how a second machine joins a server started with `--lan`.
|
|
9
|
+
A phone or tablet is [srt android](../android/docs.md).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { values, port, clientStorageArgs } from "../lib/args"
|
|
2
|
+
import { requireBinary, run } from "../lib/util"
|
|
3
|
+
import { resolveFromCwd } from "../lib/registry"
|
|
4
|
+
|
|
5
|
+
// Standalone solidrt-go client (no dev server of its own). Without flags it
|
|
6
|
+
// attaches to the dev server of the project (or file) in the current
|
|
7
|
+
// directory, resolved from the registry; --port picks a local server by port
|
|
8
|
+
// and --server names any address. A device is `srt android`.
|
|
9
|
+
export async function main() {
|
|
10
|
+
let runner = requireBinary("solidrt-go")
|
|
11
|
+
let args: string[] = [...clientStorageArgs()]
|
|
12
|
+
if (values.size) args.push("--size", values.size)
|
|
13
|
+
let address: string
|
|
14
|
+
if (values.server) {
|
|
15
|
+
if (!values.server.includes(":")) {
|
|
16
|
+
console.error(`--server needs host:port (got "${values.server}"); dev servers have no fixed port`)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
address = values.server
|
|
20
|
+
} else if (port !== undefined) {
|
|
21
|
+
address = `127.0.0.1:${port}`
|
|
22
|
+
} else {
|
|
23
|
+
let resolved = await resolveFromCwd(process.cwd())
|
|
24
|
+
if (!resolved.ok) {
|
|
25
|
+
console.error(resolved.message)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
address = `127.0.0.1:${resolved.record.port}`
|
|
29
|
+
}
|
|
30
|
+
args.push("--dev-server", address)
|
|
31
|
+
let exit = await run(runner, args)
|
|
32
|
+
process.exit(exit)
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# srt console
|
|
2
|
+
|
|
3
|
+
{{ usage console }}
|
|
4
|
+
|
|
5
|
+
The dev console: the dev servers running on this machine, the clients
|
|
6
|
+
attached to each, and a button to start a local client for one. It is not
|
|
7
|
+
served by a dev server: the CLI ships the console pre-compiled
|
|
8
|
+
(`dist/console.srtapp`, see [srt pack --app](../pack/docs.md)) and this
|
|
9
|
+
command starts the `solidrt` runner on that file, so it opens at once and
|
|
10
|
+
runs the same whether a project is open or not.
|
|
11
|
+
|
|
12
|
+
In a checkout the file is built with `make -C packages/cli
|
|
13
|
+
dist/console.srtapp`; rerun it after editing `apps/console`. Everything
|
|
14
|
+
after `--` reaches the console as its arguments.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { appArgs } from "../lib/args"
|
|
4
|
+
import { requireBinary, run } from "../lib/util"
|
|
5
|
+
|
|
6
|
+
// srt console: the dev console (apps/console) as a packed app on the plain
|
|
7
|
+
// runner. The console is pre-compiled: dist/console.srtapp (manifest +
|
|
8
|
+
// bytecode + assets, `srt pack --app`), built by `make -C packages/cli
|
|
9
|
+
// dist/console.srtapp` - the release workflow before publishing, a checkout
|
|
10
|
+
// after editing the console. This command builds nothing: it points the
|
|
11
|
+
// runner at the file, and the runner is used in place, so a signed runner
|
|
12
|
+
// stays signed.
|
|
13
|
+
export async function main() {
|
|
14
|
+
let app = fileURLToPath(new URL("../../dist/console.srtapp", import.meta.url))
|
|
15
|
+
if (!existsSync(app)) {
|
|
16
|
+
console.error("Console not built: run make -C packages/cli dist/console.srtapp")
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
let exit = await run(requireBinary("solidrt"), [app, ...appArgs])
|
|
20
|
+
process.exit(exit)
|
|
21
|
+
}
|
package/src/demo/docs.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# srt demo
|
|
2
|
+
|
|
3
|
+
{{ usage demo }}
|
|
4
|
+
|
|
5
|
+
The demos that come with the packages this project installs. Without an
|
|
6
|
+
argument it lists them, numbered:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
1 3d/the-third-dimension
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`srt demo 1` runs that one, exactly as [srt run](../server/docs.md) runs any
|
|
13
|
+
project: a dev server, a local client window, reload on save, and the same
|
|
14
|
+
server options (`--lan` to reach it from a device, `--port`, `--tunnel`). The qualified
|
|
15
|
+
name works too (`srt demo 3d/the-third-dimension`), which is the stable way to
|
|
16
|
+
name one in a script - the numbers follow the list, and the list follows what
|
|
17
|
+
is installed.
|
|
18
|
+
|
|
19
|
+
A package keeps its demos in `demos/`, and that folder is one project: its own
|
|
20
|
+
package.json, one `assets/` folder, and `src/<name>.tsx` per demo. So the
|
|
21
|
+
demos of one package share a dev server and a port - start a second one while
|
|
22
|
+
the first is up and it says so. Demos from different packages run side by
|
|
23
|
+
side.
|
|
24
|
+
|
|
25
|
+
The demos ship inside the packages, so `node_modules` is where they are found:
|
|
26
|
+
`srt demo` looks in the current directory and nowhere above it. Run it from
|
|
27
|
+
the project root.
|
package/src/demo/main.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs"
|
|
2
|
+
import { join, resolve } from "node:path"
|
|
3
|
+
import { source } from "../lib/args"
|
|
4
|
+
|
|
5
|
+
// srt demo: the demos the installed @solidrt packages ship. A package's
|
|
6
|
+
// demos are ONE project (`<package>/demos/`: package.json, one assets/, and
|
|
7
|
+
// src/<name>.tsx per demo), so a demo runs as the project it lives in - this
|
|
8
|
+
// file only lists and resolves, and main.ts starts the ordinary dev server
|
|
9
|
+
// with its cwd set to that project. Nothing downstream knows about demos.
|
|
10
|
+
|
|
11
|
+
export type Demo = { name: string; cwd: string; entry: string }
|
|
12
|
+
|
|
13
|
+
const SCOPE = join("node_modules", "@solidrt")
|
|
14
|
+
|
|
15
|
+
/** Every demo installed here, sorted so the printed numbers are stable.
|
|
16
|
+
* The cwd and nothing above it - the same rule the server's mode resolution
|
|
17
|
+
* follows (server/mode.ts), so this lists what THIS project installed. */
|
|
18
|
+
function discover(): Demo[] {
|
|
19
|
+
let demos: Demo[] = []
|
|
20
|
+
for (let pkg of names(SCOPE)) {
|
|
21
|
+
let dir = join(SCOPE, pkg, "demos")
|
|
22
|
+
for (let file of names(join(dir, "src"))) {
|
|
23
|
+
if (!file.endsWith(".tsx")) continue
|
|
24
|
+
demos.push({ name: `${pkg}/${file.slice(0, -".tsx".length)}`, cwd: resolve(dir), entry: join("src", file) })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return demos
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A missing folder is the normal case (most packages ship no demos), so it
|
|
31
|
+
// reads as an empty one rather than an error.
|
|
32
|
+
function names(dir: string): string[] {
|
|
33
|
+
try {
|
|
34
|
+
return readdirSync(dir).sort()
|
|
35
|
+
} catch {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function list(demos: Demo[]) {
|
|
41
|
+
for (let [i, demo] of demos.entries()) console.log(` ${String(i + 1).padStart(2)} ${demo.name}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Lists (no argument) or resolves one demo for the dev server to serve. */
|
|
45
|
+
export async function main(): Promise<{ cwd: string; entry: string } | undefined> {
|
|
46
|
+
let demos = discover()
|
|
47
|
+
if (demos.length === 0) {
|
|
48
|
+
console.error(`No demos installed in ${process.cwd()} (looked in ${SCOPE}/*/demos/src/)`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (source === undefined) {
|
|
53
|
+
list(demos)
|
|
54
|
+
console.log("\nRun one with: srt demo <number>")
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A number picks from the printed list; the qualified name it printed
|
|
59
|
+
// works too, so a demo can be named in a script without a fixed index.
|
|
60
|
+
let picked = /^\d+$/.test(source) ? demos[Number(source) - 1] : demos.find((d) => d.name === source)
|
|
61
|
+
if (!picked) {
|
|
62
|
+
console.error(`No such demo: ${source}`)
|
|
63
|
+
list(demos)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
return { cwd: picked.cwd, entry: picked.entry }
|
|
67
|
+
}
|
package/src/init/docs.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# srt init
|
|
2
|
+
|
|
3
|
+
{{ usage init }}
|
|
4
|
+
|
|
5
|
+
Scaffolds a new SolidRT project into a new (empty) folder: package.json,
|
|
6
|
+
tsconfig.json, AGENTS.md, a starter src/index.tsx, an empty assets/
|
|
7
|
+
(everything in it ships with the app) and an `.mcp.json`, then installs the
|
|
8
|
+
dependencies. The picker offers extensions on an interactive terminal.
|
|
9
|
+
|
|
10
|
+
The public entry point is `bun create solidrt <dir>`, which forwards here and
|
|
11
|
+
needs nothing installed first.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
-
import { source, values } from "../args"
|
|
4
|
-
import { multiselect, note, text } from "
|
|
3
|
+
import { source, values } from "../lib/args"
|
|
4
|
+
import { multiselect, note, text } from "./prompt"
|
|
5
5
|
|
|
6
6
|
const DEFAULT_NAME = "solidrt-app"
|
|
7
7
|
|
|
8
|
-
const SCAFFOLD_DIR = join(import.meta.dir, "
|
|
8
|
+
const SCAFFOLD_DIR = join(import.meta.dir, "scaffold")
|
|
9
9
|
const TEMPLATES_DIR = join(SCAFFOLD_DIR, "templates")
|
|
10
10
|
|
|
11
11
|
// Shared project files written for every template. Sources live in
|
|
12
|
-
// cli/scaffold/. The .gitignore is stored there as `gitignore` because npm
|
|
12
|
+
// cli/src/init/scaffold/. The .gitignore is stored there as `gitignore` because npm
|
|
13
13
|
// strips files literally named `.gitignore` from published packages, so it is
|
|
14
14
|
// renamed on the way out. The per-template src/ comes from scaffold/templates/.
|
|
15
15
|
const TEMPLATE_FILES: Array<{ from: string; to: string }> = [
|
|
@@ -30,11 +30,27 @@ function packageName(dir: string): string {
|
|
|
30
30
|
|
|
31
31
|
const DEFAULT_TEMPLATE = "default"
|
|
32
32
|
|
|
33
|
+
// One AGENTS.md serves every template, so the lines that point an agent at
|
|
34
|
+
// an extension's docs are fenced between `<!-- <key>:begin/end -->` markers:
|
|
35
|
+
// with the extension selected only the markers go, without it the block goes
|
|
36
|
+
// too, so an app never ships references to files that are not installed.
|
|
37
|
+
function resolveMarkers(text: string, extensions: Extension[]): string {
|
|
38
|
+
for (let ext of EXTENSIONS) {
|
|
39
|
+
let selected = extensions.includes(ext)
|
|
40
|
+
let block = new RegExp(`^<!-- ${ext.key}:begin -->\\n[\\s\\S]*?^<!-- ${ext.key}:end -->\\n`, "gm")
|
|
41
|
+
let marker = new RegExp(`^<!-- ${ext.key}:(?:begin|end) -->\\n`, "gm")
|
|
42
|
+
text = selected ? text.replace(marker, "") : text.replace(block, "")
|
|
43
|
+
}
|
|
44
|
+
return text
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// Optional packages an app can opt into on top of core. Each maps to a
|
|
34
48
|
// dependency in the scaffold package.json (kept when selected, removed
|
|
35
|
-
// otherwise)
|
|
49
|
+
// otherwise), to a marker key fencing its lines in scaffold/AGENTS.md, and
|
|
50
|
+
// optionally to a starter under scaffold/templates/.
|
|
36
51
|
interface Extension {
|
|
37
52
|
pkg: string
|
|
53
|
+
key: string
|
|
38
54
|
template?: string
|
|
39
55
|
description: string
|
|
40
56
|
}
|
|
@@ -42,30 +58,18 @@ interface Extension {
|
|
|
42
58
|
const EXTENSIONS: Extension[] = [
|
|
43
59
|
{
|
|
44
60
|
pkg: "@solidrt/components",
|
|
61
|
+
key: "components",
|
|
45
62
|
template: "components",
|
|
46
63
|
description: "component framework: widgets, theming, navigation",
|
|
47
64
|
},
|
|
48
|
-
{ pkg: "@solidrt/
|
|
65
|
+
{ pkg: "@solidrt/2d", key: "2d", description: "general purpose 2D library" },
|
|
66
|
+
{ pkg: "@solidrt/3d", key: "3d", description: "general purpose 3D library" },
|
|
49
67
|
]
|
|
50
68
|
|
|
51
|
-
// Resolve which extensions the app takes: an
|
|
52
|
-
//
|
|
69
|
+
// Resolve which extensions the app takes: an interactive picker on a TTY,
|
|
70
|
+
// else none (core only). Extensions are ordinary dependencies, so a script
|
|
71
|
+
// adds them afterwards with `bun add`.
|
|
53
72
|
async function resolveExtensions(): Promise<Extension[]> {
|
|
54
|
-
let raw = values.with
|
|
55
|
-
if (raw !== undefined) {
|
|
56
|
-
let names = raw.split(",").map((n) => n.trim()).filter(Boolean)
|
|
57
|
-
let chosen: Extension[] = []
|
|
58
|
-
for (let name of names) {
|
|
59
|
-
let found = EXTENSIONS.find((e) => e.pkg === name)
|
|
60
|
-
if (!found) {
|
|
61
|
-
let all = EXTENSIONS.map((e) => e.pkg).join(", ")
|
|
62
|
-
console.error(`!! Unknown extension "${name}"; choose from: ${all}`)
|
|
63
|
-
process.exit(1)
|
|
64
|
-
}
|
|
65
|
-
if (!chosen.includes(found)) chosen.push(found)
|
|
66
|
-
}
|
|
67
|
-
return chosen
|
|
68
|
-
}
|
|
69
73
|
if (!process.stdin.isTTY) return []
|
|
70
74
|
// Core is the runtime every app has, so it is not a choice.
|
|
71
75
|
note("@solidrt/core is always included", "Packages")
|
|
@@ -82,7 +86,7 @@ function resolveTemplate(extensions: Extension[]): string {
|
|
|
82
86
|
return extensions.find((e) => e.template)?.template ?? DEFAULT_TEMPLATE
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
export async function
|
|
89
|
+
export async function main() {
|
|
86
90
|
// The target folder comes from the positional arg, or an interactive prompt
|
|
87
91
|
// (defaulting to a suggested name) when omitted.
|
|
88
92
|
let dir = source
|
|
@@ -109,7 +113,9 @@ export async function runInitCommand() {
|
|
|
109
113
|
for (let { from, to } of TEMPLATE_FILES) {
|
|
110
114
|
let dest = join(dir, to)
|
|
111
115
|
await mkdir(dirname(dest), { recursive: true })
|
|
112
|
-
|
|
116
|
+
let body: string | Buffer = await readFile(join(SCAFFOLD_DIR, from))
|
|
117
|
+
if (to === "AGENTS.md") body = resolveMarkers(body.toString("utf8"), extensions)
|
|
118
|
+
await writeFile(dest, body)
|
|
113
119
|
console.log(` Write ${to}`)
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -0,0 +1,98 @@
|
|
|
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 extensions 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
|
+
- Extensions build on core. The first-party ones are @solidrt/components
|
|
16
|
+
(UI components), @solidrt/2d (2D graphics) and @solidrt/3d (3D graphics).
|
|
17
|
+
None is privileged - an extension is just functions returning core JSX,
|
|
18
|
+
and an app can use a third-party one or grow its own.
|
|
19
|
+
|
|
20
|
+
Match the level the code you are editing already uses. package.json shows
|
|
21
|
+
the choice this app made: only the extensions listed there are installed -
|
|
22
|
+
do not add one for a change core covers.
|
|
23
|
+
|
|
24
|
+
Convention: every @solidrt package describes itself in
|
|
25
|
+
node_modules/@solidrt/<name>/AGENTS.md, carries its prose in docs/, and
|
|
26
|
+
ships working code in examples/. AGENTS.md is the reference for the package;
|
|
27
|
+
open it before using anything from the package, and read the extensions'
|
|
28
|
+
files only when they are installed.
|
|
29
|
+
|
|
30
|
+
## Read before you
|
|
31
|
+
|
|
32
|
+
The authoritative references ship inside the installed packages. Open the one
|
|
33
|
+
that matches the work; do not work from memory of what a web framework does.
|
|
34
|
+
|
|
35
|
+
- write any reactive code (signals, effects, control flow) ->
|
|
36
|
+
node_modules/solid-js/CHEATSHEET.md - the SolidJS 2.0 model
|
|
37
|
+
- touch elements, props, events, gestures or text ->
|
|
38
|
+
node_modules/@solidrt/core/AGENTS.md, and
|
|
39
|
+
node_modules/@solidrt/core/src/types.d.ts + jsx-runtime.d.ts (source of truth)
|
|
40
|
+
- style a screen: a background, a gradient, a shadow, an effect, vector art,
|
|
41
|
+
a chart -> node_modules/@solidrt/core/agents/painting.md
|
|
42
|
+
- write per-frame code, an animation, or anything writing properties in a
|
|
43
|
+
loop -> node_modules/@solidrt/core/agents/performance.md
|
|
44
|
+
- use an installed extension (UI components, 2D, 3D) ->
|
|
45
|
+
node_modules/@solidrt/<name>/AGENTS.md and its examples/
|
|
46
|
+
- debug a running app, or drive it over MCP to verify a change ->
|
|
47
|
+
node_modules/@solidrt/cli/agents/debugging.md
|
|
48
|
+
- add an asset or font, set the app's identity, or build for distribution ->
|
|
49
|
+
node_modules/@solidrt/cli/agents/assets.md
|
|
50
|
+
- run, bundle, typecheck or render headlessly ->
|
|
51
|
+
node_modules/@solidrt/cli/AGENTS.md
|
|
52
|
+
- copy a working pattern -> node_modules/@solidrt/core/examples/ (see its
|
|
53
|
+
README.md index)
|
|
54
|
+
|
|
55
|
+
<!-- Claude Code auto-imports these; other tools read the paths above. -->
|
|
56
|
+
@./node_modules/solid-js/CHEATSHEET.md
|
|
57
|
+
@./node_modules/@solidrt/core/AGENTS.md
|
|
58
|
+
<!-- components:begin -->
|
|
59
|
+
@./node_modules/@solidrt/components/AGENTS.md
|
|
60
|
+
<!-- components:end -->
|
|
61
|
+
<!-- 2d:begin -->
|
|
62
|
+
@./node_modules/@solidrt/2d/AGENTS.md
|
|
63
|
+
<!-- 2d:end -->
|
|
64
|
+
<!-- 3d:begin -->
|
|
65
|
+
@./node_modules/@solidrt/3d/AGENTS.md
|
|
66
|
+
<!-- 3d:end -->
|
|
67
|
+
@./node_modules/@solidrt/cli/AGENTS.md
|
|
68
|
+
|
|
69
|
+
## The three traps that cost the most (this is not React/DOM)
|
|
70
|
+
|
|
71
|
+
Each package's AGENTS.md carries its own trap list; these three are
|
|
72
|
+
platform-wide and bite in every app:
|
|
73
|
+
|
|
74
|
+
1. Reading a signal/prop/store at the top level of a component body (not
|
|
75
|
+
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
76
|
+
untracked - it silently freezes at the initial value. `createEffect` is
|
|
77
|
+
two-argument here: `(compute, apply)`; the Solid 1.x single-arg form does
|
|
78
|
+
not track.
|
|
79
|
+
2. Writing a signal or store from inside an owned scope - a component body,
|
|
80
|
+
a `createMemo`, an effect's compute phase - throws
|
|
81
|
+
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev. Move the write into an event
|
|
82
|
+
handler, an effect's apply phase, `onSettled`, or an `untrack` block; opt
|
|
83
|
+
in with `createSignal(v, { ownedWrite: true })` for internal state.
|
|
84
|
+
3. An element-valued prop (children, a content/icon slot) builds a fresh
|
|
85
|
+
native subtree on EVERY read, and an uninserted subtree is never freed -
|
|
86
|
+
a permanent memory leak, not wasted work. Read such props exactly once,
|
|
87
|
+
where they mount; inspect them through the `children()` helper.
|
|
88
|
+
|
|
89
|
+
## Run / verify
|
|
90
|
+
|
|
91
|
+
- FIRST check whether a dev server and a client are already running and
|
|
92
|
+
build against those; do not start a second `srt run` when one is up.
|
|
93
|
+
- The dev loop (reload, logs, snapshots, the holds on reload-on-save and on
|
|
94
|
+
the user's input), typechecking, headless rendering and the MCP tools:
|
|
95
|
+
node_modules/@solidrt/cli/AGENTS.md and its agents/debugging.md. Read it
|
|
96
|
+
before the first reload. The `.mcp.json` here is Claude Code's convention;
|
|
97
|
+
if your client lists no `solidrt` tools, debugging.md has the entry to add
|
|
98
|
+
to its own config.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solidrt-app",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "srt run",
|
|
7
|
+
"pack": "srt pack -o out",
|
|
8
|
+
"server": "srt server",
|
|
9
|
+
"client": "srt client",
|
|
10
|
+
"android": "srt android"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@solidrt/core": "0.0.52",
|
|
14
|
+
"@solidrt/components": "0.0.52",
|
|
15
|
+
"@solidrt/2d": "0.0.52",
|
|
16
|
+
"@solidrt/3d": "0.0.52"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@solidrt/cli": "0.0.52",
|
|
20
|
+
"@solidrt/flux-types": "0.0.52",
|
|
21
|
+
"typescript": "^7"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Components template: built with the @solidrt/components framework.
|
|
2
|
-
import { render, createLinearGradient } from "@solidrt/core"
|
|
2
|
+
import { render, createLinearGradient, Logo } from "@solidrt/core"
|
|
3
3
|
import { Window, SafeArea, View, Text } from "@solidrt/components"
|
|
4
|
-
import { Icon } from "./icon"
|
|
5
4
|
|
|
6
5
|
function App() {
|
|
7
6
|
let backgroundColor = createLinearGradient(0, 0, 1, 1, [
|
|
@@ -13,7 +12,7 @@ function App() {
|
|
|
13
12
|
<Window title="The Solid Runtime" style={{ backgroundColor }}>
|
|
14
13
|
<SafeArea>
|
|
15
14
|
<View layout={{ flex: 1, gap: 20, alignItems: "center", justifyContent: "center" }}>
|
|
16
|
-
<
|
|
15
|
+
<Logo size={300} animation="loop" />
|
|
17
16
|
<Text layout={{ fontSize: 40 }} style={{ color: "#ccc" }}>
|
|
18
17
|
The Solid Runtime
|
|
19
18
|
</Text>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { render, createLinearGradient, safeArea } from "@solidrt/core"
|
|
2
|
-
import { Icon } from "./icon"
|
|
1
|
+
import { render, createLinearGradient, safeArea, Logo } from "@solidrt/core"
|
|
3
2
|
|
|
4
3
|
function App() {
|
|
5
4
|
let backgroundColor = createLinearGradient(0, 0, 1, 1, [
|
|
@@ -18,7 +17,7 @@ function App() {
|
|
|
18
17
|
paddingTop={safeArea().top}
|
|
19
18
|
paddingBottom={safeArea().bottom}
|
|
20
19
|
>
|
|
21
|
-
<
|
|
20
|
+
<Logo size={300} animation="loop" />
|
|
22
21
|
<text fontSize={40} color="#ccc">The Solid Runtime</text>
|
|
23
22
|
</view>
|
|
24
23
|
</window>
|