@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.2",
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.2",
23
- "@solidrt/linux-x64-gnu": "0.0.2",
24
- "@solidrt/win32-x64-msvc": "0.0.2"
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.2",
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 <build|run> [options] [entry.tsx]
25
+ console.error(`Usage: srt <command> [options] [file]
22
26
 
23
27
  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
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
- Options:
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 (build only)
31
- -o, --output <name> Bundle filename (build only)
32
- --stdout Write bundle to stdout (build only)`)
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
- state.child = Bun.spawn([runner], {
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
- print("[cli] get", path)
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)