@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.
- package/AGENTS.md +68 -38
- package/README.md +86 -15
- package/agents/assets.md +19 -5
- package/agents/debugging.md +173 -17
- package/dist/console.srtapp +73916 -51
- package/dist/demos/3d/assets/icon.svg +23 -0
- package/dist/demos/3d/package.json +9 -0
- package/dist/demos/3d/the-third-dimension/the-third-dimension.srt.js +7528 -0
- package/dist/demos/components/assets/icon.png +0 -0
- package/dist/demos/components/assets/icon.svg +23 -0
- package/dist/demos/components/gallery/gallery.srt.js +14087 -0
- package/dist/demos/components/package.json +9 -0
- package/dist/server.js +3936 -0
- package/package.json +10 -11
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +70 -58
- package/src/bundle/docs.md +12 -0
- package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
- package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +3 -3
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -41
- 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 +33 -0
- package/src/demo/main.ts +69 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +21 -15
- package/src/init/scaffold/AGENTS.md +100 -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/{commands/pack.ts → pack/main.ts} +28 -17
- package/src/{packer.ts → pack/trailer.ts} +32 -42
- package/src/render/docs.md +19 -0
- package/src/{commands/render.ts → render/main.ts} +17 -15
- 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 +239 -81
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +476 -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/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 -185
- 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 -76
- package/server/state.ts +0 -94
- package/src/args.ts +0 -210
- package/src/bundle-cli.ts +0 -13
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -617
- 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/repl.ts +0 -249
- package/src/util.ts +0 -122
- package/src/watcher.ts +0 -73
- /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
- /package/{server → src/server}/remap.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
|
13
13
|
"src/",
|
|
14
|
-
"
|
|
15
|
-
"scaffold/",
|
|
14
|
+
"dist/",
|
|
16
15
|
"fonts/",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"agents/"
|
|
19
18
|
],
|
|
20
19
|
"dependencies": {
|
|
21
20
|
"@babel/core": "^7.0.0",
|
|
@@ -25,22 +24,22 @@
|
|
|
25
24
|
"@jridgewell/remapping": "^2.3.0",
|
|
26
25
|
"@jridgewell/trace-mapping": "^0.3.25",
|
|
27
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
-
"babel-
|
|
27
|
+
"@solidjs/babel-plugin": "2.0.0-rc.3",
|
|
29
28
|
"bonjour-service": "^1.4.0",
|
|
30
29
|
"qrcode-generator": "^2.0.4",
|
|
31
30
|
"zod": "^4.4.3"
|
|
32
31
|
},
|
|
33
32
|
"optionalDependencies": {
|
|
34
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
35
|
-
"@solidrt/linux-arm64-gnu": "0.0.
|
|
36
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
37
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
33
|
+
"@solidrt/darwin-arm64": "0.0.53",
|
|
34
|
+
"@solidrt/linux-arm64-gnu": "0.0.53",
|
|
35
|
+
"@solidrt/linux-x64-gnu": "0.0.53",
|
|
36
|
+
"@solidrt/win32-x64-msvc": "0.0.53"
|
|
38
37
|
},
|
|
39
38
|
"peerDependencies": {
|
|
40
39
|
"typescript": "^7"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
|
-
"@solidrt/flux-types": "0.0.
|
|
42
|
+
"@solidrt/flux-types": "0.0.53",
|
|
44
43
|
"@types/babel__core": "^7.20.5",
|
|
45
44
|
"@types/bun": "latest"
|
|
46
45
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# srt android
|
|
2
|
+
|
|
3
|
+
{{ usage android }}
|
|
4
|
+
|
|
5
|
+
Launches the SolidRT client installed on a connected Android device (or
|
|
6
|
+
emulator) over adb against the dev server: the one serving the project (or
|
|
7
|
+
file) in the current directory, or `--port`. A running instance is restarted
|
|
8
|
+
so it picks the server up. The command then waits a few seconds for the
|
|
9
|
+
client to appear on the server and reports its client id. The server must
|
|
10
|
+
run with `--lan` so the device can reach it; an emulator reaches a loopback
|
|
11
|
+
server through its host alias. `--device` picks one of several connected
|
|
12
|
+
devices by serial or unique prefix.
|
|
13
|
+
|
|
14
|
+
`--install` installs (or updates) the client first, from the
|
|
15
|
+
`@solidrt/android-<abi>` dev dependency matching the device's ABI; the
|
|
16
|
+
command says which package to add when it is missing. Without `--install`
|
|
17
|
+
the installed client is left as it is, and a note says when its version is
|
|
18
|
+
not the one that package carries - a client you built and installed
|
|
19
|
+
yourself is never replaced unless you ask.
|
|
20
|
+
|
|
21
|
+
adb is a system tool (Android Platform Tools) and is never bundled.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { networkInterfaces } from "node:os"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
import { androidPackageVersion, resolveApk, ANDROID_PKG_MAP } from "../lib/artifacts"
|
|
5
|
+
import { values, port } from "../lib/args"
|
|
6
|
+
import { resolveByPort, resolveFromCwd } from "../lib/registry"
|
|
7
|
+
import type { LiveRecord } from "../types/registry"
|
|
8
|
+
|
|
9
|
+
// `srt android`: the Android client flow, decoupled from `srt client` (a
|
|
10
|
+
// local process) because a device is a different thing: find it over adb and
|
|
11
|
+
// launch the client installed there pointed at the dev server, which is
|
|
12
|
+
// resolved like `srt client` does (the project at the cwd, or --port). The
|
|
13
|
+
// APK is only touched on --install (from the @solidrt/android-<abi> package
|
|
14
|
+
// matching the device's ABI), so a client built and installed by hand stays;
|
|
15
|
+
// without --install the command just notes when the installed version is not
|
|
16
|
+
// the one the project's package carries.
|
|
17
|
+
|
|
18
|
+
// Launch component of the "go" dev-client flavor (see lattice/Makefile.android).
|
|
19
|
+
let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
|
|
20
|
+
let PACKAGE = "com.solidrt.go"
|
|
21
|
+
|
|
22
|
+
let sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
23
|
+
|
|
24
|
+
// adb is a system tool (Android Platform Tools), never bundled. Look on PATH
|
|
25
|
+
// first, then the standard SDK location.
|
|
26
|
+
function resolveAdb() {
|
|
27
|
+
let exe = process.platform === "win32" ? "adb.exe" : "adb"
|
|
28
|
+
let onPath = Bun.which(exe)
|
|
29
|
+
if (onPath) return onPath
|
|
30
|
+
for (let root of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
31
|
+
if (!root) continue
|
|
32
|
+
let candidate = resolve(root, "platform-tools", exe)
|
|
33
|
+
if (existsSync(candidate)) return candidate
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireAdb() {
|
|
39
|
+
let path = resolveAdb()
|
|
40
|
+
if (path) return path
|
|
41
|
+
console.error("Could not find adb (Android Platform Tools).")
|
|
42
|
+
console.error("Install it:")
|
|
43
|
+
console.error(" Windows: winget install Google.PlatformTools")
|
|
44
|
+
console.error(" macOS: brew install android-platform-tools")
|
|
45
|
+
console.error(" Linux: install your distro's android-tools / adb package")
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let ipToInt = (ip: string) => ip.split(".").reduce((acc, o) => (acc << 8) + (parseInt(o, 10) & 255), 0) >>> 0
|
|
50
|
+
|
|
51
|
+
// This host's IPv4 on the same subnet as `deviceIp`: the interface whose
|
|
52
|
+
// (address & netmask) matches the device's. Pure computation over what the OS
|
|
53
|
+
// reports (cross-platform, no routing socket, no hardcoded ranges).
|
|
54
|
+
function hostIpFor(deviceIp: string): string | null {
|
|
55
|
+
let d = ipToInt(deviceIp)
|
|
56
|
+
for (let addrs of Object.values(networkInterfaces())) {
|
|
57
|
+
for (let a of addrs ?? []) {
|
|
58
|
+
if (a.family === "IPv4" && !a.internal && (ipToInt(a.address) & ipToInt(a.netmask)) === (d & ipToInt(a.netmask))) {
|
|
59
|
+
return a.address
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The device's own IPv4 on its primary network: from the serial for wireless adb
|
|
67
|
+
// (ip:port), else queried over adb. `ip route get` reports the source IP of the
|
|
68
|
+
// route to a public address, i.e. the device's main interface IP.
|
|
69
|
+
function deviceIp(adb: string, target: string): string | null {
|
|
70
|
+
let m = target.match(/^(\d+\.\d+\.\d+\.\d+):\d+$/)
|
|
71
|
+
if (m) return m[1] ?? null
|
|
72
|
+
let res = Bun.spawnSync([adb, "-s", target, "shell", "ip", "route", "get", "1.1.1.1"], {
|
|
73
|
+
stdout: "pipe",
|
|
74
|
+
stderr: "pipe",
|
|
75
|
+
})
|
|
76
|
+
return res.stdout.toString().match(/src (\d+\.\d+\.\d+\.\d+)/)?.[1] ?? null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The host:port the client on `target` should dial to reach this machine's dev
|
|
80
|
+
// server, by adb transport: the emulator reaches the host through its NAT alias
|
|
81
|
+
// 10.0.2.2; every other transport shares a LAN with the host, so match the
|
|
82
|
+
// device's IP to the host interface on its subnet. Replaces the old adb-reverse
|
|
83
|
+
// loopback tunnel, which never worked over wireless adb. Returns null when the
|
|
84
|
+
// address cannot be resolved (the client then falls back to QR/recents).
|
|
85
|
+
function devServerAddress(adb: string, target: string, server: LiveRecord): string | null {
|
|
86
|
+
// The emulator's host alias reaches the host's loopback, so a loopback-only
|
|
87
|
+
// server is fine there; a real device needs a server started with --lan.
|
|
88
|
+
if (target.startsWith("emulator-")) return `10.0.2.2:${server.port}`
|
|
89
|
+
if (server.address === "127.0.0.1") {
|
|
90
|
+
console.log("[cli] The dev server is loopback-only; restart it with --lan so the device can reach it")
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
let dip = deviceIp(adb, target)
|
|
94
|
+
if (!dip) return null
|
|
95
|
+
let host = hostIpFor(dip)
|
|
96
|
+
return host ? `${host}:${server.port}` : null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The dev server the device should dial: --port picks a local server by
|
|
100
|
+
// port, otherwise the project (or file) in the current directory.
|
|
101
|
+
async function resolveServer(): Promise<LiveRecord> {
|
|
102
|
+
let resolved = port !== undefined ? await resolveByPort(port) : await resolveFromCwd(process.cwd())
|
|
103
|
+
if (!resolved.ok) {
|
|
104
|
+
console.error(resolved.message)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
return resolved.record
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Serials of connected, authorized devices (excludes offline/unauthorized).
|
|
111
|
+
function listDevices(adb: string): string[] {
|
|
112
|
+
let listed = Bun.spawnSync([adb, "devices"], { stdout: "pipe", stderr: "pipe" })
|
|
113
|
+
return listed.stdout
|
|
114
|
+
.toString()
|
|
115
|
+
.split("\n")
|
|
116
|
+
.slice(1)
|
|
117
|
+
.map((l) => l.trim())
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.filter((l) => l.endsWith("\tdevice"))
|
|
120
|
+
.map((l) => l.split("\t")[0])
|
|
121
|
+
.filter((s): s is string => Boolean(s))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Primary ABI of the connected device (e.g. "arm64-v8a", "armeabi-v7a"), used
|
|
125
|
+
// to pick the matching APK -- a fat (multi-ABI) APK reports its 64-bit ABI
|
|
126
|
+
// here and still installs fine, so this only matters for single-ABI builds.
|
|
127
|
+
function deviceAbi(adb: string, target: string): string {
|
|
128
|
+
let res = Bun.spawnSync([adb, "-s", target, "shell", "getprop", "ro.product.cpu.abi"], {
|
|
129
|
+
stdout: "pipe",
|
|
130
|
+
stderr: "pipe",
|
|
131
|
+
})
|
|
132
|
+
return res.stdout.toString().trim()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Always shown up front, regardless of how many devices are connected or
|
|
136
|
+
// whether --device narrows it down: one line per device with the ABI its APK
|
|
137
|
+
// build would target, then (with --install) a dev-dependency hint for each
|
|
138
|
+
// ABI that isn't resolvable locally yet. Returns the device->abi map so
|
|
139
|
+
// callers don't have to re-query it.
|
|
140
|
+
function printDeviceStatus(adb: string, devices: string[]): Map<string, string> {
|
|
141
|
+
let abiByDevice = new Map<string, string>()
|
|
142
|
+
let missingAbis = new Set<string>()
|
|
143
|
+
for (let d of devices) {
|
|
144
|
+
let abi = deviceAbi(adb, d)
|
|
145
|
+
abiByDevice.set(d, abi)
|
|
146
|
+
console.log(`${d} - ${abi}`)
|
|
147
|
+
if (!resolveApk(abi)) missingAbis.add(abi)
|
|
148
|
+
}
|
|
149
|
+
let missingPkgs = [...missingAbis].map((abi) => ANDROID_PKG_MAP[abi]).filter((pkg): pkg is string => Boolean(pkg))
|
|
150
|
+
if (values.install && missingPkgs.length > 0) {
|
|
151
|
+
console.log(`Add dev dependencies with: bun add -d ${missingPkgs.join(" ")}`)
|
|
152
|
+
}
|
|
153
|
+
return abiByDevice
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The versionName of the client installed on `target`, null when none is.
|
|
157
|
+
function installedVersion(adb: string, target: string): string | null {
|
|
158
|
+
let res = Bun.spawnSync([adb, "-s", target, "shell", "dumpsys", "package", PACKAGE], { stdout: "pipe", stderr: "pipe" })
|
|
159
|
+
return res.stdout.toString().match(/versionName=(\S+)/)?.[1] ?? null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Client = { id: number; platform: string; version: string }
|
|
163
|
+
|
|
164
|
+
// The clients the dev server currently lists (empty when it cannot be asked).
|
|
165
|
+
async function connectedClients(server: LiveRecord): Promise<Client[]> {
|
|
166
|
+
try {
|
|
167
|
+
let resp = await fetch(`http://127.0.0.1:${server.port}/__control__/clients`, { signal: AbortSignal.timeout(1000) })
|
|
168
|
+
return ((await resp.json()) as { clients?: Client[] }).clients ?? []
|
|
169
|
+
} catch {
|
|
170
|
+
return []
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The first Android client that appears beyond `before`, or null after ~10 s.
|
|
175
|
+
async function waitForClient(server: LiveRecord, before: Set<number>): Promise<Client | null> {
|
|
176
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
177
|
+
await sleep(500)
|
|
178
|
+
let fresh = (await connectedClients(server)).find((c) => c.platform === "android" && !before.has(c.id))
|
|
179
|
+
if (fresh) return fresh
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Resolve the target device serial (and its ABI). With --device, treat the
|
|
185
|
+
// value as a serial prefix and require it to match exactly one connected
|
|
186
|
+
// device; without it, use the sole connected device. Exits with a clear
|
|
187
|
+
// message on any ambiguity.
|
|
188
|
+
function resolveTarget(adb: string): { target: string; abi: string } {
|
|
189
|
+
let devices = listDevices(adb)
|
|
190
|
+
let abiByDevice = printDeviceStatus(adb, devices)
|
|
191
|
+
|
|
192
|
+
if (values.device) {
|
|
193
|
+
let prefix = values.device
|
|
194
|
+
let matches = devices.filter((d) => d.startsWith(prefix))
|
|
195
|
+
if (matches.length > 1) {
|
|
196
|
+
console.error(`--device "${prefix}" is ambiguous; matches: ${matches.join(", ")}`)
|
|
197
|
+
process.exit(1)
|
|
198
|
+
}
|
|
199
|
+
let [match] = matches
|
|
200
|
+
if (!match) {
|
|
201
|
+
console.error(`No connected device matches --device "${prefix}".`)
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
return { target: match, abi: abiByDevice.get(match)! }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (devices.length > 1) {
|
|
208
|
+
console.error("Pick one with --device <serial or prefix>.")
|
|
209
|
+
process.exit(1)
|
|
210
|
+
}
|
|
211
|
+
let [only] = devices
|
|
212
|
+
if (!only) {
|
|
213
|
+
console.error("No authorized Android device found. Enable USB debugging and check `adb devices`.")
|
|
214
|
+
process.exit(1)
|
|
215
|
+
}
|
|
216
|
+
return { target: only, abi: abiByDevice.get(only)! }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Launch the Android client on a connected device over adb (installing it
|
|
220
|
+
// first on --install), passing it the dev-server address to dial as a
|
|
221
|
+
// launch-intent extra (see devServerAddress), then wait briefly for it to show
|
|
222
|
+
// up on the dev server. The client is not a child process here: its lifecycle
|
|
223
|
+
// is the WS connect/disconnect the server sees.
|
|
224
|
+
export async function main() {
|
|
225
|
+
let server = await resolveServer()
|
|
226
|
+
let adb = requireAdb()
|
|
227
|
+
|
|
228
|
+
let { target, abi } = resolveTarget(adb)
|
|
229
|
+
|
|
230
|
+
if (values.install) {
|
|
231
|
+
let apk = resolveApk(abi)
|
|
232
|
+
if (!apk) {
|
|
233
|
+
console.error(`Could not find a SolidRT-Go APK for ABI "${abi}".`)
|
|
234
|
+
let pkg = ANDROID_PKG_MAP[abi]
|
|
235
|
+
if (pkg) console.error(`Add it with: bun add -d ${pkg}`)
|
|
236
|
+
process.exit(1)
|
|
237
|
+
}
|
|
238
|
+
console.log(`[cli] Installing SolidRT-Go on ${target}`)
|
|
239
|
+
let install = Bun.spawn([adb, "-s", target, "install", "-r", apk], { stdout: "pipe", stderr: "pipe" })
|
|
240
|
+
if ((await install.exited) !== 0) {
|
|
241
|
+
console.error("adb install failed:\n" + (await new Response(install.stderr).text()))
|
|
242
|
+
process.exit(1)
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
let installed = installedVersion(adb, target)
|
|
246
|
+
if (installed === null) {
|
|
247
|
+
console.error(`No SolidRT-Go client on ${target}; install one with srt android --install`)
|
|
248
|
+
process.exit(1)
|
|
249
|
+
}
|
|
250
|
+
let expected = androidPackageVersion(abi)
|
|
251
|
+
if (expected !== null && expected !== installed) {
|
|
252
|
+
console.log(
|
|
253
|
+
`[cli] Installed client is ${installed}; the project's ${ANDROID_PKG_MAP[abi]} is ${expected} (srt android --install updates it)`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Hand the client the dev-server address to dial, as a launch-intent extra that
|
|
259
|
+
// MainActivity forwards to native argv (--dev-server); the client auto-connects
|
|
260
|
+
// to it. Replaces adb reverse, which never worked over wireless adb. -S stops
|
|
261
|
+
// a running instance first: a delivered intent does not reach one, so without
|
|
262
|
+
// it the client would keep whatever server it had.
|
|
263
|
+
let before = new Set((await connectedClients(server)).map((c) => c.id))
|
|
264
|
+
let devServer = devServerAddress(adb, target, server)
|
|
265
|
+
let launchArgs = [adb, "-s", target, "shell", "am", "start", "-S", "-n", PACKAGE_ACTIVITY]
|
|
266
|
+
if (devServer) {
|
|
267
|
+
console.log(`[cli] Client will dial dev server at ${devServer}`)
|
|
268
|
+
launchArgs.push("--es", "srt_dev_server", devServer)
|
|
269
|
+
} else {
|
|
270
|
+
console.log("[cli] Could not resolve a host address for the device; client will need a manual/QR connect")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let start = Bun.spawn(launchArgs, { stdout: "pipe", stderr: "pipe" })
|
|
274
|
+
if ((await start.exited) !== 0) {
|
|
275
|
+
console.error("adb start failed:\n" + (await new Response(start.stderr).text()))
|
|
276
|
+
process.exit(1)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(`[cli] Launched SolidRT-Go on ${target}; waiting for it to connect to the dev server...`)
|
|
280
|
+
let client = await waitForClient(server, before)
|
|
281
|
+
if (client) {
|
|
282
|
+
console.log(`[cli] Client ${client.id} connected (${client.platform}, ${client.version})`)
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`[cli] No connection after 10 s. The server must run with --lan and the device must reach this machine.`)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -2,15 +2,17 @@ import { transformAsync } from "@babel/core"
|
|
|
2
2
|
import jsx from "@babel/plugin-syntax-jsx"
|
|
3
3
|
import ts from "@babel/preset-typescript"
|
|
4
4
|
import remapping from "@jridgewell/remapping"
|
|
5
|
-
import solid from "babel-
|
|
5
|
+
import solid from "@solidjs/babel-plugin"
|
|
6
6
|
import { type BunPlugin, type BuildArtifact } from "bun"
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { dirname, join, relative, resolve as resolvePath, sep } from "node:path"
|
|
9
|
-
import { values
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { values } from "../lib/args"
|
|
10
|
+
import type { Mode } from "../lib/mode"
|
|
11
|
+
import { requireBinary } from "../lib/util"
|
|
12
|
+
import type { BundleOutput } from "../types/bundle"
|
|
13
|
+
import { buildManifest, manifestAssetFor, type ManifestAsset } from "../lib/project"
|
|
12
14
|
|
|
13
|
-
// Babel plugin: rewrite `import data from "
|
|
15
|
+
// Babel plugin: rewrite `import data from "../x" with { type: "binary" }` into an
|
|
14
16
|
// inline Uint8Array of the file's bytes, and `with { type: "text" }` into an
|
|
15
17
|
// inline string of its UTF-8 contents. The import attribute is invisible to
|
|
16
18
|
// Bun's bundler and its plugins in this Bun version, so we handle it here in the
|
|
@@ -21,8 +23,10 @@ import { buildManifest, manifestAssetFor, type ManifestAsset } from "./project"
|
|
|
21
23
|
// attribute-free path later. Both attributes work on any extension, so shader
|
|
22
24
|
// and other text sources inline by attribute the same way bytes do; `.svg` is
|
|
23
25
|
// additionally text-loaded without an attribute (see Bun's `loader` below).
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// Inlined files leave no trace in the module graph, so the plugin records
|
|
27
|
+
// each one in `inputs` (see `BundleOutput.inputs`).
|
|
28
|
+
function inlineImport(inputs: Set<string>) {
|
|
29
|
+
return ({ types: t }: { types: any }) => ({
|
|
26
30
|
visitor: {
|
|
27
31
|
ImportDeclaration(path: any, pluginState: any) {
|
|
28
32
|
let attrs = path.node.attributes ?? path.node.assertions
|
|
@@ -32,12 +36,13 @@ function inlineImport({ types: t }: { types: any }) {
|
|
|
32
36
|
let def = path.node.specifiers.find((s: any) => s.type === "ImportDefaultSpecifier")
|
|
33
37
|
if (!def) {
|
|
34
38
|
throw path.buildCodeFrameError(
|
|
35
|
-
`A ${kind} import needs a default import: import data from "
|
|
39
|
+
`A ${kind} import needs a default import: import data from "../file" with { type: "${kind}" }`,
|
|
36
40
|
)
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
let importer = pluginState.file.opts.filename as string
|
|
40
44
|
let abs = resolvePath(dirname(importer), path.node.source.value)
|
|
45
|
+
inputs.add(abs)
|
|
41
46
|
|
|
42
47
|
// text: var <local> = "<contents>"
|
|
43
48
|
// binary: var <local> = Uint8Array.from(atob("<b64>"), c => c.charCodeAt(0))
|
|
@@ -56,7 +61,7 @@ function inlineImport({ types: t }: { types: any }) {
|
|
|
56
61
|
path.replaceWith(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(def.local.name), expr)]))
|
|
57
62
|
},
|
|
58
63
|
},
|
|
59
|
-
}
|
|
64
|
+
})
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// Concatenate only the JS outputs (entry point plus any code-split chunks) of a
|
|
@@ -71,7 +76,7 @@ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
|
|
|
71
76
|
return code
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
// Bun build plugin that runs JSX/TSX through babel-
|
|
79
|
+
// Bun build plugin that runs JSX/TSX through @solidjs/babel-plugin (universal
|
|
75
80
|
// generate, targeting @solidrt/core) plus the TS preset. `moduleName` only ends
|
|
76
81
|
// up as an import specifier in the emitted code, resolved from the app's tree at
|
|
77
82
|
// bundle time; the CLI itself never loads core, so core is deliberately neither
|
|
@@ -84,25 +89,27 @@ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
|
|
|
84
89
|
// collected there, keyed by absolute path, for sourcemap composition later.
|
|
85
90
|
// `isolateEntry` is the one "use isolate" module this build may load (its own
|
|
86
91
|
// entry); loading any other one means a by-value import of an isolate module,
|
|
87
|
-
// which is a build error (see isolate modules below).
|
|
88
|
-
|
|
92
|
+
// which is a build error (see isolate modules below). Every file the plugin
|
|
93
|
+
// loads or inlines is recorded in `inputs`.
|
|
94
|
+
function solidPlugin(inputs: Set<string>, babelMaps?: Map<string, object>, isolateEntry?: string): BunPlugin {
|
|
89
95
|
return {
|
|
90
96
|
name: "bun-plugin-solid",
|
|
91
97
|
setup: (build) => {
|
|
92
98
|
build.onLoad({ filter: /\.(js|ts)x?$/ }, async (args) => {
|
|
93
99
|
if (!/\.(js|ts)x$/.test(args.path) && args.path.includes("node_modules")) return
|
|
100
|
+
inputs.add(args.path)
|
|
94
101
|
let file = Bun.file(args.path)
|
|
95
102
|
let code = await file.text()
|
|
96
103
|
if (args.path !== isolateEntry && hasIsolateDirective(code)) {
|
|
97
104
|
throw new Error(
|
|
98
|
-
`${args.path} is a "use isolate" module: import its types only (import type * as W from "
|
|
105
|
+
`${args.path} is a "use isolate" module: import its types only (import type * as W from "../...") and call it through isolate() from flux:isolate`,
|
|
99
106
|
)
|
|
100
107
|
}
|
|
101
108
|
let transforms = await transformAsync(code, {
|
|
102
109
|
filename: args.path,
|
|
103
110
|
sourceMaps: !!babelMaps,
|
|
104
|
-
presets: [[
|
|
105
|
-
plugins: [jsx, inlineImport],
|
|
111
|
+
presets: [[ts]],
|
|
112
|
+
plugins: [jsx, [solid, { moduleName: "@solidrt/core", generate: "universal" }], inlineImport(inputs)],
|
|
106
113
|
})
|
|
107
114
|
if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
|
|
108
115
|
return { contents: transforms?.code ?? "", loader: "js" }
|
|
@@ -171,17 +178,13 @@ export function isolateAssetPath(id: string, ext: "js" | "bin"): string {
|
|
|
171
178
|
return `isolates/${id}.${ext}`
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
// `project` is the project root (mode.ts decides it, never a search), or
|
|
182
|
+
// null for a file on its own: no assets in the manifest and no isolate
|
|
183
|
+
// modules (isolates are a project feature).
|
|
184
|
+
export type BundleOptions = { entry: string; dev: boolean; minify: boolean; project: string | null }
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
/** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
|
|
179
|
-
map: string | null
|
|
180
|
-
/** Version manifest JSON for this bundle; clients install pushes under its hash. */
|
|
181
|
-
manifest: string
|
|
182
|
-
/** The app's isolate bundles, one per "use isolate" module, in id order; maps dev builds only. */
|
|
183
|
-
isolates: { id: string; code: string; map: string | null }[]
|
|
184
|
-
}
|
|
186
|
+
// The `srt bundle --json` stdout contract doubles as the in-process result.
|
|
187
|
+
export type BundleResult = BundleOutput
|
|
185
188
|
|
|
186
189
|
/**
|
|
187
190
|
* The bundle's sourcemaps keyed by the module name stack frames cite ("main"
|
|
@@ -196,7 +199,7 @@ export function bundleMaps(result: BundleResult): Record<string, string> | null
|
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
// The pure bundle: every input is explicit, so it runs identically in the srt
|
|
199
|
-
// (Bun) process and in the
|
|
202
|
+
// (Bun) process and in the `srt bundle --json` subprocess the dev server
|
|
200
203
|
// spawns. It never touches the ambient args/state singletons and never prints
|
|
201
204
|
// progress (callers own that), so its stdout stays clean for subprocess use.
|
|
202
205
|
export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
|
|
@@ -210,12 +213,15 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
210
213
|
"import.meta.env.DEV": opts.dev ? "true" : "false",
|
|
211
214
|
"process.env.NODE_ENV": opts.dev ? '"development"' : '"production"',
|
|
212
215
|
}
|
|
213
|
-
if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
|
|
214
216
|
|
|
215
217
|
// One Bun.build per entry: the app, then each isolate module as its own
|
|
216
218
|
// self-contained bundle (splitting is off, so a helper both import gets
|
|
217
219
|
// duplicated rather than shared). In dev every build gets a composed
|
|
218
220
|
// sourcemap, keyed downstream by its module name.
|
|
221
|
+
// Every file the bundle depends on (BundleOutput.inputs): what the plugin
|
|
222
|
+
// loaded or inlined, plus every module in the build's sourcemap, which is
|
|
223
|
+
// where dependency code (node_modules, workspace packages) shows up.
|
|
224
|
+
let inputs = new Set<string>()
|
|
219
225
|
let build = async (entry: string, babelMaps?: Map<string, object>, isolateEntry?: string) => {
|
|
220
226
|
let result = null
|
|
221
227
|
try {
|
|
@@ -228,7 +234,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
228
234
|
define,
|
|
229
235
|
loader: { ".svg": "text" },
|
|
230
236
|
sourcemap: babelMaps ? "external" : "none",
|
|
231
|
-
plugins: [solidPlugin(babelMaps, isolateEntry)],
|
|
237
|
+
plugins: [solidPlugin(inputs, babelMaps, isolateEntry)],
|
|
232
238
|
})
|
|
233
239
|
} catch (e) {
|
|
234
240
|
console.error("[cli] compile error:\n", e)
|
|
@@ -238,6 +244,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
238
244
|
for (let msg of result.logs) console.error(msg)
|
|
239
245
|
return null
|
|
240
246
|
}
|
|
247
|
+
for (let source of await mapSources(result.outputs)) inputs.add(source)
|
|
241
248
|
return result
|
|
242
249
|
}
|
|
243
250
|
|
|
@@ -247,7 +254,8 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
247
254
|
let code = await codeFromOutputs(main.outputs)
|
|
248
255
|
|
|
249
256
|
let isolates: { id: string; code: string; map: string | null }[] = []
|
|
250
|
-
|
|
257
|
+
let modules = opts.project === null ? [] : findIsolateModules(dirname(resolvePath(opts.entry)))
|
|
258
|
+
for (let module of modules) {
|
|
251
259
|
let moduleMaps = opts.dev ? new Map<string, object>() : undefined
|
|
252
260
|
let result = await build(module.path, moduleMaps, module.path)
|
|
253
261
|
if (!result) return null
|
|
@@ -258,23 +266,43 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
258
266
|
})
|
|
259
267
|
}
|
|
260
268
|
|
|
269
|
+
// The project files the manifest and the build read: package.json (app
|
|
270
|
+
// identity, icon, fonts) and tsconfig.json (resolution, JSX).
|
|
271
|
+
if (opts.project !== null) {
|
|
272
|
+
for (let name of ["package.json", "tsconfig.json"]) {
|
|
273
|
+
let abs = join(opts.project, name)
|
|
274
|
+
if (existsSync(abs)) inputs.add(abs)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
261
278
|
return {
|
|
262
279
|
code,
|
|
263
280
|
map: await composeMap(main.outputs, babelMaps),
|
|
264
|
-
manifest: buildManifest(code, opts.entry, isolateManifestAssets(isolates)),
|
|
281
|
+
manifest: buildManifest(code, opts.entry, isolateManifestAssets(isolates), opts.project),
|
|
265
282
|
isolates,
|
|
283
|
+
inputs: [...inputs].sort(),
|
|
266
284
|
}
|
|
267
285
|
}
|
|
268
286
|
|
|
287
|
+
// The source files a build's sourcemap cites, as absolute paths (Bun writes
|
|
288
|
+
// them cwd-relative). Empty for a build without a map.
|
|
289
|
+
async function mapSources(outputs: BuildArtifact[]): Promise<string[]> {
|
|
290
|
+
let out: string[] = []
|
|
291
|
+
for (let o of outputs) {
|
|
292
|
+
if ((o.kind !== "entry-point" && o.kind !== "chunk") || !o.sourcemap) continue
|
|
293
|
+
let map = JSON.parse(await o.sourcemap.text())
|
|
294
|
+
for (let source of map.sources ?? []) out.push(resolvePath(source))
|
|
295
|
+
}
|
|
296
|
+
return out
|
|
297
|
+
}
|
|
298
|
+
|
|
269
299
|
/** The manifest assets for a set of isolate bundles (dev form: isolates/<id>.js). */
|
|
270
300
|
export function isolateManifestAssets(isolates: { id: string; code: string }[]): ManifestAsset[] {
|
|
271
301
|
return isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
|
|
272
302
|
}
|
|
273
303
|
|
|
274
|
-
// Write
|
|
275
|
-
//
|
|
276
|
-
// assets the bundle lists. Stale files from removed modules stay behind
|
|
277
|
-
// unlisted, which is harmless.
|
|
304
|
+
// Write isolate bundles as `<dir>/<id>.js`, the shape the manifest lists and
|
|
305
|
+
// a prebuilt bundle's sibling isolates/ dir carries.
|
|
278
306
|
export function writeIsolates(dir: string, isolates: { id: string; code: string }[]) {
|
|
279
307
|
for (let i of isolates) {
|
|
280
308
|
let file = join(dir, `${i.id}.js`)
|
|
@@ -305,20 +333,13 @@ async function composeMap(outputs: BuildArtifact[], babelMaps?: Map<string, obje
|
|
|
305
333
|
return composed.toString()
|
|
306
334
|
}
|
|
307
335
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
336
|
+
// The one-shot commands' bundle (bundle, pack, render): production unless
|
|
337
|
+
// --dev, on what the cwd and the argument decide (mode.ts).
|
|
338
|
+
export async function bundle(mode: Mode) {
|
|
339
|
+
let dev = values.dev
|
|
311
340
|
// Keep stdout clean when the bundle itself is written to stdout.
|
|
312
|
-
if (!values.stdout)
|
|
313
|
-
|
|
314
|
-
// With a server running, its /isolates/ route serves what we write here.
|
|
315
|
-
if (result && devBase) writeIsolates(devIsolatesDir(state.projectDir), result.isolates)
|
|
316
|
-
return result
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Where a project's dev isolate bundles are written and served from. */
|
|
320
|
-
export function devIsolatesDir(projectDir: string): string {
|
|
321
|
-
return join(projectDir, ".srt-data", "isolates")
|
|
341
|
+
if (!values.stdout) console.log(`[cli] Bundling (${dev ? "development" : "production"})`)
|
|
342
|
+
return bundleWith({ entry: mode.entry, dev, minify: values.minify, project: mode.projectDir })
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
// A flux entry's isolate modules: everything under its isolates/ dir, id =
|
|
@@ -360,15 +381,6 @@ export function readPrebuiltIsolates(bundlePath: string): { id: string; code: st
|
|
|
360
381
|
return out
|
|
361
382
|
}
|
|
362
383
|
|
|
363
|
-
// Loading a prebuilt .srt.js re-publishes its sibling isolate bundles through
|
|
364
|
-
// the dev flow - written where the server's /isolates/ route serves from,
|
|
365
|
-
// listed in the manifest - so isolate() works as it does from a source build.
|
|
366
|
-
export function prebuiltManifest(code: string, path: string, projectDir: string): string {
|
|
367
|
-
let isolates = readPrebuiltIsolates(path)
|
|
368
|
-
writeIsolates(devIsolatesDir(projectDir), isolates)
|
|
369
|
-
return buildManifest(code, path, isolateManifestAssets(isolates))
|
|
370
|
-
}
|
|
371
|
-
|
|
372
384
|
// Bundle for the bare Flux runtime: no Solid plugin, flux: modules stay external.
|
|
373
385
|
export async function bundleFlux(entry: string): Promise<string> {
|
|
374
386
|
let result = await Bun.build({
|
|
@@ -388,8 +400,8 @@ export async function bundleFlux(entry: string): Promise<string> {
|
|
|
388
400
|
|
|
389
401
|
// Bundle for the SolidRT runtime via the standard Solid-aware bundler, or
|
|
390
402
|
// exit the command on a failed build.
|
|
391
|
-
export async function bundleSolid(): Promise<BundleResult> {
|
|
392
|
-
let result = await bundle()
|
|
403
|
+
export async function bundleSolid(mode: Mode): Promise<BundleResult> {
|
|
404
|
+
let result = await bundle(mode)
|
|
393
405
|
if (!result) {
|
|
394
406
|
console.error("Build failed")
|
|
395
407
|
process.exit(1)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# srt bundle
|
|
2
|
+
|
|
3
|
+
{{ usage bundle }}
|
|
4
|
+
|
|
5
|
+
Writes `dist/bundle/` (or `--output <dir>`): `<name>.srt.js` plus the app's
|
|
6
|
+
isolate modules as `isolates/<id>.js`, or bytecode with `--compile`. Move
|
|
7
|
+
the dir, not the bare file: a bundle loaded without its isolates/ dir loses
|
|
8
|
+
them.
|
|
9
|
+
|
|
10
|
+
`--flux` targets the bare [Flux runtime](/runtime/) instead of a SolidRT
|
|
11
|
+
app, for scripts and servers with no UI. `--json` is the dev server's
|
|
12
|
+
rebuild contract, one JSON object on stdout.
|