@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 +2 -0
- package/package.json +33 -0
- package/src/args.ts +33 -0
- package/src/build.ts +118 -0
- package/src/bun-plugin-solid.ts +38 -0
- package/src/client.ts +36 -0
- package/src/main.ts +80 -0
- package/src/native.ts +45 -0
- package/src/repl.ts +171 -0
- package/src/server.ts +131 -0
- package/src/util.ts +54 -0
- package/src/watcher.ts +32 -0
package/bin/srt
ADDED
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
|
+
}
|