@solidrt/cli 0.0.50 → 0.0.51
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 +20 -7
- package/agents/assets.md +32 -0
- package/agents/debugging.md +142 -0
- package/package.json +8 -6
- package/scaffold/AGENTS.md +102 -547
- package/scaffold/package.json +5 -5
- package/server/control.ts +15 -5
- package/server/main.ts +8 -8
- package/server/rebuild.ts +10 -2
- package/server/remap.ts +46 -33
- package/server/state.ts +6 -5
- package/src/args.ts +5 -7
- package/src/bundler.ts +112 -37
- package/src/commands/bundle.ts +106 -21
- package/src/commands/check.ts +7 -0
- package/src/commands/init.ts +18 -18
- package/src/commands/mcp.ts +19 -5
- package/src/commands/pack.ts +18 -8
- package/src/commands/render.ts +28 -5
- package/src/commands/server.ts +5 -5
- package/src/dev-server.ts +7 -7
- package/src/packer.ts +16 -12
- package/src/repl.ts +27 -11
- package/src/util.ts +4 -3
- package/src/watcher.ts +7 -3
package/scaffold/package.json
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"android": "srt client --android"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@solidrt/core": "0.0.
|
|
14
|
-
"@solidrt/components": "0.0.
|
|
15
|
-
"@solidrt/3d": "0.0.
|
|
13
|
+
"@solidrt/core": "0.0.51",
|
|
14
|
+
"@solidrt/components": "0.0.51",
|
|
15
|
+
"@solidrt/3d": "0.0.51"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@solidrt/cli": "0.0.
|
|
19
|
-
"@solidrt/flux-types": "0.0.
|
|
18
|
+
"@solidrt/cli": "0.0.51",
|
|
19
|
+
"@solidrt/flux-types": "0.0.51",
|
|
20
20
|
"typescript": "^7"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/server/control.ts
CHANGED
|
@@ -34,7 +34,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
34
34
|
/// A `log` message arrived from a client: buffer it and wake long-polls.
|
|
35
35
|
/// Bundle positions in stack traces are remapped to .tsx sources on the way in.
|
|
36
36
|
export function appendLog(client: number, level: string, text: string) {
|
|
37
|
-
logs.push({ seq: ++logSeq, at: Date.now(), client, level, text: remapPositions(text, state.
|
|
37
|
+
logs.push({ seq: ++logSeq, at: Date.now(), client, level, text: remapPositions(text, state.currentMaps) })
|
|
38
38
|
if (logs.length > LOG_CAP) logs.splice(0, logs.length - LOG_CAP)
|
|
39
39
|
let waiters = logWaiters
|
|
40
40
|
logWaiters = []
|
|
@@ -138,7 +138,7 @@ async function handleQuery(
|
|
|
138
138
|
)
|
|
139
139
|
// Error strings may carry stack traces (e.g. a debug command threw); remap
|
|
140
140
|
// bundle positions to .tsx sources like appendLog does for forwarded logs.
|
|
141
|
-
if (msg.error) return Response.json({ error: remapPositions(String(msg.error), state.
|
|
141
|
+
if (msg.error) return Response.json({ error: remapPositions(String(msg.error), state.currentMaps) }, { status: 502 })
|
|
142
142
|
return Response.json(msg.data)
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -225,8 +225,12 @@ export async function handleControl(req: Request, path: string, query: Map<strin
|
|
|
225
225
|
if (query.get("props") === "true") extra.props = true
|
|
226
226
|
return handleQuery(query, "tree", extra)
|
|
227
227
|
}
|
|
228
|
-
case "/__control__/stats":
|
|
229
|
-
|
|
228
|
+
case "/__control__/stats": {
|
|
229
|
+
let extra: Record<string, unknown> = {}
|
|
230
|
+
let windowMs = parseInt(query.get("window") ?? "", 10)
|
|
231
|
+
if (Number.isFinite(windowMs)) extra.windowMs = windowMs
|
|
232
|
+
return handleQuery(query, "stats", extra)
|
|
233
|
+
}
|
|
230
234
|
case "/__control__/snapshot": {
|
|
231
235
|
let nodeId = parseInt(query.get("node") ?? "", 10)
|
|
232
236
|
if (!Number.isFinite(nodeId)) return Response.json({ error: "Snapshot requires ?node=<id>" }, { status: 400 })
|
|
@@ -361,7 +365,13 @@ export async function handleControl(req: Request, path: string, query: Map<strin
|
|
|
361
365
|
// An entry outside the project root cannot resolve the project's
|
|
362
366
|
// dependencies, so the bundler would fail with misleading "bun install"
|
|
363
367
|
// advice; name the real constraint instead.
|
|
364
|
-
|
|
368
|
+
// Windows paths are case-insensitive and the same drive shows up as both
|
|
369
|
+
// `c:` and `C:` (an editor-spawned bridge keeps its parent's spelling), so a
|
|
370
|
+
// drive-letter path folds case; a POSIX path stays exact.
|
|
371
|
+
let norm = (p: string) => {
|
|
372
|
+
let s = p.replace(/\\/g, "/")
|
|
373
|
+
return /^[a-zA-Z]:\//.test(s) ? s.toLowerCase() : s
|
|
374
|
+
}
|
|
365
375
|
let root = norm(state.projectDir).replace(/\/+$/, "") + "/"
|
|
366
376
|
if (!norm(entry).startsWith(root)) {
|
|
367
377
|
return Response.json(
|
package/server/main.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// config argument; bundling, file watching, and the repl stay in srt, which
|
|
3
3
|
// drives this process over the loopback-only /__internal__/ routes. The
|
|
4
4
|
// shutdown-when-empty policy also lives in srt (it polls /__internal__/clients);
|
|
5
|
-
// this process runs until srt kills it.
|
|
5
|
+
// this process runs until srt kills it.
|
|
6
6
|
|
|
7
7
|
import { serve } from "flux:http"
|
|
8
8
|
import type { FluxRequest, Server } from "flux:http"
|
|
@@ -74,20 +74,20 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
|
|
|
74
74
|
|
|
75
75
|
switch (path) {
|
|
76
76
|
case "/__internal__/reload": {
|
|
77
|
-
// { message, clients?, latch?, sourceDir?,
|
|
77
|
+
// { message, clients?, latch?, sourceDir?, maps? }: send `message` (a
|
|
78
78
|
// full client-protocol message, built by srt) to the listed client ids,
|
|
79
79
|
// or to all when omitted. `latch` keeps it for late-joining clients
|
|
80
80
|
// (code reloads latch, one-shot bytecode loads do not); `sourceDir`
|
|
81
81
|
// moves the file-serving root (repl `load`; the project root - and with
|
|
82
|
-
// it the /assets/ root - is fixed for the life of the run); `
|
|
83
|
-
// the bundle's
|
|
84
|
-
// (absent means none).
|
|
82
|
+
// it the /assets/ root - is fixed for the life of the run); `maps` is
|
|
83
|
+
// the bundle's sourcemaps keyed by module name for log remapping,
|
|
84
|
+
// replaced on every reload (absent means none).
|
|
85
85
|
let body = await req.json()
|
|
86
86
|
if (typeof body.sourceDir === "string") state.sourceDir = body.sourceDir
|
|
87
87
|
// Keep the rebuild entry in sync when `load` moves it, so a later MCP
|
|
88
88
|
// reload bundles the newly loaded file, not the launch-time one.
|
|
89
89
|
if (typeof body.entry === "string") state.config.entry = body.entry
|
|
90
|
-
state.
|
|
90
|
+
state.currentMaps = body.maps && typeof body.maps === "object" ? body.maps : null
|
|
91
91
|
let text = JSON.stringify(body.message)
|
|
92
92
|
if (body.latch) state.currentReload = text
|
|
93
93
|
sendTo(body.clients, text)
|
|
@@ -99,7 +99,7 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
|
|
|
99
99
|
// connects afterwards starts clean.
|
|
100
100
|
if (!body.clients) {
|
|
101
101
|
state.currentReload = null
|
|
102
|
-
state.
|
|
102
|
+
state.currentMaps = null
|
|
103
103
|
}
|
|
104
104
|
sendTo(body.clients, JSON.stringify({ type: "stop" }))
|
|
105
105
|
return new Response("", { status: 204 })
|
|
@@ -298,7 +298,7 @@ if (!config.tunnel) {
|
|
|
298
298
|
}
|
|
299
299
|
console.log(`[cli] WebSocket server on ws://${state.serverUrl}`)
|
|
300
300
|
// mDNS advertise is intentionally not implemented here: the p2p ticket is the
|
|
301
|
-
// cross-device connect story (see
|
|
301
|
+
// cross-device connect story (see okf/backlog/mdns-discovery.md).
|
|
302
302
|
|
|
303
303
|
// Keepalive
|
|
304
304
|
setInterval(() => {
|
package/server/rebuild.ts
CHANGED
|
@@ -47,7 +47,12 @@ export async function rebuildAndBroadcast(): Promise<string | null> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// bundle-cli writes one JSON object { code, map, manifest, isolates } to stdout.
|
|
50
|
-
let bundle: {
|
|
50
|
+
let bundle: {
|
|
51
|
+
code?: string
|
|
52
|
+
map?: string | null
|
|
53
|
+
manifest?: string
|
|
54
|
+
isolates?: { id: string; code: string; map?: string | null }[]
|
|
55
|
+
}
|
|
51
56
|
try {
|
|
52
57
|
bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "")
|
|
53
58
|
} catch {
|
|
@@ -55,12 +60,15 @@ export async function rebuildAndBroadcast(): Promise<string | null> {
|
|
|
55
60
|
}
|
|
56
61
|
// Isolate bundles are manifest assets clients fetch from our /isolates/
|
|
57
62
|
// route (served from cacheDir), so they must be on disk before the push.
|
|
63
|
+
let maps: Record<string, string> = {}
|
|
64
|
+
if (bundle.map) maps.main = bundle.map
|
|
58
65
|
for (let isolate of bundle.isolates ?? []) {
|
|
59
66
|
let path = `${config.cacheDir}/isolates/${isolate.id}.js`
|
|
60
67
|
await dir(path.slice(0, path.lastIndexOf("/"))).create()
|
|
61
68
|
await file(path).write(isolate.code)
|
|
69
|
+
if (isolate.map) maps[isolate.id] = isolate.map
|
|
62
70
|
}
|
|
63
|
-
state.
|
|
71
|
+
state.currentMaps = Object.keys(maps).length ? maps : null
|
|
64
72
|
let text = JSON.stringify(buildReload(bundle.code ?? "", bundle.manifest))
|
|
65
73
|
state.currentReload = text
|
|
66
74
|
for (let ws of state.clients.keys()) ws.send(text)
|
package/server/remap.ts
CHANGED
|
@@ -1,47 +1,60 @@
|
|
|
1
1
|
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping"
|
|
2
2
|
|
|
3
3
|
// Stack-trace remapping for forwarded client logs. The runtime evaluates the
|
|
4
|
-
// bundle as module "main"
|
|
5
|
-
// "at boom (main:212:9)"
|
|
6
|
-
//
|
|
7
|
-
// .
|
|
4
|
+
// app bundle as module "main" and each isolate bundle under its isolate id,
|
|
5
|
+
// so QuickJS frames cite bundle positions like "at boom (main:212:9)" or
|
|
6
|
+
// "at boom (worker:65:13)". With the current reload's sourcemaps latched on
|
|
7
|
+
// the server (state.currentMaps, keyed by module name), those positions are
|
|
8
|
+
// rewritten to the original .tsx sources before a log entry is buffered. A
|
|
9
|
+
// module without a map is left as it is: a position is never remapped against
|
|
10
|
+
// another module's map.
|
|
8
11
|
|
|
9
|
-
//
|
|
10
|
-
// lookup rebuilds the
|
|
11
|
-
|
|
12
|
-
let tracer: TraceMap | null
|
|
12
|
+
// Parsed maps are cached per module name and map text; a reload swaps the
|
|
13
|
+
// texts and the next lookup rebuilds the tracers. Entries for removed modules
|
|
14
|
+
// linger unused, which is harmless.
|
|
15
|
+
let cached = new Map<string, { text: string; tracer: TraceMap | null }>()
|
|
13
16
|
|
|
14
|
-
function tracerFor(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tracer = null
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// A malformed map disables remapping until the next reload.
|
|
23
|
-
}
|
|
17
|
+
function tracerFor(name: string, text: string): TraceMap | null {
|
|
18
|
+
let entry = cached.get(name)
|
|
19
|
+
if (!entry || entry.text !== text) {
|
|
20
|
+
let tracer: TraceMap | null = null
|
|
21
|
+
try {
|
|
22
|
+
tracer = new TraceMap(JSON.parse(text))
|
|
23
|
+
} catch {
|
|
24
|
+
// A malformed map disables remapping for this module until the next reload.
|
|
24
25
|
}
|
|
26
|
+
entry = { text, tracer }
|
|
27
|
+
cached.set(name, entry)
|
|
25
28
|
}
|
|
26
|
-
return tracer
|
|
29
|
+
return entry.tracer
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
let REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
|
-
* Rewrite every "
|
|
31
|
-
* original source position, e.g.
|
|
32
|
-
*
|
|
35
|
+
* Rewrite every "NAME:LINE:COL" (or "NAME:LINE") position in `text`, for each
|
|
36
|
+
* module NAME in `maps`, to its original source position, e.g.
|
|
37
|
+
* "src/app.tsx:42:7". Positions a map has no entry for, positions of modules
|
|
38
|
+
* without a map, and all text when `maps` is null, pass through unchanged.
|
|
33
39
|
* QuickJS lines and columns are 1-based; sourcemap columns are 0-based.
|
|
34
40
|
*/
|
|
35
|
-
export function remapPositions(text: string,
|
|
36
|
-
if (!
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
export function remapPositions(text: string, maps: Record<string, string> | null): string {
|
|
42
|
+
if (!maps) return text
|
|
43
|
+
for (let [name, map] of Object.entries(maps)) {
|
|
44
|
+
if (!text.includes(name + ":")) continue
|
|
45
|
+
let t = tracerFor(name, map)
|
|
46
|
+
if (!t) continue
|
|
47
|
+
// The leading capture keeps a name from matching inside a longer one
|
|
48
|
+
// ("audio" inside "workers/audio" or "app" inside "src/app.tsx").
|
|
49
|
+
let pattern = new RegExp(`(^|[^\\w/.$-])${name.replace(REGEX_SPECIALS, "\\$&")}:(\\d+)(?::(\\d+))?\\b`, "g")
|
|
50
|
+
text = text.replace(pattern, (frame, prefix, line, column) => {
|
|
51
|
+
let pos = originalPositionFor(t, {
|
|
52
|
+
line: parseInt(line, 10),
|
|
53
|
+
column: column ? Math.max(parseInt(column, 10) - 1, 0) : 0,
|
|
54
|
+
})
|
|
55
|
+
if (pos.source == null || pos.line == null) return frame
|
|
56
|
+
return `${prefix}${pos.source}:${pos.line}:${(pos.column ?? 0) + 1}`
|
|
43
57
|
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
})
|
|
58
|
+
}
|
|
59
|
+
return text
|
|
47
60
|
}
|
package/server/state.ts
CHANGED
|
@@ -66,12 +66,13 @@ export let state = {
|
|
|
66
66
|
*/
|
|
67
67
|
currentReload: null as string | null,
|
|
68
68
|
/**
|
|
69
|
-
* The running bundle's
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* The running bundle's sourcemaps (JSON text, bundle -> .tsx sources),
|
|
70
|
+
* keyed by the module name stack frames cite ("main" for the app, the
|
|
71
|
+
* isolate id for each isolate), used to remap stack traces in forwarded
|
|
72
|
+
* client logs (see control.ts). Replaced on every reload; a reload without
|
|
73
|
+
* maps clears them so frames are never remapped against a stale map.
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
+
currentMaps: null as Record<string, string> | null,
|
|
75
76
|
sourceDir: "",
|
|
76
77
|
projectDir: "",
|
|
77
78
|
serverUrl: "",
|
package/src/args.ts
CHANGED
|
@@ -35,7 +35,6 @@ export let { values, positionals } = parseArgs({
|
|
|
35
35
|
port: { type: "string" },
|
|
36
36
|
android: { type: "boolean", default: false },
|
|
37
37
|
device: { type: "string" },
|
|
38
|
-
with: { type: "string" },
|
|
39
38
|
},
|
|
40
39
|
allowPositionals: true,
|
|
41
40
|
})
|
|
@@ -81,9 +80,11 @@ export function clientStorageArgs(): string[] {
|
|
|
81
80
|
export let command = positionals[0]
|
|
82
81
|
export let source = positionals[1]
|
|
83
82
|
export let isTsx = source?.endsWith(".tsx") || source?.endsWith(".jsx")
|
|
84
|
-
export let isTs = source?.endsWith(".ts") || source?.endsWith(".js")
|
|
85
|
-
export let isSource = isTsx || isTs
|
|
86
83
|
export let isPrebuilt = source?.endsWith(".srt.js") || source?.endsWith(".srt.bin")
|
|
84
|
+
// A .srt.js also ends with .js: prebuilt wins, or the server would re-bundle
|
|
85
|
+
// a prebuilt bundle as source and skip the prebuilt load path.
|
|
86
|
+
export let isTs = (source?.endsWith(".ts") || source?.endsWith(".js")) && !isPrebuilt
|
|
87
|
+
export let isSource = isTsx || isTs
|
|
87
88
|
|
|
88
89
|
function usage(line: string): never {
|
|
89
90
|
console.error("Usage: " + line)
|
|
@@ -158,9 +159,6 @@ Commands:
|
|
|
158
159
|
pack <file> Bundle + compile to a standalone executable (experimental)
|
|
159
160
|
mcp MCP server (stdio) exposing the running dev server to coding agents
|
|
160
161
|
|
|
161
|
-
init options:
|
|
162
|
-
--with <pkg,pkg> Extensions to include, e.g. @solidrt/components,@solidrt/3d (skips the picker)
|
|
163
|
-
|
|
164
162
|
run/server options:
|
|
165
163
|
-s, --session <N> Session number: dev server on port 34884+N, client slot N (default: 0)
|
|
166
164
|
--port <N> Dev server port (default: 34884 + session)
|
|
@@ -191,7 +189,7 @@ bundle options:
|
|
|
191
189
|
-d, --dev Use development build of SolidJS (default: production)
|
|
192
190
|
-m, --minify Minify the output
|
|
193
191
|
--compile Compile to bytecode
|
|
194
|
-
-o, --output <
|
|
192
|
+
-o, --output <dir> Output directory (default: <project>/dist/bundle)
|
|
195
193
|
--stdout Write bundle to stdout
|
|
196
194
|
|
|
197
195
|
pack options:
|
package/src/bundler.ts
CHANGED
|
@@ -4,11 +4,11 @@ import ts from "@babel/preset-typescript"
|
|
|
4
4
|
import remapping from "@jridgewell/remapping"
|
|
5
5
|
import solid from "babel-preset-solid"
|
|
6
6
|
import { type BunPlugin, type BuildArtifact } from "bun"
|
|
7
|
-
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { dirname, join, relative, resolve as resolvePath, sep } from "node:path"
|
|
9
9
|
import { values, source } from "./args"
|
|
10
10
|
import { state, print, requireBinary } from "./util"
|
|
11
|
-
import { buildManifest, manifestAssetFor } from "./project"
|
|
11
|
+
import { buildManifest, manifestAssetFor, type ManifestAsset } from "./project"
|
|
12
12
|
|
|
13
13
|
// Babel plugin: rewrite `import data from "./x" with { type: "binary" }` into an
|
|
14
14
|
// inline Uint8Array of the file's bytes, and `with { type: "text" }` into an
|
|
@@ -129,26 +129,39 @@ export function hasIsolateDirective(code: string): boolean {
|
|
|
129
129
|
|
|
130
130
|
let SKIP_DIRS = new Set(["node_modules", "dist"])
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Depth-first files under `root`, visited as (absolute path, forward-slash
|
|
134
|
+
* path relative to root). Dotfiles and `skipDirs` are skipped; a missing
|
|
135
|
+
* root visits nothing.
|
|
136
|
+
*/
|
|
137
|
+
export function walkFiles(root: string, visit: (abs: string, rel: string) => void, skipDirs?: Set<string>) {
|
|
138
|
+
if (!existsSync(root)) return
|
|
137
139
|
let walk = (dir: string) => {
|
|
138
140
|
for (let entry of readdirSync(dir, { withFileTypes: true })) {
|
|
139
|
-
if (entry.name.startsWith(".") ||
|
|
141
|
+
if (entry.name.startsWith(".") || skipDirs?.has(entry.name)) continue
|
|
140
142
|
let abs = join(dir, entry.name)
|
|
141
|
-
if (entry.isDirectory())
|
|
142
|
-
|
|
143
|
-
} else if (entry.isFile() && /\.(js|ts)x?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
144
|
-
if (hasIsolateDirective(readFileSync(abs, "utf8"))) {
|
|
145
|
-
let id = relative(root, abs).split(sep).join("/").replace(/\.(js|ts)x?$/, "")
|
|
146
|
-
out.push({ id, path: abs })
|
|
147
|
-
}
|
|
148
|
-
}
|
|
143
|
+
if (entry.isDirectory()) walk(abs)
|
|
144
|
+
else if (entry.isFile()) visit(abs, relative(root, abs).split(sep).join("/"))
|
|
149
145
|
}
|
|
150
146
|
}
|
|
151
147
|
walk(root)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type IsolateModule = { id: string; path: string }
|
|
151
|
+
|
|
152
|
+
/** Every "use isolate" module under `root`, in id order. */
|
|
153
|
+
export function findIsolateModules(root: string): IsolateModule[] {
|
|
154
|
+
let out: IsolateModule[] = []
|
|
155
|
+
walkFiles(
|
|
156
|
+
root,
|
|
157
|
+
(abs, rel) => {
|
|
158
|
+
if (!/\.(js|ts)x?$/.test(rel) || rel.endsWith(".d.ts")) return
|
|
159
|
+
if (hasIsolateDirective(readFileSync(abs, "utf8"))) {
|
|
160
|
+
out.push({ id: rel.replace(/\.(js|ts)x?$/, ""), path: abs })
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
SKIP_DIRS,
|
|
164
|
+
)
|
|
152
165
|
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
153
166
|
return out
|
|
154
167
|
}
|
|
@@ -166,8 +179,20 @@ export type BundleResult = {
|
|
|
166
179
|
map: string | null
|
|
167
180
|
/** Version manifest JSON for this bundle; clients install pushes under its hash. */
|
|
168
181
|
manifest: string
|
|
169
|
-
/** The app's isolate bundles, one per "use isolate" module, in id order. */
|
|
170
|
-
isolates: { id: string; code: string }[]
|
|
182
|
+
/** The app's isolate bundles, one per "use isolate" module, in id order; maps dev builds only. */
|
|
183
|
+
isolates: { id: string; code: string; map: string | null }[]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The bundle's sourcemaps keyed by the module name stack frames cite ("main"
|
|
188
|
+
* for the app, the isolate id for each isolate), for the server's log remap.
|
|
189
|
+
* Null when the build carries no maps (production builds).
|
|
190
|
+
*/
|
|
191
|
+
export function bundleMaps(result: BundleResult): Record<string, string> | null {
|
|
192
|
+
let maps: Record<string, string> = {}
|
|
193
|
+
if (result.map) maps.main = result.map
|
|
194
|
+
for (let i of result.isolates) if (i.map) maps[i.id] = i.map
|
|
195
|
+
return Object.keys(maps).length ? maps : null
|
|
171
196
|
}
|
|
172
197
|
|
|
173
198
|
// The pure bundle: every input is explicit, so it runs identically in the srt
|
|
@@ -189,8 +214,8 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
189
214
|
|
|
190
215
|
// One Bun.build per entry: the app, then each isolate module as its own
|
|
191
216
|
// self-contained bundle (splitting is off, so a helper both import gets
|
|
192
|
-
// duplicated rather than shared).
|
|
193
|
-
// sourcemap
|
|
217
|
+
// duplicated rather than shared). In dev every build gets a composed
|
|
218
|
+
// sourcemap, keyed downstream by its module name.
|
|
194
219
|
let build = async (entry: string, babelMaps?: Map<string, object>, isolateEntry?: string) => {
|
|
195
220
|
let result = null
|
|
196
221
|
try {
|
|
@@ -221,22 +246,31 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
221
246
|
if (!main) return null
|
|
222
247
|
let code = await codeFromOutputs(main.outputs)
|
|
223
248
|
|
|
224
|
-
let isolates: { id: string; code: string }[] = []
|
|
249
|
+
let isolates: { id: string; code: string; map: string | null }[] = []
|
|
225
250
|
for (let module of findIsolateModules(dirname(resolvePath(opts.entry)))) {
|
|
226
|
-
let
|
|
251
|
+
let moduleMaps = opts.dev ? new Map<string, object>() : undefined
|
|
252
|
+
let result = await build(module.path, moduleMaps, module.path)
|
|
227
253
|
if (!result) return null
|
|
228
|
-
isolates.push({
|
|
254
|
+
isolates.push({
|
|
255
|
+
id: module.id,
|
|
256
|
+
code: await codeFromOutputs(result.outputs),
|
|
257
|
+
map: await composeMap(result.outputs, moduleMaps),
|
|
258
|
+
})
|
|
229
259
|
}
|
|
230
260
|
|
|
231
|
-
let extra = isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
|
|
232
261
|
return {
|
|
233
262
|
code,
|
|
234
263
|
map: await composeMap(main.outputs, babelMaps),
|
|
235
|
-
manifest: buildManifest(code, opts.entry,
|
|
264
|
+
manifest: buildManifest(code, opts.entry, isolateManifestAssets(isolates)),
|
|
236
265
|
isolates,
|
|
237
266
|
}
|
|
238
267
|
}
|
|
239
268
|
|
|
269
|
+
/** The manifest assets for a set of isolate bundles (dev form: isolates/<id>.js). */
|
|
270
|
+
export function isolateManifestAssets(isolates: { id: string; code: string }[]): ManifestAsset[] {
|
|
271
|
+
return isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
|
|
272
|
+
}
|
|
273
|
+
|
|
240
274
|
// Write dev isolate bundles where the dev server serves /isolates/ from
|
|
241
275
|
// (<project>/.srt-data/isolates/<id>.js), so clients can fetch the manifest
|
|
242
276
|
// assets the bundle lists. Stale files from removed modules stay behind
|
|
@@ -287,14 +321,52 @@ export function devIsolatesDir(projectDir: string): string {
|
|
|
287
321
|
return join(projectDir, ".srt-data", "isolates")
|
|
288
322
|
}
|
|
289
323
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
324
|
+
// A flux entry's isolate modules: everything under its isolates/ dir, id =
|
|
325
|
+
// the path relative to that dir without extension. Standalone flux resolves
|
|
326
|
+
// isolates by location, not directive - module <id> is
|
|
327
|
+
// <entry dir>/isolates/<id>.bin or .js - so this is the discovery for
|
|
328
|
+
// bundling and packing flux scripts (which also lets a worker be .ts, unlike
|
|
329
|
+
// running from source).
|
|
330
|
+
export function findFluxIsolates(entryDir: string): IsolateModule[] {
|
|
331
|
+
let out: IsolateModule[] = []
|
|
332
|
+
walkFiles(join(entryDir, "isolates"), (abs, rel) => {
|
|
333
|
+
if (/\.[jt]s$/.test(rel) && !rel.endsWith(".d.ts")) {
|
|
334
|
+
out.push({ id: rel.replace(/\.[jt]s$/, ""), path: abs })
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
338
|
+
return out
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// A bundle cannot carry its isolate bundles inside itself, so they travel in
|
|
342
|
+
// the isolates/ dir next to it: `<dir>/isolates/<id>.js` (or `.bin`,
|
|
343
|
+
// compiled) beside the bundle file - the shape the flux runtime and an
|
|
344
|
+
// installed version dir resolve. Writes are confined to bundle-owned output
|
|
345
|
+
// dirs (the ensureOutDir rule in the bundle command); loads read the dir
|
|
346
|
+
// from wherever the bundle sits.
|
|
347
|
+
export function bundleIsolatesDir(bundlePath: string): string {
|
|
348
|
+
return join(dirname(bundlePath), "isolates")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** A prebuilt bundle's isolate bundles, read back from its sibling dir; no dir means none. */
|
|
352
|
+
export function readPrebuiltIsolates(bundlePath: string): { id: string; code: string }[] {
|
|
353
|
+
let out: { id: string; code: string }[] = []
|
|
354
|
+
walkFiles(bundleIsolatesDir(resolvePath(bundlePath)), (abs, rel) => {
|
|
355
|
+
if (rel.endsWith(".js")) {
|
|
356
|
+
out.push({ id: rel.replace(/\.js$/, ""), code: readFileSync(abs, "utf8") })
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
360
|
+
return out
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Loading a prebuilt .srt.js re-publishes its sibling isolate bundles through
|
|
364
|
+
// the dev flow - written where the server's /isolates/ route serves from,
|
|
365
|
+
// listed in the manifest - so isolate() works as it does from a source build.
|
|
366
|
+
export function prebuiltManifest(code: string, path: string, projectDir: string): string {
|
|
367
|
+
let isolates = readPrebuiltIsolates(path)
|
|
368
|
+
writeIsolates(devIsolatesDir(projectDir), isolates)
|
|
369
|
+
return buildManifest(code, path, isolateManifestAssets(isolates))
|
|
298
370
|
}
|
|
299
371
|
|
|
300
372
|
// Bundle for the bare Flux runtime: no Solid plugin, flux: modules stay external.
|
|
@@ -314,7 +386,8 @@ export async function bundleFlux(entry: string): Promise<string> {
|
|
|
314
386
|
return codeFromOutputs(result.outputs)
|
|
315
387
|
}
|
|
316
388
|
|
|
317
|
-
// Bundle for the SolidRT runtime via the standard Solid-aware bundler
|
|
389
|
+
// Bundle for the SolidRT runtime via the standard Solid-aware bundler, or
|
|
390
|
+
// exit the command on a failed build.
|
|
318
391
|
export async function bundleSolid(): Promise<BundleResult> {
|
|
319
392
|
let result = await bundle()
|
|
320
393
|
if (!result) {
|
|
@@ -324,10 +397,12 @@ export async function bundleSolid(): Promise<BundleResult> {
|
|
|
324
397
|
return result
|
|
325
398
|
}
|
|
326
399
|
|
|
327
|
-
// Compile JS source to QuickJS bytecode via the fluxc binary.
|
|
328
|
-
|
|
400
|
+
// Compile JS source to QuickJS bytecode via the fluxc binary. `moduleName` is
|
|
401
|
+
// what stack frames cite at runtime: "main" for the entry, the isolate id for
|
|
402
|
+
// an isolate bundle.
|
|
403
|
+
export async function compileToBytecode(jsCode: string, moduleName = "main"): Promise<Buffer> {
|
|
329
404
|
let compiler = requireBinary("fluxc")
|
|
330
|
-
let proc = Bun.spawn([compiler], {
|
|
405
|
+
let proc = Bun.spawn([compiler, moduleName], {
|
|
331
406
|
stdin: new Blob([jsCode]),
|
|
332
407
|
stdout: "pipe",
|
|
333
408
|
stderr: "inherit",
|