@seanhogg/builderforce-webcontainers 2026.10.5 → 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/README.md +5 -5
- package/dist/assets.js +2 -2
- package/dist/check/index.js +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/check/worker.js +8 -8
- package/dist/host.d.ts +32 -22
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +19 -23
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/processHost.d.ts +20 -8
- package/dist/node/processHost.d.ts.map +1 -1
- package/dist/node/processHost.js +14 -21
- package/dist/node/processHost.js.map +1 -1
- package/dist/node/protocol.d.ts +1 -1
- package/dist/node/protocol.d.ts.map +1 -1
- package/dist/node/worker.js +73 -73
- package/dist/node/workerProcess.js +2 -2
- package/dist/node/workerProcess.js.map +1 -1
- package/dist/previewMessages.d.ts +19 -0
- package/dist/previewMessages.d.ts.map +1 -0
- package/dist/previewMessages.js +52 -0
- package/dist/previewMessages.js.map +1 -0
- package/dist/process-worker.js +73 -73
- package/dist/relay.html +1 -1
- package/package.json +2 -2
- package/src/check/index.ts +1 -1
- package/src/host.ts +55 -45
- package/src/index.ts +6 -5
- package/src/node/index.ts +1 -1
- package/src/node/processHost.ts +36 -22
- package/src/node/protocol.ts +1 -1
- package/src/node/workerProcess.ts +2 -2
- package/src/previewMessages.ts +71 -0
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",
|
|
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.
|
|
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.
|
|
68
|
+
"@seanhogg/builderforce-webcontainers-core": "^2026.10.7"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@vue/compiler-sfc": "^3.5.0",
|
package/src/check/index.ts
CHANGED
|
@@ -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.
|
|
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
|
@@ -1,21 +1,25 @@
|
|
|
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,
|
|
9
|
-
type
|
|
10
|
-
type FlatFiles,
|
|
10
|
+
type FileTree,
|
|
11
11
|
type PackageCdnFactory,
|
|
12
|
+
type PortChange,
|
|
13
|
+
type ProcessOptions,
|
|
12
14
|
type ProjectProfile,
|
|
15
|
+
type RuntimeProcess,
|
|
16
|
+
type ServerAddress,
|
|
13
17
|
type Transformer,
|
|
14
|
-
type
|
|
15
|
-
type WebContainerProcess,
|
|
18
|
+
type ZipExportOptions,
|
|
16
19
|
} from '@seanhogg/builderforce-webcontainers-core';
|
|
17
20
|
import { PREVIEW_ID, isPortPath, isWireRequest, previewBase, type WireRequest, type WireResponse } from './protocol.js';
|
|
18
|
-
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';
|
|
19
23
|
import { relayTransport, sameOriginTransport } from './transport.js';
|
|
20
24
|
import { createEsbuildWasmBundler, createEsbuildWasmTransformer } from './esbuildWasm.js';
|
|
21
25
|
import { createCdnComponentCompilers } from './componentCompilers.js';
|
|
@@ -41,9 +45,9 @@ export interface RelayOptions {
|
|
|
41
45
|
scope?: never;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export type
|
|
48
|
+
export type PreviewRuntimeOptions = (SameOriginOptions | RelayOptions) & CommonRuntimeOptions;
|
|
45
49
|
|
|
46
|
-
export interface
|
|
50
|
+
export interface CommonRuntimeOptions {
|
|
47
51
|
/** Compiler. Defaults to esbuild-wasm. */
|
|
48
52
|
transformer?: Transformer;
|
|
49
53
|
/** Package CDN factory, for previews (`dev: true`) and builds (`dev: false`). Defaults to esm.sh. */
|
|
@@ -58,19 +62,15 @@ export interface CommonBootOptions {
|
|
|
58
62
|
attribution?: boolean;
|
|
59
63
|
/** Reload open previews when files change. Default true. */
|
|
60
64
|
liveReload?: boolean;
|
|
61
|
-
/**
|
|
65
|
+
/** Your scripts (JavaScript source), run in every preview page before the app — analytics, test hooks, a mock `fetch`. */
|
|
66
|
+
previewScripts?: string[];
|
|
67
|
+
/** The Node runtime behind `run` (process workers, npm registry). */
|
|
62
68
|
node?: NodeRuntimeOptions;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/** Edits arrive in bursts (a save, an agent writing several files): reload once per burst. */
|
|
66
72
|
const RELOAD_DEBOUNCE_MS = 120;
|
|
67
73
|
|
|
68
|
-
export interface PreviewError {
|
|
69
|
-
message: string;
|
|
70
|
-
stack?: string;
|
|
71
|
-
href: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
74
|
export interface RuntimeBuildOptions {
|
|
75
75
|
/** Where the site will be served from. Default `./` (works from any directory). */
|
|
76
76
|
base?: string;
|
|
@@ -84,7 +84,14 @@ export interface PreviewRuntime {
|
|
|
84
84
|
readonly url: string;
|
|
85
85
|
readonly fs: VirtualFileSystem;
|
|
86
86
|
readonly server: DevServer;
|
|
87
|
-
|
|
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;
|
|
88
95
|
/** Whether this runtime can serve the mounted project, and why not. */
|
|
89
96
|
profile(): ProjectProfile;
|
|
90
97
|
/**
|
|
@@ -95,18 +102,23 @@ export interface PreviewRuntime {
|
|
|
95
102
|
build(options?: RuntimeBuildOptions): Promise<BuildResult>;
|
|
96
103
|
/** Uncaught errors and rejections raised inside the preview. */
|
|
97
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;
|
|
98
109
|
/**
|
|
99
|
-
* Run a command — `node server.js`, `npm install`, `npm run dev`, `
|
|
100
|
-
* terminal — in its own Web Worker.
|
|
101
|
-
*
|
|
102
|
-
* `{ terminal: { cols, rows } }` when wiring it to xterm.js.
|
|
110
|
+
* Run a command — `node server.js`, `npm install`, `npm run dev`, `bfsh` for a
|
|
111
|
+
* terminal — in its own Web Worker. Pass `{ terminal: { cols, rows } }` when
|
|
112
|
+
* wiring it to xterm.js.
|
|
103
113
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
|
|
114
|
+
run(command: string, args?: string[], options?: ProcessOptions): RuntimeProcess;
|
|
115
|
+
run(command: string, options?: ProcessOptions): RuntimeProcess;
|
|
116
|
+
/** A process started listening (or a dev script handed off to the preview); `url` loads it in an iframe. */
|
|
117
|
+
onServerListening(listener: (server: ServerAddress) => void): () => void;
|
|
118
|
+
/** A virtual server opened or closed its port. */
|
|
119
|
+
onPortChange(listener: (change: PortChange) => void): () => void;
|
|
120
|
+
/** Processes still running, with the ports their servers listen on. */
|
|
121
|
+
listProcesses(): ProcessInfo[];
|
|
110
122
|
dispose(): void;
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -130,7 +142,7 @@ export function toWire(reqId: number, served: Awaited<ReturnType<DevServer['hand
|
|
|
130
142
|
* nothing else: no cross-origin isolation headers, no SharedArrayBuffer. Pass
|
|
131
143
|
* `relayUrl` to serve the preview from its own origin (see `RelayOptions`).
|
|
132
144
|
*/
|
|
133
|
-
export async function
|
|
145
|
+
export async function startPreviewRuntime(options: PreviewRuntimeOptions): Promise<PreviewRuntime> {
|
|
134
146
|
const id = options.id ?? randomId();
|
|
135
147
|
if (!PREVIEW_ID.test(id)) throw new Error(`Preview id "${id}" may only contain letters, digits, "_" and "-".`);
|
|
136
148
|
|
|
@@ -144,7 +156,7 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
|
|
|
144
156
|
let server: DevServer;
|
|
145
157
|
try {
|
|
146
158
|
const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
|
|
147
|
-
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 });
|
|
148
160
|
} catch (error) {
|
|
149
161
|
transport.dispose();
|
|
150
162
|
throw error;
|
|
@@ -181,37 +193,35 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
|
|
|
181
193
|
reloadTimer = setTimeout(() => transport.reload(base), RELOAD_DEBOUNCE_MS);
|
|
182
194
|
});
|
|
183
195
|
|
|
184
|
-
const
|
|
185
|
-
const onWindowMessage = (event: MessageEvent) => {
|
|
186
|
-
const data = event.data as Partial<PreviewError> & { type?: string };
|
|
187
|
-
if (data?.type !== ERROR_MESSAGE_TYPE || typeof data.href !== 'string') return;
|
|
188
|
-
if (event.origin !== transport.origin || !new URL(data.href).pathname.startsWith(base)) return; // another runtime's preview
|
|
189
|
-
const error: PreviewError = { message: String(data.message ?? ''), stack: data.stack, href: data.href };
|
|
190
|
-
for (const listener of errorListeners) listener(error);
|
|
191
|
-
};
|
|
192
|
-
window.addEventListener('message', onWindowMessage);
|
|
196
|
+
const previewMessages = listenToPreview(window, transport.origin, base);
|
|
193
197
|
|
|
194
198
|
return {
|
|
195
199
|
id,
|
|
196
200
|
url: previewUrl,
|
|
197
201
|
fs,
|
|
198
202
|
server,
|
|
199
|
-
|
|
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),
|
|
200
207
|
profile: () => server.profile(),
|
|
201
208
|
async build(buildOptions = {}) {
|
|
202
209
|
const bundler = options.bundler ?? (await createEsbuildWasmBundler());
|
|
203
210
|
return buildProject({ files: fs, bundler, cdn: options.cdn, components, base: buildOptions.base, minify: buildOptions.minify });
|
|
204
211
|
},
|
|
205
|
-
onError(listener)
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
onError: (listener) => previewMessages.onError(listener),
|
|
213
|
+
onPreviewConsole: (listener) => previewMessages.onConsole(listener),
|
|
214
|
+
setPreviewScripts(scripts) {
|
|
215
|
+
server.setPreviewScripts(scripts);
|
|
216
|
+
transport.reload(base);
|
|
208
217
|
},
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
run: (command: string, args?: string[] | ProcessOptions, runOptions?: ProcessOptions) => processes.run(command, args as string[], runOptions),
|
|
219
|
+
onServerListening: (listener) => processes.servers.onListening(listener),
|
|
220
|
+
onPortChange: (listener) => processes.servers.onPortChange(listener),
|
|
221
|
+
listProcesses: () => processes.list(),
|
|
211
222
|
dispose() {
|
|
212
223
|
processes.dispose();
|
|
213
|
-
|
|
214
|
-
errorListeners.clear();
|
|
224
|
+
previewMessages.dispose();
|
|
215
225
|
clearTimeout(reloadTimer);
|
|
216
226
|
stopWatching?.();
|
|
217
227
|
stopReattach();
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { startPreviewRuntime } from './host.js';
|
|
2
2
|
export type {
|
|
3
|
-
|
|
3
|
+
PreviewRuntimeOptions,
|
|
4
4
|
SameOriginOptions,
|
|
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 {
|
package/src/node/processHost.ts
CHANGED
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_ENV,
|
|
11
11
|
PortRegistry,
|
|
12
|
+
ServerEvents,
|
|
12
13
|
allocatePid,
|
|
13
|
-
|
|
14
|
+
runArguments,
|
|
15
|
+
toRuntimeProcess,
|
|
14
16
|
type ChildHandle,
|
|
15
17
|
type FsChange,
|
|
16
18
|
type HttpRequestData,
|
|
17
19
|
type HttpResponseData,
|
|
18
|
-
type
|
|
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';
|
|
@@ -48,8 +50,21 @@ export interface ProcessHostOptions {
|
|
|
48
50
|
|
|
49
51
|
const SIGNAL_CODES: Record<string, number> = { SIGINT: 130, SIGTERM: 143, SIGKILL: 137, SIGHUP: 129 };
|
|
50
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
|
+
|
|
51
65
|
interface Proc {
|
|
52
66
|
pid: number;
|
|
67
|
+
info: Omit<ProcessInfo, 'ports'>;
|
|
53
68
|
worker: WorkerLike;
|
|
54
69
|
stdout: Set<(chunk: string) => void>;
|
|
55
70
|
stderr: Set<(chunk: string) => void>;
|
|
@@ -63,9 +78,9 @@ interface Proc {
|
|
|
63
78
|
|
|
64
79
|
export class ProcessHost {
|
|
65
80
|
readonly ports = new PortRegistry();
|
|
81
|
+
/** Servers listening, ports opening and closing. */
|
|
82
|
+
readonly servers = new ServerEvents();
|
|
66
83
|
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
84
|
private applyingFrom?: Proc;
|
|
70
85
|
private flushScheduled = false;
|
|
71
86
|
private nextReq = 1;
|
|
@@ -77,8 +92,7 @@ export class ProcessHost {
|
|
|
77
92
|
this.stopPorts = this.ports.watch(({ type, port }) => {
|
|
78
93
|
const url = this.serverUrl(port);
|
|
79
94
|
for (const proc of this.procs) this.post(proc, { type: 'ports', open: type === 'open' ? [port] : [], closed: type === 'close' ? [port] : [] });
|
|
80
|
-
|
|
81
|
-
if (type === 'open') for (const l of this.serverReady) l(port, url);
|
|
95
|
+
this.servers.portChanged({ type, port, url });
|
|
82
96
|
});
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -86,19 +100,22 @@ export class ProcessHost {
|
|
|
86
100
|
return new URL(portPath(this.options.previewBase, port), this.options.previewOrigin).href;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
/** Start a command in its own Web Worker, as a stream-based {@link RuntimeProcess}. */
|
|
104
|
+
run(command: string, args?: string[], options?: ProcessOptions): RuntimeProcess;
|
|
105
|
+
run(command: string, options?: ProcessOptions): RuntimeProcess;
|
|
106
|
+
run(command: string, argsOrOptions?: string[] | ProcessOptions, maybeOptions?: ProcessOptions): RuntimeProcess {
|
|
107
|
+
const [args, options] = runArguments(argsOrOptions, maybeOptions);
|
|
108
|
+
return toRuntimeProcess(this.spawnChild(command, args, options), !!options.terminal);
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
spawnChild(command: string, args: string[] = [], options:
|
|
111
|
+
spawnChild(command: string, args: string[] = [], options: ProcessOptions = {}): ChildHandle {
|
|
96
112
|
const pid = allocatePid();
|
|
97
113
|
const worker = this.options.createWorker();
|
|
98
114
|
let resolveExit!: (code: number) => void;
|
|
99
115
|
const exit = new Promise<number>((resolve) => (resolveExit = resolve));
|
|
100
116
|
const proc: Proc = {
|
|
101
117
|
pid,
|
|
118
|
+
info: { pid, command, args: [...args], cwd: options.cwd ?? '/', startedAt: Date.now() },
|
|
102
119
|
worker,
|
|
103
120
|
stdout: new Set(),
|
|
104
121
|
stderr: new Set(),
|
|
@@ -151,19 +168,16 @@ export class ProcessHost {
|
|
|
151
168
|
};
|
|
152
169
|
}
|
|
153
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
|
+
|
|
154
176
|
/** Dispatch an HTTP request to the server on `port` (the preview service worker's `/__port/<port>/` path). */
|
|
155
177
|
request(port: number, request: HttpRequestData): Promise<HttpResponseData | undefined> {
|
|
156
178
|
return this.ports.request(port, request);
|
|
157
179
|
}
|
|
158
180
|
|
|
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
181
|
dispose(): void {
|
|
168
182
|
for (const proc of [...this.procs]) {
|
|
169
183
|
proc.worker.terminate();
|
|
@@ -291,8 +305,8 @@ export class ProcessHost {
|
|
|
291
305
|
case 'loopback':
|
|
292
306
|
void this.ports.request(message.port, message.request).then((response) => this.post(proc, { type: 'loopback-response', reqId: message.reqId, response: response ?? null }));
|
|
293
307
|
return;
|
|
294
|
-
case '
|
|
295
|
-
|
|
308
|
+
case 'listening':
|
|
309
|
+
this.servers.announce({ port: message.port, url: message.url });
|
|
296
310
|
return;
|
|
297
311
|
}
|
|
298
312
|
}
|
package/src/node/protocol.ts
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: `
|
|
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
|
}
|
|
@@ -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
|
+
}
|