@solidrt/cli 0.0.32 → 0.0.34

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/AGENTS.md CHANGED
@@ -10,8 +10,8 @@ bundled `flux` runtime, not on Bun. Invoke via `bunx srt <command>`.
10
10
  ## Commands
11
11
 
12
12
  - `bunx srt init <dir>` - scaffold a new SolidRT project into a new (empty)
13
- folder: package.json, tsconfig.json, AGENTS.md, a starter src/index.tsx, then
14
- installs deps. Greenfield shortcut (no install needed first):
13
+ folder: package.json, tsconfig.json, AGENTS.md, a starter src/index.tsx, an
14
+ empty assets/ (everything in it ships with the app), then installs deps. Greenfield shortcut (no install needed first):
15
15
  `bun create solidrt <dir>`.
16
16
  - `bunx srt run src/index.tsx` - dev server + a local client window, watches and
17
17
  hot-reloads. NEEDS A DISPLAY (opens a GUI window). Not usable headless.
@@ -45,9 +45,7 @@ Two reliable checks that need no GUI:
45
45
  ## Dev server proxies (when clients on other devices need your machine's data)
46
46
 
47
47
  - `--proxy-http` - route `fetch` through the dev server; responses cached in
48
- `.srt-cache.db` (delete the file to clear).
49
- - `--proxy-files` - route flux:fs (`file`/`dir`/`write`) through the dev server.
50
- Exposes your dev machine's files to all clients; use with care.
48
+ `.srt-data/http-cache.db` (delete the file to clear).
51
49
 
52
50
  ## REPL (opened by `run`/`server`)
53
51
 
Binary file
Binary file
Binary file
package/fonts/OFL.txt ADDED
@@ -0,0 +1,93 @@
1
+ Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  "src/",
13
13
  "server/",
14
14
  "scaffold/",
15
+ "fonts/",
15
16
  "AGENTS.md"
16
17
  ],
17
18
  "dependencies": {
@@ -27,17 +28,17 @@
27
28
  "zod": "^4.4.3"
28
29
  },
29
30
  "optionalDependencies": {
30
- "@solidrt/darwin-arm64": "0.0.32",
31
- "@solidrt/linux-arm64-gnu": "0.0.32",
32
- "@solidrt/linux-x64-gnu": "0.0.32",
33
- "@solidrt/win32-x64-msvc": "0.0.32"
31
+ "@solidrt/darwin-arm64": "0.0.34",
32
+ "@solidrt/linux-arm64-gnu": "0.0.34",
33
+ "@solidrt/linux-x64-gnu": "0.0.34",
34
+ "@solidrt/win32-x64-msvc": "0.0.34"
34
35
  },
35
36
  "peerDependencies": {
36
- "@solidrt/core": "0.0.32",
37
+ "@solidrt/core": "0.0.34",
37
38
  "typescript": "^7"
38
39
  },
39
40
  "devDependencies": {
40
- "@solidrt/flux-types": "0.0.32",
41
+ "@solidrt/flux-types": "0.0.34",
41
42
  "@types/bun": "latest"
42
43
  }
43
44
  }
@@ -153,6 +153,32 @@ nearly free. Rules, in order of leverage:
153
153
  creating many at once (dealing a board of 64 sprites) is a visible
154
154
  one-frame hiccup - pool or pre-warm if that moment matters.
155
155
 
156
+ ## Assets and app identity
157
+
158
+ - Everything under `assets/` ships with the app: the folder is collected
159
+ wholesale into each build's version manifest (no bundler analysis, no
160
+ registration step). Reference assets by path - `file("assets/sounds/x.ogg")`
161
+ from `flux:fs` - and treat them as read-only at runtime; writes belong in
162
+ plain relative paths, which land in the app's private data dir.
163
+ - Small text-like assets (SVG documents, shaders) can instead be inlined via
164
+ imports (`import icon from "./icon.svg"` yields the file's text;
165
+ `with { type: "binary" }` yields a Uint8Array). Inlining trades update
166
+ granularity for zero I/O - keep big or streamable files (audio, images) in
167
+ `assets/`.
168
+ - Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
169
+ map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
170
+ replace the built-in defaults, `false` drops one, other keys add fonts
171
+ selectable via fontFamily). A newly added font shows after restarting the
172
+ client.
173
+ - The `solidrt` key in package.json is the app's identity: set a stable
174
+ reverse-DNS `appId` before distributing - it keys the app's storage
175
+ folder, defaults from the package name in dev, and `srt pack` warns
176
+ while defaulted. `org` and `displayName` are optional display metadata
177
+ (future launcher/window naming) with no storage meaning.
178
+ - `bunx srt pack src/index.tsx` builds a single-file executable;
179
+ `bunx srt pack --folder src/index.tsx` writes the flat app folder
180
+ (runner + manifest.json + bundle + assets/) to `dist/`.
181
+
156
182
  ## Run / verify
157
183
 
158
184
  - bunx srt run src/index.tsx - dev server + window (needs a display)
@@ -1,4 +1,5 @@
1
1
  node_modules/
2
2
  *.srt.js
3
3
  *.srt.bin
4
- .srt-cache.db
4
+ .srt-data/
5
+ dist/
@@ -9,12 +9,12 @@
9
9
  "android": "srt client --android"
10
10
  },
11
11
  "dependencies": {
12
- "@solidrt/core": "0.0.32",
13
- "@solidrt/components": "0.0.32"
12
+ "@solidrt/core": "0.0.34",
13
+ "@solidrt/components": "0.0.34"
14
14
  },
15
15
  "devDependencies": {
16
- "@solidrt/cli": "0.0.32",
17
- "@solidrt/flux-types": "0.0.32",
16
+ "@solidrt/cli": "0.0.34",
17
+ "@solidrt/flux-types": "0.0.34",
18
18
  "typescript": "^7"
19
19
  }
20
20
  }
package/server/cache.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SQLite-backed HTTP response cache for the dev server's /__proxy__ endpoint.
2
2
  //
3
- // Project-local: stored at <dir>/.srt-cache.db. Entries live forever; delete
4
- // .srt-cache.db to drop them.
3
+ // Project-local: stored at <dir>/http-cache.db, where <dir> is the project's
4
+ // .srt-data. Entries live forever; delete the file to drop them.
5
5
  //
6
6
  // Cached: GET (and HEAD) 2xx responses with no Authorization on the request
7
7
  // and no Cache-Control: no-store on either side. The cache key is
@@ -10,9 +10,10 @@
10
10
  // old hashed entries simply miss and re-populate.)
11
11
 
12
12
  import { Database } from "flux:sqlite"
13
+ import { dir } from "flux:fs"
13
14
  import { join } from "flux:path"
14
15
 
15
- const CACHE_FILE = ".srt-cache.db"
16
+ const CACHE_FILE = "http-cache.db"
16
17
 
17
18
  export type Decision = "hit" | "miss" | "bypass" | "skip"
18
19
 
@@ -29,6 +30,7 @@ let db: Database | null = null
29
30
  let enabled = false
30
31
 
31
32
  export async function initCache(opts: { dir: string }) {
33
+ await dir(opts.dir).create()
32
34
  let d = await Database.connect(join(opts.dir, CACHE_FILE), "rw+")
33
35
  await d.exec(`CREATE TABLE IF NOT EXISTS entries (
34
36
  key TEXT PRIMARY KEY,
package/server/main.ts CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  import { serve } from "flux:http"
8
8
  import type { FluxRequest, Server } from "flux:http"
9
- import { file, dir } from "flux:fs"
9
+ import { file } from "flux:fs"
10
10
  import { argv } from "flux:process"
11
- import { resolveWithin, join } from "flux:path"
11
+ import { resolveWithin } from "flux:path"
12
12
  import { state, type Config } from "./state"
13
13
  import * as cache from "./cache"
14
14
  import { handleProxy } from "./proxy"
@@ -20,6 +20,7 @@ import { createTunnelEndpoint, TUNNEL_PROTOCOL } from "./tunnel"
20
20
  let config: Config = JSON.parse(argv[argv.length - 1]!)
21
21
  state.config = config
22
22
  state.sourceDir = config.sourceDir
23
+ state.projectDir = config.projectDir
23
24
  state.stats = config.stats
24
25
  state.serverUrl = `${config.address}:${config.port}`
25
26
 
@@ -74,14 +75,16 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
74
75
 
75
76
  switch (path) {
76
77
  case "/__internal__/reload": {
77
- // { message, clients?, latch?, sourceDir?, map? }: send `message` (a full
78
- // client-protocol message, built by srt) to the listed client ids, or to
79
- // all when omitted. `latch` keeps it for late-joining clients (code
80
- // reloads latch, one-shot bytecode loads do not); `sourceDir` moves the
81
- // file-serving root (repl `load`); `map` is the bundle's sourcemap for
82
- // log remapping, replaced on every reload (absent means none).
78
+ // { message, clients?, latch?, sourceDir?, projectDir?, map? }: send
79
+ // `message` (a full client-protocol message, built by srt) to the listed
80
+ // client ids, or to all when omitted. `latch` keeps it for late-joining
81
+ // clients (code reloads latch, one-shot bytecode loads do not);
82
+ // `sourceDir` moves the file-serving root and `projectDir` the /assets/
83
+ // root (repl `load`); `map` is the bundle's sourcemap for log remapping,
84
+ // replaced on every reload (absent means none).
83
85
  let body = await req.json()
84
86
  if (typeof body.sourceDir === "string") state.sourceDir = body.sourceDir
87
+ if (typeof body.projectDir === "string") state.projectDir = body.projectDir
85
88
  // Keep the rebuild entry in sync when `load` moves it, so a later MCP
86
89
  // reload bundles the newly loaded file, not the launch-time one.
87
90
  if (typeof body.entry === "string") state.config.entry = body.entry
@@ -119,21 +122,15 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
119
122
  }
120
123
  }
121
124
 
122
- // The file routes: GET file (with single-range 206 support) or directory
123
- // listing, PUT file write. All paths are contained in the source directory.
124
- async function handleFiles(req: FluxRequest, path: string): Promise<Response> {
125
- let filePath = resolveWithin(state.sourceDir, "." + path)
125
+ // The file routes: GET file with single-range 206 support. All paths are
126
+ // contained in `root` (the source directory, or the project dir for the
127
+ // /assets/ convention route).
128
+ async function handleFiles(req: FluxRequest, path: string, root: string): Promise<Response> {
129
+ let filePath = resolveWithin(root, "." + path)
126
130
  if (!filePath) {
127
131
  return new Response("Forbidden", { status: 403 })
128
132
  }
129
133
 
130
- if (req.method === "PUT") {
131
- console.log("[cli] put " + path)
132
- let bytes = await req.bytes()
133
- await file(filePath).write(bytes)
134
- return new Response("", { status: 204 })
135
- }
136
-
137
134
  console.log("[cli] get " + path)
138
135
 
139
136
  let stat
@@ -143,27 +140,11 @@ async function handleFiles(req: FluxRequest, path: string): Promise<Response> {
143
140
  console.log(`[cli] file not found ${path}`)
144
141
  return new Response("Not found", { status: 404 })
145
142
  }
146
-
147
143
  if (stat.type === "directory") {
148
- let dirents = await dir(filePath).entries()
149
- let entries = await Promise.all(
150
- dirents.map(async (d) => {
151
- let entry = { name: d.name, type: d.type === "directory" ? 2 : 1, size: 0, modified: 0 }
152
- if (d.type !== "directory") {
153
- try {
154
- let s = await file(join(filePath, d.name)).stat()
155
- entry.size = s.size
156
- entry.modified = Math.floor(s.mtime ?? 0)
157
- } catch {}
158
- }
159
- return entry
160
- }),
161
- )
162
- entries.sort((a, b) => a.name.localeCompare(b.name))
163
- return Response.json(entries, { headers: { "X-SRT-Type": "directory" } })
144
+ return new Response("Not found", { status: 404 })
164
145
  }
165
146
 
166
- let baseHeaders: Record<string, string> = { "X-SRT-Type": "file", "Accept-Ranges": "bytes" }
147
+ let baseHeaders: Record<string, string> = { "Accept-Ranges": "bytes" }
167
148
 
168
149
  // Honor a single byte-range request (e.g. streaming audio decoding on the
169
150
  // client, which seeks and reads on demand). Only the common "bytes=a-b" /
@@ -206,7 +187,7 @@ async function handleFiles(req: FluxRequest, path: string): Promise<Response> {
206
187
 
207
188
  // Ticket-paired clients connect through this endpoint; serve() accepts its
208
189
  // connections directly alongside the TCP listener.
209
- let tunnel = config.tunnel ? await createTunnelEndpoint(config.port, config.cacheDir) : null
190
+ let tunnel = config.tunnel ? await createTunnelEndpoint(config.port, config.keyDir) : null
210
191
 
211
192
  serve({
212
193
  port: config.port,
@@ -225,7 +206,13 @@ serve({
225
206
  if (path.startsWith("/__internal__/")) {
226
207
  return handleInternal(req, server, path)
227
208
  }
228
- return handleFiles(req, path)
209
+ // The assets/ convention roots at the project dir (package.json), which is
210
+ // not necessarily the entry's dir the file routes serve; clients fetch
211
+ // manifest asset paths here (live proxy reads and store installs alike).
212
+ if (path === "/assets" || path.startsWith("/assets/")) {
213
+ return handleFiles(req, path, state.projectDir)
214
+ }
215
+ return handleFiles(req, path, state.sourceDir)
229
216
  },
230
217
  websocket: {
231
218
  open(ws) {
package/server/rebuild.ts CHANGED
@@ -10,10 +10,15 @@ import { state } from "./state"
10
10
 
11
11
  // Build the reload message the same way srt's buildReload does, so a
12
12
  // server-triggered reload is indistinguishable from a repl-triggered one to
13
- // clients. proxyFiles/proxyHttp are message flags, not build inputs.
14
- function buildReload(code: string) {
13
+ // clients. proxyHttp is a message flag, not a build input.
14
+ function buildReload(code: string, manifest?: string) {
15
15
  let config = state.config
16
- return { type: "reload", proxyFiles: config.proxyFiles, proxyHttp: config.proxyHttp, code }
16
+ return {
17
+ type: "reload",
18
+ proxyHttp: config.proxyHttp,
19
+ ...(manifest ? { manifest } : {}),
20
+ code,
21
+ }
17
22
  }
18
23
 
19
24
  // Rebuild from state.config.entry via the external bundle-cli subprocess, then
@@ -39,15 +44,15 @@ export async function rebuildAndBroadcast(): Promise<string | null> {
39
44
  return `Rebuild failed:\n${stderr.trim()}`
40
45
  }
41
46
 
42
- // bundle-cli writes one JSON object { code, map } to stdout.
43
- let bundle: { code?: string; map?: string | null }
47
+ // bundle-cli writes one JSON object { code, map, manifest } to stdout.
48
+ let bundle: { code?: string; map?: string | null; manifest?: string }
44
49
  try {
45
50
  bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "")
46
51
  } catch {
47
52
  return "Rebuild failed: unreadable bundler output"
48
53
  }
49
54
  state.currentMap = bundle.map ?? null
50
- let text = JSON.stringify(buildReload(bundle.code ?? ""))
55
+ let text = JSON.stringify(buildReload(bundle.code ?? "", bundle.manifest))
51
56
  state.currentReload = text
52
57
  for (let ws of state.clients.keys()) ws.send(text)
53
58
  return null
package/server/state.ts CHANGED
@@ -7,9 +7,11 @@ export type Config = {
7
7
  port: number
8
8
  /** Directory served by the file routes (updatable via /__internal__/reload). */
9
9
  sourceDir: string
10
+ /** Project root whose assets/ folder the /assets/ route serves (updatable
11
+ * via /__internal__/reload, moves with the repl `load`). */
12
+ projectDir: string
10
13
  /** The address clients can reach this machine on (LAN IP or 127.0.0.1). */
11
14
  address: string
12
- proxyFiles: boolean
13
15
  proxyHttp: boolean
14
16
  /** The app entry (absolute .tsx/.jsx path) the server rebuilds on an
15
17
  * MCP-triggered reload, or undefined when srt was started without a source.
@@ -22,8 +24,10 @@ export type Config = {
22
24
  bundlerCmd: string[]
23
25
  /** Enable the sqlite-backed proxy cache. */
24
26
  cache: boolean
25
- /** Directory holding .srt-cache.db. */
27
+ /** Directory holding the proxy cache db (the project-local .srt-data). */
26
28
  cacheDir: string
29
+ /** Directory holding .srt-tunnel-key (the project root). */
30
+ keyDir: string
27
31
  /** Destination for captured key events, or unset when off. */
28
32
  capture?: string
29
33
  stats: boolean
@@ -57,6 +61,7 @@ export let state = {
57
61
  */
58
62
  currentMap: null as string | null,
59
63
  sourceDir: "",
64
+ projectDir: "",
60
65
  serverUrl: "",
61
66
  stats: false,
62
67
  /**
package/server/tunnel.ts CHANGED
@@ -15,21 +15,21 @@ import { printQr } from "./qr"
15
15
  // the handshake instead of desyncing.
16
16
  export const TUNNEL_PROTOCOL = "solidrt-dev/0"
17
17
 
18
- // The persisted identity file, project-local next to the HTTP cache. Delete it
19
- // to rotate the tunnel's identity (which invalidates any old ticket).
18
+ // The persisted identity file, at the project root. Delete it to rotate the
19
+ // tunnel's identity (which invalidates any old ticket).
20
20
  const KEY_FILE = ".srt-tunnel-key"
21
21
 
22
22
  /**
23
23
  * Bind the tunnel endpoint and print its ticket (text + QR). The endpoint is
24
24
  * kept stable across restarts so a paired client can re-dial the old ticket
25
25
  * without re-scanning: the UDP port is pinned to the dev server's port, and the
26
- * secret key is persisted in <cacheDir>/.srt-tunnel-key (generated on first
26
+ * secret key is persisted in <keyDir>/.srt-tunnel-key (generated on first
27
27
  * run). Both are needed - a moving port or a fresh key each start would change
28
28
  * the ticket. Stable across restarts on the same network only; a new machine IP
29
29
  * still stales the ticket's addresses (that is the discovery/off-LAN story).
30
30
  */
31
- export async function createTunnelEndpoint(port: number, cacheDir: string): Promise<Endpoint> {
32
- let keyPath = join(cacheDir, KEY_FILE)
31
+ export async function createTunnelEndpoint(port: number, keyDir: string): Promise<Endpoint> {
32
+ let keyPath = join(keyDir, KEY_FILE)
33
33
 
34
34
  let secretKey: string | undefined
35
35
  let keyFile = file(keyPath)
package/src/args.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from "node:util"
2
+ import { resolve } from "node:path"
2
3
 
3
4
  export let { values, positionals } = parseArgs({
4
5
  options: {
@@ -6,9 +7,9 @@ export let { values, positionals } = parseArgs({
6
7
  minify: { type: "boolean", short: "m", default: false },
7
8
  compile: { type: "boolean", short: "c", default: false },
8
9
  flux: { type: "boolean", short: "f", default: false },
10
+ folder: { type: "boolean", default: false },
9
11
  stdout: { type: "boolean", default: false },
10
12
  output: { type: "string", short: "o" },
11
- "proxy-files": { type: "boolean", default: false },
12
13
  "proxy-http": { type: "boolean", default: false },
13
14
  fps: { type: "string" },
14
15
  duration: { type: "string" },
@@ -17,6 +18,9 @@ export let { values, positionals } = parseArgs({
17
18
  capture: { type: "string" },
18
19
  tunnel: { type: "boolean", default: false },
19
20
  stats: { type: "boolean", default: false },
21
+ "data-root": { type: "string" },
22
+ client: { type: "string" },
23
+ server: { type: "string" },
20
24
  android: { type: "boolean", default: false },
21
25
  device: { type: "string" },
22
26
  template: { type: "string", short: "t" },
@@ -24,6 +28,16 @@ export let { values, positionals } = parseArgs({
24
28
  allowPositionals: true,
25
29
  })
26
30
 
31
+ // Storage flags for a locally spawned client. The data root defaults to the
32
+ // project-local .srt-data so dev state stays with the project; it is passed
33
+ // absolute because the client chdirs into its app sandbox at startup. Remote
34
+ // clients resolve their own local root instead (see lattice/src/storage.rs).
35
+ export function clientStorageArgs(): string[] {
36
+ let args = ["--data-root", resolve(values["data-root"] ?? ".srt-data")]
37
+ if (values.client) args.push("--client", values.client)
38
+ return args
39
+ }
40
+
27
41
  export let command = positionals[0]
28
42
  export let source = positionals[1]
29
43
  export let isTsx = source?.endsWith(".tsx") || source?.endsWith(".jsx")
@@ -66,6 +80,11 @@ export function validateArgs() {
66
80
  if (values.android && command !== "client") {
67
81
  usage("srt client --android (--android is only valid with the client command)")
68
82
  }
83
+ // --server points a standalone client at a dev server; `run` and `server`
84
+ // own their server side, so it is only valid for `client`.
85
+ if (values.server && command !== "client") {
86
+ usage("srt client --server <host[:port]> (--server is only valid with the client command)")
87
+ }
69
88
  }
70
89
 
71
90
  export function printUsage() {
@@ -86,7 +105,6 @@ init options:
86
105
  -t, --template <name> Start from a named template (skips the interactive picker)
87
106
 
88
107
  run/server options:
89
- --proxy-files Route file/dir access through the dev server
90
108
  --proxy-http Route fetch calls through the dev server (HTTP cache enabled)
91
109
  --capture <file> Record connected clients' key events to a script file
92
110
  --tunnel Accept ticket-paired clients through the p2p tunnel
@@ -94,8 +112,11 @@ run/server options:
94
112
  run/client options:
95
113
  --size <WxH> Window size (default: 1280x720)
96
114
  --stats Show the debug stats overlay (FPS, memory, frame timings)
115
+ --data-root <dir> Client data root (default: ./.srt-data)
116
+ --client <name> Client name: its own data dir under the data root (default: "default")
97
117
 
98
118
  client options:
119
+ --server <host[:port]> Connect to a dev server at this address (default port: 34884)
99
120
  --android Install and launch the client on a connected Android device
100
121
  --device <serial> Target a specific adb device by serial or unique prefix
101
122
 
@@ -108,6 +129,8 @@ bundle options:
108
129
  --stdout Write bundle to stdout
109
130
 
110
131
  pack options:
132
+ --folder Write the flat app folder (runner + manifest + bundle + assets)
133
+ instead of the single-file executable
111
134
  -f, --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
112
135
  -m, --minify Minify the output
113
136
  -o, --output <name> Output filename
package/src/bundler.ts CHANGED
@@ -8,6 +8,7 @@ import { readFileSync } from "node:fs"
8
8
  import { dirname, resolve as resolvePath } from "node:path"
9
9
  import { values, source } from "./args"
10
10
  import { state, print, requireBinary } from "./util"
11
+ import { buildManifest } from "./project"
11
12
 
12
13
  // Babel plugin: rewrite `import data from "./x" with { type: "binary" }` into an
13
14
  // inline Uint8Array of the file's bytes. The import attribute is invisible to
@@ -96,6 +97,8 @@ export type BundleResult = {
96
97
  code: string
97
98
  /** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
98
99
  map: string | null
100
+ /** Version manifest JSON for this bundle; clients install pushes under its hash. */
101
+ manifest: string
99
102
  }
100
103
 
101
104
  // The pure bundle: every input is explicit, so it runs identically in the srt
@@ -134,7 +137,8 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
134
137
  return null
135
138
  }
136
139
 
137
- return { code: await codeFromOutputs(result.outputs), map: await composeMap(result.outputs, babelMaps) }
140
+ let code = await codeFromOutputs(result.outputs)
141
+ return { code, map: await composeMap(result.outputs, babelMaps), manifest: buildManifest(code, opts.entry) }
138
142
  }
139
143
 
140
144
  // Compose Bun's bundle map (babel output -> bundle) with the per-file Babel
@@ -1,11 +1,13 @@
1
- import { values } from "../args"
1
+ import { values, clientStorageArgs } from "../args"
2
2
  import { requireBinary, run } from "../util"
3
3
  import { spawnAndroidClient } from "../dev-android"
4
+ import { DEV_PORT } from "../dev-server"
4
5
 
5
6
  // 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.
7
+ // spawnClient() to launch a client tied to the dev-server lifecycle. --server
8
+ // auto-connects to a dev server at the given address (otherwise the client
9
+ // starts on the connect screen); with --android it is installed and launched
10
+ // on a connected Android device instead of run locally.
9
11
  export async function runClientCommand() {
10
12
  if (values.android) {
11
13
  await spawnAndroidClient()
@@ -13,10 +15,12 @@ export async function runClientCommand() {
13
15
  }
14
16
 
15
17
  let runner = requireBinary("solidrt-go")
16
- let args: string[] = []
18
+ let args: string[] = [...clientStorageArgs()]
17
19
  if (values.size) args.push("--size", values.size)
18
- //TODO add dev server connection
19
- // if (source) args.push("--dev-server", source)
20
+ if (values.server) {
21
+ let address = values.server.includes(":") ? values.server : `${values.server}:${DEV_PORT}`
22
+ args.push("--dev-server", address)
23
+ }
20
24
  let exit = await run(runner, args)
21
25
  process.exit(exit)
22
26
  }
@@ -139,6 +139,12 @@ export async function runInitCommand() {
139
139
  console.log(` Write src/${file}`)
140
140
  }
141
141
 
142
+ // The assets/ convention folder, created up front: everything in it ships
143
+ // with the app, and the dev watcher only picks up an assets/ folder that
144
+ // exists when it starts.
145
+ await mkdir(join(dir, "assets"), { recursive: true })
146
+ console.log(" Write assets/")
147
+
142
148
  // The scaffold package.json carries a placeholder name; set it from the
143
149
  // target folder. A core-level app gets no component framework dependency.
144
150
  let pkgPath = join(dir, "package.json")