@seanhogg/builderforce-webcontainers 2026.10.6 → 2026.10.7

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/dist/relay.html CHANGED
@@ -1,2 +1,2 @@
1
- <!doctype html><html><head><meta charset="utf-8"><title>Preview relay</title></head><body><script>"use strict";(()=>{var a="bfwc:attach",c="bfwc:reattach",n="bfwc:relay-ready",p="bfwc:relay-attach",d="bfwc:relay-reattach",f="bfwc:relay-reload",m="bfwc:relay-spawn";function l(e,t){t.onmessage=r=>e.postMessage({kind:"message",data:r.data}),t.onerror=r=>e.postMessage({kind:"error",message:r.message||"The process worker failed."}),e.onmessage=r=>{let o=r.data;o?.kind==="message"?t.postMessage(o.data):o?.kind==="terminate"&&(t.terminate(),e.close())}}var g="process-worker.js";function i(e){return`bfwc:reload:${e}`}function u(e){if(e.active)return Promise.resolve(e.active);let t=e.installing??e.waiting;return t?new Promise((r,o)=>{t.addEventListener("statechange",()=>{t.state==="activated"&&e.active&&r(e.active),t.state==="redundant"&&o(new Error("The preview service worker failed to install."))})}):Promise.reject(new Error("The preview service worker did not install."))}function E(e){if(typeof BroadcastChannel>"u")return;let t=new BroadcastChannel(i(e));t.postMessage("reload"),t.close()}var s=window.parent;async function R(){let e=navigator.serviceWorker;if(!e)throw new Error("Service workers are unavailable on the preview origin.");let t=await e.register(new URL("./sw.js",location.href).href,{scope:"./"});await u(t),e.addEventListener("message",r=>{r.data?.type===c&&typeof r.data.id=="string"&&s.postMessage({type:d,id:r.data.id},"*")}),addEventListener("message",r=>{if(r.source!==s)return;let o=r.data;o?.type===p&&typeof o.id=="string"&&r.ports[0]?t.active?.postMessage({type:a,id:o.id},[r.ports[0]]):o?.type===f&&typeof o.base=="string"?E(o.base):o?.type===m&&r.ports[0]&&l(r.ports[0],new Worker(new URL(`./${g}`,location.href),{name:"bfwc-process"}))}),s.postMessage({type:n},"*")}R().catch(e=>{s.postMessage({type:n,error:e instanceof Error?e.message:String(e)},"*")});})();
1
+ <!doctype html><html><head><meta charset="utf-8"><title>Preview relay</title></head><body><script>"use strict";(()=>{var a="bfwc:attach",c="bfwc:reattach",n="bfwc:relay-ready",p="bfwc:relay-attach",f="bfwc:relay-reattach",l="bfwc:relay-reload",d="bfwc:relay-spawn";function m(e,t){t.onmessage=r=>e.postMessage({kind:"message",data:r.data}),t.onerror=r=>e.postMessage({kind:"error",message:r.message||"The process worker failed."}),e.onmessage=r=>{let o=r.data;o?.kind==="message"?t.postMessage(o.data):o?.kind==="terminate"&&(t.terminate(),e.close())}}var u="process-worker.js";function i(e){return`bfwc:reload:${e}`}function g(e){if(e.active)return Promise.resolve(e.active);let t=e.installing??e.waiting;return t?new Promise((r,o)=>{t.addEventListener("statechange",()=>{t.state==="activated"&&e.active&&r(e.active),t.state==="redundant"&&o(new Error("The preview service worker failed to install."))})}):Promise.reject(new Error("The preview service worker did not install."))}function E(e){if(typeof BroadcastChannel>"u")return;let t=new BroadcastChannel(i(e));t.postMessage("reload"),t.close()}var s=window.parent;async function x(){let e=navigator.serviceWorker;if(!e)throw new Error("Service workers are unavailable on the preview origin.");let t=await e.register(new URL("./sw.js",location.href).href,{scope:"./"});await g(t),e.addEventListener("message",r=>{r.data?.type===c&&typeof r.data.id=="string"&&s.postMessage({type:f,id:r.data.id},"*")}),addEventListener("message",r=>{if(r.source!==s)return;let o=r.data;o?.type===p&&typeof o.id=="string"&&r.ports[0]?t.active?.postMessage({type:a,id:o.id},[r.ports[0]]):o?.type===l&&typeof o.base=="string"?E(o.base):o?.type===d&&r.ports[0]&&m(r.ports[0],new Worker(new URL(`./${u}`,location.href),{name:"bfwc-process"}))}),s.postMessage({type:n},"*")}x().catch(e=>{s.postMessage({type:n,error:e instanceof Error?e.message:String(e)},"*")});})();
2
2
  </script></body></html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-webcontainers",
3
- "version": "2026.10.6",
3
+ "version": "2026.10.7",
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.6"
68
+ "@seanhogg/builderforce-webcontainers-core": "^2026.10.7"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@vue/compiler-sfc": "^3.5.0",
package/src/host.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  DevServer,
3
- ERROR_MESSAGE_TYPE,
4
3
  VirtualFileSystem,
5
4
  buildProject,
5
+ exportZip,
6
+ importZip,
6
7
  type BuildResult,
7
8
  type Bundler,
8
9
  type ComponentCompilers,
@@ -14,9 +15,11 @@ import {
14
15
  type RuntimeProcess,
15
16
  type ServerAddress,
16
17
  type Transformer,
18
+ type ZipExportOptions,
17
19
  } from '@seanhogg/builderforce-webcontainers-core';
18
20
  import { PREVIEW_ID, isPortPath, isWireRequest, previewBase, type WireRequest, type WireResponse } from './protocol.js';
19
- import { createProcessHost, servePortRequest, type NodeRuntimeOptions } from './node/index.js';
21
+ import { createProcessHost, servePortRequest, type NodeRuntimeOptions, type ProcessInfo } from './node/index.js';
22
+ import { listenToPreview, type PreviewConsoleMessage, type PreviewError } from './previewMessages.js';
20
23
  import { relayTransport, sameOriginTransport } from './transport.js';
21
24
  import { createEsbuildWasmBundler, createEsbuildWasmTransformer } from './esbuildWasm.js';
22
25
  import { createCdnComponentCompilers } from './componentCompilers.js';
@@ -59,6 +62,8 @@ export interface CommonRuntimeOptions {
59
62
  attribution?: boolean;
60
63
  /** Reload open previews when files change. Default true. */
61
64
  liveReload?: boolean;
65
+ /** Your scripts (JavaScript source), run in every preview page before the app — analytics, test hooks, a mock `fetch`. */
66
+ previewScripts?: string[];
62
67
  /** The Node runtime behind `run` (process workers, npm registry). */
63
68
  node?: NodeRuntimeOptions;
64
69
  }
@@ -66,12 +71,6 @@ export interface CommonRuntimeOptions {
66
71
  /** Edits arrive in bursts (a save, an agent writing several files): reload once per burst. */
67
72
  const RELOAD_DEBOUNCE_MS = 120;
68
73
 
69
- export interface PreviewError {
70
- message: string;
71
- stack?: string;
72
- href: string;
73
- }
74
-
75
74
  export interface RuntimeBuildOptions {
76
75
  /** Where the site will be served from. Default `./` (works from any directory). */
77
76
  base?: string;
@@ -85,8 +84,14 @@ export interface PreviewRuntime {
85
84
  readonly url: string;
86
85
  readonly fs: VirtualFileSystem;
87
86
  readonly server: DevServer;
88
- /** Write files into the runtime (nested or flat; see `FileTree`). Open previews reload. */
89
- writeTree(files: FileTree): void;
87
+ /** Write files into the runtime (nested or flat; see `FileTree`), under `at` (default `/`). Open previews reload. */
88
+ writeTree(files: FileTree, at?: string): void;
89
+ /** The project as a zip — "Download project" with `exclude: ['node_modules']`, or a full snapshot to restore later. */
90
+ exportZip(options?: ZipExportOptions): Uint8Array;
91
+ /** Write a zip's contents (an `exportZip` snapshot, a downloaded template) into the runtime, under `at`. */
92
+ importZip(zip: Uint8Array, at?: string): void;
93
+ /** Reload every frame showing this preview now (live reload does this on edits by itself). */
94
+ reload(): void;
90
95
  /** Whether this runtime can serve the mounted project, and why not. */
91
96
  profile(): ProjectProfile;
92
97
  /**
@@ -97,6 +102,10 @@ export interface PreviewRuntime {
97
102
  build(options?: RuntimeBuildOptions): Promise<BuildResult>;
98
103
  /** Uncaught errors and rejections raised inside the preview. */
99
104
  onError(listener: (error: PreviewError) => void): () => void;
105
+ /** The preview's `console.log/info/warn/error/debug` output (first 1,000 messages per page load). */
106
+ onPreviewConsole(listener: (message: PreviewConsoleMessage) => void): () => void;
107
+ /** Replace the scripts run in preview pages; open previews reload to pick them up. */
108
+ setPreviewScripts(scripts: string[]): void;
100
109
  /**
101
110
  * Run a command — `node server.js`, `npm install`, `npm run dev`, `bfsh` for a
102
111
  * terminal — in its own Web Worker. Pass `{ terminal: { cols, rows } }` when
@@ -108,6 +117,8 @@ export interface PreviewRuntime {
108
117
  onServerListening(listener: (server: ServerAddress) => void): () => void;
109
118
  /** A virtual server opened or closed its port. */
110
119
  onPortChange(listener: (change: PortChange) => void): () => void;
120
+ /** Processes still running, with the ports their servers listen on. */
121
+ listProcesses(): ProcessInfo[];
111
122
  dispose(): void;
112
123
  }
113
124
 
@@ -145,7 +156,7 @@ export async function startPreviewRuntime(options: PreviewRuntimeOptions): Promi
145
156
  let server: DevServer;
146
157
  try {
147
158
  const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
148
- server = new DevServer({ fs, transformer, base, cdn: options.cdn, components, attribution: options.attribution });
159
+ server = new DevServer({ fs, transformer, base, cdn: options.cdn, components, attribution: options.attribution, previewScripts: options.previewScripts });
149
160
  } catch (error) {
150
161
  transport.dispose();
151
162
  throw error;
@@ -182,38 +193,35 @@ export async function startPreviewRuntime(options: PreviewRuntimeOptions): Promi
182
193
  reloadTimer = setTimeout(() => transport.reload(base), RELOAD_DEBOUNCE_MS);
183
194
  });
184
195
 
185
- const errorListeners = new Set<(error: PreviewError) => void>();
186
- const onWindowMessage = (event: MessageEvent) => {
187
- const data = event.data as Partial<PreviewError> & { type?: string };
188
- if (data?.type !== ERROR_MESSAGE_TYPE || typeof data.href !== 'string') return;
189
- if (event.origin !== transport.origin || !new URL(data.href).pathname.startsWith(base)) return; // another runtime's preview
190
- const error: PreviewError = { message: String(data.message ?? ''), stack: data.stack, href: data.href };
191
- for (const listener of errorListeners) listener(error);
192
- };
193
- window.addEventListener('message', onWindowMessage);
196
+ const previewMessages = listenToPreview(window, transport.origin, base);
194
197
 
195
198
  return {
196
199
  id,
197
200
  url: previewUrl,
198
201
  fs,
199
202
  server,
200
- writeTree: (files) => fs.writeTree(files),
203
+ writeTree: (files, at) => fs.writeTree(files, at),
204
+ exportZip: (zipOptions) => exportZip(fs, zipOptions),
205
+ importZip: (zip, at) => importZip(fs, zip, at),
206
+ reload: () => transport.reload(base),
201
207
  profile: () => server.profile(),
202
208
  async build(buildOptions = {}) {
203
209
  const bundler = options.bundler ?? (await createEsbuildWasmBundler());
204
210
  return buildProject({ files: fs, bundler, cdn: options.cdn, components, base: buildOptions.base, minify: buildOptions.minify });
205
211
  },
206
- onError(listener) {
207
- errorListeners.add(listener);
208
- return () => errorListeners.delete(listener);
212
+ onError: (listener) => previewMessages.onError(listener),
213
+ onPreviewConsole: (listener) => previewMessages.onConsole(listener),
214
+ setPreviewScripts(scripts) {
215
+ server.setPreviewScripts(scripts);
216
+ transport.reload(base);
209
217
  },
210
218
  run: (command: string, args?: string[] | ProcessOptions, runOptions?: ProcessOptions) => processes.run(command, args as string[], runOptions),
211
219
  onServerListening: (listener) => processes.servers.onListening(listener),
212
220
  onPortChange: (listener) => processes.servers.onPortChange(listener),
221
+ listProcesses: () => processes.list(),
213
222
  dispose() {
214
223
  processes.dispose();
215
- window.removeEventListener('message', onWindowMessage);
216
- errorListeners.clear();
224
+ previewMessages.dispose();
217
225
  clearTimeout(reloadTimer);
218
226
  stopWatching?.();
219
227
  stopReattach();
package/src/index.ts CHANGED
@@ -5,9 +5,10 @@ export type {
5
5
  RelayOptions,
6
6
  CommonRuntimeOptions,
7
7
  PreviewRuntime,
8
- PreviewError,
9
8
  RuntimeBuildOptions,
10
9
  } from './host.js';
10
+ export { listenToPreview } from './previewMessages.js';
11
+ export type { PreviewError, PreviewConsoleMessage, ConsoleLevel, PreviewMessages } from './previewMessages.js';
11
12
  export type { PreviewTransport } from './transport.js';
12
13
  export { createEsbuildWasmTransformer, createEsbuildWasmBundler, loadEsbuildWasm } from './esbuildWasm.js';
13
14
  export type { EsbuildWasmOptions } from './esbuildWasm.js';
@@ -15,7 +16,7 @@ export { createCdnComponentCompilers, defaultVueCompilerUrl, defaultSvelteCompil
15
16
  export type { CdnComponentCompilerOptions } from './componentCompilers.js';
16
17
  export { createCacheStoragePackageCache } from './packageCache.js';
17
18
  export { ProcessHost, createWorkerProcess } from './node/index.js';
18
- export type { NodeRuntimeOptions, ProcessHostOptions, WorkerLike } from './node/index.js';
19
+ export type { NodeRuntimeOptions, ProcessHostOptions, ProcessInfo, WorkerLike } from './node/index.js';
19
20
  export { createSwRouter } from './swRouter.js';
20
21
  export type { SwRouter, SwRouterOptions, PortLike, PreviewTarget, RoutedResponse, RequestDetails } from './swRouter.js';
21
22
  export { previewBase, previewPrefix, PREVIEW_SEGMENT, PREVIEW_ID, PORT_SEGMENT, isPortPath } from './protocol.js';
package/src/node/index.ts CHANGED
@@ -8,7 +8,7 @@ import type { WireRequest, WireResponse } from '../protocol.js';
8
8
  import { ProcessHost, type WorkerLike } from './processHost.js';
9
9
 
10
10
  export { ProcessHost } from './processHost.js';
11
- export type { ProcessHostOptions, WorkerLike } from './processHost.js';
11
+ export type { ProcessHostOptions, ProcessInfo, WorkerLike } from './processHost.js';
12
12
  export { createWorkerProcess } from './workerProcess.js';
13
13
 
14
14
  export interface NodeRuntimeOptions {
@@ -50,8 +50,21 @@ export interface ProcessHostOptions {
50
50
 
51
51
  const SIGNAL_CODES: Record<string, number> = { SIGINT: 130, SIGTERM: 143, SIGKILL: 137, SIGHUP: 129 };
52
52
 
53
+ /** A running process, as `listProcesses()` reports it. */
54
+ export interface ProcessInfo {
55
+ pid: number;
56
+ command: string;
57
+ args: string[];
58
+ cwd: string;
59
+ /** Epoch ms. */
60
+ startedAt: number;
61
+ /** Ports its servers listen on. */
62
+ ports: number[];
63
+ }
64
+
53
65
  interface Proc {
54
66
  pid: number;
67
+ info: Omit<ProcessInfo, 'ports'>;
55
68
  worker: WorkerLike;
56
69
  stdout: Set<(chunk: string) => void>;
57
70
  stderr: Set<(chunk: string) => void>;
@@ -102,6 +115,7 @@ export class ProcessHost {
102
115
  const exit = new Promise<number>((resolve) => (resolveExit = resolve));
103
116
  const proc: Proc = {
104
117
  pid,
118
+ info: { pid, command, args: [...args], cwd: options.cwd ?? '/', startedAt: Date.now() },
105
119
  worker,
106
120
  stdout: new Set(),
107
121
  stderr: new Set(),
@@ -154,6 +168,11 @@ export class ProcessHost {
154
168
  };
155
169
  }
156
170
 
171
+ /** Every process still running, oldest first — children started by a shell included. */
172
+ list(): ProcessInfo[] {
173
+ return [...this.procs].map((proc) => ({ ...proc.info, args: [...proc.info.args], ports: [...proc.registrations.keys()] }));
174
+ }
175
+
157
176
  /** Dispatch an HTTP request to the server on `port` (the preview service worker's `/__port/<port>/` path). */
158
177
  request(port: number, request: HttpRequestData): Promise<HttpResponseData | undefined> {
159
178
  return this.ports.request(port, request);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * What a preview reports to the host page: uncaught errors and console output,
3
+ * posted by the bridge script every served page starts with. One window
4
+ * listener per runtime, accepting only messages from the preview origin whose
5
+ * page sits under this runtime's base, so two runtimes on one page never hear
6
+ * each other's previews.
7
+ */
8
+ import { CONSOLE_MESSAGE_TYPE, ERROR_MESSAGE_TYPE } from '@seanhogg/builderforce-webcontainers-core';
9
+
10
+ export interface PreviewError {
11
+ message: string;
12
+ stack?: string;
13
+ href: string;
14
+ }
15
+
16
+ export type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
17
+
18
+ export interface PreviewConsoleMessage {
19
+ level: ConsoleLevel;
20
+ /** The arguments flattened to text, capped at 4,000 characters. */
21
+ text: string;
22
+ href: string;
23
+ }
24
+
25
+ export interface PreviewMessages {
26
+ onError(listener: (error: PreviewError) => void): () => void;
27
+ onConsole(listener: (message: PreviewConsoleMessage) => void): () => void;
28
+ dispose(): void;
29
+ }
30
+
31
+ const LEVELS = new Set<string>(['log', 'info', 'warn', 'error', 'debug']);
32
+
33
+ function subscribe<T>(set: Set<(value: T) => void>, listener: (value: T) => void): () => void {
34
+ set.add(listener);
35
+ return () => set.delete(listener);
36
+ }
37
+
38
+ export function listenToPreview(target: Pick<Window, 'addEventListener' | 'removeEventListener'>, origin: string, base: string): PreviewMessages {
39
+ const errors = new Set<(error: PreviewError) => void>();
40
+ const consoles = new Set<(message: PreviewConsoleMessage) => void>();
41
+
42
+ const onMessage = (event: MessageEvent) => {
43
+ const data = event.data as { type?: unknown; href?: unknown; message?: unknown; stack?: unknown; level?: unknown; text?: unknown } | null;
44
+ if (!data || typeof data.href !== 'string' || event.origin !== origin) return;
45
+ let path: string;
46
+ try {
47
+ path = new URL(data.href).pathname;
48
+ } catch {
49
+ return;
50
+ }
51
+ if (!path.startsWith(base)) return; // another runtime's preview
52
+ if (data.type === ERROR_MESSAGE_TYPE) {
53
+ const error: PreviewError = { message: String(data.message ?? ''), stack: typeof data.stack === 'string' ? data.stack : undefined, href: data.href };
54
+ for (const listener of errors) listener(error);
55
+ } else if (data.type === CONSOLE_MESSAGE_TYPE && typeof data.level === 'string' && LEVELS.has(data.level)) {
56
+ const message: PreviewConsoleMessage = { level: data.level as ConsoleLevel, text: String(data.text ?? ''), href: data.href };
57
+ for (const listener of consoles) listener(message);
58
+ }
59
+ };
60
+ target.addEventListener('message', onMessage as EventListener);
61
+
62
+ return {
63
+ onError: (listener) => subscribe(errors, listener),
64
+ onConsole: (listener) => subscribe(consoles, listener),
65
+ dispose() {
66
+ target.removeEventListener('message', onMessage as EventListener);
67
+ errors.clear();
68
+ consoles.clear();
69
+ },
70
+ };
71
+ }