@solidrt/cli 0.0.2 → 0.0.4
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 +5 -5
- package/src/args.ts +25 -8
- package/src/cache.ts +129 -0
- package/src/client.ts +3 -1
- package/src/main.ts +30 -3
- package/src/server.ts +89 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"qrcode-generator": "^2.0.4"
|
|
20
20
|
},
|
|
21
21
|
"optionalDependencies": {
|
|
22
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
23
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
24
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
22
|
+
"@solidrt/darwin-arm64": "0.0.4",
|
|
23
|
+
"@solidrt/linux-x64-gnu": "0.0.4",
|
|
24
|
+
"@solidrt/win32-x64-msvc": "0.0.4"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@solidrt/core": "0.0.
|
|
27
|
+
"@solidrt/core": "0.0.4",
|
|
28
28
|
"typescript": "^5"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/args.ts
CHANGED
|
@@ -8,6 +8,10 @@ export let { values, positionals } = parseArgs({
|
|
|
8
8
|
output: { type: "string", short: "o" },
|
|
9
9
|
client: { type: "boolean", default: false },
|
|
10
10
|
server: { type: "boolean", default: false },
|
|
11
|
+
cache: { type: "boolean", default: false },
|
|
12
|
+
fps: { type: "string" },
|
|
13
|
+
duration: { type: "string" },
|
|
14
|
+
size: { type: "string" },
|
|
11
15
|
},
|
|
12
16
|
allowPositionals: true,
|
|
13
17
|
})
|
|
@@ -18,16 +22,29 @@ export let isTsx = source?.endsWith(".tsx")
|
|
|
18
22
|
export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
|
|
19
23
|
|
|
20
24
|
export function printUsage() {
|
|
21
|
-
console.error(`Usage: srt <
|
|
25
|
+
console.error(`Usage: srt <command> [options] [file]
|
|
22
26
|
|
|
23
27
|
Commands:
|
|
24
|
-
run [file.tsx]
|
|
25
|
-
run --client
|
|
26
|
-
run --server [file]
|
|
28
|
+
run [file.tsx] Start dev server + local solidrt-go client
|
|
29
|
+
run --client Start solidrt-go client only
|
|
30
|
+
run --server [file.tsx] Start dev server only
|
|
31
|
+
build <file.tsx> Bundle
|
|
32
|
+
record <file.tsx> Capture frames for video generation
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
run options:
|
|
35
|
+
--client Run client only
|
|
36
|
+
--server Run server only
|
|
37
|
+
--cache Enable HTTP cache
|
|
38
|
+
--size <WxH> Window size (default: 1280x720)
|
|
39
|
+
|
|
40
|
+
build options:
|
|
29
41
|
-m, --minify Minify the output
|
|
30
|
-
-c, --compile Compile to bytecode
|
|
31
|
-
-o, --output <name>
|
|
32
|
-
--stdout Write bundle to stdout
|
|
42
|
+
-c, --compile Compile to bytecode
|
|
43
|
+
-o, --output <name> Output filename
|
|
44
|
+
--stdout Write bundle to stdout
|
|
45
|
+
|
|
46
|
+
record options:
|
|
47
|
+
--fps <N> Frames per second (default: 60)
|
|
48
|
+
--duration <N> Duration in seconds (default: 1)
|
|
49
|
+
--size <WxH> Frame size (default: 1280x720)`)
|
|
33
50
|
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// SQLite-backed HTTP response cache for the dev server's /__proxy__ endpoint.
|
|
2
|
+
//
|
|
3
|
+
// Project-local: stored at <cwd>/.srt-cache/cache.db. Opt-in via the --cache
|
|
4
|
+
// flag. Entries live forever; delete the .srt-cache directory to drop them.
|
|
5
|
+
//
|
|
6
|
+
// Cached: GET (and HEAD) 2xx responses with no Authorization on the request
|
|
7
|
+
// and no Cache-Control: no-store on either side. The cache key is
|
|
8
|
+
// sha256(method + "\n" + url); headers are intentionally not part of the key.
|
|
9
|
+
|
|
10
|
+
import { Database } from "bun:sqlite"
|
|
11
|
+
import { resolve, join } from "path"
|
|
12
|
+
import { mkdirSync } from "node:fs"
|
|
13
|
+
import { createHash } from "node:crypto"
|
|
14
|
+
|
|
15
|
+
const CACHE_DIR = ".srt-cache"
|
|
16
|
+
const CACHE_DB = "cache.db"
|
|
17
|
+
|
|
18
|
+
export type Decision = "hit" | "miss" | "bypass" | "skip"
|
|
19
|
+
|
|
20
|
+
export type Entry = {
|
|
21
|
+
method: string
|
|
22
|
+
url: string
|
|
23
|
+
status: number
|
|
24
|
+
headers: Record<string, string>
|
|
25
|
+
body: Uint8Array
|
|
26
|
+
cachedAt: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let db: Database | null = null
|
|
30
|
+
let enabled = false
|
|
31
|
+
|
|
32
|
+
export function initCache(opts: { dir: string }) {
|
|
33
|
+
mkdirSync(resolve(opts.dir, CACHE_DIR), { recursive: true })
|
|
34
|
+
let d = new Database(join(resolve(opts.dir, CACHE_DIR), CACHE_DB), { create: true })
|
|
35
|
+
d.run(`CREATE TABLE IF NOT EXISTS entries (
|
|
36
|
+
key TEXT PRIMARY KEY,
|
|
37
|
+
method TEXT NOT NULL,
|
|
38
|
+
url TEXT NOT NULL,
|
|
39
|
+
status INTEGER NOT NULL,
|
|
40
|
+
headers TEXT NOT NULL,
|
|
41
|
+
body BLOB NOT NULL,
|
|
42
|
+
cached_at INTEGER NOT NULL
|
|
43
|
+
)`)
|
|
44
|
+
db = d
|
|
45
|
+
enabled = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isEnabled(): boolean {
|
|
49
|
+
return enabled
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function keyFor(method: string, url: string): string {
|
|
53
|
+
let h = createHash("sha256")
|
|
54
|
+
h.update(method)
|
|
55
|
+
h.update("\n")
|
|
56
|
+
h.update(url)
|
|
57
|
+
return h.digest("hex")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cacheableMethod(method: string): boolean {
|
|
61
|
+
return method === "GET" || method === "HEAD"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasNoStore(headerVal: string | null): boolean {
|
|
65
|
+
if (!headerVal) return false
|
|
66
|
+
return /(^|,)\s*no-store(\s*,|$)/i.test(headerVal)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasNoCache(headerVal: string | null): boolean {
|
|
70
|
+
if (!headerVal) return false
|
|
71
|
+
return /(^|,)\s*no-cache(\s*,|$)/i.test(headerVal)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function shouldConsider(method: string, reqHeaders: Headers): { skip: boolean } {
|
|
75
|
+
if (!enabled) return { skip: true }
|
|
76
|
+
if (!cacheableMethod(method)) return { skip: true }
|
|
77
|
+
if (reqHeaders.has("authorization")) return { skip: true }
|
|
78
|
+
if (hasNoStore(reqHeaders.get("cache-control"))) return { skip: true }
|
|
79
|
+
return { skip: false }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isBypass(reqHeaders: Headers): boolean {
|
|
83
|
+
if (reqHeaders.get("x-srt-cache")?.toLowerCase() === "bypass") return true
|
|
84
|
+
if (hasNoCache(reqHeaders.get("cache-control"))) return true
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function get(method: string, url: string): Entry | null {
|
|
89
|
+
if (!db || !enabled) return null
|
|
90
|
+
let row = db
|
|
91
|
+
.query("SELECT method, url, status, headers, body, cached_at FROM entries WHERE key = ?")
|
|
92
|
+
.get(keyFor(method, url)) as
|
|
93
|
+
| {
|
|
94
|
+
method: string
|
|
95
|
+
url: string
|
|
96
|
+
status: number
|
|
97
|
+
headers: string
|
|
98
|
+
body: Uint8Array
|
|
99
|
+
cached_at: number
|
|
100
|
+
}
|
|
101
|
+
| null
|
|
102
|
+
if (!row) return null
|
|
103
|
+
return {
|
|
104
|
+
method: row.method,
|
|
105
|
+
url: row.url,
|
|
106
|
+
status: row.status,
|
|
107
|
+
headers: JSON.parse(row.headers),
|
|
108
|
+
body: row.body,
|
|
109
|
+
cachedAt: row.cached_at,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function put(
|
|
114
|
+
method: string,
|
|
115
|
+
url: string,
|
|
116
|
+
status: number,
|
|
117
|
+
headers: Record<string, string>,
|
|
118
|
+
body: Uint8Array,
|
|
119
|
+
) {
|
|
120
|
+
if (!db || !enabled) return
|
|
121
|
+
if (status < 200 || status >= 300) return
|
|
122
|
+
if (hasNoStore(headers["cache-control"] ?? null)) return
|
|
123
|
+
db.run(
|
|
124
|
+
`INSERT OR REPLACE INTO entries
|
|
125
|
+
(key, method, url, status, headers, body, cached_at)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
127
|
+
[keyFor(method, url), method, url, status, JSON.stringify(headers), body, Date.now()],
|
|
128
|
+
)
|
|
129
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEV_HOST, DEV_PORT, state, print, requireBinary } from "./util"
|
|
2
|
+
import { values } from "./args"
|
|
2
3
|
|
|
3
4
|
function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteStream) {
|
|
4
5
|
let reader = stream.getReader()
|
|
@@ -15,7 +16,8 @@ function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteSt
|
|
|
15
16
|
|
|
16
17
|
export function spawnClient() {
|
|
17
18
|
let runner = requireBinary("solidrt-go")
|
|
18
|
-
|
|
19
|
+
let args = values.size ? ["--size", values.size] : []
|
|
20
|
+
state.child = Bun.spawn([runner, ...args], {
|
|
19
21
|
//TODO implement dev server connection
|
|
20
22
|
// state.child = Bun.spawn([runner, "--dev-server", `${DEV_HOST}:${DEV_PORT}`], {
|
|
21
23
|
stdio: ["ignore", "pipe", "pipe"],
|
package/src/main.ts
CHANGED
|
@@ -10,20 +10,22 @@
|
|
|
10
10
|
// srt build examples/hello.tsx - bundle TSX to .srt.js
|
|
11
11
|
// srt build -c examples/hello.tsx - bundle TSX to .srt.js + compile to .srt.bin
|
|
12
12
|
// srt build examples/hello.srt.js - compile .srt.js to .srt.bin
|
|
13
|
+
// srt record examples/hello.tsx - bundle TSX and run with frame capture
|
|
13
14
|
|
|
14
15
|
import pkg from "../package.json"
|
|
15
16
|
import { values, command, source, isTsx, isPrebuilt, printUsage } from "./args"
|
|
16
17
|
import { state, requireBinary, run, shutdown } from "./util"
|
|
17
|
-
import { bundle, runBuildCommand } from "./build"
|
|
18
|
+
import { bundle, bundleTo, runBuildCommand } from "./build"
|
|
18
19
|
import { startServer } from "./server"
|
|
19
20
|
import { spawnClient } from "./client"
|
|
20
21
|
import { startRepl } from "./repl"
|
|
21
22
|
import { startWatcher } from "./watcher"
|
|
23
|
+
import * as cache from "./cache"
|
|
22
24
|
import { resolve, dirname } from "path"
|
|
23
25
|
|
|
24
26
|
// -- Validate args --
|
|
25
27
|
|
|
26
|
-
if (!command || (command !== "build" && command !== "run")) {
|
|
28
|
+
if (!command || (command !== "build" && command !== "run" && command !== "record")) {
|
|
27
29
|
printUsage()
|
|
28
30
|
process.exit(1)
|
|
29
31
|
}
|
|
@@ -33,17 +35,37 @@ if (command === "build" && (!source || (!isTsx && !isPrebuilt))) {
|
|
|
33
35
|
process.exit(1)
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
if (command === "record" && (!source || !isTsx)) {
|
|
39
|
+
console.error("Usage: srt record <entry.tsx>")
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
// -- Build command --
|
|
37
44
|
|
|
38
45
|
if (command === "build") {
|
|
39
46
|
await runBuildCommand()
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
// -- Record command --
|
|
50
|
+
|
|
51
|
+
if (command === "record") {
|
|
52
|
+
let jsOutfile = source!.replace(/\.tsx$/, "") + ".srt.js"
|
|
53
|
+
await bundleTo(jsOutfile)
|
|
54
|
+
let runner = requireBinary("solidrt-go")
|
|
55
|
+
let recordArgs = ["--record", resolve(jsOutfile)]
|
|
56
|
+
if (values.fps) recordArgs.push("--fps", values.fps)
|
|
57
|
+
if (values.duration) recordArgs.push("--duration", values.duration)
|
|
58
|
+
if (values.size) recordArgs.push("--size", values.size)
|
|
59
|
+
let exit = await run(runner, recordArgs)
|
|
60
|
+
process.exit(exit)
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
// -- Run command --
|
|
43
64
|
|
|
44
65
|
if (values.client) {
|
|
45
66
|
let runner = requireBinary("solidrt-go")
|
|
46
|
-
let args = []
|
|
67
|
+
let args: string[] = []
|
|
68
|
+
if (values.size) args.push("--size", values.size)
|
|
47
69
|
//TODO add dev server connection
|
|
48
70
|
// if (source) args.push("--dev-server", source)
|
|
49
71
|
let exit = await run(runner, args)
|
|
@@ -54,6 +76,11 @@ if (values.client) {
|
|
|
54
76
|
state.source = source
|
|
55
77
|
state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
|
|
56
78
|
|
|
79
|
+
if (values.cache) {
|
|
80
|
+
cache.initCache({ dir: process.cwd() })
|
|
81
|
+
console.log("[cli] HTTP cache enabled (.srt-cache/)")
|
|
82
|
+
}
|
|
83
|
+
|
|
57
84
|
startServer()
|
|
58
85
|
|
|
59
86
|
// Bundle initial code if source file given (after server start so the
|
package/src/server.ts
CHANGED
|
@@ -4,6 +4,82 @@ import { networkInterfaces } from "node:os"
|
|
|
4
4
|
import { createSocket } from "node:dgram"
|
|
5
5
|
import qrcode from "qrcode-generator"
|
|
6
6
|
import { DEV_HOST, DEV_PORT, state, print } from "./util"
|
|
7
|
+
import * as cache from "./cache"
|
|
8
|
+
|
|
9
|
+
function headersToObject(h: Headers): Record<string, string> {
|
|
10
|
+
let out: Record<string, string> = {}
|
|
11
|
+
h.forEach((v, k) => {
|
|
12
|
+
out[k] = v
|
|
13
|
+
})
|
|
14
|
+
return out
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function handleProxy(req: Request): Promise<Response> {
|
|
18
|
+
let target = req.headers.get("x-srt-proxy-url")
|
|
19
|
+
if (!target) {
|
|
20
|
+
return new Response("Missing X-SRT-Proxy-Url", { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let forwardHeaders = new Headers(req.headers)
|
|
24
|
+
forwardHeaders.delete("host")
|
|
25
|
+
forwardHeaders.delete("x-srt-proxy-url")
|
|
26
|
+
forwardHeaders.delete("x-srt-cache")
|
|
27
|
+
forwardHeaders.delete("content-length")
|
|
28
|
+
|
|
29
|
+
let cacheStatus: cache.Decision = "skip"
|
|
30
|
+
let cacheable = !cache.shouldConsider(req.method, req.headers).skip
|
|
31
|
+
let bypass = cacheable && cache.isBypass(req.headers)
|
|
32
|
+
|
|
33
|
+
if (cacheable && !bypass) {
|
|
34
|
+
let hit = cache.get(req.method, target)
|
|
35
|
+
if (hit) {
|
|
36
|
+
print("[cli] proxy %s %s [cache hit]", req.method, target)
|
|
37
|
+
let respHeaders = new Headers(hit.headers)
|
|
38
|
+
respHeaders.set("x-srt-cache", "hit")
|
|
39
|
+
return new Response(hit.body, { status: hit.status, headers: respHeaders })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let hasBody = req.method !== "GET" && req.method !== "HEAD"
|
|
44
|
+
if (cacheable) {
|
|
45
|
+
cacheStatus = bypass ? "bypass" : "miss"
|
|
46
|
+
print("[cli] proxy %s %s [%s]", req.method, target, cacheStatus)
|
|
47
|
+
} else {
|
|
48
|
+
print("[cli] proxy %s %s", req.method, target)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let upstream = await fetch(target, {
|
|
53
|
+
method: req.method,
|
|
54
|
+
headers: forwardHeaders,
|
|
55
|
+
body: hasBody ? await req.arrayBuffer() : undefined,
|
|
56
|
+
redirect: "follow",
|
|
57
|
+
})
|
|
58
|
+
let respHeaders = new Headers(upstream.headers)
|
|
59
|
+
respHeaders.delete("content-encoding")
|
|
60
|
+
respHeaders.delete("transfer-encoding")
|
|
61
|
+
|
|
62
|
+
let bodyBytes = new Uint8Array(await upstream.arrayBuffer())
|
|
63
|
+
if (cacheable) {
|
|
64
|
+
cache.put(
|
|
65
|
+
req.method,
|
|
66
|
+
target,
|
|
67
|
+
upstream.status,
|
|
68
|
+
headersToObject(respHeaders),
|
|
69
|
+
bodyBytes,
|
|
70
|
+
)
|
|
71
|
+
respHeaders.set("x-srt-cache", cacheStatus)
|
|
72
|
+
}
|
|
73
|
+
return new Response(bodyBytes, {
|
|
74
|
+
status: upstream.status,
|
|
75
|
+
statusText: upstream.statusText,
|
|
76
|
+
headers: respHeaders,
|
|
77
|
+
})
|
|
78
|
+
} catch (e) {
|
|
79
|
+
print("[cli] proxy error %s: %s", target, String(e))
|
|
80
|
+
return new Response(`Proxy error: ${String(e)}`, { status: 502 })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
7
83
|
|
|
8
84
|
export function startServer() {
|
|
9
85
|
state.server = Bun.serve({
|
|
@@ -14,12 +90,24 @@ export function startServer() {
|
|
|
14
90
|
let url = new URL(req.url)
|
|
15
91
|
let path = decodeURIComponent(url.pathname)
|
|
16
92
|
|
|
17
|
-
|
|
93
|
+
if (path === "/__proxy__") {
|
|
94
|
+
return handleProxy(req)
|
|
95
|
+
}
|
|
18
96
|
|
|
19
97
|
let filePath = resolve(state.sourceDir, "." + path)
|
|
20
98
|
if (!filePath.startsWith(state.sourceDir)) {
|
|
21
99
|
return new Response("Forbidden", { status: 403 })
|
|
22
100
|
}
|
|
101
|
+
|
|
102
|
+
if (req.method === "PUT") {
|
|
103
|
+
print("[cli] put", path)
|
|
104
|
+
let bytes = new Uint8Array(await req.arrayBuffer())
|
|
105
|
+
await Bun.write(filePath, bytes)
|
|
106
|
+
return new Response(null, { status: 204 })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
print("[cli] get", path)
|
|
110
|
+
|
|
23
111
|
let stat
|
|
24
112
|
try {
|
|
25
113
|
stat = await fsStat(filePath)
|