@seanhogg/builderforce-webcontainers 2026.10.1 → 2026.10.2

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 CHANGED
@@ -20,5 +20,11 @@ if (runtime.profile().supported) iframe.src = runtime.url;
20
20
  else showFallback(runtime.profile().reason);
21
21
  ```
22
22
 
23
+ To run untrusted code (AI-written, arbitrary packages), isolate the preview on its
24
+ own origin with `bootPreviewRuntime({ relayUrl })`. That origin serves
25
+ `relay.html` and `sw.js` (strings in `@seanhogg/builderforce-webcontainers/assets`)
26
+ with `Content-Security-Policy: frame-ancestors` limited to your app. See the
27
+ [root README](../../README.md#isolating-the-preview-relay-mode).
28
+
23
29
  The package re-exports everything from
24
30
  [`@seanhogg/builderforce-webcontainers-core`](../core).
@@ -0,0 +1,4 @@
1
+ /** The preview service worker's source — serve as `<scope>/sw.js`, text/javascript. */
2
+ export declare const serviceWorkerSource: string;
3
+ /** The cross-origin relay page — serve as `<scope>/relay.html`, text/html, with frame-ancestors set. */
4
+ export declare const relayHtml: string;
package/dist/assets.js ADDED
@@ -0,0 +1,4 @@
1
+ /** The preview service worker's source — serve as `<scope>/sw.js`, text/javascript. */
2
+ export const serviceWorkerSource = "\"use strict\";(()=>{var h=\"__bfwc\",g=/^[A-Za-z0-9_-]+$/,w=\"bfwc:attach\",m=\"bfwc:reattach\";function y(e){let r=e.endsWith(\"/\")?e:e+\"/\";return r.endsWith(`/${h}/`)?r:`${r}${h}/`}function R(e){let r=e;return!!r&&r.type===\"response\"&&typeof r.reqId==\"number\"}var v={\"content-type\":\"text/plain; charset=utf-8\",\"cache-control\":\"no-store\"};function A(e){let r=y(e.scopePath),i=e.timeoutMs??3e4,T=e.reattachWaitMs??3e3,c=new Map,d=new Map,p=new Map,b=0;function k(t,s){c.set(t,s),s.onmessage=n=>{R(n.data)&&(p.get(n.data.reqId)?.(n.data),p.delete(n.data.reqId))};for(let n of d.get(t)??[])n(s);d.delete(t)}function x(t){if(!t.pathname.startsWith(r))return null;let s=t.pathname.slice(r.length),n=s.indexOf(\"/\"),o=n<0?s:s.slice(0,n);return!o||!g.test(o)?null:{id:o,path:n<0?\"/\":s.slice(n),search:t.search}}async function E(t){let s=c.get(t);if(s)return s;let n=new Promise(o=>{let u=d.get(t)??[];u.push(o),d.set(t,u),setTimeout(()=>o(void 0),T)});return await e.requestReattach(t),n}async function L(t){let s=await E(t.id);if(!s)return{status:503,headers:v,body:\"This preview is not running in any open tab.\"};let n=++b;return new Promise(o=>{let u=setTimeout(()=>{p.delete(n),c.get(t.id)===s&&c.delete(t.id),o({status:504,headers:v,body:\"The preview did not answer in time.\"})},i);p.set(n,l=>{clearTimeout(u),o({status:l.status,headers:l.headers,body:l.body})}),s.postMessage({type:\"request\",reqId:n,path:t.path,search:t.search})})}return{attach:k,match:x,respond:L,detach:t=>c.delete(t)}}var a=globalThis,f=A({scopePath:new URL(a.registration.scope).pathname,async requestReattach(e){let r=await a.clients.matchAll({type:\"window\",includeUncontrolled:!0});for(let i of r)i.postMessage({type:m,id:e})}});a.addEventListener(\"install\",()=>{a.skipWaiting()});a.addEventListener(\"activate\",e=>{e.waitUntil(a.clients.claim())});a.addEventListener(\"message\",e=>{let r=e.data,i=e.ports[0];r?.type===w&&typeof r.id==\"string\"&&i&&f.attach(r.id,i)});a.addEventListener(\"fetch\",e=>{if(e.request.method!==\"GET\")return;let r=f.match(new URL(e.request.url));r&&e.respondWith(f.respond(r).then(i=>new Response(i.body,{status:i.status,headers:i.headers})))});})();\n";
3
+ /** The cross-origin relay page — serve as `<scope>/relay.html`, text/html, with frame-ancestors set. */
4
+ export const relayHtml = "<!doctype html><html><head><meta charset=\"utf-8\"><title>Preview relay</title></head><body><script>\"use strict\";(()=>{var a=\"bfwc:attach\",c=\"bfwc:reattach\",s=\"bfwc:relay-ready\",p=\"bfwc:relay-attach\",f=\"bfwc:relay-reattach\",d=\"bfwc:relay-reload\";function i(e){return`bfwc:reload:${e}`}function l(e){if(e.active)return Promise.resolve(e.active);let r=e.installing??e.waiting;return r?new Promise((t,n)=>{r.addEventListener(\"statechange\",()=>{r.state===\"activated\"&&e.active&&t(e.active),r.state===\"redundant\"&&n(new Error(\"The preview service worker failed to install.\"))})}):Promise.reject(new Error(\"The preview service worker did not install.\"))}function u(e){if(typeof BroadcastChannel>\"u\")return;let r=new BroadcastChannel(i(e));r.postMessage(\"reload\"),r.close()}var o=window.parent;async function m(){let e=navigator.serviceWorker;if(!e)throw new Error(\"Service workers are unavailable on the preview origin.\");let r=await e.register(new URL(\"./sw.js\",location.href).href,{scope:\"./\"});await l(r),e.addEventListener(\"message\",t=>{t.data?.type===c&&typeof t.data.id==\"string\"&&o.postMessage({type:f,id:t.data.id},\"*\")}),addEventListener(\"message\",t=>{if(t.source!==o)return;let n=t.data;n?.type===p&&typeof n.id==\"string\"&&t.ports[0]?r.active?.postMessage({type:a,id:n.id},[t.ports[0]]):n?.type===d&&typeof n.base==\"string\"&&u(n.base)}),o.postMessage({type:s},\"*\")}m().catch(e=>{o.postMessage({type:s,error:e instanceof Error?e.message:String(e)},\"*\")});})();\n</script></body></html>\n";
package/dist/host.d.ts CHANGED
@@ -1,17 +1,35 @@
1
1
  import { DevServer, VirtualFileSystem, type DevServerOptions, type FileSystemTree, type FlatFiles, type ProjectProfile, type Transformer } from '@seanhogg/builderforce-webcontainers-core';
2
- export interface BootOptions {
2
+ /** Serve previews on THIS page's origin. Only for code the page's own user trusts. */
3
+ export interface SameOriginOptions {
3
4
  /** Where the host serves this package's `dist/sw.js` (e.g. `/__bfwc/sw.js`). */
4
5
  serviceWorkerUrl: string;
5
6
  /** Worker scope. Defaults to the worker script's directory. */
6
7
  scope?: string;
8
+ relayUrl?: never;
9
+ }
10
+ /**
11
+ * Serve previews on a SEPARATE origin, isolated from this page's cookies, storage
12
+ * and DOM. That origin serves `dist/relay.html` and `dist/sw.js` side by side, with
13
+ * `frame-ancestors` limited to the pages allowed to drive it.
14
+ */
15
+ export interface RelayOptions {
16
+ /** e.g. `https://preview.example.com/__bfwc/relay.html`. */
17
+ relayUrl: string;
18
+ serviceWorkerUrl?: never;
19
+ scope?: never;
20
+ }
21
+ export type BootOptions = (SameOriginOptions | RelayOptions) & CommonBootOptions;
22
+ export interface CommonBootOptions {
7
23
  /** Compiler. Defaults to esbuild-wasm. */
8
24
  transformer?: Transformer;
9
25
  /** Package CDN factory. Defaults to esm.sh. */
10
26
  cdn?: DevServerOptions['cdn'];
11
- /** Stable id (e.g. the project id) — keeps the preview URL stable across reloads. */
27
+ /** Stable id (e.g. the project id; `[A-Za-z0-9_-]`) — keeps the preview URL stable across reloads. */
12
28
  id?: string;
13
29
  /** The "Built with Builderforce.ai" badge in the preview. Default true — please keep it. */
14
30
  attribution?: boolean;
31
+ /** Reload open previews when files change. Default true. */
32
+ liveReload?: boolean;
15
33
  }
16
34
  export interface PreviewError {
17
35
  message: string;
@@ -36,7 +54,8 @@ export interface PreviewRuntime {
36
54
  * it, and a service worker that routes the preview iframe's requests back here.
37
55
  *
38
56
  * Needs a secure context (https or localhost) for the service worker — and
39
- * nothing else: no cross-origin isolation headers, no SharedArrayBuffer.
57
+ * nothing else: no cross-origin isolation headers, no SharedArrayBuffer. Pass
58
+ * `relayUrl` to serve the preview from its own origin (see `RelayOptions`).
40
59
  */
41
60
  export declare function bootPreviewRuntime(options: BootOptions): Promise<PreviewRuntime>;
42
61
  //# sourceMappingURL=host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAET,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,2CAA2C,CAAC;AAInD,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,+CAA+C;IAC/C,GAAG,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC9B,qFAAqF;IACrF,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4FAA4F;IAC5F,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;IAC/C,uEAAuE;IACvE,OAAO,IAAI,cAAc,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC7D,OAAO,IAAI,IAAI,CAAC;CACjB;AA0BD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CA2DtF"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAET,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,2CAA2C,CAAC;AAKnD,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,KAAK,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,iBAAiB,GAAG,YAAY,CAAC,GAAG,iBAAiB,CAAC;AAEjF,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,+CAA+C;IAC/C,GAAG,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC9B,sGAAsG;IACtG,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4FAA4F;IAC5F,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAKD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;IAC/C,uEAAuE;IACvE,OAAO,IAAI,cAAc,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC7D,OAAO,IAAI,IAAI,CAAC;CACjB;AAcD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAyEtF"}
package/dist/host.js CHANGED
@@ -1,21 +1,9 @@
1
1
  import { DevServer, ERROR_MESSAGE_TYPE, VirtualFileSystem, } from '@seanhogg/builderforce-webcontainers-core';
2
- import { ATTACH, REATTACH, isWireRequest, previewBase } from './protocol.js';
2
+ import { PREVIEW_ID, isWireRequest, previewBase } from './protocol.js';
3
+ import { relayTransport, sameOriginTransport } from './transport.js';
3
4
  import { createEsbuildWasmTransformer } from './esbuildWasm.js';
4
- function waitForActive(registration) {
5
- if (registration.active)
6
- return Promise.resolve(registration.active);
7
- const installing = registration.installing ?? registration.waiting;
8
- if (!installing)
9
- return Promise.reject(new Error('The preview service worker did not install.'));
10
- return new Promise((resolve, reject) => {
11
- installing.addEventListener('statechange', () => {
12
- if (installing.state === 'activated' && registration.active)
13
- resolve(registration.active);
14
- if (installing.state === 'redundant')
15
- reject(new Error('The preview service worker failed to install.'));
16
- });
17
- });
18
- }
5
+ /** Edits arrive in bursts (a save, an agent writing several files): reload once per burst. */
6
+ const RELOAD_DEBOUNCE_MS = 120;
19
7
  function randomId() {
20
8
  return globalThis.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 16) ?? Math.random().toString(36).slice(2, 18);
21
9
  }
@@ -31,22 +19,28 @@ function toWire(reqId, served) {
31
19
  * it, and a service worker that routes the preview iframe's requests back here.
32
20
  *
33
21
  * Needs a secure context (https or localhost) for the service worker — and
34
- * nothing else: no cross-origin isolation headers, no SharedArrayBuffer.
22
+ * nothing else: no cross-origin isolation headers, no SharedArrayBuffer. Pass
23
+ * `relayUrl` to serve the preview from its own origin (see `RelayOptions`).
35
24
  */
36
25
  export async function bootPreviewRuntime(options) {
37
- const container = navigator.serviceWorker;
38
- if (!container)
39
- throw new Error('Service workers are unavailable here (a secure context — https or localhost — is required).');
40
- const scriptUrl = new URL(options.serviceWorkerUrl, location.href);
41
- const scope = new URL(options.scope ?? './', scriptUrl).pathname;
42
- const registration = await container.register(scriptUrl.href, { scope });
43
- await waitForActive(registration);
44
26
  const id = options.id ?? randomId();
45
- const base = previewBase(scope, id);
27
+ if (!PREVIEW_ID.test(id))
28
+ throw new Error(`Preview id "${id}" may only contain letters, digits, "_" and "-".`);
29
+ const transport = options.relayUrl
30
+ ? await relayTransport(options.relayUrl)
31
+ : await sameOriginTransport(options.serviceWorkerUrl, options.scope);
32
+ const base = previewBase(transport.scope, id);
46
33
  const fs = new VirtualFileSystem();
47
- const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
48
- const server = new DevServer({ fs, transformer, base, cdn: options.cdn, attribution: options.attribution });
49
- const attach = (worker) => {
34
+ let server;
35
+ try {
36
+ const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
37
+ server = new DevServer({ fs, transformer, base, cdn: options.cdn, attribution: options.attribution });
38
+ }
39
+ catch (error) {
40
+ transport.dispose();
41
+ throw error;
42
+ }
43
+ const attach = () => {
50
44
  const channel = new MessageChannel();
51
45
  channel.port1.onmessage = async (event) => {
52
46
  if (!isWireRequest(event.data))
@@ -54,20 +48,27 @@ export async function bootPreviewRuntime(options) {
54
48
  const { message, transfer } = toWire(event.data.reqId, await server.handle(event.data.path, event.data.search));
55
49
  channel.port1.postMessage(message, transfer);
56
50
  };
57
- worker.postMessage({ type: ATTACH, id }, [channel.port2]);
58
- };
59
- attach(registration.active);
60
- const onWorkerMessage = (event) => {
61
- if (event.data?.type === REATTACH && event.data.id === id && registration.active)
62
- attach(registration.active);
51
+ transport.attach(id, channel.port2);
63
52
  };
64
- container.addEventListener('message', onWorkerMessage);
53
+ attach();
54
+ const stopReattach = transport.onReattach((wanted) => {
55
+ if (wanted === id)
56
+ attach();
57
+ });
58
+ // Live reload: any change to the files reloads every frame showing this preview.
59
+ let reloadTimer;
60
+ const stopWatching = options.liveReload === false
61
+ ? undefined
62
+ : fs.watch(() => {
63
+ clearTimeout(reloadTimer);
64
+ reloadTimer = setTimeout(() => transport.reload(base), RELOAD_DEBOUNCE_MS);
65
+ });
65
66
  const errorListeners = new Set();
66
67
  const onWindowMessage = (event) => {
67
68
  const data = event.data;
68
69
  if (data?.type !== ERROR_MESSAGE_TYPE || typeof data.href !== 'string')
69
70
  return;
70
- if (!new URL(data.href).pathname.startsWith(base))
71
+ if (event.origin !== transport.origin || !new URL(data.href).pathname.startsWith(base))
71
72
  return; // another runtime's preview
72
73
  const error = { message: String(data.message ?? ''), stack: data.stack, href: data.href };
73
74
  for (const listener of errorListeners)
@@ -76,7 +77,7 @@ export async function bootPreviewRuntime(options) {
76
77
  window.addEventListener('message', onWindowMessage);
77
78
  return {
78
79
  id,
79
- url: new URL(base, location.origin).href,
80
+ url: new URL(base, transport.origin).href,
80
81
  fs,
81
82
  server,
82
83
  mount: (files) => fs.mount(files),
@@ -86,9 +87,12 @@ export async function bootPreviewRuntime(options) {
86
87
  return () => errorListeners.delete(listener);
87
88
  },
88
89
  dispose() {
89
- container.removeEventListener('message', onWorkerMessage);
90
90
  window.removeEventListener('message', onWindowMessage);
91
91
  errorListeners.clear();
92
+ clearTimeout(reloadTimer);
93
+ stopWatching?.();
94
+ stopReattach();
95
+ transport.dispose();
92
96
  server.dispose();
93
97
  },
94
98
  };
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { bootPreviewRuntime } from './host.js';
2
- export type { BootOptions, PreviewRuntime, PreviewError } from './host.js';
2
+ export type { BootOptions, SameOriginOptions, RelayOptions, CommonBootOptions, PreviewRuntime, PreviewError } from './host.js';
3
+ export type { PreviewTransport } from './transport.js';
3
4
  export { createEsbuildWasmTransformer } from './esbuildWasm.js';
4
5
  export type { EsbuildWasmOptions } from './esbuildWasm.js';
5
6
  export { createSwRouter } from './swRouter.js';
6
7
  export type { SwRouter, SwRouterOptions, PortLike, PreviewTarget, RoutedResponse } from './swRouter.js';
7
- export { previewBase, previewPrefix, PREVIEW_SEGMENT } from './protocol.js';
8
+ export { previewBase, previewPrefix, PREVIEW_SEGMENT, PREVIEW_ID } from './protocol.js';
8
9
  export * from '@seanhogg/builderforce-webcontainers-core';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5E,cAAc,2CAA2C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC/H,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACxF,cAAc,2CAA2C,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { bootPreviewRuntime } from './host.js';
2
2
  export { createEsbuildWasmTransformer } from './esbuildWasm.js';
3
3
  export { createSwRouter } from './swRouter.js';
4
- export { previewBase, previewPrefix, PREVIEW_SEGMENT } from './protocol.js';
4
+ export { previewBase, previewPrefix, PREVIEW_SEGMENT, PREVIEW_ID } from './protocol.js';
5
5
  export * from '@seanhogg/builderforce-webcontainers-core';
6
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5E,cAAc,2CAA2C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAG/C,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACxF,cAAc,2CAA2C,CAAC"}
@@ -4,10 +4,28 @@
4
4
  * state: every request it intercepts is forwarded over a MessagePort to the page.
5
5
  */
6
6
  export declare const PREVIEW_SEGMENT = "__bfwc";
7
+ /**
8
+ * What a preview id may contain. No dots, so a file served beside the worker
9
+ * (`sw.js`, `relay.html`) can never be mistaken for a preview.
10
+ */
11
+ export declare const PREVIEW_ID: RegExp;
7
12
  /** page → worker, with a MessagePort in `ports[0]`: "serve preview `id` over this port". */
8
13
  export declare const ATTACH = "bfwc:attach";
9
14
  /** worker → pages: "I lost the port for `id` (I was restarted) — attach again". */
10
15
  export declare const REATTACH = "bfwc:reattach";
16
+ /**
17
+ * The cross-origin relay (`relay.html`, served on the preview origin and framed,
18
+ * hidden, by the host). The host cannot talk to a worker on another origin, so the
19
+ * relay registers it and passes messages both ways.
20
+ */
21
+ /** relay → host: the worker is active; send ports. */
22
+ export declare const RELAY_READY = "bfwc:relay-ready";
23
+ /** host → relay, with a MessagePort: forward as ATTACH for `id`. */
24
+ export declare const RELAY_ATTACH = "bfwc:relay-attach";
25
+ /** relay → host: the worker asked for `id` again. */
26
+ export declare const RELAY_REATTACH = "bfwc:relay-reattach";
27
+ /** host → relay: reload every frame showing the preview at `base`. */
28
+ export declare const RELAY_RELOAD = "bfwc:relay-reload";
11
29
  export interface AttachMessage {
12
30
  type: typeof ATTACH;
13
31
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC,4FAA4F;AAC5F,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,mFAAmF;AACnF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,QAAQ,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAGlE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAGpE"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAqB,CAAC;AAE7C,4FAA4F;AAC5F,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,mFAAmF;AACnF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC;;;;GAIG;AACH,sDAAsD;AACtD,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAC9C,oEAAoE;AACpE,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAChD,qDAAqD;AACrD,eAAO,MAAM,cAAc,wBAAwB,CAAC;AACpD,sEAAsE;AACtE,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,QAAQ,CAAC;IACtB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAGlE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAGpE"}
package/dist/protocol.js CHANGED
@@ -4,10 +4,28 @@
4
4
  * state: every request it intercepts is forwarded over a MessagePort to the page.
5
5
  */
6
6
  export const PREVIEW_SEGMENT = '__bfwc';
7
+ /**
8
+ * What a preview id may contain. No dots, so a file served beside the worker
9
+ * (`sw.js`, `relay.html`) can never be mistaken for a preview.
10
+ */
11
+ export const PREVIEW_ID = /^[A-Za-z0-9_-]+$/;
7
12
  /** page → worker, with a MessagePort in `ports[0]`: "serve preview `id` over this port". */
8
13
  export const ATTACH = 'bfwc:attach';
9
14
  /** worker → pages: "I lost the port for `id` (I was restarted) — attach again". */
10
15
  export const REATTACH = 'bfwc:reattach';
16
+ /**
17
+ * The cross-origin relay (`relay.html`, served on the preview origin and framed,
18
+ * hidden, by the host). The host cannot talk to a worker on another origin, so the
19
+ * relay registers it and passes messages both ways.
20
+ */
21
+ /** relay → host: the worker is active; send ports. */
22
+ export const RELAY_READY = 'bfwc:relay-ready';
23
+ /** host → relay, with a MessagePort: forward as ATTACH for `id`. */
24
+ export const RELAY_ATTACH = 'bfwc:relay-attach';
25
+ /** relay → host: the worker asked for `id` again. */
26
+ export const RELAY_REATTACH = 'bfwc:relay-reattach';
27
+ /** host → relay: reload every frame showing the preview at `base`. */
28
+ export const RELAY_RELOAD = 'bfwc:relay-reload';
11
29
  /**
12
30
  * The URL prefix previews live under, for a worker scope. Serving the worker from
13
31
  * `/__bfwc/sw.js` (scope `/__bfwc/`) keeps it off every other page of the host app;
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;AAExC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC;AACpC,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,eAAe,CAAC;AA2BxC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;IACpE,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,eAAe,GAAG,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,EAAU;IACvD,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,MAAM,CAAC,GAAG,KAA2B,CAAC;IACtC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,CAAC,GAAG,KAA4B,CAAC;IACvC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC;AACrE,CAAC"}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;AAExC;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,kBAAkB,CAAC;AAE7C,4FAA4F;AAC5F,MAAM,CAAC,MAAM,MAAM,GAAG,aAAa,CAAC;AACpC,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,eAAe,CAAC;AAExC;;;;GAIG;AACH,sDAAsD;AACtD,MAAM,CAAC,MAAM,WAAW,GAAG,kBAAkB,CAAC;AAC9C,oEAAoE;AACpE,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAChD,qDAAqD;AACrD,MAAM,CAAC,MAAM,cAAc,GAAG,qBAAqB,CAAC;AACpD,sEAAsE;AACtE,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,CAAC;AA2BhD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;IACpE,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,eAAe,GAAG,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,EAAU;IACvD,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,MAAM,CAAC,GAAG,KAA2B,CAAC;IACtC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,CAAC,GAAG,KAA4B,CAAC;IACvC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC;AACrE,CAAC"}
@@ -0,0 +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",s="bfwc:relay-ready",p="bfwc:relay-attach",f="bfwc:relay-reattach",d="bfwc:relay-reload";function i(e){return`bfwc:reload:${e}`}function l(e){if(e.active)return Promise.resolve(e.active);let r=e.installing??e.waiting;return r?new Promise((t,n)=>{r.addEventListener("statechange",()=>{r.state==="activated"&&e.active&&t(e.active),r.state==="redundant"&&n(new Error("The preview service worker failed to install."))})}):Promise.reject(new Error("The preview service worker did not install."))}function u(e){if(typeof BroadcastChannel>"u")return;let r=new BroadcastChannel(i(e));r.postMessage("reload"),r.close()}var o=window.parent;async function m(){let e=navigator.serviceWorker;if(!e)throw new Error("Service workers are unavailable on the preview origin.");let r=await e.register(new URL("./sw.js",location.href).href,{scope:"./"});await l(r),e.addEventListener("message",t=>{t.data?.type===c&&typeof t.data.id=="string"&&o.postMessage({type:f,id:t.data.id},"*")}),addEventListener("message",t=>{if(t.source!==o)return;let n=t.data;n?.type===p&&typeof n.id=="string"&&t.ports[0]?r.active?.postMessage({type:a,id:n.id},[t.ports[0]]):n?.type===d&&typeof n.base=="string"&&u(n.base)}),o.postMessage({type:s},"*")}m().catch(e=>{o.postMessage({type:s,error:e instanceof Error?e.message:String(e)},"*")});})();
2
+ </script></body></html>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- "use strict";(()=>{var f="__bfwc",g="bfwc:attach",m="bfwc:reattach";function w(e){let n=e.endsWith("/")?e:e+"/";return n.endsWith(`/${f}/`)?n:`${n}${f}/`}function y(e){let n=e;return!!n&&n.type==="response"&&typeof n.reqId=="number"}var v={"content-type":"text/plain; charset=utf-8","cache-control":"no-store"};function R(e){let n=w(e.scopePath),i=e.timeoutMs??3e4,k=e.reattachWaitMs??3e3,c=new Map,d=new Map,p=new Map,T=0;function W(t,r){c.set(t,r),r.onmessage=s=>{y(s.data)&&(p.get(s.data.reqId)?.(s.data),p.delete(s.data.reqId))};for(let s of d.get(t)??[])s(r);d.delete(t)}function P(t){if(!t.pathname.startsWith(n))return null;let r=t.pathname.slice(n.length),s=r.indexOf("/"),o=s<0?r:r.slice(0,s);return o?{id:o,path:s<0?"/":r.slice(s),search:t.search}:null}async function b(t){let r=c.get(t);if(r)return r;let s=new Promise(o=>{let u=d.get(t)??[];u.push(o),d.set(t,u),setTimeout(()=>o(void 0),k)});return await e.requestReattach(t),s}async function x(t){let r=await b(t.id);if(!r)return{status:503,headers:v,body:"This preview is not running in any open tab."};let s=++T;return new Promise(o=>{let u=setTimeout(()=>{p.delete(s),c.get(t.id)===r&&c.delete(t.id),o({status:504,headers:v,body:"The preview did not answer in time."})},i);p.set(s,l=>{clearTimeout(u),o({status:l.status,headers:l.headers,body:l.body})}),r.postMessage({type:"request",reqId:s,path:t.path,search:t.search})})}return{attach:W,match:P,respond:x,detach:t=>c.delete(t)}}var a=globalThis,h=R({scopePath:new URL(a.registration.scope).pathname,async requestReattach(e){let n=await a.clients.matchAll({type:"window",includeUncontrolled:!0});for(let i of n)i.postMessage({type:m,id:e})}});a.addEventListener("install",()=>{a.skipWaiting()});a.addEventListener("activate",e=>{e.waitUntil(a.clients.claim())});a.addEventListener("message",e=>{let n=e.data,i=e.ports[0];n?.type===g&&typeof n.id=="string"&&i&&h.attach(n.id,i)});a.addEventListener("fetch",e=>{if(e.request.method!=="GET")return;let n=h.match(new URL(e.request.url));n&&e.respondWith(h.respond(n).then(i=>new Response(i.body,{status:i.status,headers:i.headers})))});})();
1
+ "use strict";(()=>{var h="__bfwc",g=/^[A-Za-z0-9_-]+$/,w="bfwc:attach",m="bfwc:reattach";function y(e){let r=e.endsWith("/")?e:e+"/";return r.endsWith(`/${h}/`)?r:`${r}${h}/`}function R(e){let r=e;return!!r&&r.type==="response"&&typeof r.reqId=="number"}var v={"content-type":"text/plain; charset=utf-8","cache-control":"no-store"};function A(e){let r=y(e.scopePath),i=e.timeoutMs??3e4,T=e.reattachWaitMs??3e3,c=new Map,d=new Map,p=new Map,b=0;function k(t,s){c.set(t,s),s.onmessage=n=>{R(n.data)&&(p.get(n.data.reqId)?.(n.data),p.delete(n.data.reqId))};for(let n of d.get(t)??[])n(s);d.delete(t)}function x(t){if(!t.pathname.startsWith(r))return null;let s=t.pathname.slice(r.length),n=s.indexOf("/"),o=n<0?s:s.slice(0,n);return!o||!g.test(o)?null:{id:o,path:n<0?"/":s.slice(n),search:t.search}}async function E(t){let s=c.get(t);if(s)return s;let n=new Promise(o=>{let u=d.get(t)??[];u.push(o),d.set(t,u),setTimeout(()=>o(void 0),T)});return await e.requestReattach(t),n}async function L(t){let s=await E(t.id);if(!s)return{status:503,headers:v,body:"This preview is not running in any open tab."};let n=++b;return new Promise(o=>{let u=setTimeout(()=>{p.delete(n),c.get(t.id)===s&&c.delete(t.id),o({status:504,headers:v,body:"The preview did not answer in time."})},i);p.set(n,l=>{clearTimeout(u),o({status:l.status,headers:l.headers,body:l.body})}),s.postMessage({type:"request",reqId:n,path:t.path,search:t.search})})}return{attach:k,match:x,respond:L,detach:t=>c.delete(t)}}var a=globalThis,f=A({scopePath:new URL(a.registration.scope).pathname,async requestReattach(e){let r=await a.clients.matchAll({type:"window",includeUncontrolled:!0});for(let i of r)i.postMessage({type:m,id:e})}});a.addEventListener("install",()=>{a.skipWaiting()});a.addEventListener("activate",e=>{e.waitUntil(a.clients.claim())});a.addEventListener("message",e=>{let r=e.data,i=e.ports[0];r?.type===w&&typeof r.id=="string"&&i&&f.attach(r.id,i)});a.addEventListener("fetch",e=>{if(e.request.method!=="GET")return;let r=f.match(new URL(e.request.url));r&&e.respondWith(f.respond(r).then(i=>new Response(i.body,{status:i.status,headers:i.headers})))});})();
@@ -1 +1 @@
1
- {"version":3,"file":"swRouter.d.ts","sourceRoot":"","sources":["../src/swRouter.ts"],"names":[],"mappings":"AAEA,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe;iBASjC,MAAM,QAAQ,QAAQ,KAAG,IAAI;iBAW7B,GAAG,KAAG,aAAa,GAAG,IAAI;sBAsBf,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;iBAkBxB,MAAM;EACrD;AAED,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
1
+ {"version":3,"file":"swRouter.d.ts","sourceRoot":"","sources":["../src/swRouter.ts"],"names":[],"mappings":"AAEA,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe;iBASjC,MAAM,QAAQ,QAAQ,KAAG,IAAI;iBAW7B,GAAG,KAAG,aAAa,GAAG,IAAI;sBAwBf,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;iBAkBxB,MAAM;EACrD;AAED,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
package/dist/swRouter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isWireResponse, previewPrefix } from './protocol.js';
1
+ import { PREVIEW_ID, isWireResponse, previewPrefix } from './protocol.js';
2
2
  const TEXT = { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' };
3
3
  /**
4
4
  * Request routing for the preview service worker, with no worker globals in it.
@@ -35,7 +35,9 @@ export function createSwRouter(options) {
35
35
  const rest = url.pathname.slice(prefix.length);
36
36
  const slash = rest.indexOf('/');
37
37
  const id = slash < 0 ? rest : rest.slice(0, slash);
38
- if (!id)
38
+ // Ids are `[A-Za-z0-9_-]`; a dotted segment is a real file beside the worker
39
+ // (`sw.js`, `relay.html`), which must load from the network.
40
+ if (!id || !PREVIEW_ID.test(id))
39
41
  return null;
40
42
  return { id, path: slash < 0 ? '/' : rest.slice(slash), search: url.search };
41
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"swRouter.js","sourceRoot":"","sources":["../src/swRouter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"swRouter.js","sourceRoot":"","sources":["../src/swRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export interface PreviewTransport {
2
+ /** The origin previews are served from. */
3
+ readonly origin: string;
4
+ /** The worker's scope path on that origin. */
5
+ readonly scope: string;
6
+ /** Hand the worker the port that serves preview `id`. */
7
+ attach(id: string, port: MessagePort): void;
8
+ /** The worker restarted and wants `id` again. */
9
+ onReattach(listener: (id: string) => void): () => void;
10
+ /** Reload every frame showing the preview at `base`. */
11
+ reload(base: string): void;
12
+ dispose(): void;
13
+ }
14
+ export declare function sameOriginTransport(serviceWorkerUrl: string, scopeOption?: string): Promise<PreviewTransport>;
15
+ export declare function relayTransport(relayUrl: string): Promise<PreviewTransport>;
16
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C,iDAAiD;IACjD,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvD,wDAAwD;IACxD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;CACjB;AAKD,wBAAsB,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6BnH;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwDhF"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * How the host page reaches the preview service worker.
3
+ *
4
+ * Same origin: the page registers the worker itself. Simple, but the preview then
5
+ * runs on the host's origin, with the host's storage and session — fine for a
6
+ * page that only previews code its own user wrote and trusts.
7
+ *
8
+ * Relay: the worker lives on a SEPARATE preview origin. A hidden `relay.html` on
9
+ * that origin registers it and passes ports and reloads across. The preview can
10
+ * then never read the host's cookies, storage or DOM, which is what an editor
11
+ * running AI-written code or arbitrary npm packages needs.
12
+ */
13
+ import { ATTACH, REATTACH, RELAY_ATTACH, RELAY_READY, RELAY_REATTACH, RELAY_RELOAD, } from './protocol.js';
14
+ import { broadcastReload, waitForActive } from './worker.js';
15
+ /** How long the relay has to register its worker before boot gives up. */
16
+ const RELAY_TIMEOUT_MS = 15_000;
17
+ export async function sameOriginTransport(serviceWorkerUrl, scopeOption) {
18
+ const container = navigator.serviceWorker;
19
+ if (!container)
20
+ throw new Error('Service workers are unavailable here (a secure context — https or localhost — is required).');
21
+ const scriptUrl = new URL(serviceWorkerUrl, location.href);
22
+ const scope = new URL(scopeOption ?? './', scriptUrl).pathname;
23
+ const registration = await container.register(scriptUrl.href, { scope });
24
+ await waitForActive(registration);
25
+ const listeners = new Set();
26
+ const onWorkerMessage = (event) => {
27
+ if (event.data?.type === REATTACH && typeof event.data.id === 'string')
28
+ for (const listener of listeners)
29
+ listener(event.data.id);
30
+ };
31
+ container.addEventListener('message', onWorkerMessage);
32
+ return {
33
+ origin: location.origin,
34
+ scope,
35
+ attach: (id, port) => registration.active?.postMessage({ type: ATTACH, id }, [port]),
36
+ onReattach(listener) {
37
+ listeners.add(listener);
38
+ return () => listeners.delete(listener);
39
+ },
40
+ reload: broadcastReload,
41
+ dispose() {
42
+ container.removeEventListener('message', onWorkerMessage);
43
+ listeners.clear();
44
+ },
45
+ };
46
+ }
47
+ export async function relayTransport(relayUrl) {
48
+ const url = new URL(relayUrl, location.href);
49
+ const frame = document.createElement('iframe');
50
+ frame.src = url.href;
51
+ frame.title = 'Preview relay';
52
+ frame.setAttribute('aria-hidden', 'true');
53
+ frame.tabIndex = -1;
54
+ frame.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden';
55
+ const fromRelay = (event) => event.origin === url.origin && event.source === frame.contentWindow;
56
+ const ready = new Promise((resolve, reject) => {
57
+ const timer = setTimeout(() => {
58
+ window.removeEventListener('message', onReady);
59
+ reject(new Error(`The preview relay at ${url.origin} did not start.`));
60
+ }, RELAY_TIMEOUT_MS);
61
+ function onReady(event) {
62
+ if (!fromRelay(event) || event.data?.type !== RELAY_READY)
63
+ return;
64
+ clearTimeout(timer);
65
+ window.removeEventListener('message', onReady);
66
+ if (typeof event.data.error === 'string')
67
+ reject(new Error(event.data.error));
68
+ else
69
+ resolve();
70
+ }
71
+ window.addEventListener('message', onReady);
72
+ });
73
+ document.body.appendChild(frame);
74
+ try {
75
+ await ready;
76
+ }
77
+ catch (error) {
78
+ frame.remove();
79
+ throw error;
80
+ }
81
+ const listeners = new Set();
82
+ const onRelayMessage = (event) => {
83
+ if (!fromRelay(event) || event.data?.type !== RELAY_REATTACH || typeof event.data.id !== 'string')
84
+ return;
85
+ for (const listener of listeners)
86
+ listener(event.data.id);
87
+ };
88
+ window.addEventListener('message', onRelayMessage);
89
+ const post = (message, transfer = []) => frame.contentWindow?.postMessage(message, url.origin, transfer);
90
+ return {
91
+ origin: url.origin,
92
+ scope: new URL('./', url).pathname,
93
+ attach: (id, port) => post({ type: RELAY_ATTACH, id }, [port]),
94
+ onReattach(listener) {
95
+ listeners.add(listener);
96
+ return () => listeners.delete(listener);
97
+ },
98
+ reload: (base) => post({ type: RELAY_RELOAD, base }),
99
+ dispose() {
100
+ window.removeEventListener('message', onRelayMessage);
101
+ listeners.clear();
102
+ frame.remove();
103
+ },
104
+ };
105
+ }
106
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Resolve once the registration has an active worker. */
2
+ export declare function waitForActive(registration: ServiceWorkerRegistration): Promise<ServiceWorker>;
3
+ /** Reload every frame (on this origin) showing the preview served at `base`. */
4
+ export declare function broadcastReload(base: string): void;
5
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAKA,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,YAAY,EAAE,yBAAyB,GAAG,OAAO,CAAC,aAAa,CAAC,CAU7F;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAKlD"}
package/dist/worker.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Small helpers shared by the page-side transports and the relay page.
3
+ */
4
+ import { reloadChannelName } from '@seanhogg/builderforce-webcontainers-core';
5
+ /** Resolve once the registration has an active worker. */
6
+ export function waitForActive(registration) {
7
+ if (registration.active)
8
+ return Promise.resolve(registration.active);
9
+ const installing = registration.installing ?? registration.waiting;
10
+ if (!installing)
11
+ return Promise.reject(new Error('The preview service worker did not install.'));
12
+ return new Promise((resolve, reject) => {
13
+ installing.addEventListener('statechange', () => {
14
+ if (installing.state === 'activated' && registration.active)
15
+ resolve(registration.active);
16
+ if (installing.state === 'redundant')
17
+ reject(new Error('The preview service worker failed to install.'));
18
+ });
19
+ });
20
+ }
21
+ /** Reload every frame (on this origin) showing the preview served at `base`. */
22
+ export function broadcastReload(base) {
23
+ if (typeof BroadcastChannel === 'undefined')
24
+ return;
25
+ const channel = new BroadcastChannel(reloadChannelName(base));
26
+ channel.postMessage('reload');
27
+ channel.close();
28
+ }
29
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAE9E,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,YAAuC;IACnE,IAAI,YAAY,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACrE,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,IAAI,YAAY,CAAC,OAAO,CAAC;IACnE,IAAI,CAAC,UAAU;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC,CAAC;IACjG,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,UAAU,CAAC,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE;YAC9C,IAAI,UAAU,CAAC,KAAK,KAAK,WAAW,IAAI,YAAY,CAAC,MAAM;gBAAE,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1F,IAAI,UAAU,CAAC,KAAK,KAAK,WAAW;gBAAE,MAAM,CAAC,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC,CAAC;QAC3G,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,OAAO,gBAAgB,KAAK,WAAW;QAAE,OAAO;IACpD,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,CAAC,KAAK,EAAE,CAAC;AAClB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-webcontainers",
3
- "version": "2026.10.1",
3
+ "version": "2026.10.2",
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",
@@ -10,7 +10,12 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
12
  },
13
- "./sw.js": "./dist/sw.js"
13
+ "./assets": {
14
+ "types": "./dist/assets.d.ts",
15
+ "import": "./dist/assets.js"
16
+ },
17
+ "./sw.js": "./dist/sw.js",
18
+ "./relay.html": "./dist/relay.html"
14
19
  },
15
20
  "type": "module",
16
21
  "sideEffects": false,
@@ -53,7 +58,7 @@
53
58
  },
54
59
  "dependencies": {
55
60
  "esbuild-wasm": "^0.24.0",
56
- "@seanhogg/builderforce-webcontainers-core": "^2026.10.1"
61
+ "@seanhogg/builderforce-webcontainers-core": "^2026.10.2"
57
62
  },
58
63
  "devDependencies": {
59
64
  "esbuild": "^0.24.0",
package/src/host.ts CHANGED
@@ -8,24 +8,49 @@ import {
8
8
  type ProjectProfile,
9
9
  type Transformer,
10
10
  } from '@seanhogg/builderforce-webcontainers-core';
11
- import { ATTACH, REATTACH, isWireRequest, previewBase, type WireResponse } from './protocol.js';
11
+ import { PREVIEW_ID, isWireRequest, previewBase, type WireResponse } from './protocol.js';
12
+ import { relayTransport, sameOriginTransport } from './transport.js';
12
13
  import { createEsbuildWasmTransformer } from './esbuildWasm.js';
13
14
 
14
- export interface BootOptions {
15
+ /** Serve previews on THIS page's origin. Only for code the page's own user trusts. */
16
+ export interface SameOriginOptions {
15
17
  /** Where the host serves this package's `dist/sw.js` (e.g. `/__bfwc/sw.js`). */
16
18
  serviceWorkerUrl: string;
17
19
  /** Worker scope. Defaults to the worker script's directory. */
18
20
  scope?: string;
21
+ relayUrl?: never;
22
+ }
23
+
24
+ /**
25
+ * Serve previews on a SEPARATE origin, isolated from this page's cookies, storage
26
+ * and DOM. That origin serves `dist/relay.html` and `dist/sw.js` side by side, with
27
+ * `frame-ancestors` limited to the pages allowed to drive it.
28
+ */
29
+ export interface RelayOptions {
30
+ /** e.g. `https://preview.example.com/__bfwc/relay.html`. */
31
+ relayUrl: string;
32
+ serviceWorkerUrl?: never;
33
+ scope?: never;
34
+ }
35
+
36
+ export type BootOptions = (SameOriginOptions | RelayOptions) & CommonBootOptions;
37
+
38
+ export interface CommonBootOptions {
19
39
  /** Compiler. Defaults to esbuild-wasm. */
20
40
  transformer?: Transformer;
21
41
  /** Package CDN factory. Defaults to esm.sh. */
22
42
  cdn?: DevServerOptions['cdn'];
23
- /** Stable id (e.g. the project id) — keeps the preview URL stable across reloads. */
43
+ /** Stable id (e.g. the project id; `[A-Za-z0-9_-]`) — keeps the preview URL stable across reloads. */
24
44
  id?: string;
25
45
  /** The "Built with Builderforce.ai" badge in the preview. Default true — please keep it. */
26
46
  attribution?: boolean;
47
+ /** Reload open previews when files change. Default true. */
48
+ liveReload?: boolean;
27
49
  }
28
50
 
51
+ /** Edits arrive in bursts (a save, an agent writing several files): reload once per burst. */
52
+ const RELOAD_DEBOUNCE_MS = 120;
53
+
29
54
  export interface PreviewError {
30
55
  message: string;
31
56
  stack?: string;
@@ -46,18 +71,6 @@ export interface PreviewRuntime {
46
71
  dispose(): void;
47
72
  }
48
73
 
49
- function waitForActive(registration: ServiceWorkerRegistration): Promise<ServiceWorker> {
50
- if (registration.active) return Promise.resolve(registration.active);
51
- const installing = registration.installing ?? registration.waiting;
52
- if (!installing) return Promise.reject(new Error('The preview service worker did not install.'));
53
- return new Promise((resolve, reject) => {
54
- installing.addEventListener('statechange', () => {
55
- if (installing.state === 'activated' && registration.active) resolve(registration.active);
56
- if (installing.state === 'redundant') reject(new Error('The preview service worker failed to install.'));
57
- });
58
- });
59
- }
60
-
61
74
  function randomId(): string {
62
75
  return globalThis.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 16) ?? Math.random().toString(36).slice(2, 18);
63
76
  }
@@ -75,44 +88,56 @@ function toWire(reqId: number, served: Awaited<ReturnType<DevServer['handle']>>)
75
88
  * it, and a service worker that routes the preview iframe's requests back here.
76
89
  *
77
90
  * Needs a secure context (https or localhost) for the service worker — and
78
- * nothing else: no cross-origin isolation headers, no SharedArrayBuffer.
91
+ * nothing else: no cross-origin isolation headers, no SharedArrayBuffer. Pass
92
+ * `relayUrl` to serve the preview from its own origin (see `RelayOptions`).
79
93
  */
80
94
  export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewRuntime> {
81
- const container = navigator.serviceWorker;
82
- if (!container) throw new Error('Service workers are unavailable here (a secure context — https or localhost — is required).');
95
+ const id = options.id ?? randomId();
96
+ if (!PREVIEW_ID.test(id)) throw new Error(`Preview id "${id}" may only contain letters, digits, "_" and "-".`);
83
97
 
84
- const scriptUrl = new URL(options.serviceWorkerUrl, location.href);
85
- const scope = new URL(options.scope ?? './', scriptUrl).pathname;
86
- const registration = await container.register(scriptUrl.href, { scope });
87
- await waitForActive(registration);
98
+ const transport = options.relayUrl
99
+ ? await relayTransport(options.relayUrl)
100
+ : await sameOriginTransport((options as SameOriginOptions).serviceWorkerUrl, options.scope);
88
101
 
89
- const id = options.id ?? randomId();
90
- const base = previewBase(scope, id);
102
+ const base = previewBase(transport.scope, id);
91
103
  const fs = new VirtualFileSystem();
92
- const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
93
- const server = new DevServer({ fs, transformer, base, cdn: options.cdn, attribution: options.attribution });
104
+ let server: DevServer;
105
+ try {
106
+ const transformer = options.transformer ?? (await createEsbuildWasmTransformer());
107
+ server = new DevServer({ fs, transformer, base, cdn: options.cdn, attribution: options.attribution });
108
+ } catch (error) {
109
+ transport.dispose();
110
+ throw error;
111
+ }
94
112
 
95
- const attach = (worker: ServiceWorker) => {
113
+ const attach = () => {
96
114
  const channel = new MessageChannel();
97
115
  channel.port1.onmessage = async (event) => {
98
116
  if (!isWireRequest(event.data)) return;
99
117
  const { message, transfer } = toWire(event.data.reqId, await server.handle(event.data.path, event.data.search));
100
118
  channel.port1.postMessage(message, transfer);
101
119
  };
102
- worker.postMessage({ type: ATTACH, id }, [channel.port2]);
120
+ transport.attach(id, channel.port2);
103
121
  };
104
- attach(registration.active!);
122
+ attach();
123
+ const stopReattach = transport.onReattach((wanted) => {
124
+ if (wanted === id) attach();
125
+ });
105
126
 
106
- const onWorkerMessage = (event: MessageEvent) => {
107
- if (event.data?.type === REATTACH && event.data.id === id && registration.active) attach(registration.active);
108
- };
109
- container.addEventListener('message', onWorkerMessage);
127
+ // Live reload: any change to the files reloads every frame showing this preview.
128
+ let reloadTimer: ReturnType<typeof setTimeout> | undefined;
129
+ const stopWatching = options.liveReload === false
130
+ ? undefined
131
+ : fs.watch(() => {
132
+ clearTimeout(reloadTimer);
133
+ reloadTimer = setTimeout(() => transport.reload(base), RELOAD_DEBOUNCE_MS);
134
+ });
110
135
 
111
136
  const errorListeners = new Set<(error: PreviewError) => void>();
112
137
  const onWindowMessage = (event: MessageEvent) => {
113
138
  const data = event.data as Partial<PreviewError> & { type?: string };
114
139
  if (data?.type !== ERROR_MESSAGE_TYPE || typeof data.href !== 'string') return;
115
- if (!new URL(data.href).pathname.startsWith(base)) return; // another runtime's preview
140
+ if (event.origin !== transport.origin || !new URL(data.href).pathname.startsWith(base)) return; // another runtime's preview
116
141
  const error: PreviewError = { message: String(data.message ?? ''), stack: data.stack, href: data.href };
117
142
  for (const listener of errorListeners) listener(error);
118
143
  };
@@ -120,7 +145,7 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
120
145
 
121
146
  return {
122
147
  id,
123
- url: new URL(base, location.origin).href,
148
+ url: new URL(base, transport.origin).href,
124
149
  fs,
125
150
  server,
126
151
  mount: (files) => fs.mount(files),
@@ -130,9 +155,12 @@ export async function bootPreviewRuntime(options: BootOptions): Promise<PreviewR
130
155
  return () => errorListeners.delete(listener);
131
156
  },
132
157
  dispose() {
133
- container.removeEventListener('message', onWorkerMessage);
134
158
  window.removeEventListener('message', onWindowMessage);
135
159
  errorListeners.clear();
160
+ clearTimeout(reloadTimer);
161
+ stopWatching?.();
162
+ stopReattach();
163
+ transport.dispose();
136
164
  server.dispose();
137
165
  },
138
166
  };
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { bootPreviewRuntime } from './host.js';
2
- export type { BootOptions, PreviewRuntime, PreviewError } from './host.js';
2
+ export type { BootOptions, SameOriginOptions, RelayOptions, CommonBootOptions, PreviewRuntime, PreviewError } from './host.js';
3
+ export type { PreviewTransport } from './transport.js';
3
4
  export { createEsbuildWasmTransformer } from './esbuildWasm.js';
4
5
  export type { EsbuildWasmOptions } from './esbuildWasm.js';
5
6
  export { createSwRouter } from './swRouter.js';
6
7
  export type { SwRouter, SwRouterOptions, PortLike, PreviewTarget, RoutedResponse } from './swRouter.js';
7
- export { previewBase, previewPrefix, PREVIEW_SEGMENT } from './protocol.js';
8
+ export { previewBase, previewPrefix, PREVIEW_SEGMENT, PREVIEW_ID } from './protocol.js';
8
9
  export * from '@seanhogg/builderforce-webcontainers-core';
package/src/protocol.ts CHANGED
@@ -6,11 +6,31 @@
6
6
 
7
7
  export const PREVIEW_SEGMENT = '__bfwc';
8
8
 
9
+ /**
10
+ * What a preview id may contain. No dots, so a file served beside the worker
11
+ * (`sw.js`, `relay.html`) can never be mistaken for a preview.
12
+ */
13
+ export const PREVIEW_ID = /^[A-Za-z0-9_-]+$/;
14
+
9
15
  /** page → worker, with a MessagePort in `ports[0]`: "serve preview `id` over this port". */
10
16
  export const ATTACH = 'bfwc:attach';
11
17
  /** worker → pages: "I lost the port for `id` (I was restarted) — attach again". */
12
18
  export const REATTACH = 'bfwc:reattach';
13
19
 
20
+ /**
21
+ * The cross-origin relay (`relay.html`, served on the preview origin and framed,
22
+ * hidden, by the host). The host cannot talk to a worker on another origin, so the
23
+ * relay registers it and passes messages both ways.
24
+ */
25
+ /** relay → host: the worker is active; send ports. */
26
+ export const RELAY_READY = 'bfwc:relay-ready';
27
+ /** host → relay, with a MessagePort: forward as ATTACH for `id`. */
28
+ export const RELAY_ATTACH = 'bfwc:relay-attach';
29
+ /** relay → host: the worker asked for `id` again. */
30
+ export const RELAY_REATTACH = 'bfwc:relay-reattach';
31
+ /** host → relay: reload every frame showing the preview at `base`. */
32
+ export const RELAY_RELOAD = 'bfwc:relay-reload';
33
+
14
34
  export interface AttachMessage {
15
35
  type: typeof ATTACH;
16
36
  id: string;
package/src/relay.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The relay page's script (bundled inline into dist/relay.html). Served on the
3
+ * preview origin and framed, hidden, by the host page, it registers the preview
4
+ * worker on ITS origin and passes ports, re-attach requests and reloads between
5
+ * the two. See `relayTransport`.
6
+ *
7
+ * Whoever may frame this page may drive it, so the server that hosts relay.html
8
+ * must restrict that with `Content-Security-Policy: frame-ancestors <host origins>`.
9
+ */
10
+ import { ATTACH, REATTACH, RELAY_ATTACH, RELAY_READY, RELAY_REATTACH, RELAY_RELOAD } from './protocol.js';
11
+ import { broadcastReload, waitForActive } from './worker.js';
12
+
13
+ const host = window.parent;
14
+
15
+ async function start(): Promise<void> {
16
+ const container = navigator.serviceWorker;
17
+ if (!container) throw new Error('Service workers are unavailable on the preview origin.');
18
+ const registration = await container.register(new URL('./sw.js', location.href).href, { scope: './' });
19
+ await waitForActive(registration);
20
+
21
+ container.addEventListener('message', (event) => {
22
+ if (event.data?.type === REATTACH && typeof event.data.id === 'string') host.postMessage({ type: RELAY_REATTACH, id: event.data.id }, '*');
23
+ });
24
+
25
+ addEventListener('message', (event) => {
26
+ if (event.source !== host) return;
27
+ const data = event.data as { type?: string; id?: unknown; base?: unknown } | null;
28
+ if (data?.type === RELAY_ATTACH && typeof data.id === 'string' && event.ports[0]) {
29
+ registration.active?.postMessage({ type: ATTACH, id: data.id }, [event.ports[0]]);
30
+ } else if (data?.type === RELAY_RELOAD && typeof data.base === 'string') {
31
+ broadcastReload(data.base);
32
+ }
33
+ });
34
+
35
+ host.postMessage({ type: RELAY_READY }, '*');
36
+ }
37
+
38
+ start().catch((error: unknown) => {
39
+ host.postMessage({ type: RELAY_READY, error: error instanceof Error ? error.message : String(error) }, '*');
40
+ });
package/src/swRouter.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isWireResponse, previewPrefix, type WireResponse } from './protocol.js';
1
+ import { PREVIEW_ID, isWireResponse, previewPrefix, type WireResponse } from './protocol.js';
2
2
 
3
3
  /** The part of MessagePort the router uses — a fake in tests, a real port in the worker. */
4
4
  export interface PortLike {
@@ -65,7 +65,9 @@ export function createSwRouter(options: SwRouterOptions) {
65
65
  const rest = url.pathname.slice(prefix.length);
66
66
  const slash = rest.indexOf('/');
67
67
  const id = slash < 0 ? rest : rest.slice(0, slash);
68
- if (!id) return null;
68
+ // Ids are `[A-Za-z0-9_-]`; a dotted segment is a real file beside the worker
69
+ // (`sw.js`, `relay.html`), which must load from the network.
70
+ if (!id || !PREVIEW_ID.test(id)) return null;
69
71
  return { id, path: slash < 0 ? '/' : rest.slice(slash), search: url.search };
70
72
  }
71
73
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * How the host page reaches the preview service worker.
3
+ *
4
+ * Same origin: the page registers the worker itself. Simple, but the preview then
5
+ * runs on the host's origin, with the host's storage and session — fine for a
6
+ * page that only previews code its own user wrote and trusts.
7
+ *
8
+ * Relay: the worker lives on a SEPARATE preview origin. A hidden `relay.html` on
9
+ * that origin registers it and passes ports and reloads across. The preview can
10
+ * then never read the host's cookies, storage or DOM, which is what an editor
11
+ * running AI-written code or arbitrary npm packages needs.
12
+ */
13
+ import {
14
+ ATTACH,
15
+ REATTACH,
16
+ RELAY_ATTACH,
17
+ RELAY_READY,
18
+ RELAY_REATTACH,
19
+ RELAY_RELOAD,
20
+ } from './protocol.js';
21
+ import { broadcastReload, waitForActive } from './worker.js';
22
+
23
+ export interface PreviewTransport {
24
+ /** The origin previews are served from. */
25
+ readonly origin: string;
26
+ /** The worker's scope path on that origin. */
27
+ readonly scope: string;
28
+ /** Hand the worker the port that serves preview `id`. */
29
+ attach(id: string, port: MessagePort): void;
30
+ /** The worker restarted and wants `id` again. */
31
+ onReattach(listener: (id: string) => void): () => void;
32
+ /** Reload every frame showing the preview at `base`. */
33
+ reload(base: string): void;
34
+ dispose(): void;
35
+ }
36
+
37
+ /** How long the relay has to register its worker before boot gives up. */
38
+ const RELAY_TIMEOUT_MS = 15_000;
39
+
40
+ export async function sameOriginTransport(serviceWorkerUrl: string, scopeOption?: string): Promise<PreviewTransport> {
41
+ const container = navigator.serviceWorker;
42
+ if (!container) throw new Error('Service workers are unavailable here (a secure context — https or localhost — is required).');
43
+
44
+ const scriptUrl = new URL(serviceWorkerUrl, location.href);
45
+ const scope = new URL(scopeOption ?? './', scriptUrl).pathname;
46
+ const registration = await container.register(scriptUrl.href, { scope });
47
+ await waitForActive(registration);
48
+
49
+ const listeners = new Set<(id: string) => void>();
50
+ const onWorkerMessage = (event: MessageEvent) => {
51
+ if (event.data?.type === REATTACH && typeof event.data.id === 'string') for (const listener of listeners) listener(event.data.id);
52
+ };
53
+ container.addEventListener('message', onWorkerMessage);
54
+
55
+ return {
56
+ origin: location.origin,
57
+ scope,
58
+ attach: (id, port) => registration.active?.postMessage({ type: ATTACH, id }, [port]),
59
+ onReattach(listener) {
60
+ listeners.add(listener);
61
+ return () => listeners.delete(listener);
62
+ },
63
+ reload: broadcastReload,
64
+ dispose() {
65
+ container.removeEventListener('message', onWorkerMessage);
66
+ listeners.clear();
67
+ },
68
+ };
69
+ }
70
+
71
+ export async function relayTransport(relayUrl: string): Promise<PreviewTransport> {
72
+ const url = new URL(relayUrl, location.href);
73
+ const frame = document.createElement('iframe');
74
+ frame.src = url.href;
75
+ frame.title = 'Preview relay';
76
+ frame.setAttribute('aria-hidden', 'true');
77
+ frame.tabIndex = -1;
78
+ frame.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden';
79
+
80
+ const fromRelay = (event: MessageEvent) => event.origin === url.origin && event.source === frame.contentWindow;
81
+
82
+ const ready = new Promise<void>((resolve, reject) => {
83
+ const timer = setTimeout(() => {
84
+ window.removeEventListener('message', onReady);
85
+ reject(new Error(`The preview relay at ${url.origin} did not start.`));
86
+ }, RELAY_TIMEOUT_MS);
87
+ function onReady(event: MessageEvent) {
88
+ if (!fromRelay(event) || event.data?.type !== RELAY_READY) return;
89
+ clearTimeout(timer);
90
+ window.removeEventListener('message', onReady);
91
+ if (typeof event.data.error === 'string') reject(new Error(event.data.error));
92
+ else resolve();
93
+ }
94
+ window.addEventListener('message', onReady);
95
+ });
96
+ document.body.appendChild(frame);
97
+ try {
98
+ await ready;
99
+ } catch (error) {
100
+ frame.remove();
101
+ throw error;
102
+ }
103
+
104
+ const listeners = new Set<(id: string) => void>();
105
+ const onRelayMessage = (event: MessageEvent) => {
106
+ if (!fromRelay(event) || event.data?.type !== RELAY_REATTACH || typeof event.data.id !== 'string') return;
107
+ for (const listener of listeners) listener(event.data.id);
108
+ };
109
+ window.addEventListener('message', onRelayMessage);
110
+ const post = (message: unknown, transfer: Transferable[] = []) => frame.contentWindow?.postMessage(message, url.origin, transfer);
111
+
112
+ return {
113
+ origin: url.origin,
114
+ scope: new URL('./', url).pathname,
115
+ attach: (id, port) => post({ type: RELAY_ATTACH, id }, [port]),
116
+ onReattach(listener) {
117
+ listeners.add(listener);
118
+ return () => listeners.delete(listener);
119
+ },
120
+ reload: (base) => post({ type: RELAY_RELOAD, base }),
121
+ dispose() {
122
+ window.removeEventListener('message', onRelayMessage);
123
+ listeners.clear();
124
+ frame.remove();
125
+ },
126
+ };
127
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Small helpers shared by the page-side transports and the relay page.
3
+ */
4
+ import { reloadChannelName } from '@seanhogg/builderforce-webcontainers-core';
5
+
6
+ /** Resolve once the registration has an active worker. */
7
+ export function waitForActive(registration: ServiceWorkerRegistration): Promise<ServiceWorker> {
8
+ if (registration.active) return Promise.resolve(registration.active);
9
+ const installing = registration.installing ?? registration.waiting;
10
+ if (!installing) return Promise.reject(new Error('The preview service worker did not install.'));
11
+ return new Promise((resolve, reject) => {
12
+ installing.addEventListener('statechange', () => {
13
+ if (installing.state === 'activated' && registration.active) resolve(registration.active);
14
+ if (installing.state === 'redundant') reject(new Error('The preview service worker failed to install.'));
15
+ });
16
+ });
17
+ }
18
+
19
+ /** Reload every frame (on this origin) showing the preview served at `base`. */
20
+ export function broadcastReload(base: string): void {
21
+ if (typeof BroadcastChannel === 'undefined') return;
22
+ const channel = new BroadcastChannel(reloadChannelName(base));
23
+ channel.postMessage('reload');
24
+ channel.close();
25
+ }