@solidrt/cli 0.0.50 → 0.0.52

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.
Files changed (98) hide show
  1. package/AGENTS.md +79 -36
  2. package/README.md +86 -15
  3. package/agents/assets.md +46 -0
  4. package/agents/debugging.md +294 -0
  5. package/dist/console.srtapp +94123 -73
  6. package/dist/server.js +3936 -0
  7. package/package.json +11 -10
  8. package/src/android/docs.md +21 -0
  9. package/src/android/main.ts +286 -0
  10. package/src/{bundler.ts → bundle/bundler.ts} +163 -76
  11. package/src/bundle/docs.md +12 -0
  12. package/src/bundle/main.ts +203 -0
  13. package/src/check/docs.md +10 -0
  14. package/src/check/main.ts +85 -0
  15. package/src/{commands/check.ts → check/typecheck.ts} +11 -34
  16. package/src/client/docs.md +9 -0
  17. package/src/client/main.ts +33 -0
  18. package/src/console/docs.md +14 -0
  19. package/src/console/main.ts +21 -0
  20. package/src/demo/docs.md +27 -0
  21. package/src/demo/main.ts +67 -0
  22. package/src/init/docs.md +11 -0
  23. package/src/{commands/init.ts → init/main.ts} +31 -25
  24. package/src/init/scaffold/AGENTS.md +98 -0
  25. package/src/init/scaffold/package.json +23 -0
  26. package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
  27. package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
  28. package/src/lib/args.ts +194 -0
  29. package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
  30. package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
  31. package/src/{fonts.ts → lib/fonts.ts} +12 -26
  32. package/src/lib/mode.ts +77 -0
  33. package/src/{project.ts → lib/project.ts} +109 -61
  34. package/src/lib/registry.ts +120 -0
  35. package/src/lib/server-bundle.ts +24 -0
  36. package/src/lib/usage.ts +117 -0
  37. package/src/lib/util.ts +36 -0
  38. package/src/main.ts +109 -31
  39. package/src/mcp/docs.md +22 -0
  40. package/src/mcp/main.ts +719 -0
  41. package/src/pack/docs.md +18 -0
  42. package/src/{pack-folder.ts → pack/layout.ts} +13 -22
  43. package/src/pack/main.ts +86 -0
  44. package/src/pack/trailer.ts +97 -0
  45. package/src/render/docs.md +19 -0
  46. package/src/render/main.ts +49 -0
  47. package/src/server/args.ts +126 -0
  48. package/src/server/binaries.ts +47 -0
  49. package/src/server/config.ts +54 -0
  50. package/{server → src/server}/control.ts +246 -78
  51. package/src/server/docs.md +51 -0
  52. package/src/server/line-editor.ts +200 -0
  53. package/src/server/main.ts +473 -0
  54. package/src/server/mode.ts +92 -0
  55. package/src/server/rebuild.ts +90 -0
  56. package/src/server/registry.ts +138 -0
  57. package/src/server/remap.ts +60 -0
  58. package/src/server/repl.ts +223 -0
  59. package/src/server/state.ts +54 -0
  60. package/{server → src/server}/tsconfig.json +1 -1
  61. package/{server → src/server}/tunnel.ts +6 -6
  62. package/src/server/watcher.ts +121 -0
  63. package/src/tool/main.ts +70 -0
  64. package/src/types/bundle.d.ts +24 -0
  65. package/src/types/control.d.ts +90 -0
  66. package/src/types/registry.d.ts +16 -0
  67. package/scaffold/AGENTS.md +0 -630
  68. package/scaffold/package.json +0 -22
  69. package/scaffold/templates/components/icon.tsx +0 -48
  70. package/scaffold/templates/default/icon.tsx +0 -48
  71. package/server/main.ts +0 -308
  72. package/server/rebuild.ts +0 -68
  73. package/server/remap.ts +0 -47
  74. package/server/state.ts +0 -93
  75. package/src/args.ts +0 -212
  76. package/src/bundle-cli.ts +0 -13
  77. package/src/commands/bundle.ts +0 -76
  78. package/src/commands/client.ts +0 -34
  79. package/src/commands/mcp.ts +0 -603
  80. package/src/commands/pack.ts +0 -65
  81. package/src/commands/render.ts +0 -24
  82. package/src/commands/server.ts +0 -73
  83. package/src/dev-android.ts +0 -176
  84. package/src/dev-client.ts +0 -29
  85. package/src/dev-server.ts +0 -302
  86. package/src/packer.ts +0 -103
  87. package/src/repl.ts +0 -233
  88. package/src/util.ts +0 -121
  89. package/src/watcher.ts +0 -69
  90. /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
  91. /package/src/{prompt.ts → init/prompt.ts} +0 -0
  92. /package/{scaffold → src/init/scaffold}/gitignore +0 -0
  93. /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
  94. /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
  95. /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
  96. /package/{server → src/server}/cache.ts +0 -0
  97. /package/{server → src/server}/proxy.ts +0 -0
  98. /package/{server → src/server}/qr.ts +0 -0
@@ -4,13 +4,15 @@ 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
- import { values, source } from "./args"
10
- import { state, print, requireBinary } from "./util"
11
- import { buildManifest, manifestAssetFor } from "./project"
9
+ import { values } from "../lib/args"
10
+ import type { Mode } from "../lib/mode"
11
+ import { requireBinary } from "../lib/util"
12
+ import type { BundleOutput } from "../types/bundle"
13
+ import { buildManifest, manifestAssetFor, type ManifestAsset } from "../lib/project"
12
14
 
13
- // Babel plugin: rewrite `import data from "./x" with { type: "binary" }` into an
15
+ // Babel plugin: rewrite `import data from "../x" with { type: "binary" }` into an
14
16
  // inline Uint8Array of the file's bytes, and `with { type: "text" }` into an
15
17
  // inline string of its UTF-8 contents. The import attribute is invisible to
16
18
  // Bun's bundler and its plugins in this Bun version, so we handle it here in the
@@ -21,8 +23,10 @@ import { buildManifest, manifestAssetFor } from "./project"
21
23
  // attribute-free path later. Both attributes work on any extension, so shader
22
24
  // and other text sources inline by attribute the same way bytes do; `.svg` is
23
25
  // additionally text-loaded without an attribute (see Bun's `loader` below).
24
- function inlineImport({ types: t }: { types: any }) {
25
- return {
26
+ // Inlined files leave no trace in the module graph, so the plugin records
27
+ // each one in `inputs` (see `BundleOutput.inputs`).
28
+ function inlineImport(inputs: Set<string>) {
29
+ return ({ types: t }: { types: any }) => ({
26
30
  visitor: {
27
31
  ImportDeclaration(path: any, pluginState: any) {
28
32
  let attrs = path.node.attributes ?? path.node.assertions
@@ -32,12 +36,13 @@ function inlineImport({ types: t }: { types: any }) {
32
36
  let def = path.node.specifiers.find((s: any) => s.type === "ImportDefaultSpecifier")
33
37
  if (!def) {
34
38
  throw path.buildCodeFrameError(
35
- `A ${kind} import needs a default import: import data from "./file" with { type: "${kind}" }`,
39
+ `A ${kind} import needs a default import: import data from "../file" with { type: "${kind}" }`,
36
40
  )
37
41
  }
38
42
 
39
43
  let importer = pluginState.file.opts.filename as string
40
44
  let abs = resolvePath(dirname(importer), path.node.source.value)
45
+ inputs.add(abs)
41
46
 
42
47
  // text: var <local> = "<contents>"
43
48
  // binary: var <local> = Uint8Array.from(atob("<b64>"), c => c.charCodeAt(0))
@@ -56,7 +61,7 @@ function inlineImport({ types: t }: { types: any }) {
56
61
  path.replaceWith(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(def.local.name), expr)]))
57
62
  },
58
63
  },
59
- }
64
+ })
60
65
  }
61
66
 
62
67
  // Concatenate only the JS outputs (entry point plus any code-split chunks) of a
@@ -84,25 +89,27 @@ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
84
89
  // collected there, keyed by absolute path, for sourcemap composition later.
85
90
  // `isolateEntry` is the one "use isolate" module this build may load (its own
86
91
  // entry); loading any other one means a by-value import of an isolate module,
87
- // which is a build error (see isolate modules below).
88
- function solidPlugin(babelMaps?: Map<string, object>, isolateEntry?: string): BunPlugin {
92
+ // which is a build error (see isolate modules below). Every file the plugin
93
+ // loads or inlines is recorded in `inputs`.
94
+ function solidPlugin(inputs: Set<string>, babelMaps?: Map<string, object>, isolateEntry?: string): BunPlugin {
89
95
  return {
90
96
  name: "bun-plugin-solid",
91
97
  setup: (build) => {
92
98
  build.onLoad({ filter: /\.(js|ts)x?$/ }, async (args) => {
93
99
  if (!/\.(js|ts)x$/.test(args.path) && args.path.includes("node_modules")) return
100
+ inputs.add(args.path)
94
101
  let file = Bun.file(args.path)
95
102
  let code = await file.text()
96
103
  if (args.path !== isolateEntry && hasIsolateDirective(code)) {
97
104
  throw new Error(
98
- `${args.path} is a "use isolate" module: import its types only (import type * as W from "./...") and call it through isolate() from flux:isolate`,
105
+ `${args.path} is a "use isolate" module: import its types only (import type * as W from "../...") and call it through isolate() from flux:isolate`,
99
106
  )
100
107
  }
101
108
  let transforms = await transformAsync(code, {
102
109
  filename: args.path,
103
110
  sourceMaps: !!babelMaps,
104
111
  presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
105
- plugins: [jsx, inlineImport],
112
+ plugins: [jsx, inlineImport(inputs)],
106
113
  })
107
114
  if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
108
115
  return { contents: transforms?.code ?? "", loader: "js" }
@@ -129,26 +136,39 @@ export function hasIsolateDirective(code: string): boolean {
129
136
 
130
137
  let SKIP_DIRS = new Set(["node_modules", "dist"])
131
138
 
132
- export type IsolateModule = { id: string; path: string }
133
-
134
- /** Every "use isolate" module under `root`, in id order. */
135
- export function findIsolateModules(root: string): IsolateModule[] {
136
- let out: IsolateModule[] = []
139
+ /**
140
+ * Depth-first files under `root`, visited as (absolute path, forward-slash
141
+ * path relative to root). Dotfiles and `skipDirs` are skipped; a missing
142
+ * root visits nothing.
143
+ */
144
+ export function walkFiles(root: string, visit: (abs: string, rel: string) => void, skipDirs?: Set<string>) {
145
+ if (!existsSync(root)) return
137
146
  let walk = (dir: string) => {
138
147
  for (let entry of readdirSync(dir, { withFileTypes: true })) {
139
- if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue
148
+ if (entry.name.startsWith(".") || skipDirs?.has(entry.name)) continue
140
149
  let abs = join(dir, entry.name)
141
- if (entry.isDirectory()) {
142
- walk(abs)
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
- }
150
+ if (entry.isDirectory()) walk(abs)
151
+ else if (entry.isFile()) visit(abs, relative(root, abs).split(sep).join("/"))
149
152
  }
150
153
  }
151
154
  walk(root)
155
+ }
156
+
157
+ export type IsolateModule = { id: string; path: string }
158
+
159
+ /** Every "use isolate" module under `root`, in id order. */
160
+ export function findIsolateModules(root: string): IsolateModule[] {
161
+ let out: IsolateModule[] = []
162
+ walkFiles(
163
+ root,
164
+ (abs, rel) => {
165
+ if (!/\.(js|ts)x?$/.test(rel) || rel.endsWith(".d.ts")) return
166
+ if (hasIsolateDirective(readFileSync(abs, "utf8"))) {
167
+ out.push({ id: rel.replace(/\.(js|ts)x?$/, ""), path: abs })
168
+ }
169
+ },
170
+ SKIP_DIRS,
171
+ )
152
172
  out.sort((a, b) => (a.id < b.id ? -1 : 1))
153
173
  return out
154
174
  }
@@ -158,20 +178,28 @@ export function isolateAssetPath(id: string, ext: "js" | "bin"): string {
158
178
  return `isolates/${id}.${ext}`
159
179
  }
160
180
 
161
- export type BundleOptions = { entry: string; devBase?: string; dev: boolean; minify: boolean }
181
+ // `project` is the project root (mode.ts decides it, never a search), or
182
+ // null for a file on its own: no assets in the manifest and no isolate
183
+ // modules (isolates are a project feature).
184
+ export type BundleOptions = { entry: string; dev: boolean; minify: boolean; project: string | null }
185
+
186
+ // The `srt bundle --json` stdout contract doubles as the in-process result.
187
+ export type BundleResult = BundleOutput
162
188
 
163
- export type BundleResult = {
164
- code: string
165
- /** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
166
- map: string | null
167
- /** Version manifest JSON for this bundle; clients install pushes under its hash. */
168
- manifest: string
169
- /** The app's isolate bundles, one per "use isolate" module, in id order. */
170
- isolates: { id: string; code: string }[]
189
+ /**
190
+ * The bundle's sourcemaps keyed by the module name stack frames cite ("main"
191
+ * for the app, the isolate id for each isolate), for the server's log remap.
192
+ * Null when the build carries no maps (production builds).
193
+ */
194
+ export function bundleMaps(result: BundleResult): Record<string, string> | null {
195
+ let maps: Record<string, string> = {}
196
+ if (result.map) maps.main = result.map
197
+ for (let i of result.isolates) if (i.map) maps[i.id] = i.map
198
+ return Object.keys(maps).length ? maps : null
171
199
  }
172
200
 
173
201
  // The pure bundle: every input is explicit, so it runs identically in the srt
174
- // (Bun) process and in the standalone bundle-cli subprocess the dev server
202
+ // (Bun) process and in the `srt bundle --json` subprocess the dev server
175
203
  // spawns. It never touches the ambient args/state singletons and never prints
176
204
  // progress (callers own that), so its stdout stays clean for subprocess use.
177
205
  export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
@@ -185,12 +213,15 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
185
213
  "import.meta.env.DEV": opts.dev ? "true" : "false",
186
214
  "process.env.NODE_ENV": opts.dev ? '"development"' : '"production"',
187
215
  }
188
- if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
189
216
 
190
217
  // One Bun.build per entry: the app, then each isolate module as its own
191
218
  // self-contained bundle (splitting is off, so a helper both import gets
192
- // duplicated rather than shared). Only the app's build gets a composed
193
- // sourcemap for now.
219
+ // duplicated rather than shared). In dev every build gets a composed
220
+ // sourcemap, keyed downstream by its module name.
221
+ // Every file the bundle depends on (BundleOutput.inputs): what the plugin
222
+ // loaded or inlined, plus every module in the build's sourcemap, which is
223
+ // where dependency code (node_modules, workspace packages) shows up.
224
+ let inputs = new Set<string>()
194
225
  let build = async (entry: string, babelMaps?: Map<string, object>, isolateEntry?: string) => {
195
226
  let result = null
196
227
  try {
@@ -203,7 +234,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
203
234
  define,
204
235
  loader: { ".svg": "text" },
205
236
  sourcemap: babelMaps ? "external" : "none",
206
- plugins: [solidPlugin(babelMaps, isolateEntry)],
237
+ plugins: [solidPlugin(inputs, babelMaps, isolateEntry)],
207
238
  })
208
239
  } catch (e) {
209
240
  console.error("[cli] compile error:\n", e)
@@ -213,6 +244,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
213
244
  for (let msg of result.logs) console.error(msg)
214
245
  return null
215
246
  }
247
+ for (let source of await mapSources(result.outputs)) inputs.add(source)
216
248
  return result
217
249
  }
218
250
 
@@ -221,26 +253,56 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
221
253
  if (!main) return null
222
254
  let code = await codeFromOutputs(main.outputs)
223
255
 
224
- let isolates: { id: string; code: string }[] = []
225
- for (let module of findIsolateModules(dirname(resolvePath(opts.entry)))) {
226
- let result = await build(module.path, undefined, module.path)
256
+ let isolates: { id: string; code: string; map: string | null }[] = []
257
+ let modules = opts.project === null ? [] : findIsolateModules(dirname(resolvePath(opts.entry)))
258
+ for (let module of modules) {
259
+ let moduleMaps = opts.dev ? new Map<string, object>() : undefined
260
+ let result = await build(module.path, moduleMaps, module.path)
227
261
  if (!result) return null
228
- isolates.push({ id: module.id, code: await codeFromOutputs(result.outputs) })
262
+ isolates.push({
263
+ id: module.id,
264
+ code: await codeFromOutputs(result.outputs),
265
+ map: await composeMap(result.outputs, moduleMaps),
266
+ })
267
+ }
268
+
269
+ // The project files the manifest and the build read: package.json (app
270
+ // identity, icon, fonts) and tsconfig.json (resolution, JSX).
271
+ if (opts.project !== null) {
272
+ for (let name of ["package.json", "tsconfig.json"]) {
273
+ let abs = join(opts.project, name)
274
+ if (existsSync(abs)) inputs.add(abs)
275
+ }
229
276
  }
230
277
 
231
- let extra = isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
232
278
  return {
233
279
  code,
234
280
  map: await composeMap(main.outputs, babelMaps),
235
- manifest: buildManifest(code, opts.entry, extra),
281
+ manifest: buildManifest(code, opts.entry, isolateManifestAssets(isolates), opts.project),
236
282
  isolates,
283
+ inputs: [...inputs].sort(),
237
284
  }
238
285
  }
239
286
 
240
- // Write dev isolate bundles where the dev server serves /isolates/ from
241
- // (<project>/.srt-data/isolates/<id>.js), so clients can fetch the manifest
242
- // assets the bundle lists. Stale files from removed modules stay behind
243
- // unlisted, which is harmless.
287
+ // The source files a build's sourcemap cites, as absolute paths (Bun writes
288
+ // them cwd-relative). Empty for a build without a map.
289
+ async function mapSources(outputs: BuildArtifact[]): Promise<string[]> {
290
+ let out: string[] = []
291
+ for (let o of outputs) {
292
+ if ((o.kind !== "entry-point" && o.kind !== "chunk") || !o.sourcemap) continue
293
+ let map = JSON.parse(await o.sourcemap.text())
294
+ for (let source of map.sources ?? []) out.push(resolvePath(source))
295
+ }
296
+ return out
297
+ }
298
+
299
+ /** The manifest assets for a set of isolate bundles (dev form: isolates/<id>.js). */
300
+ export function isolateManifestAssets(isolates: { id: string; code: string }[]): ManifestAsset[] {
301
+ return isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
302
+ }
303
+
304
+ // Write isolate bundles as `<dir>/<id>.js`, the shape the manifest lists and
305
+ // a prebuilt bundle's sibling isolates/ dir carries.
244
306
  export function writeIsolates(dir: string, isolates: { id: string; code: string }[]) {
245
307
  for (let i of isolates) {
246
308
  let file = join(dir, `${i.id}.js`)
@@ -271,30 +333,52 @@ async function composeMap(outputs: BuildArtifact[], babelMaps?: Map<string, obje
271
333
  return composed.toString()
272
334
  }
273
335
 
274
- export async function bundle(entry = source) {
275
- let devBase = state.serverUrl ?? undefined
276
- let dev = !!devBase || values.dev
336
+ // The one-shot commands' bundle (bundle, pack, render): production unless
337
+ // --dev, on what the cwd and the argument decide (mode.ts).
338
+ export async function bundle(mode: Mode) {
339
+ let dev = values.dev
277
340
  // Keep stdout clean when the bundle itself is written to stdout.
278
- if (!values.stdout) print(`[cli] Bundling (${dev ? "development" : "production"})`)
279
- let result = await bundleWith({ entry: entry!, devBase, dev, minify: values.minify })
280
- // With a server running, its /isolates/ route serves what we write here.
281
- if (result && devBase) writeIsolates(devIsolatesDir(state.projectDir), result.isolates)
282
- return result
341
+ if (!values.stdout) console.log(`[cli] Bundling (${dev ? "development" : "production"})`)
342
+ return bundleWith({ entry: mode.entry, dev, minify: values.minify, project: mode.projectDir })
283
343
  }
284
344
 
285
- /** Where a project's dev isolate bundles are written and served from. */
286
- export function devIsolatesDir(projectDir: string): string {
287
- return join(projectDir, ".srt-data", "isolates")
345
+ // A flux entry's isolate modules: everything under its isolates/ dir, id =
346
+ // the path relative to that dir without extension. Standalone flux resolves
347
+ // isolates by location, not directive - module <id> is
348
+ // <entry dir>/isolates/<id>.bin or .js - so this is the discovery for
349
+ // bundling and packing flux scripts (which also lets a worker be .ts, unlike
350
+ // running from source).
351
+ export function findFluxIsolates(entryDir: string): IsolateModule[] {
352
+ let out: IsolateModule[] = []
353
+ walkFiles(join(entryDir, "isolates"), (abs, rel) => {
354
+ if (/\.[jt]s$/.test(rel) && !rel.endsWith(".d.ts")) {
355
+ out.push({ id: rel.replace(/\.[jt]s$/, ""), path: abs })
356
+ }
357
+ })
358
+ out.sort((a, b) => (a.id < b.id ? -1 : 1))
359
+ return out
288
360
  }
289
361
 
290
- export async function bundleTo(outfile: string) {
291
- let result = await bundle()
292
- if (!result) {
293
- console.error("Build failed")
294
- process.exit(1)
295
- }
296
- await Bun.write(outfile, result.code)
297
- return result
362
+ // A bundle cannot carry its isolate bundles inside itself, so they travel in
363
+ // the isolates/ dir next to it: `<dir>/isolates/<id>.js` (or `.bin`,
364
+ // compiled) beside the bundle file - the shape the flux runtime and an
365
+ // installed version dir resolve. Writes are confined to bundle-owned output
366
+ // dirs (the ensureOutDir rule in the bundle command); loads read the dir
367
+ // from wherever the bundle sits.
368
+ export function bundleIsolatesDir(bundlePath: string): string {
369
+ return join(dirname(bundlePath), "isolates")
370
+ }
371
+
372
+ /** A prebuilt bundle's isolate bundles, read back from its sibling dir; no dir means none. */
373
+ export function readPrebuiltIsolates(bundlePath: string): { id: string; code: string }[] {
374
+ let out: { id: string; code: string }[] = []
375
+ walkFiles(bundleIsolatesDir(resolvePath(bundlePath)), (abs, rel) => {
376
+ if (rel.endsWith(".js")) {
377
+ out.push({ id: rel.replace(/\.js$/, ""), code: readFileSync(abs, "utf8") })
378
+ }
379
+ })
380
+ out.sort((a, b) => (a.id < b.id ? -1 : 1))
381
+ return out
298
382
  }
299
383
 
300
384
  // Bundle for the bare Flux runtime: no Solid plugin, flux: modules stay external.
@@ -314,9 +398,10 @@ export async function bundleFlux(entry: string): Promise<string> {
314
398
  return codeFromOutputs(result.outputs)
315
399
  }
316
400
 
317
- // Bundle for the SolidRT runtime via the standard Solid-aware bundler.
318
- export async function bundleSolid(): Promise<BundleResult> {
319
- let result = await bundle()
401
+ // Bundle for the SolidRT runtime via the standard Solid-aware bundler, or
402
+ // exit the command on a failed build.
403
+ export async function bundleSolid(mode: Mode): Promise<BundleResult> {
404
+ let result = await bundle(mode)
320
405
  if (!result) {
321
406
  console.error("Build failed")
322
407
  process.exit(1)
@@ -324,10 +409,12 @@ export async function bundleSolid(): Promise<BundleResult> {
324
409
  return result
325
410
  }
326
411
 
327
- // Compile JS source to QuickJS bytecode via the fluxc binary.
328
- export async function compileToBytecode(jsCode: string): Promise<Buffer> {
412
+ // Compile JS source to QuickJS bytecode via the fluxc binary. `moduleName` is
413
+ // what stack frames cite at runtime: "main" for the entry, the isolate id for
414
+ // an isolate bundle.
415
+ export async function compileToBytecode(jsCode: string, moduleName = "main"): Promise<Buffer> {
329
416
  let compiler = requireBinary("fluxc")
330
- let proc = Bun.spawn([compiler], {
417
+ let proc = Bun.spawn([compiler, moduleName], {
331
418
  stdin: new Blob([jsCode]),
332
419
  stdout: "pipe",
333
420
  stderr: "inherit",
@@ -0,0 +1,12 @@
1
+ # srt bundle
2
+
3
+ {{ usage bundle }}
4
+
5
+ Writes `dist/bundle/` (or `--output <dir>`): `<name>.srt.js` plus the app's
6
+ isolate modules as `isolates/<id>.js`, or bytecode with `--compile`. Move
7
+ the dir, not the bare file: a bundle loaded without its isolates/ dir loses
8
+ them.
9
+
10
+ `--flux` targets the bare [Flux runtime](/runtime/) instead of a SolidRT
11
+ app, for scripts and servers with no UI. `--json` is the dev server's
12
+ rebuild contract, one JSON object on stdout.
@@ -0,0 +1,203 @@
1
+ import { values, source, isPrebuilt } from "../lib/args"
2
+ import {
3
+ bundleFlux,
4
+ bundleSolid,
5
+ bundleWith,
6
+ compileToBytecode,
7
+ findFluxIsolates,
8
+ isolateManifestAssets,
9
+ readPrebuiltIsolates,
10
+ walkFiles,
11
+ writeIsolates,
12
+ } from "./bundler"
13
+ import { resolveMode } from "../lib/mode"
14
+ import { buildManifest } from "../lib/project"
15
+ import type { BundleOutput } from "../types/bundle"
16
+ import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"
17
+ import { basename, dirname, join, resolve } from "node:path"
18
+
19
+ // Write to stdout and resolve only once the whole payload is flushed.
20
+ // process.stdout.write to a pipe is async and applies backpressure; the
21
+ // callback fires after every byte is drained, so it is safe to exit after.
22
+ function writeStdout(data: string): Promise<void> {
23
+ return new Promise((resolve, reject) => {
24
+ process.stdout.write(data, (err) => (err ? reject(err) : resolve()))
25
+ })
26
+ }
27
+
28
+ // The bundle output dir (okf/backlog/build-output-dirs.md): the bundle flow's
29
+ // subdir of the build root, or an explicit --output dir. The build root is
30
+ // the cwd: a project command runs in its root (mode.ts), and a file on its
31
+ // own builds where it is run from. Only reused when it is empty or already a
32
+ // bundle output (a *.srt.* or *.flux.* bundle at top level) - the
33
+ // writePackFolder rule - so it never writes into an unrelated directory.
34
+ function ensureOutDir(defaultDir = join("dist", "bundle")): string {
35
+ let outDir = values.output ?? defaultDir
36
+ let existing = existsSync(outDir) ? readdirSync(outDir) : null
37
+ if (existing && existing.length > 0 && !existing.some((name) => /\.(srt|flux)\.(js|bin)$/.test(name))) {
38
+ console.error(`${resolve(outDir)} exists and is not a bundle output; choose another --output`)
39
+ process.exit(1)
40
+ }
41
+ mkdirSync(outDir, { recursive: true })
42
+ return outDir
43
+ }
44
+
45
+ // Clear one form's files from the output's isolates/ dir before rewriting it,
46
+ // so removed modules cannot go stale. The dir is shared by a bundle's .js and
47
+ // .bin forms, so only the form being rewritten is cleared - a --compile must
48
+ // not delete the .js set the .js bundle pairs with, nor the reverse.
49
+ function clearIsolates(dir: string, ext: ".js" | ".bin") {
50
+ walkFiles(dir, (abs) => {
51
+ if (abs.endsWith(ext)) rmSync(abs)
52
+ })
53
+ }
54
+
55
+ // Compile one isolate bundle to `<dir>/<id>.bin` (module name = its id, for
56
+ // stack attribution).
57
+ async function writeIsolateBytecode(dir: string, isolate: { id: string; code: string }) {
58
+ let outfile = join(dir, isolate.id + ".bin")
59
+ mkdirSync(dirname(outfile), { recursive: true })
60
+ await Bun.write(outfile, await compileToBytecode(isolate.code, isolate.id))
61
+ }
62
+
63
+ // Compile JS to a bytecode file and report its size.
64
+ async function writeBytecode(jsCode: string, outfile: string) {
65
+ let bytecode = await compileToBytecode(jsCode)
66
+ await Bun.write(outfile, bytecode)
67
+ let binSize = (await Bun.file(outfile).stat()).size
68
+ console.log(`>> wrote ${binSize} bytes to ${outfile}`)
69
+ }
70
+
71
+ export async function main() {
72
+ if (values.flux) {
73
+ let entry = resolve(source!)
74
+ let name = basename(entry).replace(/\.[jt]s$/, "")
75
+ let jsCode = await bundleFlux(entry)
76
+ // Standalone flux resolves isolates by location, not directive: module
77
+ // <id> is <entry dir>/isolates/<id>.js. Bundling keeps that shape - every
78
+ // module under the entry's isolates/ dir is built bare like the entry
79
+ // (which also lets a worker be .ts, unlike running from source) and lands
80
+ // as isolates/<id>.js next to the bundle.
81
+ let isolateModules = findFluxIsolates(dirname(entry))
82
+
83
+ if (values.stdout) {
84
+ if (isolateModules.length) {
85
+ console.error("[cli] Warning: this script has isolate modules; --stdout carries only the main bundle")
86
+ }
87
+ await writeStdout(jsCode)
88
+ process.exit()
89
+ }
90
+ let outDir = ensureOutDir()
91
+ if (values.compile) {
92
+ await writeBytecode(jsCode, join(outDir, name + ".flux.bin"))
93
+ } else {
94
+ let outfile = join(outDir, name + ".flux.js")
95
+ await Bun.write(outfile, jsCode)
96
+ console.log(`>> wrote ${jsCode.length} bytes to ${outfile}`)
97
+ }
98
+ // Isolates follow the main bundle's form: source beside a .flux.js,
99
+ // bytecode beside a .flux.bin (the flux resolver reads .bin first).
100
+ let isolatesDir = join(outDir, "isolates")
101
+ if (values.compile) {
102
+ clearIsolates(isolatesDir, ".bin")
103
+ for (let module of isolateModules) {
104
+ await writeIsolateBytecode(isolatesDir, { id: module.id, code: await bundleFlux(module.path) })
105
+ }
106
+ } else {
107
+ clearIsolates(isolatesDir, ".js")
108
+ for (let module of isolateModules) {
109
+ let file = join(isolatesDir, module.id + ".js")
110
+ mkdirSync(dirname(file), { recursive: true })
111
+ await Bun.write(file, await bundleFlux(module.path))
112
+ }
113
+ }
114
+ process.exit()
115
+ }
116
+
117
+ // --json: the dev server's rebuild (server/rebuild.ts spawns it in the
118
+ // project root or the entry's directory with the entry and --project or
119
+ // --file, so the mode resolves the same). One BundleOutput object on
120
+ // stdout (types/bundle.d.ts), diagnostics on stderr, exit 1 with an empty
121
+ // stdout on a build failure. A prebuilt .srt.js is read as-is with its
122
+ // sibling isolate bundles.
123
+ if (values.json) {
124
+ let mode = resolveMode()
125
+ let result: BundleOutput | null
126
+ if (mode.entry.endsWith(".srt.js")) {
127
+ let code = await Bun.file(mode.entry).text()
128
+ let isolates = readPrebuiltIsolates(mode.entry).map((i) => ({ ...i, map: null }))
129
+ result = {
130
+ code,
131
+ map: null,
132
+ manifest: buildManifest(code, mode.entry, isolateManifestAssets(isolates), mode.projectDir),
133
+ isolates,
134
+ inputs: [mode.entry],
135
+ }
136
+ } else {
137
+ result = await bundleWith({
138
+ entry: mode.entry,
139
+ project: mode.projectDir,
140
+ dev: values.dev,
141
+ minify: values.minify,
142
+ })
143
+ }
144
+ if (!result) process.exit(1)
145
+ await writeStdout(JSON.stringify(result))
146
+ process.exit()
147
+ }
148
+
149
+ // A prebuilt .srt.js (validateArgs admits no other prebuilt form) is
150
+ // compiled to bytecode: the only step left, so --compile is implied. The
151
+ // output lands in the bundle's own dir unless --output says otherwise; its
152
+ // isolate bundles compile along into the output's isolates/ (the ids match,
153
+ // the extension picks the form).
154
+ if (isPrebuilt) {
155
+ let jsFile = resolve(source!)
156
+ if (!existsSync(jsFile)) {
157
+ console.error(`Entry not found: ${source}`)
158
+ process.exit(1)
159
+ }
160
+ let outDir = ensureOutDir(dirname(jsFile))
161
+ await writeBytecode(await Bun.file(jsFile).text(), join(outDir, basename(jsFile).replace(/\.js$/, ".bin")))
162
+ let isolatesDir = join(outDir, "isolates")
163
+ clearIsolates(isolatesDir, ".bin")
164
+ for (let isolate of readPrebuiltIsolates(jsFile)) {
165
+ await writeIsolateBytecode(isolatesDir, isolate)
166
+ }
167
+ process.exit()
168
+ }
169
+
170
+ let mode = resolveMode()
171
+ let entry = mode.entry
172
+ let name = basename(entry).replace(/\.[jt]sx?$/, "")
173
+
174
+ if (values.stdout) {
175
+ let result = await bundleSolid(mode)
176
+ if (result.isolates.length) {
177
+ console.error("[cli] Warning: this app has isolate modules; --stdout carries only the main bundle")
178
+ }
179
+ await writeStdout(result.code)
180
+ process.exit()
181
+ }
182
+
183
+ let outDir = ensureOutDir()
184
+ let isolatesDir = join(outDir, "isolates")
185
+
186
+ if (values.compile) {
187
+ let result = await bundleSolid(mode)
188
+ await writeBytecode(result.code, join(outDir, name + ".srt.bin"))
189
+ clearIsolates(isolatesDir, ".bin")
190
+ for (let isolate of result.isolates) {
191
+ await writeIsolateBytecode(isolatesDir, isolate)
192
+ }
193
+ process.exit()
194
+ }
195
+
196
+ let result = await bundleSolid(mode)
197
+ let jsOutfile = join(outDir, name + ".srt.js")
198
+ await Bun.write(jsOutfile, result.code)
199
+ clearIsolates(isolatesDir, ".js")
200
+ writeIsolates(isolatesDir, result.isolates)
201
+ console.log(`>> wrote ${result.code.length} bytes to ${jsOutfile}`)
202
+ process.exit()
203
+ }
@@ -0,0 +1,10 @@
1
+ # srt check
2
+
3
+ {{ usage check }}
4
+
5
+ The gate worth wiring into CI: it builds and typechecks the app without
6
+ producing build output. With a folder it covers every entry under it
7
+ (`src/index.tsx`, `examples/*`, and in a monorepo every example app,
8
+ package example and package demo), so one call at the repo root answers
9
+ "did I break any example" before pushing. The dev server runs the same check once at
10
+ startup, without gating on it.