@solidrt/cli 0.0.51 → 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.
Files changed (93) 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 +169 -17
  5. package/dist/console.srtapp +94123 -73
  6. package/dist/server.js +3936 -0
  7. package/package.json +10 -11
  8. package/src/android/docs.md +21 -0
  9. package/src/android/main.ts +286 -0
  10. package/src/{bundler.ts → bundle/bundler.ts} +67 -55
  11. package/src/bundle/docs.md +12 -0
  12. package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
  13. package/src/check/docs.md +10 -0
  14. package/src/check/main.ts +85 -0
  15. package/src/{commands/check.ts → check/typecheck.ts} +11 -41
  16. package/src/client/docs.md +9 -0
  17. package/src/client/main.ts +33 -0
  18. package/src/console/docs.md +14 -0
  19. package/src/console/main.ts +21 -0
  20. package/src/demo/docs.md +27 -0
  21. package/src/demo/main.ts +67 -0
  22. package/src/init/docs.md +11 -0
  23. package/src/{commands/init.ts → init/main.ts} +21 -15
  24. package/src/init/scaffold/AGENTS.md +98 -0
  25. package/src/init/scaffold/package.json +23 -0
  26. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  27. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  28. package/src/lib/args.ts +194 -0
  29. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  30. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  31. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  32. package/src/lib/mode.ts +77 -0
  33. package/src/{project.ts → lib/project.ts} +109 -61
  34. package/src/lib/registry.ts +120 -0
  35. package/src/lib/server-bundle.ts +24 -0
  36. package/src/lib/usage.ts +117 -0
  37. package/src/lib/util.ts +36 -0
  38. package/src/main.ts +109 -31
  39. package/src/mcp/docs.md +22 -0
  40. package/src/mcp/main.ts +719 -0
  41. package/src/pack/docs.md +18 -0
  42. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  43. package/src/{commands/pack.ts → pack/main.ts} +28 -17
  44. package/src/{packer.ts → pack/trailer.ts} +32 -42
  45. package/src/render/docs.md +19 -0
  46. package/src/{commands/render.ts → render/main.ts} +17 -15
  47. package/src/server/args.ts +126 -0
  48. package/src/server/binaries.ts +47 -0
  49. package/src/server/config.ts +54 -0
  50. package/{server → src/server}/control.ts +239 -81
  51. package/src/server/docs.md +51 -0
  52. package/src/server/line-editor.ts +200 -0
  53. package/src/server/main.ts +473 -0
  54. package/src/server/mode.ts +92 -0
  55. package/src/server/rebuild.ts +90 -0
  56. package/src/server/registry.ts +138 -0
  57. package/src/server/repl.ts +223 -0
  58. package/src/server/state.ts +54 -0
  59. package/{server → src/server}/tsconfig.json +1 -1
  60. package/{server → src/server}/tunnel.ts +6 -6
  61. package/src/server/watcher.ts +121 -0
  62. package/src/tool/main.ts +70 -0
  63. package/src/types/bundle.d.ts +24 -0
  64. package/src/types/control.d.ts +90 -0
  65. package/src/types/registry.d.ts +16 -0
  66. package/scaffold/AGENTS.md +0 -185
  67. package/scaffold/package.json +0 -22
  68. package/scaffold/templates/components/icon.tsx +0 -48
  69. package/scaffold/templates/default/icon.tsx +0 -48
  70. package/server/main.ts +0 -308
  71. package/server/rebuild.ts +0 -76
  72. package/server/state.ts +0 -94
  73. package/src/args.ts +0 -210
  74. package/src/bundle-cli.ts +0 -13
  75. package/src/commands/client.ts +0 -34
  76. package/src/commands/mcp.ts +0 -617
  77. package/src/commands/server.ts +0 -73
  78. package/src/dev-android.ts +0 -176
  79. package/src/dev-client.ts +0 -29
  80. package/src/dev-server.ts +0 -302
  81. package/src/repl.ts +0 -249
  82. package/src/util.ts +0 -122
  83. package/src/watcher.ts +0 -73
  84. /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
  85. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  86. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  87. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  88. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  89. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  90. /package/{server → src/server}/cache.ts +0 -0
  91. /package/{server → src/server}/proxy.ts +0 -0
  92. /package/{server → src/server}/qr.ts +0 -0
  93. /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.51",
3
+ "version": "0.0.52",
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
- "server/",
15
- "scaffold/",
14
+ "dist/",
16
15
  "fonts/",
17
- "agents/",
18
- "AGENTS.md"
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-preset-solid": "2.0.0-rc.0",
27
+ "babel-preset-solid": "2.0.0-rc.1",
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.51",
35
- "@solidrt/linux-arm64-gnu": "0.0.51",
36
- "@solidrt/linux-x64-gnu": "0.0.51",
37
- "@solidrt/win32-x64-msvc": "0.0.51"
33
+ "@solidrt/darwin-arm64": "0.0.52",
34
+ "@solidrt/linux-arm64-gnu": "0.0.52",
35
+ "@solidrt/linux-x64-gnu": "0.0.52",
36
+ "@solidrt/win32-x64-msvc": "0.0.52"
38
37
  },
39
38
  "peerDependencies": {
40
39
  "typescript": "^7"
41
40
  },
42
41
  "devDependencies": {
43
- "@solidrt/flux-types": "0.0.51",
42
+ "@solidrt/flux-types": "0.0.52",
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
+ }
@@ -6,11 +6,13 @@ import solid from "babel-preset-solid"
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, source } from "./args"
10
- import { state, print, requireBinary } from "./util"
11
- import { buildManifest, manifestAssetFor, type ManifestAsset } from "./project"
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 "./x" with { type: "binary" }` into an
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
- function inlineImport({ types: t }: { types: any }) {
25
- return {
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 "./file" with { type: "${kind}" }`,
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
@@ -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
- function solidPlugin(babelMaps?: Map<string, object>, isolateEntry?: string): BunPlugin {
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 "./...") and call it through isolate() from flux:isolate`,
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
111
  presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
105
- plugins: [jsx, inlineImport],
112
+ plugins: [jsx, 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
- export type BundleOptions = { entry: string; devBase?: string; dev: boolean; minify: boolean }
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
- export type BundleResult = {
177
- code: string
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 standalone bundle-cli subprocess the dev server
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
- for (let module of findIsolateModules(dirname(resolvePath(opts.entry)))) {
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 dev isolate bundles where the dev server serves /isolates/ from
275
- // (<project>/.srt-data/isolates/<id>.js), so clients can fetch the manifest
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
- export async function bundle(entry = source) {
309
- let devBase = state.serverUrl ?? undefined
310
- let dev = !!devBase || values.dev
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) print(`[cli] Bundling (${dev ? "development" : "production"})`)
313
- let result = await bundleWith({ entry: entry!, devBase, dev, minify: values.minify })
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.