@solidrt/cli 0.0.8 → 0.0.10
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 +6 -5
- package/src/args.ts +48 -1
- package/src/{native.ts → artifacts.ts} +25 -0
- package/src/bundler.ts +117 -0
- package/src/cache.ts +16 -1
- package/src/commands/bundle.ts +84 -0
- package/src/commands/client.ts +22 -0
- package/src/commands/pack.ts +25 -0
- package/src/commands/record.ts +16 -0
- package/src/commands/server.ts +45 -0
- package/src/dev-android.ts +108 -0
- package/src/{client.ts → dev-client.ts} +2 -1
- package/src/{server.ts → dev-server.ts} +50 -21
- package/src/main.ts +22 -112
- package/src/packer.ts +27 -0
- package/src/repl.ts +3 -2
- package/src/util.ts +46 -16
- package/src/watcher.ts +3 -2
- package/src/bun-plugin-solid.ts +0 -21
- package/src/bundle.ts +0 -126
- package/src/pack.ts +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -16,15 +16,16 @@
|
|
|
16
16
|
"@babel/core": "^7.28.5",
|
|
17
17
|
"@babel/preset-typescript": "^7.28.5",
|
|
18
18
|
"babel-preset-solid": "2.0.0-beta.14",
|
|
19
|
+
"bonjour-service": "^1.4.0",
|
|
19
20
|
"qrcode-generator": "^2.0.4"
|
|
20
21
|
},
|
|
21
22
|
"optionalDependencies": {
|
|
22
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
23
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
24
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
23
|
+
"@solidrt/darwin-arm64": "0.0.10",
|
|
24
|
+
"@solidrt/linux-x64-gnu": "0.0.10",
|
|
25
|
+
"@solidrt/win32-x64-msvc": "0.0.10"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
|
-
"@solidrt/core": "0.0.
|
|
28
|
+
"@solidrt/core": "0.0.10",
|
|
28
29
|
"typescript": "^5"
|
|
29
30
|
},
|
|
30
31
|
"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", short: "f", 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
|
})
|
|
@@ -23,6 +26,40 @@ export let isTs = source?.endsWith(".ts") || source?.endsWith(".js")
|
|
|
23
26
|
export let isSource = isTsx || isTs
|
|
24
27
|
export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
|
|
25
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 (values.flux) {
|
|
39
|
+
if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
|
|
40
|
+
} else if (!source || (!isSource && !isPrebuilt)) {
|
|
41
|
+
usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
|
|
42
|
+
}
|
|
43
|
+
break
|
|
44
|
+
case "record":
|
|
45
|
+
if (!source || !isTsx) usage("srt record <entry.[tsx|jsx]>")
|
|
46
|
+
break
|
|
47
|
+
case "pack":
|
|
48
|
+
if (values.flux) {
|
|
49
|
+
if (!source || !isTs) usage("srt pack --flux [options] <entry.[ts|js]>")
|
|
50
|
+
} else if (!source || !isSource) {
|
|
51
|
+
usage("srt pack [options] <entry.[tsx|jsx|ts|js]>")
|
|
52
|
+
}
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --android installs/launches the client on a device; it is a client action,
|
|
57
|
+
// so it is only valid for `client`.
|
|
58
|
+
if (values.android && command !== "client") {
|
|
59
|
+
usage("srt client --android (--android is only valid with the client command)")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
export function printUsage() {
|
|
27
64
|
console.error(`Usage: srt <command> [options] [file]
|
|
28
65
|
|
|
@@ -32,7 +69,7 @@ Commands:
|
|
|
32
69
|
client Start solidrt-go client only
|
|
33
70
|
bundle <file> Transpile TS/JS/TSX/JSX to JS or bytecode
|
|
34
71
|
record <file.tsx|jsx> Capture frames for video generation
|
|
35
|
-
pack <file
|
|
72
|
+
pack <file> Bundle + compile to a standalone executable (experimental)
|
|
36
73
|
|
|
37
74
|
run/server options:
|
|
38
75
|
--proxy-files Route file/dir access through the dev server
|
|
@@ -41,13 +78,23 @@ run/server options:
|
|
|
41
78
|
run/client options:
|
|
42
79
|
--size <WxH> Window size (default: 1280x720)
|
|
43
80
|
|
|
81
|
+
client options:
|
|
82
|
+
--android Install and launch the client on a connected Android device
|
|
83
|
+
--device <serial> Target a specific adb device by serial or unique prefix
|
|
84
|
+
|
|
44
85
|
bundle options:
|
|
86
|
+
-f, --flux Bundle for the bare Flux runtime, without SolidJS (entry must be .ts|.js)
|
|
45
87
|
-d, --dev Use development build of SolidJS (default: production)
|
|
46
88
|
-m, --minify Minify the output
|
|
47
89
|
-c, --compile Compile to bytecode
|
|
48
90
|
-o, --output <name> Output filename
|
|
49
91
|
--stdout Write bundle to stdout
|
|
50
92
|
|
|
93
|
+
pack options:
|
|
94
|
+
-f, --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
|
|
95
|
+
-m, --minify Minify the output
|
|
96
|
+
-o, --output <name> Output filename
|
|
97
|
+
|
|
51
98
|
record options:
|
|
52
99
|
--fps <N> Frames per second (default: 60)
|
|
53
100
|
--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:*", "srt:*"],
|
|
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
|
+
}
|
package/src/cache.ts
CHANGED
|
@@ -46,6 +46,21 @@ export function isEnabled(): boolean {
|
|
|
46
46
|
return enabled
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Strings returned by bun:sqlite carry an internal representation that
|
|
50
|
+
// Headers.set() rejects, even when value-identical to an acceptable string
|
|
51
|
+
// (Bun bug, present through 1.3.14). Rebuilding each char yields a clean string.
|
|
52
|
+
function reflatten(s: string): string {
|
|
53
|
+
return Array.from(s, (c) => String.fromCharCode(c.charCodeAt(0))).join("")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function reflattenHeaders(obj: Record<string, string>): Record<string, string> {
|
|
57
|
+
let out: Record<string, string> = {}
|
|
58
|
+
for (let key in obj) {
|
|
59
|
+
out[reflatten(key)] = reflatten(obj[key]!)
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
function keyFor(method: string, url: string): string {
|
|
50
65
|
let h = createHash("sha256")
|
|
51
66
|
h.update(method)
|
|
@@ -101,7 +116,7 @@ export function get(method: string, url: string): Entry | null {
|
|
|
101
116
|
method: row.method,
|
|
102
117
|
url: row.url,
|
|
103
118
|
status: row.status,
|
|
104
|
-
headers: JSON.parse(row.headers),
|
|
119
|
+
headers: reflattenHeaders(JSON.parse(row.headers)),
|
|
105
120
|
body: row.body,
|
|
106
121
|
cachedAt: row.cached_at,
|
|
107
122
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { values, source, isPrebuilt } from "../args"
|
|
2
|
+
import { bundle, bundleTo, bundleFlux, compileToBytecode } from "../bundler"
|
|
3
|
+
import { resolve } from "path"
|
|
4
|
+
|
|
5
|
+
// Write to stdout and resolve only once the whole payload is flushed.
|
|
6
|
+
// process.stdout.write to a pipe is async and applies backpressure; the
|
|
7
|
+
// callback fires after every byte is drained, so it is safe to exit after.
|
|
8
|
+
function writeStdout(data: string): Promise<void> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
process.stdout.write(data, (err) => (err ? reject(err) : resolve()))
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Compile JS to a .srt.bin file and report its size.
|
|
15
|
+
async function writeBytecode(jsCode: string, outfile: string) {
|
|
16
|
+
let bytecode = await compileToBytecode(jsCode)
|
|
17
|
+
await Bun.write(outfile, bytecode)
|
|
18
|
+
let binSize = (await Bun.file(outfile).stat()).size
|
|
19
|
+
console.log(`>> wrote ${binSize} bytes to ${outfile}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runBundleCommand() {
|
|
23
|
+
if (values.flux) {
|
|
24
|
+
let baseName = values.output ?? source!.replace(/\.[jt]s$/, "")
|
|
25
|
+
let jsCode = await bundleFlux(source!)
|
|
26
|
+
|
|
27
|
+
if (values.stdout) {
|
|
28
|
+
await writeStdout(jsCode)
|
|
29
|
+
} else if (values.compile) {
|
|
30
|
+
await writeBytecode(jsCode, baseName + ".flux.bin")
|
|
31
|
+
} else {
|
|
32
|
+
let outfile = baseName + ".flux.js"
|
|
33
|
+
await Bun.write(outfile, jsCode)
|
|
34
|
+
console.log(`>> wrote ${jsCode.length} bytes to ${outfile}`)
|
|
35
|
+
}
|
|
36
|
+
process.exit()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isPrebuilt) {
|
|
40
|
+
if (!source!.endsWith(".srt.js")) {
|
|
41
|
+
console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
let jsFile = resolve(source!)
|
|
45
|
+
let binOut = jsFile.replace(/\.srt\.js$/, ".srt.bin").replace(/\.js$/, ".bin")
|
|
46
|
+
await writeBytecode(await Bun.file(jsFile).text(), binOut)
|
|
47
|
+
process.exit()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let baseName = values.output ?? source!.replace(/\.[jt]sx?$/, "")
|
|
51
|
+
|
|
52
|
+
if (values.stdout) {
|
|
53
|
+
let result = await bundle()
|
|
54
|
+
if (!result) {
|
|
55
|
+
console.error("Build failed")
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
for (let output of result.outputs) {
|
|
59
|
+
await writeStdout(await output.text())
|
|
60
|
+
}
|
|
61
|
+
process.exit()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (values.compile) {
|
|
65
|
+
let result = await bundle()
|
|
66
|
+
if (!result) {
|
|
67
|
+
console.error("Build failed")
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
let jsCode = ""
|
|
71
|
+
for (let output of result.outputs) {
|
|
72
|
+
jsCode += await output.text()
|
|
73
|
+
}
|
|
74
|
+
await writeBytecode(jsCode, baseName + ".srt.bin")
|
|
75
|
+
process.exit()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let jsOutfile = baseName + ".srt.js"
|
|
79
|
+
let result = await bundleTo(jsOutfile)
|
|
80
|
+
for (let output of result.outputs) {
|
|
81
|
+
console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
|
|
82
|
+
}
|
|
83
|
+
process.exit()
|
|
84
|
+
}
|
|
@@ -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,108 @@
|
|
|
1
|
+
import { print, requireAdb } from "./util"
|
|
2
|
+
import { resolveApk } from "./artifacts"
|
|
3
|
+
import { values } from "./args"
|
|
4
|
+
import { DEV_PORT } from "./dev-server"
|
|
5
|
+
|
|
6
|
+
// Launch component of the "go" dev-client flavor (see lattice/Makefile.android).
|
|
7
|
+
let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
|
|
8
|
+
|
|
9
|
+
// Forward the device's loopback DEV_PORT to the host dev server, so the client
|
|
10
|
+
// reaches it at 127.0.0.1:DEV_PORT (see lattice/src/go/connection.rs). This is
|
|
11
|
+
// the adb-reverse path: it works for the emulator (behind NAT, cannot reach the
|
|
12
|
+
// host via LAN mDNS discovery) and for USB-tethered devices alike, and is
|
|
13
|
+
// harmless on any adb connection. (Android only uses this path; the client's
|
|
14
|
+
// mDNS discovery is desktop-only -- see lattice/src/go/connection.rs.)
|
|
15
|
+
function setupAdbReverse(adb: string, target: string) {
|
|
16
|
+
print(`[cli] Forwarding 127.0.0.1:${DEV_PORT} on ${target} to host dev server`)
|
|
17
|
+
let res = Bun.spawnSync([adb, "-s", target, "reverse", `tcp:${DEV_PORT}`, `tcp:${DEV_PORT}`], {
|
|
18
|
+
stdout: "pipe",
|
|
19
|
+
stderr: "pipe",
|
|
20
|
+
})
|
|
21
|
+
if (res.exitCode !== 0) {
|
|
22
|
+
print(`[cli] adb reverse failed (client will fall back to discovery):\n${res.stderr.toString()}`)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Serials of connected, authorized devices (excludes offline/unauthorized).
|
|
27
|
+
function listDevices(adb: string): string[] {
|
|
28
|
+
let listed = Bun.spawnSync([adb, "devices"], { stdout: "pipe", stderr: "pipe" })
|
|
29
|
+
return listed.stdout
|
|
30
|
+
.toString()
|
|
31
|
+
.split("\n")
|
|
32
|
+
.slice(1)
|
|
33
|
+
.map((l) => l.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.filter((l) => l.endsWith("\tdevice"))
|
|
36
|
+
.map((l) => l.split("\t")[0])
|
|
37
|
+
.filter((s): s is string => Boolean(s))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve the target device serial. With --device, treat the value as a serial
|
|
41
|
+
// prefix and require it to match exactly one connected device; without it, use
|
|
42
|
+
// the sole connected device. Exits with a clear message on any ambiguity.
|
|
43
|
+
function resolveTarget(adb: string): string {
|
|
44
|
+
let devices = listDevices(adb)
|
|
45
|
+
|
|
46
|
+
if (values.device) {
|
|
47
|
+
let prefix = values.device
|
|
48
|
+
let matches = devices.filter((d) => d.startsWith(prefix))
|
|
49
|
+
if (matches.length > 1) {
|
|
50
|
+
console.error(`--device "${prefix}" is ambiguous; matches: ${matches.join(", ")}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
let [match] = matches
|
|
54
|
+
if (!match) {
|
|
55
|
+
console.error(`No connected device matches --device "${prefix}". Connected: ${devices.join(", ") || "none"}`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
return match
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (devices.length > 1) {
|
|
62
|
+
console.error(`Multiple devices connected (${devices.join(", ")}); pick one with --device <serial or prefix>.`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
let [only] = devices
|
|
66
|
+
if (!only) {
|
|
67
|
+
console.error("No authorized Android device found. Enable USB debugging and check `adb devices`.")
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
return only
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Install + launch the Android client on a connected device over adb, forwarding
|
|
74
|
+
// its loopback to the host dev server so the client connects at 127.0.0.1 (see
|
|
75
|
+
// setupAdbReverse). Fire-and-forget: the client's lifecycle is tracked via WS
|
|
76
|
+
// connect/disconnect in dev-server.ts, not as a child process here.
|
|
77
|
+
export async function spawnAndroidClient() {
|
|
78
|
+
let adb = requireAdb()
|
|
79
|
+
|
|
80
|
+
let apk = resolveApk()
|
|
81
|
+
if (!apk) {
|
|
82
|
+
console.error("Could not find the SolidRT-Go APK.")
|
|
83
|
+
console.error("Add it with: bun add -d @solidrt/android-arm64-v8a")
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let target = resolveTarget(adb)
|
|
88
|
+
|
|
89
|
+
print(`[cli] Installing SolidRT-Go on ${target}`)
|
|
90
|
+
let install = Bun.spawn([adb, "-s", target, "install", "-r", apk], { stdout: "pipe", stderr: "pipe" })
|
|
91
|
+
if ((await install.exited) !== 0) {
|
|
92
|
+
console.error("adb install failed:\n" + (await new Response(install.stderr).text()))
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setupAdbReverse(adb, target)
|
|
97
|
+
|
|
98
|
+
let start = Bun.spawn([adb, "-s", target, "shell", "am", "start", "-n", PACKAGE_ACTIVITY], {
|
|
99
|
+
stdout: "pipe",
|
|
100
|
+
stderr: "pipe",
|
|
101
|
+
})
|
|
102
|
+
if ((await start.exited) !== 0) {
|
|
103
|
+
console.error("adb start failed:\n" + (await new Response(start.stderr).text()))
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
print(`[cli] Launched SolidRT-Go on ${target}; waiting for it to connect to the dev server...`)
|
|
108
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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) {
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { resolve } from "path"
|
|
2
2
|
import { stat as fsStat, readdir } from "node:fs/promises"
|
|
3
3
|
import { networkInterfaces } from "node:os"
|
|
4
|
-
import {
|
|
4
|
+
import { Bonjour } from "bonjour-service"
|
|
5
5
|
import qrcode from "qrcode-generator"
|
|
6
|
-
import {
|
|
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) => {
|
|
@@ -13,7 +28,6 @@ function headersToObject(h: Headers): Record<string, string> {
|
|
|
13
28
|
})
|
|
14
29
|
return out
|
|
15
30
|
}
|
|
16
|
-
|
|
17
31
|
async function handleProxy(req: Request): Promise<Response> {
|
|
18
32
|
let target = req.headers.get("x-srt-proxy-url")
|
|
19
33
|
if (!target) {
|
|
@@ -141,6 +155,9 @@ export function startServer() {
|
|
|
141
155
|
open(ws) {
|
|
142
156
|
state.clients.set(ws, { platform: "unknown", version: "unknown" })
|
|
143
157
|
print(`[cli] Client connected ${ws.remoteAddress}`)
|
|
158
|
+
// Advertise our real LAN address so clients dialed over the adb loopback
|
|
159
|
+
// can show/remember the directly reachable address (see connection.rs).
|
|
160
|
+
ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl }))
|
|
144
161
|
if (state.currentCode) {
|
|
145
162
|
ws.send(buildReload({ code: state.currentCode }))
|
|
146
163
|
}
|
|
@@ -184,31 +201,43 @@ export function startServer() {
|
|
|
184
201
|
qr.addData(serverUrl)
|
|
185
202
|
qr.make()
|
|
186
203
|
let modCount = qr.getModuleCount()
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
// Render as a white tile with black modules (explicit ANSI colors) plus the
|
|
205
|
+
// 4-module quiet zone the QR spec requires. Drawing with the terminal's
|
|
206
|
+
// default foreground inverts the code on dark themes, which standard
|
|
207
|
+
// decoders reject.
|
|
208
|
+
const QR_INK = "\x1b[30;107m" // black modules on bright-white tile (tile = background)
|
|
209
|
+
const QR_TILE_FG = "\x1b[97m" // bright-white as foreground over the default background
|
|
210
|
+
const QR_RESET = "\x1b[0m"
|
|
211
|
+
const QUIET_ZONE = 2 // modules (spec says 4, but scanners cope and it reads tighter)
|
|
212
|
+
let qrWidth = modCount + 2 * QUIET_ZONE
|
|
213
|
+
let dark = (y: number, x: number) => y >= 0 && y < modCount && x >= 0 && x < modCount && qr.isDark(y, x)
|
|
214
|
+
// modCount is always odd, so the tile is a half-line taller than an even row
|
|
215
|
+
// count. The loop packs two module-rows per line via half-blocks and stops on
|
|
216
|
+
// the last content row, leaving the bottom quiet zone half a line short of the
|
|
217
|
+
// full-line top quiet zone.
|
|
218
|
+
for (let y = -QUIET_ZONE; y < modCount + QUIET_ZONE - 1; y += 2) {
|
|
219
|
+
let row = " " + QR_INK
|
|
220
|
+
for (let x = -QUIET_ZONE; x < modCount + QUIET_ZONE; x++) {
|
|
221
|
+
let top = dark(y, x)
|
|
222
|
+
let bot = dark(y + 1, x)
|
|
192
223
|
row += top && bot ? "\u2588" : top ? "\u2580" : bot ? "\u2584" : " "
|
|
193
224
|
}
|
|
194
|
-
console.log(row)
|
|
225
|
+
console.log(row + QR_RESET)
|
|
195
226
|
}
|
|
227
|
+
// Close that gap with a half-height tile line: upper half painted in the tile
|
|
228
|
+
// color (foreground), lower half the terminal background. The 0.5 here plus
|
|
229
|
+
// the 0.5 already under the last content row equal the full-line top margin.
|
|
230
|
+
console.log(" " + QR_TILE_FG + "\u2580".repeat(qrWidth) + QR_RESET)
|
|
196
231
|
|
|
197
232
|
console.log("")
|
|
198
233
|
console.log(`[cli] WebSocket server on ws://${serverUrl}`)
|
|
199
234
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
udp.bind(DEV_PORT, () => {
|
|
209
|
-
udp.setBroadcast(true)
|
|
210
|
-
print("[cli] UDP discovery listener on port " + DEV_PORT)
|
|
211
|
-
})
|
|
235
|
+
// LAN discovery: advertise the dev server as a DNS-SD service so go clients on
|
|
236
|
+
// the same network can find it (see lattice/src/go/connection.rs). Stored on
|
|
237
|
+
// state so shutdown() can send the mDNS goodbye.
|
|
238
|
+
state.bonjour = new Bonjour()
|
|
239
|
+
state.bonjour.publish({ name: "SolidRT Dev Server", type: "solidrt", protocol: "tcp", port: DEV_PORT })
|
|
240
|
+
print(`[cli] Advertising _solidrt._tcp on port ${DEV_PORT} via mDNS`)
|
|
212
241
|
|
|
213
242
|
// Keepalive
|
|
214
243
|
setInterval(() => {
|
package/src/main.ts
CHANGED
|
@@ -1,52 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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, isSource, 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
|
-
|
|
30
|
-
|
|
31
|
-
if (!command || !COMMANDS.includes(command)) {
|
|
32
|
-
printUsage()
|
|
33
|
-
process.exit(1)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (command === "bundle" && (!source || (!isSource && !isPrebuilt))) {
|
|
37
|
-
console.error("Usage: srt bundle [options] <entry.[tsx|jsx|ts|js|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
|
-
// --
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
if (command === "
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 && isSource) {
|
|
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.
|
|
139
|
-
|
|
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,
|
|
5
|
-
import {
|
|
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,10 +1,9 @@
|
|
|
1
|
-
import { resolveBinary } from "./
|
|
2
|
-
import {
|
|
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
|
-
export const DEV_HOST = "127.0.0.1"
|
|
7
|
-
export const DEV_PORT = 15194
|
|
6
|
+
import type { Bonjour } from "bonjour-service"
|
|
8
7
|
|
|
9
8
|
export let state = {
|
|
10
9
|
clients: new Map<any, { platform: string; version: string }>(),
|
|
@@ -15,13 +14,53 @@ export let state = {
|
|
|
15
14
|
server: null as BunServer<undefined> | null,
|
|
16
15
|
serverUrl: null as string | null,
|
|
17
16
|
rl: null as ReadlineInterface | null,
|
|
17
|
+
bonjour: null as Bonjour | null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Build target per binary, for the "not found" hint. Run from the repo root.
|
|
21
|
+
let BUILD_HINTS: Record<string, string> = {
|
|
22
|
+
"solidrt-go": "make client",
|
|
23
|
+
solidrt: "make runtime",
|
|
24
|
+
flux: "make -C flux flux",
|
|
25
|
+
fluxc: "make -C flux fluxc",
|
|
26
|
+
fluxrt: "make -C flux fluxrt PROFILE=release-opt",
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export function requireBinary(name: string) {
|
|
21
30
|
let path = resolveBinary(name)
|
|
22
31
|
if (path) return path
|
|
32
|
+
let hint = BUILD_HINTS[name]
|
|
23
33
|
console.error(`Could not find ${name} binary.`)
|
|
24
|
-
|
|
34
|
+
if (hint) {
|
|
35
|
+
console.error(`Build it from source: run ${hint}, with SRT_HOME pointing at your SolidRT checkout.`)
|
|
36
|
+
} else {
|
|
37
|
+
console.error("Build it from source, with SRT_HOME pointing at your SolidRT checkout.")
|
|
38
|
+
}
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// adb is a system tool (Android Platform Tools), never bundled. Look on PATH
|
|
43
|
+
// first, then the standard SDK location.
|
|
44
|
+
export function resolveAdb() {
|
|
45
|
+
let exe = process.platform === "win32" ? "adb.exe" : "adb"
|
|
46
|
+
let onPath = Bun.which(exe)
|
|
47
|
+
if (onPath) return onPath
|
|
48
|
+
for (let root of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
49
|
+
if (!root) continue
|
|
50
|
+
let candidate = resolve(root, "platform-tools", exe)
|
|
51
|
+
if (existsSync(candidate)) return candidate
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function requireAdb() {
|
|
57
|
+
let path = resolveAdb()
|
|
58
|
+
if (path) return path
|
|
59
|
+
console.error("Could not find adb (Android Platform Tools).")
|
|
60
|
+
console.error("Install it:")
|
|
61
|
+
console.error(" Windows: winget install Google.PlatformTools")
|
|
62
|
+
console.error(" macOS: brew install android-platform-tools")
|
|
63
|
+
console.error(" Linux: install your distro's android-tools / adb package")
|
|
25
64
|
process.exit(1)
|
|
26
65
|
}
|
|
27
66
|
|
|
@@ -42,18 +81,9 @@ export function printErr(...args: any[]) {
|
|
|
42
81
|
state.rl?.prompt(true)
|
|
43
82
|
}
|
|
44
83
|
|
|
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
84
|
export function shutdown() {
|
|
56
85
|
if (state.child) state.child.kill()
|
|
57
86
|
if (state.server) state.server.stop()
|
|
87
|
+
if (state.bonjour) state.bonjour.destroy()
|
|
58
88
|
process.exit(0)
|
|
59
89
|
}
|
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
|
|
4
|
-
import {
|
|
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
|
|
package/src/bun-plugin-solid.ts
DELETED
|
@@ -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: ["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: ["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
|
-
}
|