@solidrt/cli 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -19,12 +19,12 @@
19
19
  "qrcode-generator": "^2.0.4"
20
20
  },
21
21
  "optionalDependencies": {
22
- "@solidrt/darwin-arm64": "0.0.7",
23
- "@solidrt/linux-x64-gnu": "0.0.7",
24
- "@solidrt/win32-x64-msvc": "0.0.7"
22
+ "@solidrt/darwin-arm64": "0.0.9",
23
+ "@solidrt/linux-x64-gnu": "0.0.9",
24
+ "@solidrt/win32-x64-msvc": "0.0.9"
25
25
  },
26
26
  "peerDependencies": {
27
- "@solidrt/core": "0.0.7",
27
+ "@solidrt/core": "0.0.9",
28
28
  "typescript": "^5"
29
29
  },
30
30
  "devDependencies": {
package/src/args.ts CHANGED
@@ -5,6 +5,7 @@ export let { values, positionals } = parseArgs({
5
5
  dev: { type: "boolean", short: "d", default: false },
6
6
  minify: { type: "boolean", short: "m", default: false },
7
7
  compile: { type: "boolean", short: "c", default: false },
8
+ flux: { type: "boolean", default: false },
8
9
  stdout: { type: "boolean", default: false },
9
10
  output: { type: "string", short: "o" },
10
11
  "proxy-files": { type: "boolean", default: false },
@@ -12,6 +13,8 @@ export let { values, positionals } = parseArgs({
12
13
  fps: { type: "string" },
13
14
  duration: { type: "string" },
14
15
  size: { type: "string" },
16
+ android: { type: "boolean", default: false },
17
+ device: { type: "string" },
15
18
  },
16
19
  allowPositionals: true,
17
20
  })
@@ -20,18 +23,50 @@ export let command = positionals[0]
20
23
  export let source = positionals[1]
21
24
  export let isTsx = source?.endsWith(".tsx") || source?.endsWith(".jsx")
22
25
  export let isTs = source?.endsWith(".ts") || source?.endsWith(".js")
26
+ export let isSource = isTsx || isTs
23
27
  export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
24
28
 
29
+ function usage(line: string): never {
30
+ console.error("Usage: " + line)
31
+ process.exit(1)
32
+ }
33
+
34
+ // Per-command argument requirements. Called once before dispatch.
35
+ export function validateArgs() {
36
+ switch (command) {
37
+ case "bundle":
38
+ if (!source || (!isSource && !isPrebuilt))
39
+ usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
40
+ break
41
+ case "record":
42
+ if (!source || !isTsx) usage("srt record <entry.[tsx|jsx]>")
43
+ break
44
+ case "pack":
45
+ if (values.flux) {
46
+ if (!source || !isTs) usage("srt pack --flux [options] <entry.[ts|js]>")
47
+ } else if (!source || !isSource) {
48
+ usage("srt pack [options] <entry.[tsx|jsx|ts|js]>")
49
+ }
50
+ break
51
+ }
52
+
53
+ // --android installs/launches the client on a device; it is a client action,
54
+ // so it is only valid for `client`.
55
+ if (values.android && command !== "client") {
56
+ usage("srt client --android (--android is only valid with the client command)")
57
+ }
58
+ }
59
+
25
60
  export function printUsage() {
26
61
  console.error(`Usage: srt <command> [options] [file]
27
62
 
28
63
  Commands:
29
- run [file.tsx|jsx] Start dev server + local solidrt-go client
30
- server [file.tsx|jsx] Start dev server only
64
+ run [file] Start dev server + local solidrt-go client
65
+ server [file] Start dev server only
31
66
  client Start solidrt-go client only
32
- bundle <file.tsx|jsx> Transpile TSX/JSX to JS or bytecode
67
+ bundle <file> Transpile TS/JS/TSX/JSX to JS or bytecode
33
68
  record <file.tsx|jsx> Capture frames for video generation
34
- pack <file.ts|js> Bundle + compile to a standalone executable (experimental, Flux only)
69
+ pack <file> Bundle + compile to a standalone executable (experimental)
35
70
 
36
71
  run/server options:
37
72
  --proxy-files Route file/dir access through the dev server
@@ -40,6 +75,10 @@ run/server options:
40
75
  run/client options:
41
76
  --size <WxH> Window size (default: 1280x720)
42
77
 
78
+ client options:
79
+ --android Install and launch the client on a connected Android device
80
+ --device <serial> Target a specific adb device (when several are connected)
81
+
43
82
  bundle options:
44
83
  -d, --dev Use development build of SolidJS (default: production)
45
84
  -m, --minify Minify the output
@@ -47,6 +86,11 @@ bundle options:
47
86
  -o, --output <name> Output filename
48
87
  --stdout Write bundle to stdout
49
88
 
89
+ pack options:
90
+ --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
91
+ -m, --minify Minify the output
92
+ -o, --output <name> Output filename
93
+
50
94
  record options:
51
95
  --fps <N> Frames per second (default: 60)
52
96
  --duration <N> Duration in seconds (default: 1)
@@ -41,5 +41,30 @@ export function resolveBinary(name: string) {
41
41
  } catch {}
42
42
  }
43
43
 
44
+ return null
45
+ }
46
+
47
+ // The Android client APK is per-ABI (it bundles native .so for one architecture)
48
+ // and host-independent, so it lives under dist/android/<abi>/ rather than the
49
+ // host triple map. Only arm64-v8a is supported for now.
50
+ let ANDROID_ABI = "arm64-v8a"
51
+ let ANDROID_PKG = "@solidrt/android-arm64-v8a"
52
+
53
+ export function resolveApk() {
54
+ // 1. SRT_HOME: contributor checkout, where `make dist-android` stages the APK
55
+ // under dist/android/<abi>/.
56
+ let srtRoot = process.env.SRT_HOME
57
+ if (srtRoot) {
58
+ let apk = resolve(srtRoot, "dist/android", ANDROID_ABI, "solidrt-go.apk")
59
+ if (existsSync(apk)) return apk
60
+ }
61
+
62
+ // 2. Platform npm package
63
+ try {
64
+ let pkgDir = dirname(require.resolve(`${ANDROID_PKG}/package.json`))
65
+ let apk = resolve(pkgDir, "solidrt-go.apk")
66
+ if (existsSync(apk)) return apk
67
+ } catch {}
68
+
44
69
  return null
45
70
  }
package/src/bundler.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { transformAsync } from "@babel/core"
2
+ import ts from "@babel/preset-typescript"
3
+ import solid from "babel-preset-solid"
4
+ import { type BunPlugin } from "bun"
5
+ import { values, source } from "./args"
6
+ import { state, print, requireBinary } from "./util"
7
+
8
+ // Bun build plugin that runs JSX/TSX through babel-preset-solid (universal
9
+ // generate, targeting @solidrt/core) plus the TS preset.
10
+ function solidPlugin(): BunPlugin {
11
+ return {
12
+ name: "bun-plugin-solid",
13
+ setup: (build) => {
14
+ build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
15
+ let file = Bun.file(args.path)
16
+ let code = await file.text()
17
+ let transforms = await transformAsync(code, {
18
+ filename: args.path,
19
+ presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
20
+ })
21
+ return { contents: transforms?.code ?? "", loader: "js" }
22
+ })
23
+ },
24
+ }
25
+ }
26
+
27
+ export async function bundle(entry = source) {
28
+ let result = null
29
+
30
+ let devBase = state.serverUrl ?? undefined
31
+ let dev = !!devBase || values.dev
32
+ print(`[cli] Bundling (${dev ? "development" : "production"})`)
33
+ let define: Record<string, string> = {
34
+ "process.env.NODE_ENV": dev ? "development" : "production",
35
+ }
36
+ if (devBase) define.__SRT_DEV_BASE__ = devBase
37
+
38
+ try {
39
+ result = await Bun.build({
40
+ entrypoints: [entry!],
41
+ target: "browser",
42
+ format: "esm",
43
+ minify: values.minify,
44
+ external: ["flux:*"],
45
+ define,
46
+ plugins: [solidPlugin()],
47
+ })
48
+ } catch (e) {
49
+ console.error("[cli] compile error:\n", e)
50
+ return null
51
+ }
52
+
53
+ if (result?.success) {
54
+ return result
55
+ }
56
+
57
+ if (result) {
58
+ for (let msg of result?.logs) console.error(msg)
59
+ }
60
+ return null
61
+ }
62
+
63
+ export async function bundleTo(outfile: string) {
64
+ let result = await bundle()
65
+ if (!result) {
66
+ console.error("Build failed")
67
+ process.exit(1)
68
+ }
69
+ for (let output of result.outputs) {
70
+ await Bun.write(outfile, output)
71
+ }
72
+ return result
73
+ }
74
+
75
+ // Bundle for the bare Flux runtime: no Solid plugin, flux: modules stay external.
76
+ export async function bundleFlux(entry: string): Promise<string> {
77
+ let result = await Bun.build({
78
+ entrypoints: [entry],
79
+ target: "browser",
80
+ format: "esm",
81
+ minify: values.minify,
82
+ external: ["flux:*"],
83
+ })
84
+ if (!result.success) {
85
+ for (let msg of result.logs) console.error(msg)
86
+ console.error("Build failed")
87
+ process.exit(1)
88
+ }
89
+ let jsCode = ""
90
+ for (let output of result.outputs) jsCode += await output.text()
91
+ return jsCode
92
+ }
93
+
94
+ // Bundle for the SolidRT runtime via the standard Solid-aware bundler.
95
+ export async function bundleSolid(): Promise<string> {
96
+ let result = await bundle()
97
+ if (!result) {
98
+ console.error("Build failed")
99
+ process.exit(1)
100
+ }
101
+ let jsCode = ""
102
+ for (let output of result.outputs) jsCode += await output.text()
103
+ return jsCode
104
+ }
105
+
106
+ // Compile JS source to QuickJS bytecode via the fluxc binary.
107
+ export async function compileToBytecode(jsCode: string): Promise<Buffer> {
108
+ let compiler = requireBinary("fluxc")
109
+ let proc = Bun.spawn([compiler], {
110
+ stdin: new Blob([jsCode]),
111
+ stdout: "pipe",
112
+ stderr: "inherit",
113
+ })
114
+ let [bytecode, code] = await Promise.all([new Response(proc.stdout).arrayBuffer(), proc.exited])
115
+ if (code !== 0) process.exit(code)
116
+ return Buffer.from(bytecode)
117
+ }
@@ -0,0 +1,59 @@
1
+ import { values, source, isPrebuilt } from "../args"
2
+ import { bundle, bundleTo, compileToBytecode } from "../bundler"
3
+ import { resolve } from "path"
4
+
5
+ // Compile JS to a .srt.bin file and report its size.
6
+ async function writeBytecode(jsCode: string, outfile: string) {
7
+ let bytecode = await compileToBytecode(jsCode)
8
+ await Bun.write(outfile, bytecode)
9
+ let binSize = (await Bun.file(outfile).stat()).size
10
+ console.log(`>> wrote ${binSize} bytes to ${outfile}`)
11
+ }
12
+
13
+ export async function runBundleCommand() {
14
+ if (isPrebuilt) {
15
+ if (!source!.endsWith(".srt.js")) {
16
+ console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
17
+ process.exit(1)
18
+ }
19
+ let jsFile = resolve(source!)
20
+ let binOut = jsFile.replace(/\.srt\.js$/, ".srt.bin").replace(/\.js$/, ".bin")
21
+ await writeBytecode(await Bun.file(jsFile).text(), binOut)
22
+ process.exit()
23
+ }
24
+
25
+ let baseName = values.output ?? source!.replace(/\.[jt]sx?$/, "")
26
+
27
+ if (values.stdout) {
28
+ let result = await bundle()
29
+ if (!result) {
30
+ console.error("Build failed")
31
+ process.exit(1)
32
+ }
33
+ for (let output of result.outputs) {
34
+ process.stdout.write(await output.text())
35
+ }
36
+ process.exit()
37
+ }
38
+
39
+ if (values.compile) {
40
+ let result = await bundle()
41
+ if (!result) {
42
+ console.error("Build failed")
43
+ process.exit(1)
44
+ }
45
+ let jsCode = ""
46
+ for (let output of result.outputs) {
47
+ jsCode += await output.text()
48
+ }
49
+ await writeBytecode(jsCode, baseName + ".srt.bin")
50
+ process.exit()
51
+ }
52
+
53
+ let jsOutfile = baseName + ".srt.js"
54
+ let result = await bundleTo(jsOutfile)
55
+ for (let output of result.outputs) {
56
+ console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
57
+ }
58
+ process.exit()
59
+ }
@@ -0,0 +1,22 @@
1
+ import { values } from "../args"
2
+ import { requireBinary, run } from "../util"
3
+ import { spawnAndroidClient } from "../dev-android"
4
+
5
+ // Standalone solidrt-go client (no dev server). The `run` command instead uses
6
+ // spawnClient() to launch a client tied to the dev-server lifecycle. Either way
7
+ // the client discovers a dev server over the LAN; with --android it is installed
8
+ // and launched on a connected Android device instead of run locally.
9
+ export async function runClientCommand() {
10
+ if (values.android) {
11
+ await spawnAndroidClient()
12
+ return
13
+ }
14
+
15
+ let runner = requireBinary("solidrt-go")
16
+ let args: string[] = []
17
+ if (values.size) args.push("--size", values.size)
18
+ //TODO add dev server connection
19
+ // if (source) args.push("--dev-server", source)
20
+ let exit = await run(runner, args)
21
+ process.exit(exit)
22
+ }
@@ -0,0 +1,25 @@
1
+ import { values, source } from "../args"
2
+ import { bundleFlux, bundleSolid } from "../bundler"
3
+ import { packRunner } from "../packer"
4
+
5
+ // Write the packed executable, mark it runnable, and report its size.
6
+ async function writeExecutable(packed: Buffer, outfile: string) {
7
+ await Bun.write(outfile, packed)
8
+ if (process.platform !== "win32") {
9
+ Bun.spawnSync(["chmod", "+x", outfile])
10
+ }
11
+ console.log(`>> wrote ${packed.length} bytes to ${outfile}`)
12
+ }
13
+
14
+ export async function runPackCommand() {
15
+ let outfile = values.output ?? source!.replace(/\.[jt]sx?$/, "")
16
+ // On Windows the packed image is a PE executable; it needs a .exe name to run.
17
+ if (process.platform === "win32" && !outfile.toLowerCase().endsWith(".exe")) {
18
+ outfile += ".exe"
19
+ }
20
+ let packed = values.flux
21
+ ? await packRunner("fluxrt", await bundleFlux(source!))
22
+ : await packRunner("solidrt", await bundleSolid())
23
+ await writeExecutable(packed, outfile)
24
+ process.exit()
25
+ }
@@ -0,0 +1,16 @@
1
+ import { source, values } from "../args"
2
+ import { requireBinary, run } from "../util"
3
+ import { bundleTo } from "../bundler"
4
+ import { resolve } from "path"
5
+
6
+ export async function runRecordCommand() {
7
+ let jsOutfile = source!.replace(/\.[jt]sx$/, "") + ".srt.js"
8
+ await bundleTo(jsOutfile)
9
+ let runner = requireBinary("solidrt-go")
10
+ let recordArgs = ["--record", resolve(jsOutfile)]
11
+ if (values.fps) recordArgs.push("--fps", values.fps)
12
+ if (values.duration) recordArgs.push("--duration", values.duration)
13
+ if (values.size) recordArgs.push("--size", values.size)
14
+ let exit = await run(runner, recordArgs)
15
+ process.exit(exit)
16
+ }
@@ -0,0 +1,45 @@
1
+ import pkg from "../../package.json"
2
+ import { source, isSource, isPrebuilt, values } from "../args"
3
+ import { state, shutdown } from "../util"
4
+ import { bundle } from "../bundler"
5
+ import { startServer } from "../dev-server"
6
+ import { startRepl } from "../repl"
7
+ import { startWatcher } from "../watcher"
8
+ import * as cache from "../cache"
9
+ import { resolve, dirname } from "path"
10
+
11
+ // Brings up the dev server (HTTP/WS + initial bundle + repl + watcher). The
12
+ // `run` command spawns a local client on top of this from main.ts.
13
+ export async function runServerCommand() {
14
+ // Initialize state from args
15
+ state.source = source
16
+ state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
17
+
18
+ if (values["proxy-http"]) {
19
+ cache.initCache({ dir: process.cwd() })
20
+ console.log("[cli] HTTP cache enabled")
21
+ }
22
+
23
+ startServer()
24
+
25
+ // Bundle initial code if source file given (after server start so the
26
+ // dev base URL is available to the bundler).
27
+ if (source && isSource) {
28
+ let initialResult = await bundle()
29
+ if (initialResult) {
30
+ for (let output of initialResult.outputs) {
31
+ state.currentCode = await output.text()
32
+ }
33
+ }
34
+ } else if (source && isPrebuilt && source.endsWith(".srt.js")) {
35
+ state.currentCode = await Bun.file(resolve(source)).text()
36
+ }
37
+
38
+ process.on("SIGINT", shutdown)
39
+ process.on("SIGTERM", shutdown)
40
+
41
+ let version = pkg.version === "0.0.0" ? "" : " version " + pkg.version
42
+ console.log(`[cli] Welcome to SolidRT${version}!`)
43
+ startRepl()
44
+ startWatcher()
45
+ }
@@ -0,0 +1,69 @@
1
+ import { print, requireAdb } from "./util"
2
+ import { resolveApk } from "./artifacts"
3
+ import { values } from "./args"
4
+
5
+ // Launch component of the "go" dev-client flavor (see lattice/Makefile.x-android).
6
+ let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
7
+
8
+ // Prefix adb args with `-s <serial>` only when the user pinned a device; with a
9
+ // single connected device adb selects it on its own.
10
+ function adbArgs(extra: string[]) {
11
+ return values.device ? ["-s", values.device, ...extra] : extra
12
+ }
13
+
14
+ // Install + launch the Android client on a connected device over adb, then let
15
+ // the device discover the running dev server over LAN UDP (the same path as a
16
+ // manually launched client). Fire-and-forget: the client's lifecycle is tracked
17
+ // via WS connect/disconnect in dev-server.ts, not as a child process here.
18
+ export async function spawnAndroidClient() {
19
+ let adb = requireAdb()
20
+
21
+ let apk = resolveApk()
22
+ if (!apk) {
23
+ console.error("Could not find the SolidRT-Go APK.")
24
+ console.error("Add it with: bun add -d @solidrt/android-arm64-v8a")
25
+ process.exit(1)
26
+ }
27
+
28
+ // Resolve the target device: 0 -> error, 1 -> use it, many -> require --device.
29
+ let target = values.device
30
+ if (!target) {
31
+ let listed = Bun.spawnSync([adb, "devices"], { stdout: "pipe", stderr: "pipe" })
32
+ let devices = listed.stdout
33
+ .toString()
34
+ .split("\n")
35
+ .slice(1)
36
+ .map((l) => l.trim())
37
+ .filter(Boolean)
38
+ .filter((l) => l.endsWith("\tdevice"))
39
+ .map((l) => l.split("\t")[0])
40
+
41
+ if (devices.length === 0) {
42
+ console.error("No authorized Android device found. Enable USB debugging and check `adb devices`.")
43
+ process.exit(1)
44
+ }
45
+ if (devices.length > 1) {
46
+ console.error(`Multiple devices connected (${devices.join(", ")}); pick one with --device <serial>.`)
47
+ process.exit(1)
48
+ }
49
+ target = devices[0]
50
+ }
51
+
52
+ print(`[cli] Installing SolidRT-Go on ${target}`)
53
+ let install = Bun.spawn([adb, ...adbArgs(["install", "-r", apk])], { stdout: "pipe", stderr: "pipe" })
54
+ if ((await install.exited) !== 0) {
55
+ console.error("adb install failed:\n" + (await new Response(install.stderr).text()))
56
+ process.exit(1)
57
+ }
58
+
59
+ let start = Bun.spawn([adb, ...adbArgs(["shell", "am", "start", "-n", PACKAGE_ACTIVITY])], {
60
+ stdout: "pipe",
61
+ stderr: "pipe",
62
+ })
63
+ if ((await start.exited) !== 0) {
64
+ console.error("adb start failed:\n" + (await new Response(start.stderr).text()))
65
+ process.exit(1)
66
+ }
67
+
68
+ print(`[cli] Launched SolidRT-Go on ${target}; waiting for it to discover the dev server...`)
69
+ }
@@ -1,4 +1,5 @@
1
- import { DEV_HOST, DEV_PORT, state, print, requireBinary } from "./util"
1
+ import { state, print, requireBinary } from "./util"
2
+ import { DEV_HOST, DEV_PORT } from "./dev-server"
2
3
  import { values } from "./args"
3
4
 
4
5
  function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteStream) {
@@ -3,9 +3,24 @@ import { stat as fsStat, readdir } from "node:fs/promises"
3
3
  import { networkInterfaces } from "node:os"
4
4
  import { createSocket } from "node:dgram"
5
5
  import qrcode from "qrcode-generator"
6
- import { DEV_HOST, DEV_PORT, state, print, buildReload } from "./util"
6
+ import { state, print } from "./util"
7
+ import { values } from "./args"
7
8
  import * as cache from "./cache"
8
9
 
10
+ export const DEV_HOST = "127.0.0.1"
11
+ export const DEV_PORT = 15194
12
+
13
+ // Dev-server WS protocol helpers: the reload message shape and the stop broadcast.
14
+ export function buildReload(payload: { code?: string | null; bytecode?: string }) {
15
+ return JSON.stringify({ type: "reload", proxyFiles: values["proxy-files"], proxyHttp: values["proxy-http"], ...payload })
16
+ }
17
+
18
+ export function broadcastStop() {
19
+ for (let ws of state.clients.keys()) {
20
+ ws.send(JSON.stringify({ type: "stop" }))
21
+ }
22
+ }
23
+
9
24
  function headersToObject(h: Headers): Record<string, string> {
10
25
  let out: Record<string, string> = {}
11
26
  h.forEach((v, k) => {
package/src/main.ts CHANGED
@@ -1,52 +1,16 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- // Build/run script that bundles/runs a Solid-RT app for the QuickJS runtime.
4
- //
5
- // Usage:
6
- // srt run - start dev server + client
7
- // srt run examples/hello.tsx - start dev server + client, bundle + push via WS
8
- // srt server examples/hello.tsx - start dev server only, no client
9
- // srt client - start dev client only (connects to WS server)
10
- // srt bundle examples/hello.tsx - bundle TSX to .srt.js
11
- // srt bundle -c examples/hello.tsx - bundle TSX to .srt.js + compile to .srt.bin
12
- // srt bundle examples/hello.srt.js - compile .srt.js to .srt.bin
13
- // srt record examples/hello.tsx - bundle TSX and run with frame capture
14
-
15
- import pkg from "../package.json"
16
- import { values, command, source, isTsx, isTs, isPrebuilt, printUsage } from "./args"
17
- import { state, requireBinary, run, shutdown } from "./util"
18
- import { bundle, bundleTo, runBundleCommand } from "./bundle"
19
- import { runPackCommand } from "./pack"
20
- import { startServer } from "./server"
21
- import { spawnClient } from "./client"
22
- import { startRepl } from "./repl"
23
- import { startWatcher } from "./watcher"
24
- import * as cache from "./cache"
25
- import { resolve, dirname } from "path"
3
+ import { values, command, validateArgs, printUsage } from "./args"
4
+ import { runBundleCommand } from "./commands/bundle"
5
+ import { runPackCommand } from "./commands/pack"
6
+ import { runRecordCommand } from "./commands/record"
7
+ import { runServerCommand } from "./commands/server"
8
+ import { runClientCommand } from "./commands/client"
9
+ import { spawnClient } from "./dev-client"
26
10
 
27
11
  // -- Validate args --
28
12
 
29
- let COMMANDS = ["run", "server", "client", "bundle", "record", "pack"]
30
-
31
- if (!command || !COMMANDS.includes(command)) {
32
- printUsage()
33
- process.exit(1)
34
- }
35
-
36
- if (command === "bundle" && (!source || (!isTsx && !isPrebuilt))) {
37
- console.error("Usage: srt bundle [options] <entry.[tsx|jsx|srt.js|srt.bin]>")
38
- process.exit(1)
39
- }
40
-
41
- if (command === "record" && (!source || !isTsx)) {
42
- console.error("Usage: srt record <entry.[tsx|jsx]>")
43
- process.exit(1)
44
- }
45
-
46
- if (command === "pack" && (!source || !isTs)) {
47
- console.error("Usage: srt pack [options] <entry.[ts|js]>")
48
- process.exit(1)
49
- }
13
+ validateArgs()
50
14
 
51
15
  // Force the production export condition for prod bundles. Bun auto-activates the
52
16
  // "development" condition whenever NODE_ENV != "production" (read once at startup),
@@ -69,76 +33,22 @@ if (isProdBuild && process.env.NODE_ENV !== "production") {
69
33
  process.exit(proc.exitCode ?? 0)
70
34
  }
71
35
 
72
- // -- Bundle command --
36
+ // -- Dispatch --
73
37
 
74
38
  if (command === "bundle") {
75
39
  await runBundleCommand()
76
- }
77
-
78
- if (command === "pack") {
40
+ } else if (command === "pack") {
79
41
  await runPackCommand()
80
- }
81
-
82
- // -- Record command --
83
-
84
- if (command === "record") {
85
- let jsOutfile = source!.replace(/\.[jt]sx$/, "") + ".srt.js"
86
- await bundleTo(jsOutfile)
87
- let runner = requireBinary("solidrt-go")
88
- let recordArgs = ["--record", resolve(jsOutfile)]
89
- if (values.fps) recordArgs.push("--fps", values.fps)
90
- if (values.duration) recordArgs.push("--duration", values.duration)
91
- if (values.size) recordArgs.push("--size", values.size)
92
- let exit = await run(runner, recordArgs)
93
- process.exit(exit)
94
- }
95
-
96
- // -- Client command --
97
-
98
- if (command === "client") {
99
- let runner = requireBinary("solidrt-go")
100
- let args: string[] = []
101
- if (values.size) args.push("--size", values.size)
102
- //TODO add dev server connection
103
- // if (source) args.push("--dev-server", source)
104
- let exit = await run(runner, args)
105
- process.exit(exit)
106
- }
107
-
108
- // -- Server / Run command --
109
-
110
- // Initialize state from args
111
- state.source = source
112
- state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
113
-
114
- if (values["proxy-http"]) {
115
- cache.initCache({ dir: process.cwd() })
116
- console.log("[cli] HTTP cache enabled")
117
- }
118
-
119
- startServer()
120
-
121
- // Bundle initial code if source file given (after server start so the
122
- // dev base URL is available to the bundler).
123
- if (source && isTsx) {
124
- let initialResult = await bundle()
125
- if (initialResult) {
126
- for (let output of initialResult.outputs) {
127
- state.currentCode = await output.text()
128
- }
129
- }
130
- } else if (source && isPrebuilt && source.endsWith(".srt.js")) {
131
- state.currentCode = await Bun.file(resolve(source)).text()
132
- }
133
-
134
- if (command === "run") {
42
+ } else if (command === "record") {
43
+ await runRecordCommand()
44
+ } else if (command === "client") {
45
+ await runClientCommand()
46
+ } else if (command === "server") {
47
+ await runServerCommand()
48
+ } else if (command === "run") {
49
+ await runServerCommand()
135
50
  spawnClient()
136
- }
137
-
138
- process.on("SIGINT", shutdown)
139
- process.on("SIGTERM", shutdown)
140
-
141
- let version = pkg.version === "0.0.0" ? "" : " version " + pkg.version
142
- console.log(`[cli] Welcome to SolidRT${version}!`)
143
- startRepl()
144
- startWatcher()
51
+ } else {
52
+ printUsage()
53
+ process.exit(1)
54
+ }
package/src/packer.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { requireBinary } from "./util"
2
+ import { compileToBytecode } from "./bundler"
3
+
4
+ // Trailer magic identifying the runner an embedded payload belongs to. Must match
5
+ // the runner-side checks: fluxrt -> flux/src/bin/fluxrt.rs, solidrt ->
6
+ // lattice/src/main.rs (load_embedded_bytecode).
7
+ const MAGIC = {
8
+ fluxrt: Buffer.from([0x46, 0x4c, 0x55, 0x58, 0x52, 0x54, 0x88, 0x44]), // "FLUXRT\x88\x44"
9
+ solidrt: Buffer.from([0x53, 0x4f, 0x4c, 0x49, 0x44, 0x52, 0x54, 0x88, 0x44]), // "SOLIDRT\x88\x44"
10
+ }
11
+
12
+ export type Runner = keyof typeof MAGIC
13
+
14
+ // Compile JS to bytecode and append it to the runner binary, followed by a
15
+ // trailer of [u64 offset LE][8-byte magic]. The runner reads its own image at
16
+ // startup, validates the magic, and slices the bytecode back out.
17
+ export async function packRunner(runner: Runner, jsCode: string): Promise<Buffer> {
18
+ let bytecode = await compileToBytecode(jsCode)
19
+
20
+ let runnerPath = requireBinary(runner)
21
+ let runnerBytes = Buffer.from(await Bun.file(runnerPath).arrayBuffer())
22
+
23
+ let offsetBuf = Buffer.allocUnsafe(8)
24
+ offsetBuf.writeBigUInt64LE(BigInt(runnerBytes.length))
25
+
26
+ return Buffer.concat([runnerBytes, bytecode, offsetBuf, MAGIC[runner]])
27
+ }
package/src/repl.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createInterface } from "node:readline"
2
2
  import { resolve, dirname } from "path"
3
3
  import { readdirSync } from "node:fs"
4
- import { state, print, printErr, broadcastStop, buildReload, shutdown } from "./util"
5
- import { bundle } from "./bundle"
4
+ import { state, print, printErr, shutdown } from "./util"
5
+ import { buildReload, broadcastStop } from "./dev-server"
6
+ import { bundle } from "./bundler"
6
7
  import { startWatcher, stopWatcher } from "./watcher"
7
8
 
8
9
  function cmdStop(args: string) {
package/src/util.ts CHANGED
@@ -1,11 +1,9 @@
1
- import { resolveBinary } from "./native"
2
- import { values } from "./args"
1
+ import { resolveBinary } from "./artifacts"
2
+ import { existsSync } from "node:fs"
3
+ import { resolve } from "node:path"
3
4
  import type { Interface as ReadlineInterface } from "node:readline"
4
5
  import type { Server as BunServer } from "bun"
5
6
 
6
- export const DEV_HOST = "127.0.0.1"
7
- export const DEV_PORT = 15194
8
-
9
7
  export let state = {
10
8
  clients: new Map<any, { platform: string; version: string }>(),
11
9
  currentCode: null as string | null,
@@ -17,11 +15,50 @@ export let state = {
17
15
  rl: null as ReadlineInterface | null,
18
16
  }
19
17
 
18
+ // Build target per binary, for the "not found" hint. Run from the repo root.
19
+ let BUILD_HINTS: Record<string, string> = {
20
+ "solidrt-go": "make solidrt-go",
21
+ solidrt: "make runtime",
22
+ flux: "make -C flux flux",
23
+ fluxc: "make -C flux fluxc",
24
+ fluxrt: "make -C flux fluxrt PROFILE=release-opt",
25
+ }
26
+
20
27
  export function requireBinary(name: string) {
21
28
  let path = resolveBinary(name)
22
29
  if (path) return path
30
+ let hint = BUILD_HINTS[name]
23
31
  console.error(`Could not find ${name} binary.`)
24
- console.error("Build from source: run make solidrt-go, then set SRT_HOME=<SolidRT project home>")
32
+ if (hint) {
33
+ console.error(`Build it from source: run ${hint}, with SRT_HOME pointing at your SolidRT checkout.`)
34
+ } else {
35
+ console.error("Build it from source, with SRT_HOME pointing at your SolidRT checkout.")
36
+ }
37
+ process.exit(1)
38
+ }
39
+
40
+ // adb is a system tool (Android Platform Tools), never bundled. Look on PATH
41
+ // first, then the standard SDK location.
42
+ export function resolveAdb() {
43
+ let exe = process.platform === "win32" ? "adb.exe" : "adb"
44
+ let onPath = Bun.which(exe)
45
+ if (onPath) return onPath
46
+ for (let root of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
47
+ if (!root) continue
48
+ let candidate = resolve(root, "platform-tools", exe)
49
+ if (existsSync(candidate)) return candidate
50
+ }
51
+ return null
52
+ }
53
+
54
+ export function requireAdb() {
55
+ let path = resolveAdb()
56
+ if (path) return path
57
+ console.error("Could not find adb (Android Platform Tools).")
58
+ console.error("Install it:")
59
+ console.error(" Windows: winget install Google.PlatformTools")
60
+ console.error(" macOS: brew install android-platform-tools")
61
+ console.error(" Linux: install your distro's android-tools / adb package")
25
62
  process.exit(1)
26
63
  }
27
64
 
@@ -42,16 +79,6 @@ export function printErr(...args: any[]) {
42
79
  state.rl?.prompt(true)
43
80
  }
44
81
 
45
- export function buildReload(payload: { code?: string | null; bytecode?: string }) {
46
- return JSON.stringify({ type: "reload", proxyFiles: values["proxy-files"], proxyHttp: values["proxy-http"], ...payload })
47
- }
48
-
49
- export function broadcastStop() {
50
- for (let ws of state.clients.keys()) {
51
- ws.send(JSON.stringify({ type: "stop" }))
52
- }
53
- }
54
-
55
82
  export function shutdown() {
56
83
  if (state.child) state.child.kill()
57
84
  if (state.server) state.server.stop()
package/src/watcher.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { watch } from "node:fs"
2
2
  import { resolve, dirname } from "path"
3
- import { state, print, printErr, buildReload } from "./util"
4
- import { bundle } from "./bundle"
3
+ import { state, print, printErr } from "./util"
4
+ import { buildReload } from "./dev-server"
5
+ import { bundle } from "./bundler"
5
6
 
6
7
  let currentWatcher: ReturnType<typeof watch> | null = null
7
8
 
@@ -1,21 +0,0 @@
1
- import { transformAsync } from "@babel/core"
2
- import ts from "@babel/preset-typescript"
3
- import solid from "babel-preset-solid"
4
- import { type BunPlugin } from "bun"
5
-
6
- export function solidPlugin(): BunPlugin {
7
- return {
8
- name: "bun-plugin-solid",
9
- setup: (build) => {
10
- build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
11
- let file = Bun.file(args.path)
12
- let code = await file.text()
13
- let transforms = await transformAsync(code, {
14
- filename: args.path,
15
- presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
16
- })
17
- return { contents: transforms?.code ?? "", loader: "js" }
18
- })
19
- },
20
- }
21
- }
package/src/bundle.ts DELETED
@@ -1,126 +0,0 @@
1
- import { solidPlugin } from "./bun-plugin-solid"
2
- import { values, source, isPrebuilt } from "./args"
3
- import { requireBinary, state, print } from "./util"
4
- import { resolve } from "path"
5
-
6
- export async function bundle(entry = source) {
7
- let result = null
8
-
9
- let devBase = state.serverUrl ?? undefined
10
- let dev = !!devBase || values.dev
11
- print(`[cli] Bundling (${dev ? "development" : "production"})`)
12
- let define: Record<string, string> = {
13
- "process.env.NODE_ENV": dev ? "development" : "production",
14
- }
15
- if (devBase) define.__SRT_DEV_BASE__ = devBase
16
-
17
- try {
18
- result = await Bun.build({
19
- entrypoints: [entry!],
20
- target: "browser",
21
- format: "esm",
22
- minify: values.minify,
23
- external: ["qjs:*", "flux:*"],
24
- define,
25
- plugins: [solidPlugin()],
26
- })
27
- } catch (e) {
28
- console.error("[cli] compile error:\n", e)
29
- return null
30
- }
31
-
32
- if (result?.success) {
33
- return result
34
- }
35
-
36
- if (result) {
37
- for (let msg of result?.logs) console.error(msg)
38
- }
39
- return null
40
- }
41
-
42
- export async function bundleTo(outfile: string) {
43
- let result = await bundle()
44
- if (!result) {
45
- console.error("Build failed")
46
- process.exit(1)
47
- }
48
- for (let output of result.outputs) {
49
- await Bun.write(outfile, output)
50
- }
51
- return result
52
- }
53
-
54
- async function compileJs(jsCode: string, outfile: string) {
55
- let compiler = requireBinary("fluxc")
56
- let proc = Bun.spawn([compiler], {
57
- stdin: new Blob([jsCode]),
58
- stdout: "pipe",
59
- stderr: "inherit",
60
- })
61
- let [bytecode, code] = await Promise.all([new Response(proc.stdout).arrayBuffer(), proc.exited])
62
- if (code !== 0) process.exit(code)
63
- await Bun.write(outfile, bytecode)
64
- return outfile
65
- }
66
-
67
- async function compileToBytecode(jsFile: string, outFile?: string) {
68
- let jsCode = await Bun.file(jsFile).text()
69
- let dest = outFile ?? jsFile.replace(/\.srt\.js$/, ".srt.bin").replace(/\.js$/, ".bin")
70
- return compileJs(jsCode, dest)
71
- }
72
-
73
- async function compileFromStdin(jsCode: string, outfile: string) {
74
- return compileJs(jsCode, outfile)
75
- }
76
-
77
- export async function runBundleCommand() {
78
- if (isPrebuilt) {
79
- if (!source!.endsWith(".srt.js")) {
80
- console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
81
- process.exit(1)
82
- }
83
- let binOut = await compileToBytecode(resolve(source!))
84
- let binSize = (await Bun.file(binOut).stat()).size
85
- console.log(`>> wrote ${binSize} bytes to ${binOut}`)
86
- process.exit()
87
- }
88
-
89
- let baseName = values.output ?? source!.replace(/\.[jt]sx$/, "")
90
-
91
- if (values.stdout) {
92
- let result = await bundle()
93
- if (!result) {
94
- console.error("Build failed")
95
- process.exit(1)
96
- }
97
- for (let output of result.outputs) {
98
- process.stdout.write(await output.text())
99
- }
100
- process.exit()
101
- }
102
-
103
- if (values.compile) {
104
- let result = await bundle()
105
- if (!result) {
106
- console.error("Build failed")
107
- process.exit(1)
108
- }
109
- let jsCode = ""
110
- for (let output of result.outputs) {
111
- jsCode += await output.text()
112
- }
113
- let binOutfile = baseName + ".srt.bin"
114
- await compileFromStdin(jsCode, binOutfile)
115
- let binSize = (await Bun.file(binOutfile).stat()).size
116
- console.log(`>> wrote ${binSize} bytes to ${binOutfile}`)
117
- process.exit()
118
- }
119
-
120
- let jsOutfile = baseName + ".srt.js"
121
- let result = await bundleTo(jsOutfile)
122
- for (let output of result.outputs) {
123
- console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
124
- }
125
- process.exit()
126
- }
package/src/pack.ts DELETED
@@ -1,58 +0,0 @@
1
- import { values, source } from "./args"
2
- import { requireBinary } from "./util"
3
-
4
- const MAGIC = Buffer.from([0x46, 0x4c, 0x55, 0x58, 0x52, 0x54, 0x00, 0x01]) // "FLUXRT\x00\x01"
5
-
6
- async function bundleFlux(entry: string): Promise<string> {
7
- let result = await Bun.build({
8
- entrypoints: [entry],
9
- target: "browser",
10
- format: "esm",
11
- minify: values.minify,
12
- external: ["qjs:*", "flux:*"],
13
- })
14
- if (!result.success) {
15
- for (let msg of result.logs) console.error(msg)
16
- console.error("Build failed")
17
- process.exit(1)
18
- }
19
- let jsCode = ""
20
- for (let output of result.outputs) jsCode += await output.text()
21
- return jsCode
22
- }
23
-
24
- async function compileToBytecode(jsCode: string): Promise<Buffer> {
25
- let compiler = requireBinary("fluxc")
26
- let proc = Bun.spawn([compiler], {
27
- stdin: new Blob([jsCode]),
28
- stdout: "pipe",
29
- stderr: "inherit",
30
- })
31
- let [bytecode, code] = await Promise.all([new Response(proc.stdout).arrayBuffer(), proc.exited])
32
- if (code !== 0) process.exit(code)
33
- return Buffer.from(bytecode)
34
- }
35
-
36
- export async function runPackCommand() {
37
- let outfile = values.output ?? source!.replace(/\.[jt]s$/, "")
38
-
39
- let jsCode = await bundleFlux(source!)
40
-
41
- let bytecode = await compileToBytecode(jsCode)
42
-
43
- let runner = requireBinary("fluxrt")
44
- let runnerBytes = Buffer.from(await Bun.file(runner).arrayBuffer())
45
-
46
- let offsetBuf = Buffer.allocUnsafe(8)
47
- offsetBuf.writeBigUInt64LE(BigInt(runnerBytes.length))
48
-
49
- let packed = Buffer.concat([runnerBytes, bytecode, offsetBuf, MAGIC])
50
- await Bun.write(outfile, packed)
51
-
52
- if (process.platform !== "win32") {
53
- Bun.spawnSync(["chmod", "+x", outfile])
54
- }
55
-
56
- console.log(`>> wrote ${packed.length} bytes to ${outfile}`)
57
- process.exit()
58
- }