@solidrt/cli 0.0.1-exp.1

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/bin/srt ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/main"
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@solidrt/cli",
3
+ "version": "0.0.1-exp.1",
4
+ "author": "Antoine van Wel",
5
+ "type": "module",
6
+ "bin": {
7
+ "srt": "./bin/srt"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "dependencies": {
14
+ "@babel/core": "^7.28.5",
15
+ "@babel/preset-typescript": "^7.28.5",
16
+ "babel-preset-solid": "2.0.0-beta.7",
17
+ "qrcode-generator": "^2.0.4"
18
+ },
19
+ "optionalDependencies": {
20
+ "@solidrt/android-arm64-v8a": "0.0.0",
21
+ "@solidrt/darwin-arm64": "0.0.0",
22
+ "@solidrt/linux-x64-gnu": "0.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@solidjs/signals": "2.0.0-beta.7",
26
+ "@solidjs/universal": "2.0.0-beta.7",
27
+ "@solidrt/core": "0.0.0",
28
+ "typescript": "^5"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest"
32
+ }
33
+ }
package/src/args.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { parseArgs } from "node:util"
2
+
3
+ export let { values, positionals } = parseArgs({
4
+ options: {
5
+ minify: { type: "boolean", short: "m", default: false },
6
+ compile: { type: "boolean", short: "c", default: false },
7
+ stdout: { type: "boolean", default: false },
8
+ output: { type: "string", short: "o" },
9
+ client: { type: "boolean", default: false },
10
+ server: { type: "boolean", default: false },
11
+ },
12
+ allowPositionals: true,
13
+ })
14
+
15
+ export let command = positionals[0]
16
+ export let source = positionals[1]
17
+ export let isTsx = source?.endsWith(".tsx")
18
+ export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
19
+
20
+ export function printUsage() {
21
+ console.error(`Usage: srt <build|run> [options] [entry.tsx]
22
+
23
+ Commands:
24
+ run [file.tsx] Start dev server + client (no file = embedded default)
25
+ run --client Start dev client only (connects to WS server)
26
+ run --server [file] Start dev server only, no client
27
+
28
+ Options:
29
+ -m, --minify Minify the output
30
+ -c, --compile Compile to bytecode (build only)
31
+ -o, --output <name> Bundle filename (build only)
32
+ --stdout Write bundle to stdout (build only)`)
33
+ }
package/src/build.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { solidPlugin } from "./bun-plugin-solid"
2
+ import { values, source, isPrebuilt } from "./args"
3
+ import { requireBinary, state } 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 define: Record<string, string> = { "process.env.NODE_ENV": "production" }
11
+ if (devBase) define.__SRT_DEV_BASE__ = JSON.stringify(devBase)
12
+
13
+ try {
14
+ result = await Bun.build({
15
+ entrypoints: [entry!],
16
+ target: "browser",
17
+ format: "esm",
18
+ minify: values.minify,
19
+ external: ["qjs:*"],
20
+ define,
21
+ plugins: [solidPlugin({ devBase })],
22
+ })
23
+ } catch (e) {
24
+ console.error("[dev] compile error:\n", e)
25
+ return null
26
+ }
27
+
28
+ if (result?.success) {
29
+ return result
30
+ }
31
+
32
+ if (result) {
33
+ for (let msg of result?.logs) console.error(msg)
34
+ }
35
+ return null
36
+ }
37
+
38
+ export async function bundleTo(outfile: string) {
39
+ let result = await bundle()
40
+ if (!result) {
41
+ console.error("Build failed")
42
+ process.exit(1)
43
+ }
44
+ for (let output of result.outputs) {
45
+ await Bun.write(outfile, output)
46
+ }
47
+ return result
48
+ }
49
+
50
+ async function compileJs(jsCode: string, outfile: string) {
51
+ let compiler = requireBinary("flux")
52
+ let proc = Bun.spawn([compiler], {
53
+ stdin: new Blob([jsCode]),
54
+ stdout: "pipe",
55
+ stderr: "inherit",
56
+ })
57
+ let [bytecode, code] = await Promise.all([new Response(proc.stdout).arrayBuffer(), proc.exited])
58
+ if (code !== 0) process.exit(code)
59
+ await Bun.write(outfile, bytecode)
60
+ return outfile
61
+ }
62
+
63
+ async function compileToBytecode(jsFile: string, outFile?: string) {
64
+ let jsCode = await Bun.file(jsFile).text()
65
+ let dest = outFile ?? jsFile.replace(/\.srt\.js$/, ".srt.bin").replace(/\.js$/, ".bin")
66
+ return compileJs(jsCode, dest)
67
+ }
68
+
69
+ async function compileFromStdin(jsCode: string, outfile: string) {
70
+ return compileJs(jsCode, outfile)
71
+ }
72
+
73
+ export async function runBuildCommand() {
74
+ if (isPrebuilt) {
75
+ if (!source!.endsWith(".srt.js")) {
76
+ console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
77
+ process.exit(1)
78
+ }
79
+ await compileToBytecode(resolve(source!))
80
+ process.exit()
81
+ }
82
+
83
+ let baseName = values.output ?? source!.replace(/\.tsx$/, "")
84
+
85
+ if (values.stdout) {
86
+ let result = await bundle()
87
+ if (!result) {
88
+ console.error("Build failed")
89
+ process.exit(1)
90
+ }
91
+ for (let output of result.outputs) {
92
+ process.stdout.write(await output.text())
93
+ }
94
+ process.exit()
95
+ }
96
+
97
+ if (values.compile) {
98
+ let result = await bundle()
99
+ if (!result) {
100
+ console.error("Build failed")
101
+ process.exit(1)
102
+ }
103
+ let jsCode = ""
104
+ for (let output of result.outputs) {
105
+ jsCode += await output.text()
106
+ }
107
+ let binOutfile = baseName + ".srt.bin"
108
+ await compileFromStdin(jsCode, binOutfile)
109
+ process.exit()
110
+ }
111
+
112
+ let jsOutfile = baseName + ".srt.js"
113
+ let result = await bundleTo(jsOutfile)
114
+ for (let output of result.outputs) {
115
+ console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
116
+ }
117
+ process.exit()
118
+ }
@@ -0,0 +1,38 @@
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 { resolve } from "path"
6
+
7
+ // let ioDevPath = resolve(import.meta.dir, "io-dev.ts")
8
+
9
+ export function solidPlugin(opts: { devBase?: string } = {}): BunPlugin {
10
+ return {
11
+ name: "bun-plugin-solid",
12
+ setup: (build) => {
13
+ build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
14
+ let file = Bun.file(args.path)
15
+ let code = await file.text()
16
+ let transforms = await transformAsync(code, {
17
+ filename: args.path,
18
+ presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
19
+ })
20
+ return { contents: transforms?.code ?? "", loader: "js" }
21
+ })
22
+
23
+ // // -- dev-mode qjs:io rewrite ----------------------------------
24
+ // // In dev, user code's `import * as io from "qjs:io"` is redirected
25
+ // // to a wrapper that proxies non-http targets through the dev
26
+ // // server. The wrapper itself imports `qjs:io` -- that one import
27
+ // // is short-circuited to `external` to break the cycle.
28
+ // if (opts.devBase) {
29
+ // build.onResolve({ filter: /^qjs:io$/ }, (args) => {
30
+ // if (args.importer === ioDevPath) {
31
+ // return { path: "qjs:io", external: true }
32
+ // }
33
+ // return { path: ioDevPath }
34
+ // })
35
+ // }
36
+ },
37
+ }
38
+ }
package/src/client.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { DEV_HOST, DEV_PORT, state, print, requireBinary } from "./util"
2
+
3
+ function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteStream) {
4
+ let reader = stream.getReader()
5
+ ;(async () => {
6
+ while (true) {
7
+ let { done, value } = await reader.read()
8
+ if (done || !value) break
9
+ process.stdout.write("\r\x1b[K")
10
+ out.write(value)
11
+ state.rl?.prompt(true)
12
+ }
13
+ })()
14
+ }
15
+
16
+ export function spawnClient() {
17
+ let runner = requireBinary("solidrt-go")
18
+ state.child = Bun.spawn([runner], {
19
+ //TODO implement dev server connection
20
+ // state.child = Bun.spawn([runner, "--dev-server", `${DEV_HOST}:${DEV_PORT}`], {
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ })
23
+
24
+ if (state.child.stdout && typeof state.child.stdout !== "number")
25
+ pipeAbovePrompt(state.child.stdout, process.stdout)
26
+ if (state.child.stderr && typeof state.child.stderr !== "number")
27
+ pipeAbovePrompt(state.child.stderr, process.stderr)
28
+
29
+ state.child.exited.then(() => {
30
+ if (state.clients.size === 0) {
31
+ state.server?.stop()
32
+ process.exit(0)
33
+ }
34
+ print(`[dev] Local client exited, ${state.clients.size} remote client(s) still connected`)
35
+ })
36
+ }
package/src/main.ts ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bun
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 run --client - start dev client only (connects to WS server)
9
+ // srt run --server examples/hello.tsx - start dev server only, no client
10
+ // srt build examples/hello.tsx - bundle TSX to .srt.js
11
+ // srt build -c examples/hello.tsx - bundle TSX to .srt.js + compile to .srt.bin
12
+ // srt build examples/hello.srt.js - compile .srt.js to .srt.bin
13
+
14
+ import pkg from "../package.json"
15
+ import { values, command, source, isTsx, isPrebuilt, printUsage } from "./args"
16
+ import { state, requireBinary, run, shutdown } from "./util"
17
+ import { bundle, runBuildCommand } from "./build"
18
+ import { startServer } from "./server"
19
+ import { spawnClient } from "./client"
20
+ import { startRepl } from "./repl"
21
+ import { startWatcher } from "./watcher"
22
+ import { resolve, dirname } from "path"
23
+
24
+ // -- Validate args --
25
+
26
+ if (!command || (command !== "build" && command !== "run")) {
27
+ printUsage()
28
+ process.exit(1)
29
+ }
30
+
31
+ if (command === "build" && (!source || (!isTsx && !isPrebuilt))) {
32
+ console.error("Usage: srt build [options] <entry.tsx|.srt.js|.srt.bin>")
33
+ process.exit(1)
34
+ }
35
+
36
+ // -- Build command --
37
+
38
+ if (command === "build") {
39
+ await runBuildCommand()
40
+ }
41
+
42
+ // -- Run command --
43
+
44
+ if (values.client) {
45
+ let runner = requireBinary("solidrt-go")
46
+ let args = []
47
+ //TODO add dev server connection
48
+ // if (source) args.push("--dev-server", source)
49
+ let exit = await run(runner, args)
50
+ process.exit(exit)
51
+ }
52
+
53
+ // Initialize state from args
54
+ state.source = source
55
+ state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
56
+
57
+ startServer()
58
+
59
+ // Bundle initial code if source file given (after server start so the
60
+ // dev base URL is available to the bundler).
61
+ if (source && isTsx) {
62
+ let initialResult = await bundle()
63
+ if (initialResult) {
64
+ for (let output of initialResult.outputs) {
65
+ state.currentCode = await output.text()
66
+ }
67
+ }
68
+ }
69
+
70
+ if (!values.server) {
71
+ spawnClient()
72
+ }
73
+
74
+ process.on("SIGINT", shutdown)
75
+ process.on("SIGTERM", shutdown)
76
+
77
+ let version = pkg.version === "0.0.0" ? "" : " version " + pkg.version
78
+ console.log(`Welcome to SolidRT${version}!`)
79
+ startRepl()
80
+ startWatcher()
package/src/native.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { createRequire } from "node:module"
2
+ import { existsSync } from "node:fs"
3
+ import { resolve, dirname } from "node:path"
4
+ import process from "node:process"
5
+
6
+ let require = createRequire(import.meta.url)
7
+
8
+ let TRIPLE_MAP: Record<string, string> = {
9
+ "linux-x64": "linux-x64-gnu",
10
+ "darwin-arm64": "darwin-arm64",
11
+ "win32-x64": "win32-x64-msvc",
12
+ }
13
+
14
+ let PKG_MAP: Record<string, string> = {
15
+ "linux-x64": "@solidrt/linux-x64-gnu",
16
+ "darwin-arm64": "@solidrt/darwin-arm64",
17
+ "win32-x64": "@solidrt/win32-x64-msvc",
18
+ }
19
+
20
+ export function resolveBinary(name: string) {
21
+ let key = `${process.platform}-${process.arch}`
22
+ let ext = process.platform === "win32" ? ".exe" : ""
23
+
24
+ // 1. SRT_HOME: contributors pointing at their local solidrt checkout
25
+ let srtRoot = process.env.SRT_HOME
26
+ if (srtRoot) {
27
+ let triple = TRIPLE_MAP[key]
28
+ if (triple) {
29
+ let bin = resolve(srtRoot, "dist", triple, name + ext)
30
+ if (existsSync(bin)) return bin
31
+ }
32
+ }
33
+
34
+ // 2. Platform npm package (installed via optionalDependencies)
35
+ let pkg = PKG_MAP[key]
36
+ if (pkg) {
37
+ try {
38
+ let pkgDir = dirname(require.resolve(`${pkg}/package.json`))
39
+ let bin = resolve(pkgDir, name + ext)
40
+ if (existsSync(bin)) return bin
41
+ } catch {}
42
+ }
43
+
44
+ return null
45
+ }
package/src/repl.ts ADDED
@@ -0,0 +1,171 @@
1
+ import { createInterface } from "node:readline"
2
+ import { resolve, dirname } from "path"
3
+ import { readdirSync } from "node:fs"
4
+ import { state, print, printErr, broadcastStop, shutdown } from "./util"
5
+ import { bundle } from "./build"
6
+ import { startWatcher } from "./watcher"
7
+
8
+ function cmdStop(args: string) {
9
+ if (!args) {
10
+ broadcastStop()
11
+ print("[dev] Sent stop to all clients")
12
+ return
13
+ }
14
+ let clientList = [...state.clients.keys()]
15
+ for (let token of args.split(/\s+/)) {
16
+ let idx = parseInt(token, 10)
17
+ if (isNaN(idx) || idx < 0 || idx >= clientList.length) {
18
+ print(`Invalid client index: ${token}`)
19
+ continue
20
+ }
21
+ clientList[idx].send(JSON.stringify({ type: "stop" }))
22
+ print(`[dev] Sent stop to client ${idx}`)
23
+ }
24
+ }
25
+
26
+ async function cmdReload(args: string) {
27
+ if (state.source && state.source.endsWith(".tsx")) {
28
+ let result = await bundle(state.source)
29
+ if (!result) {
30
+ printErr("[dev] Build failed, reload aborted")
31
+ return
32
+ }
33
+ for (let output of result.outputs) {
34
+ state.currentCode = await output.text()
35
+ }
36
+ }
37
+ let msg = JSON.stringify({ type: "reload", code: state.currentCode })
38
+ if (!args) {
39
+ for (let ws of state.clients.keys()) ws.send(msg)
40
+ print("[dev] Sent reload to all clients")
41
+ return
42
+ }
43
+ let clientList = [...state.clients.keys()]
44
+ for (let token of args.split(/\s+/)) {
45
+ let idx = parseInt(token, 10)
46
+ if (isNaN(idx) || idx < 0 || idx >= clientList.length) {
47
+ print(`Invalid client index: ${token}`)
48
+ continue
49
+ }
50
+ clientList[idx].send(msg)
51
+ print(`[dev] Sent reload to client ${idx}`)
52
+ }
53
+ }
54
+
55
+ function cmdList() {
56
+ if (state.clients.size === 0) {
57
+ print("No connected clients")
58
+ return
59
+ }
60
+ print(`${state.clients.size} connected client(s):`)
61
+ let i = 0
62
+ for (let [ws, info] of state.clients) {
63
+ print(` ${i++}: ${ws.remoteAddress} [${info.platform}, ${info.version}]`)
64
+ }
65
+ }
66
+
67
+ async function cmdLoad(file: string) {
68
+ if (!file) {
69
+ print("Usage: load <file.tsx|.srt.js|.srt.bin>")
70
+ return
71
+ }
72
+ let path = resolve(file)
73
+ if (file.endsWith(".tsx")) {
74
+ let result = await bundle(path)
75
+ if (!result) {
76
+ printErr("[dev] Build failed")
77
+ return
78
+ }
79
+ for (let output of result.outputs) {
80
+ state.currentCode = await output.text()
81
+ }
82
+ } else if (file.endsWith(".srt.js")) {
83
+ state.currentCode = await Bun.file(path).text()
84
+ } else if (file.endsWith(".srt.bin")) {
85
+ let bytes = await Bun.file(path).arrayBuffer()
86
+ let msg = { type: "reload", bytecode: Buffer.from(bytes).toString("base64") }
87
+ for (let ws of state.clients.keys()) ws.send(JSON.stringify(msg))
88
+ print(`[dev] Loaded ${file} (bytecode, ${bytes.byteLength} bytes)`)
89
+ return
90
+ } else {
91
+ print("Unsupported file type. Use .tsx, .srt.js, or .srt.bin")
92
+ return
93
+ }
94
+ state.source = path
95
+ state.sourceDir = dirname(path)
96
+ startWatcher()
97
+ for (let ws of state.clients.keys()) {
98
+ ws.send(JSON.stringify({ type: "reload", code: state.currentCode }))
99
+ }
100
+ print(`[dev] Loaded ${file}`)
101
+ }
102
+
103
+ let COMMANDS = ["load ", "stop", "reload", "list", "quit", "exit", "help"]
104
+ let LOAD_EXTENSIONS = [".tsx", ".srt.js", ".srt.bin"]
105
+
106
+ function completer(line: string): [string[], string] {
107
+ if (line.startsWith("load ")) {
108
+ let partial = line.slice(5)
109
+ let dir = partial.includes("/") ? partial.slice(0, partial.lastIndexOf("/") + 1) : ""
110
+ let prefix = partial.slice(dir.length)
111
+ let absDir = resolve(dir || ".")
112
+ try {
113
+ let entries = readdirSync(absDir, { withFileTypes: true })
114
+ let matches: string[] = []
115
+ for (let entry of entries) {
116
+ if (!entry.name.startsWith(prefix)) continue
117
+ if (entry.isDirectory()) {
118
+ matches.push(entry.name + "/")
119
+ } else if (LOAD_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
120
+ matches.push(entry.name)
121
+ }
122
+ }
123
+ return [matches, prefix]
124
+ } catch {
125
+ return [[], line]
126
+ }
127
+ }
128
+ let matches = COMMANDS.filter((c) => c.startsWith(line))
129
+ return [matches, line]
130
+ }
131
+
132
+ export function startRepl() {
133
+ state.rl = createInterface({ input: process.stdin, output: process.stdout, completer })
134
+ state.rl.setPrompt("srt> ")
135
+
136
+ state.rl.on("close", shutdown)
137
+
138
+ state.rl.on("line", (line) => {
139
+ let cmd = line.trim()
140
+ if (cmd === "stop" || cmd.startsWith("stop ")) {
141
+ cmdStop(cmd.slice(5).trim())
142
+ } else if (cmd === "reload" || cmd.startsWith("reload ")) {
143
+ cmdReload(cmd.slice(7).trim())
144
+ } else if (cmd.startsWith("load ")) {
145
+ cmdLoad(cmd.slice(5).trim())
146
+ } else if (cmd === "list") {
147
+ cmdList()
148
+ } else if (cmd === "quit" || cmd === "exit") {
149
+ shutdown()
150
+ } else if (cmd.startsWith("!")) {
151
+ let shell = cmd.slice(1)
152
+ if (shell) {
153
+ Bun.$`${{ raw: shell }}`.quiet().then(
154
+ (r) => {
155
+ if (r.stdout.length) print(r.text())
156
+ },
157
+ (e) => {
158
+ printErr(e.stderr.toString())
159
+ },
160
+ )
161
+ }
162
+ } else if (cmd === "help") {
163
+ print("Commands: load, stop, reload, list, !<cmd>, quit, help")
164
+ } else if (cmd) {
165
+ print(`Unknown command: ${cmd}`)
166
+ }
167
+ state.rl!.prompt()
168
+ })
169
+
170
+ state.rl.prompt()
171
+ }
package/src/server.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { resolve } from "path"
2
+ import { stat as fsStat, readdir } from "node:fs/promises"
3
+ import { networkInterfaces } from "node:os"
4
+ import { createSocket } from "node:dgram"
5
+ import qrcode from "qrcode-generator"
6
+ import { DEV_HOST, DEV_PORT, state, print } from "./util"
7
+
8
+ export function startServer() {
9
+ state.server = Bun.serve({
10
+ port: DEV_PORT,
11
+ async fetch(req, server) {
12
+ if (server.upgrade(req)) return
13
+
14
+ let url = new URL(req.url)
15
+ let path = decodeURIComponent(url.pathname)
16
+
17
+ print("[http] get", path)
18
+
19
+ let filePath = resolve(state.sourceDir, "." + path)
20
+ if (!filePath.startsWith(state.sourceDir)) {
21
+ return new Response("Forbidden", { status: 403 })
22
+ }
23
+ let stat
24
+ try {
25
+ stat = await fsStat(filePath)
26
+ } catch {
27
+ print("[http] file not found %s", path)
28
+ return new Response("Not found", { status: 404 })
29
+ }
30
+
31
+ if (stat.isDirectory()) {
32
+ let dirents = await readdir(filePath, { withFileTypes: true })
33
+ let entries = await Promise.all(
34
+ dirents.map(async (d) => {
35
+ let entry = { name: d.name, type: d.isDirectory() ? 2 : 1, size: 0, modified: 0 }
36
+ if (!d.isDirectory()) {
37
+ try {
38
+ let s = await fsStat(resolve(filePath, d.name))
39
+ entry.size = s.size
40
+ entry.modified = Math.floor(s.mtimeMs)
41
+ } catch {}
42
+ }
43
+ return entry
44
+ }),
45
+ )
46
+ entries.sort((a, b) => a.name.localeCompare(b.name))
47
+ return Response.json(entries)
48
+ }
49
+
50
+ return new Response(Bun.file(filePath))
51
+ },
52
+ websocket: {
53
+ open(ws) {
54
+ state.clients.set(ws, { platform: "unknown", version: "unknown" })
55
+ print(`[dev] Client connected ${ws.remoteAddress}`)
56
+ if (state.currentCode) {
57
+ ws.send(JSON.stringify({ type: "reload", code: state.currentCode }))
58
+ }
59
+ },
60
+ close(ws) {
61
+ let info = state.clients.get(ws)
62
+ state.clients.delete(ws)
63
+ print(`[dev] Client disconnected: ${info?.platform ?? "unknown"}`)
64
+ if (state.child && state.clients.size === 0 && state.child.exitCode !== null) {
65
+ print("[dev] All clients disconnected, shutting down")
66
+ state.server?.stop()
67
+ process.exit(0)
68
+ }
69
+ },
70
+ message(ws, msg) {
71
+ try {
72
+ let data = JSON.parse(typeof msg === "string" ? msg : Buffer.from(msg).toString())
73
+ if (data.type === "info") {
74
+ state.clients.set(ws, {
75
+ platform: data.platform ?? "unknown",
76
+ version: data.version ?? "unknown",
77
+ })
78
+ print(`[dev] Client info ${ws.remoteAddress} ${data.platform} (${data.version})`)
79
+ }
80
+ } catch {}
81
+ },
82
+ },
83
+ })
84
+
85
+ let lanAddress = Object.values(networkInterfaces())
86
+ .flat()
87
+ .find((i) => i?.family === "IPv4" && !i.internal)?.address
88
+
89
+ let address = lanAddress ?? DEV_HOST
90
+ let serverUrl = `${address}:${state.server.port}`
91
+ state.serverUrl = serverUrl
92
+
93
+ console.log("")
94
+
95
+ let qr = qrcode(0, "L")
96
+ qr.addData(serverUrl)
97
+ qr.make()
98
+ let modCount = qr.getModuleCount()
99
+ for (let y = 0; y < modCount; y += 2) {
100
+ let row = " "
101
+ for (let x = 0; x < modCount; x++) {
102
+ let top = qr.isDark(y, x)
103
+ let bot = y + 1 < modCount && qr.isDark(y + 1, x)
104
+ row += top && bot ? "\u2588" : top ? "\u2580" : bot ? "\u2584" : " "
105
+ }
106
+ console.log(row)
107
+ }
108
+
109
+ console.log("")
110
+ console.log(`[dev] WebSocket server on ws://${serverUrl}`)
111
+
112
+ // UDP discovery
113
+ let udp = createSocket("udp4")
114
+ udp.on("message", (msg, rinfo) => {
115
+ if (msg.toString() === "SRT_DISCOVER") {
116
+ print(`[dev] Discovery request from ${rinfo.address}:${rinfo.port}`)
117
+ udp.send("SRT_SERVER", rinfo.port, rinfo.address)
118
+ }
119
+ })
120
+ udp.bind(DEV_PORT, () => {
121
+ udp.setBroadcast(true)
122
+ print("[dev] UDP discovery listener on port " + DEV_PORT)
123
+ })
124
+
125
+ // Keepalive
126
+ setInterval(() => {
127
+ for (let ws of state.clients.keys()) {
128
+ ws.ping()
129
+ }
130
+ }, 5000)
131
+ }
package/src/util.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { resolveBinary } from "./native"
2
+ import type { Interface as ReadlineInterface } from "node:readline"
3
+ import type { Server as BunServer } from "bun"
4
+
5
+ export const DEV_HOST = "127.0.0.1"
6
+ export const DEV_PORT = 15194
7
+
8
+ export let state = {
9
+ clients: new Map<any, { platform: string; version: string }>(),
10
+ currentCode: null as string | null,
11
+ source: undefined as string | undefined,
12
+ sourceDir: process.cwd(),
13
+ child: null as ReturnType<typeof Bun.spawn> | null,
14
+ server: null as BunServer<unknown> | null,
15
+ serverUrl: null as string | null,
16
+ rl: null as ReadlineInterface | null,
17
+ }
18
+
19
+ export function requireBinary(name: string) {
20
+ let path = resolveBinary(name)
21
+ if (path) return path
22
+ console.error(`Could not find ${name} binary.`)
23
+ console.error("Build from source: run make solidrt-go, then set SRT_HOME=<SolidRT project home>")
24
+ process.exit(1)
25
+ }
26
+
27
+ export async function run(binary: string, args: string[]) {
28
+ let proc = Bun.spawn([binary, ...args], { stdio: ["inherit", "inherit", "inherit"] })
29
+ return proc.exited
30
+ }
31
+
32
+ export function print(...args: any[]) {
33
+ process.stdout.write("\r\x1b[K")
34
+ console.log(...args)
35
+ state.rl?.prompt(true)
36
+ }
37
+
38
+ export function printErr(...args: any[]) {
39
+ process.stdout.write("\r\x1b[K")
40
+ console.error(...args)
41
+ state.rl?.prompt(true)
42
+ }
43
+
44
+ export function broadcastStop() {
45
+ for (let ws of state.clients.keys()) {
46
+ ws.send(JSON.stringify({ type: "stop" }))
47
+ }
48
+ }
49
+
50
+ export function shutdown() {
51
+ if (state.child) state.child.kill()
52
+ if (state.server) state.server.stop()
53
+ process.exit(0)
54
+ }
package/src/watcher.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { watch } from "node:fs"
2
+ import { resolve, dirname } from "path"
3
+ import { state, print, printErr } from "./util"
4
+ import { bundle } from "./build"
5
+
6
+ let currentWatcher: ReturnType<typeof watch> | null = null
7
+
8
+ export function startWatcher() {
9
+ if (!state.source) return
10
+ let watchDir = dirname(resolve(state.source))
11
+
12
+ if (currentWatcher) currentWatcher.close()
13
+
14
+ print(`[dev] Watching ${watchDir} for changes...`)
15
+ currentWatcher = watch(watchDir, { recursive: true }, async (_event, filename) => {
16
+ if (!filename) return
17
+ if (!/\.(tsx?|jsx?)$/.test(filename)) return
18
+
19
+ print(`[watch] Change detected: ${filename}`)
20
+ let result = await bundle(state.source)
21
+ if (!result) {
22
+ printErr("[dev] Build failed, waiting for changes...")
23
+ return
24
+ }
25
+ for (let output of result.outputs) {
26
+ state.currentCode = await output.text()
27
+ }
28
+ for (let ws of state.clients.keys()) {
29
+ ws.send(JSON.stringify({ type: "reload", code: state.currentCode }))
30
+ }
31
+ })
32
+ }