@seanhogg/builderforce-webcontainers 2026.10.4 → 2026.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-webcontainers",
3
- "version": "2026.10.4",
3
+ "version": "2026.10.6",
4
4
  "description": "BuilderForce WebContainers — browser runtime. Boots an in-browser dev server for a project held in memory and serves it to an iframe through a service worker. Open source, no COOP/COEP, no session caps.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "esbuild-wasm": "^0.24.0",
68
- "@seanhogg/builderforce-webcontainers-core": "^2026.10.4"
68
+ "@seanhogg/builderforce-webcontainers-core": "^2026.10.6"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@vue/compiler-sfc": "^3.5.0",
@@ -48,7 +48,7 @@ export function checkInputs(files: VirtualFileSystem | FlatFiles): Record<string
48
48
  if (files instanceof VirtualFileSystem) fs = files;
49
49
  else {
50
50
  fs = new VirtualFileSystem();
51
- fs.mount(files);
51
+ fs.writeTree(files);
52
52
  }
53
53
  const out: Record<string, string> = {};
54
54
  for (const path of fs.list()) if (isCheckInput(path)) out[path.slice(1)] = fs.readText(path) ?? '';
package/src/host.ts CHANGED
@@ -6,13 +6,14 @@ import {
6
6
  type BuildResult,
7
7
  type Bundler,
8
8
  type ComponentCompilers,
9
- type FileSystemTree,
10
- type FlatFiles,
9
+ type FileTree,
11
10
  type PackageCdnFactory,
11
+ type PortChange,
12
+ type ProcessOptions,
12
13
  type ProjectProfile,
14
+ type RuntimeProcess,
15
+ type ServerAddress,
13
16
  type Transformer,
14
- type SpawnOptions,
15
- type WebContainerProcess,
16
17
  } from '@seanhogg/builderforce-webcontainers-core';
17
18
  import { PREVIEW_ID, isPortPath, isWireRequest, previewBase, type WireRequest, type WireResponse } from './protocol.js';
18
19
  import { createProcessHost, servePortRequest, type NodeRuntimeOptions } from './node/index.js';
@@ -41,9 +42,9 @@ export interface RelayOptions {
41
42
  scope?: never;
42
43
  }
43
44
 
44
- export type BootOptions = (SameOriginOptions | RelayOptions) & CommonBootOptions;
45
+ export type PreviewRuntimeOptions = (SameOriginOptions | RelayOptions) & CommonRuntimeOptions;
45
46
 
46
- export interface CommonBootOptions {
47
+ export interface CommonRuntimeOptions {
47
48
  /** Compiler. Defaults to esbuild-wasm. */
48
49
  transformer?: Transformer;
49
50
  /** Package CDN factory, for previews (`dev: true`) and builds (`dev: false`). Defaults to esm.sh. */
@@ -58,7 +59,7 @@ export interface CommonBootOptions {
58
59
  attribution?: boolean;
59
60
  /** Reload open previews when files change. Default true. */
60
61
  liveReload?: boolean;
61
- /** The Node runtime behind `spawn` (process workers, npm registry). */
62
+ /** The Node runtime behind `run` (process workers, npm registry). */
62
63
  node?: NodeRuntimeOptions;
63
64
  }
64
65
 
@@ -84,7 +85,8 @@ export interface PreviewRuntime {
84
85
  readonly url: string;
85
86
  readonly fs: VirtualFileSystem;
86
87
  readonly server: DevServer;
87
- mount(files: FileSystemTree | FlatFiles): void;
88
+ /** Write files into the runtime (nested or flat; see `FileTree`). Open previews reload. */
89
+ writeTree(files: FileTree): void;
88
90
  /** Whether this runtime can serve the mounted project, and why not. */
89
91
  profile(): ProjectProfile;
90
92
  /**
@@ -96,17 +98,16 @@ export interface PreviewRuntime {
96
98
  /** Uncaught errors and rejections raised inside the preview. */
97
99
  onError(listener: (error: PreviewError) => void): () => void;
98
100
  /**
99
- * Run a command — `node server.js`, `npm install`, `npm run dev`, `jsh` for a
100
- * terminal — in its own Web Worker. Same shape as `@webcontainer/api`'s
101
- * `spawn`: `output`/`input` streams, `exit`, `kill()`, `resize()`. Pass
102
- * `{ terminal: { cols, rows } }` when wiring it to xterm.js.
101
+ * Run a command — `node server.js`, `npm install`, `npm run dev`, `bfsh` for a
102
+ * terminal — in its own Web Worker. Pass `{ terminal: { cols, rows } }` when
103
+ * wiring it to xterm.js.
103
104
  */
104
- spawn(command: string, args?: string[], options?: SpawnOptions): WebContainerProcess;
105
- spawn(command: string, options?: SpawnOptions): WebContainerProcess;
106
- /** `server-ready`: a process started listening (or a dev script handed off to the preview); `url` loads it in an iframe. */
107
- on(event: 'server-ready', listener: (port: number, url: string) => void): () => void;
108
- /** `port`: a virtual server opened or closed. */
109
- on(event: 'port', listener: (port: number, type: 'open' | 'close', url: string) => void): () => void;
105
+ run(command: string, args?: string[], options?: ProcessOptions): RuntimeProcess;
106
+ run(command: string, options?: ProcessOptions): RuntimeProcess;
107
+ /** A process started listening (or a dev script handed off to the preview); `url` loads it in an iframe. */
108
+ onServerListening(listener: (server: ServerAddress) => void): () => void;
109
+ /** A virtual server opened or closed its port. */
110
+ onPortChange(listener: (change: PortChange) => void): () => void;
110
111
  dispose(): void;
111
112
  }
112
113
 
@@ -130,7 +131,7 @@ export function toWire(reqId: number, served: Awaited<ReturnType<DevServer['hand
130
131
  * nothing else: no cross-origin isolation headers, no SharedArrayBuffer. Pass
131
132
  * `relayUrl` to serve the preview from its own origin (see `RelayOptions`).
132
133
  */
133
- export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewRuntime> {
134
+ export async function startPreviewRuntime(options: PreviewRuntimeOptions): Promise<PreviewRuntime> {
134
135
  const id = options.id ?? randomId();
135
136
  if (!PREVIEW_ID.test(id)) throw new Error(`Preview id "${id}" may only contain letters, digits, "_" and "-".`);
136
137
 
@@ -196,7 +197,7 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
196
197
  url: previewUrl,
197
198
  fs,
198
199
  server,
199
- mount: (files) => fs.mount(files),
200
+ writeTree: (files) => fs.writeTree(files),
200
201
  profile: () => server.profile(),
201
202
  async build(buildOptions = {}) {
202
203
  const bundler = options.bundler ?? (await createEsbuildWasmBundler());
@@ -206,8 +207,9 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
206
207
  errorListeners.add(listener);
207
208
  return () => errorListeners.delete(listener);
208
209
  },
209
- spawn: (command: string, args?: string[] | SpawnOptions, spawnOptions?: SpawnOptions) => processes.spawn(command, args as string[], spawnOptions),
210
- on: ((event: 'server-ready' | 'port', listener: (...args: any[]) => void) => processes.on(event as 'port', listener)) as PreviewRuntime['on'],
210
+ run: (command: string, args?: string[] | ProcessOptions, runOptions?: ProcessOptions) => processes.run(command, args as string[], runOptions),
211
+ onServerListening: (listener) => processes.servers.onListening(listener),
212
+ onPortChange: (listener) => processes.servers.onPortChange(listener),
211
213
  dispose() {
212
214
  processes.dispose();
213
215
  window.removeEventListener('message', onWindowMessage);
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- export { bootPreviewRuntime } from './host.js';
1
+ export { startPreviewRuntime } from './host.js';
2
2
  export type {
3
- BootOptions,
3
+ PreviewRuntimeOptions,
4
4
  SameOriginOptions,
5
5
  RelayOptions,
6
- CommonBootOptions,
6
+ CommonRuntimeOptions,
7
7
  PreviewRuntime,
8
8
  PreviewError,
9
9
  RuntimeBuildOptions,
@@ -9,15 +9,17 @@
9
9
  import {
10
10
  DEFAULT_ENV,
11
11
  PortRegistry,
12
+ ServerEvents,
12
13
  allocatePid,
13
- toWebContainerProcess,
14
+ runArguments,
15
+ toRuntimeProcess,
14
16
  type ChildHandle,
15
17
  type FsChange,
16
18
  type HttpRequestData,
17
19
  type HttpResponseData,
18
- type SpawnOptions,
20
+ type ProcessOptions,
21
+ type RuntimeProcess,
19
22
  type VirtualFileSystem,
20
- type WebContainerProcess,
21
23
  } from '@seanhogg/builderforce-webcontainers-core';
22
24
  import type { FsOp, HostToWorker, WorkerToHost } from './protocol.js';
23
25
  import { portPath } from './protocol.js';
@@ -63,9 +65,9 @@ interface Proc {
63
65
 
64
66
  export class ProcessHost {
65
67
  readonly ports = new PortRegistry();
68
+ /** Servers listening, ports opening and closing. */
69
+ readonly servers = new ServerEvents();
66
70
  private readonly procs = new Set<Proc>();
67
- private readonly serverReady = new Set<(port: number, url: string) => void>();
68
- private readonly portListeners = new Set<(port: number, type: 'open' | 'close', url: string) => void>();
69
71
  private applyingFrom?: Proc;
70
72
  private flushScheduled = false;
71
73
  private nextReq = 1;
@@ -77,8 +79,7 @@ export class ProcessHost {
77
79
  this.stopPorts = this.ports.watch(({ type, port }) => {
78
80
  const url = this.serverUrl(port);
79
81
  for (const proc of this.procs) this.post(proc, { type: 'ports', open: type === 'open' ? [port] : [], closed: type === 'close' ? [port] : [] });
80
- for (const l of this.portListeners) l(port, type, url);
81
- if (type === 'open') for (const l of this.serverReady) l(port, url);
82
+ this.servers.portChanged({ type, port, url });
82
83
  });
83
84
  }
84
85
 
@@ -86,13 +87,15 @@ export class ProcessHost {
86
87
  return new URL(portPath(this.options.previewBase, port), this.options.previewOrigin).href;
87
88
  }
88
89
 
89
- /** Same shape as `WebContainer#spawn`. */
90
- spawn(command: string, args: string[] | SpawnOptions = [], options: SpawnOptions = {}): WebContainerProcess {
91
- if (!Array.isArray(args)) [options, args] = [args, []];
92
- return toWebContainerProcess(this.spawnChild(command, args, options), !!options.terminal);
90
+ /** Start a command in its own Web Worker, as a stream-based {@link RuntimeProcess}. */
91
+ run(command: string, args?: string[], options?: ProcessOptions): RuntimeProcess;
92
+ run(command: string, options?: ProcessOptions): RuntimeProcess;
93
+ run(command: string, argsOrOptions?: string[] | ProcessOptions, maybeOptions?: ProcessOptions): RuntimeProcess {
94
+ const [args, options] = runArguments(argsOrOptions, maybeOptions);
95
+ return toRuntimeProcess(this.spawnChild(command, args, options), !!options.terminal);
93
96
  }
94
97
 
95
- spawnChild(command: string, args: string[] = [], options: SpawnOptions = {}): ChildHandle {
98
+ spawnChild(command: string, args: string[] = [], options: ProcessOptions = {}): ChildHandle {
96
99
  const pid = allocatePid();
97
100
  const worker = this.options.createWorker();
98
101
  let resolveExit!: (code: number) => void;
@@ -156,14 +159,6 @@ export class ProcessHost {
156
159
  return this.ports.request(port, request);
157
160
  }
158
161
 
159
- on(event: 'server-ready', listener: (port: number, url: string) => void): () => void;
160
- on(event: 'port', listener: (port: number, type: 'open' | 'close', url: string) => void): () => void;
161
- on(event: 'server-ready' | 'port', listener: (...args: any[]) => void): () => void {
162
- const set = (event === 'server-ready' ? this.serverReady : this.portListeners) as Set<(...args: any[]) => void>;
163
- set.add(listener);
164
- return () => set.delete(listener);
165
- }
166
-
167
162
  dispose(): void {
168
163
  for (const proc of [...this.procs]) {
169
164
  proc.worker.terminate();
@@ -291,8 +286,8 @@ export class ProcessHost {
291
286
  case 'loopback':
292
287
  void this.ports.request(message.port, message.request).then((response) => this.post(proc, { type: 'loopback-response', reqId: message.reqId, response: response ?? null }));
293
288
  return;
294
- case 'server-ready':
295
- for (const l of this.serverReady) l(message.port, message.url);
289
+ case 'listening':
290
+ this.servers.announce({ port: message.port, url: message.url });
296
291
  return;
297
292
  }
298
293
  }
@@ -55,7 +55,7 @@ export type WorkerToHost =
55
55
  | { type: 'unlisten'; port: number }
56
56
  | { type: 'http-response'; reqId: number; response: HttpResponseData }
57
57
  | { type: 'loopback'; reqId: number; port: number; request: HttpRequestData }
58
- | { type: 'server-ready'; port: number; url: string };
58
+ | { type: 'listening'; port: number; url: string };
59
59
 
60
60
  /** The preview-relative URL a virtual server on `port` is reachable at. */
61
61
  export function portPath(previewBase: string, port: number): string {
@@ -144,7 +144,7 @@ export function createWorkerProcess(deps: WorkerProcessDeps): { receive(message:
144
144
  });
145
145
  },
146
146
  serverUrl: (port) => new URL(portPath(message.previewBase, port), message.previewOrigin).href,
147
- announceServer: (port, url) => deps.post({ type: 'server-ready', port, url }),
147
+ announceServer: (port, url) => deps.post({ type: 'listening', port, url }),
148
148
  previewUrl: message.previewUrl,
149
149
  registry: message.registry,
150
150
  packageCache: message.packageCache ? deps.packageCache?.() : undefined,
@@ -154,7 +154,7 @@ export function createWorkerProcess(deps: WorkerProcessDeps): { receive(message:
154
154
 
155
155
  const resolved = resolveCommand(fs, programs, message.command, message.args, message.cwd, message.env);
156
156
  if (!resolved) {
157
- deps.post({ type: 'stderr', data: `jsh: command not found: ${message.command}\n` });
157
+ deps.post({ type: 'stderr', data: `bfsh: command not found: ${message.command}\n` });
158
158
  deps.post({ type: 'exit', code: 127 });
159
159
  return;
160
160
  }