@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 +6 -0
- package/dist/assets.d.ts +4 -0
- package/dist/assets.js +4 -0
- package/dist/host.d.ts +22 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +42 -38
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +18 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +18 -0
- package/dist/protocol.js.map +1 -1
- package/dist/relay.html +2 -0
- package/dist/sw.js +1 -1
- package/dist/swRouter.d.ts.map +1 -1
- package/dist/swRouter.js +4 -2
- package/dist/swRouter.js.map +1 -1
- package/dist/transport.d.ts +16 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +106 -0
- package/dist/transport.js.map +1 -0
- package/dist/worker.d.ts +5 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +29 -0
- package/dist/worker.js.map +1 -0
- package/package.json +8 -3
- package/src/host.ts +64 -36
- package/src/index.ts +3 -2
- package/src/protocol.ts +20 -0
- package/src/relay.ts +40 -0
- package/src/swRouter.ts +4 -2
- package/src/transport.ts +127 -0
- package/src/worker.ts +25 -0
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).
|
package/dist/assets.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
package/dist/host.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
2
|
+
import { PREVIEW_ID, isWireRequest, previewBase } from './protocol.js';
|
|
3
|
+
import { relayTransport, sameOriginTransport } from './transport.js';
|
|
3
4
|
import { createEsbuildWasmTransformer } from './esbuildWasm.js';
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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":"AAAA,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,GAMlB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/protocol.d.ts
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 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;
|
package/dist/protocol.d.ts.map
CHANGED
|
@@ -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;
|
package/dist/protocol.js.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/relay.html
ADDED
|
@@ -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
|
|
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})))});})();
|
package/dist/swRouter.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
}
|
package/dist/swRouter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swRouter.js","sourceRoot":"","sources":["../src/swRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAqB,MAAM,eAAe,CAAC;AA+
|
|
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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"}
|
package/dist/worker.d.ts
ADDED
|
@@ -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.
|
|
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
|
-
"./
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
82
|
-
if (!
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
90
|
-
const base = previewBase(scope, id);
|
|
102
|
+
const base = previewBase(transport.scope, id);
|
|
91
103
|
const fs = new VirtualFileSystem();
|
|
92
|
-
|
|
93
|
-
|
|
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 = (
|
|
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
|
-
|
|
120
|
+
transport.attach(id, channel.port2);
|
|
103
121
|
};
|
|
104
|
-
attach(
|
|
122
|
+
attach();
|
|
123
|
+
const stopReattach = transport.onReattach((wanted) => {
|
|
124
|
+
if (wanted === id) attach();
|
|
125
|
+
});
|
|
105
126
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
package/src/transport.ts
ADDED
|
@@ -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
|
+
}
|