@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 +3 -5
- package/fonts/NotoSans.ttf +0 -0
- package/fonts/NotoSansMono.ttf +0 -0
- package/fonts/NotoSerif.ttf +0 -0
- package/fonts/OFL.txt +93 -0
- package/package.json +8 -7
- package/scaffold/AGENTS.md +26 -0
- package/scaffold/gitignore +2 -1
- package/scaffold/package.json +4 -4
- package/server/cache.ts +5 -3
- package/server/main.ts +26 -39
- package/server/rebuild.ts +11 -6
- package/server/state.ts +7 -2
- package/server/tunnel.ts +5 -5
- package/src/args.ts +25 -2
- package/src/bundler.ts +5 -1
- package/src/commands/client.ts +11 -7
- package/src/commands/init.ts +6 -0
- package/src/commands/pack.ts +43 -7
- package/src/commands/server.ts +11 -3
- package/src/dev-client.ts +2 -2
- package/src/dev-server.ts +29 -9
- package/src/fonts.ts +91 -0
- package/src/pack-folder.ts +112 -0
- package/src/packer.ts +55 -12
- package/src/project.ts +171 -0
- package/src/repl.ts +11 -3
- package/src/util.ts +6 -0
- package/src/watcher.ts +53 -27
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,
|
|
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.
|
|
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.
|
|
31
|
-
"@solidrt/linux-arm64-gnu": "0.0.
|
|
32
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
33
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
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.
|
|
37
|
+
"@solidrt/core": "0.0.34",
|
|
37
38
|
"typescript": "^7"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@solidrt/flux-types": "0.0.
|
|
41
|
+
"@solidrt/flux-types": "0.0.34",
|
|
41
42
|
"@types/bun": "latest"
|
|
42
43
|
}
|
|
43
44
|
}
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -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)
|
package/scaffold/gitignore
CHANGED
package/scaffold/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"android": "srt client --android"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@solidrt/core": "0.0.
|
|
13
|
-
"@solidrt/components": "0.0.
|
|
12
|
+
"@solidrt/core": "0.0.34",
|
|
13
|
+
"@solidrt/components": "0.0.34"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
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
|
|
4
|
-
// .srt-
|
|
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 = "
|
|
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
|
|
9
|
+
import { file } from "flux:fs"
|
|
10
10
|
import { argv } from "flux:process"
|
|
11
|
-
import { resolveWithin
|
|
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
|
|
78
|
-
// client-protocol message, built by srt) to the listed
|
|
79
|
-
// all when omitted. `latch` keeps it for late-joining
|
|
80
|
-
// reloads latch, one-shot bytecode loads do not);
|
|
81
|
-
// file-serving root
|
|
82
|
-
//
|
|
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
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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> = { "
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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-
|
|
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,
|
|
19
|
-
//
|
|
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 <
|
|
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,
|
|
32
|
-
let keyPath = join(
|
|
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
|
-
|
|
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
|
package/src/commands/client.ts
CHANGED
|
@@ -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.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
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
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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")
|