diff --git a/package.json b/package.json
index 7175dc26..d573237d 100644
--- a/package.json
+++ b/package.json
@@ -58,10 +58,10 @@
"start": "npm run dev",
"dev": "tsc && concurrently \"npm:build.watch\" \"npm:tsc.watch\" -n build,tsc -c magenta,yellow",
"release": "npm run build && npm test && np --no-tests",
- "serve": "node scripts/serve.cjs 4000",
- "serve.test": "node scripts/serve.cjs 4001",
- "serve.atomics": "node scripts/serve.cjs 4002 --atomics",
- "serve.atomics.test": "node scripts/serve.cjs 4003 --atomics",
+ "serve": "node scripts/serve.cjs 4000 4100",
+ "serve.test": "node scripts/serve.cjs 4001 4100",
+ "serve.atomics": "node scripts/serve.cjs 4002 4100 --atomics",
+ "serve.atomics.test": "node scripts/serve.cjs 4003 4100 --atomics",
"test": "npm run test.unit && npm run test.chromium",
"test.atomics": "playwright test tests/integrations tests/platform --config playwright.atomics.config.ts --browser=chromium",
"test.chromium": "playwright test tests/integrations tests/platform --browser=chromium",
diff --git a/scripts/build-isolation-iframe.ts b/scripts/build-isolation-iframe.ts
new file mode 100644
index 00000000..0ab2e9b6
--- /dev/null
+++ b/scripts/build-isolation-iframe.ts
@@ -0,0 +1,61 @@
+import type { OutputOptions, Plugin, RollupOptions } from 'rollup';
+import { BuildOptions, jsBannerPlugin, MessageType, watchDir } from './utils';
+import { join } from 'path';
+import { minifyPlugin } from './minify';
+import { webWorkerBlobUrlPlugin } from './build-web-worker';
+
+export function buildIsolation(opts: BuildOptions): RollupOptions[] {
+ const rollups: RollupOptions[] = [];
+
+ rollups.push(buildIsolationIframe(opts, 'sw', true), buildIsolationIframe(opts, 'atomics', true));
+ if (!opts.isDev) {
+ rollups.push(
+ buildIsolationIframe(opts, 'sw', false),
+ buildIsolationIframe(opts, 'atomics', false)
+ );
+ }
+
+ return rollups;
+}
+
+function buildIsolationIframe(
+ opts: BuildOptions,
+ msgType: MessageType,
+ debug: boolean
+): RollupOptions {
+ const outName = `partytown-isolation-${msgType}.html`;
+ const outDir = debug ? opts.distLibDebugDir : opts.distLibDir;
+ const output: OutputOptions = {
+ file: join(outDir, outName),
+ format: 'es',
+ exports: 'none',
+ plugins: [...minifyPlugin(opts, debug)],
+ };
+
+ return {
+ input: join(opts.tscLibDir, 'isolation-iframe', 'index.js'),
+ output,
+ plugins: [
+ webWorkerBlobUrlPlugin(opts, msgType, debug),
+ watchDir(opts, join(opts.tscLibDir, 'isolation-iframe')),
+ watchDir(opts, join(opts.tscLibDir, 'web-worker')),
+ jsBannerPlugin(opts),
+ isolationIframeHtmlPlugin(),
+ ],
+ };
+}
+
+function isolationIframeHtmlPlugin(): Plugin {
+ return {
+ name: 'isolationIframeHtml',
+ async generateBundle(_, bundles) {
+ for (const f in bundles) {
+ const bundle = bundles[f];
+ if (bundle.type === 'chunk') {
+ bundle.code = `
`;
+ }
+ }
+ },
+ };
+}
+
diff --git a/scripts/index.ts b/scripts/index.ts
index bfeb52b8..949d6cd8 100644
--- a/scripts/index.ts
+++ b/scripts/index.ts
@@ -8,6 +8,7 @@ import { buildReact } from './build-react';
import { buildServiceWorker } from './build-service-worker';
import { buildServices } from './build-services';
import { buildUtils } from './build-utils';
+import { buildIsolation } from './build-isolation-iframe';
import { emptyDir, ensureDir, readJsonSync, writeFile } from 'fs-extra';
import { join } from 'path';
@@ -24,6 +25,7 @@ export async function runBuild(rootDir: string, isDev: boolean, isReleaseBuild:
buildMainSnippet(opts),
buildServiceWorker(opts),
...buildAtomics(opts),
+ ...buildIsolation(opts),
buildMediaImplementation(opts),
buildIntegration(opts),
buildServices(opts),
diff --git a/scripts/serve.cjs b/scripts/serve.cjs
index d4df044a..98de36ff 100644
--- a/scripts/serve.cjs
+++ b/scripts/serve.cjs
@@ -1,9 +1,15 @@
const { createServer } = require('./server.cjs');
const port = parseInt(process.argv[2], 10);
+const sandboxPort = process.argv[3] && parseInt(process.argv[3], 10);
const enableAtomics = process.argv.includes('--atomics');
(async () => {
const server = await createServer(port, enableAtomics);
console.log(`Serving${server.enableAtomics ? ` (atomics)` : ``}: ${server.address}tests/`);
+
+ if (sandboxPort) {
+ const sandboxServer = await createServer(sandboxPort, enableAtomics);
+ console.log(`Sandbox: ${sandboxServer.address}~partytown/`);
+ }
})();
diff --git a/scripts/server.cjs b/scripts/server.cjs
index e1bc764a..55fd7c50 100644
--- a/scripts/server.cjs
+++ b/scripts/server.cjs
@@ -60,9 +60,11 @@ exports.createServer = function (port, enableAtomics) {
if (url.searchParams.get('coep') === 'require-corp') {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
} else if (url.searchParams.get('coep') !== 'false') {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
}
}
diff --git a/src/lib/isolation-iframe/index.ts b/src/lib/isolation-iframe/index.ts
new file mode 100644
index 00000000..5d16f826
--- /dev/null
+++ b/src/lib/isolation-iframe/index.ts
@@ -0,0 +1,70 @@
+import WebWorkerBlob from '../build-modules/web-worker-blob';
+import WebWorkerUrl from '../build-modules/web-worker-url';
+import {
+ IsolationIframeMessageType,
+ MessageFromSandboxToIsolationIframe,
+ PartytownWebWorker,
+ PassMessagePortToServiceWorkerRequest,
+ WorkerMessageType,
+} from '../types';
+import { createPartytownWorker, registerServiceWorker } from '../utils';
+
+const useAtomics = crossOriginIsolated;
+
+window.addEventListener(
+ 'message',
+ (iframeEv: MessageEvent) => {
+ if (iframeEv.data[0] === IsolationIframeMessageType.InitializeIframe) {
+ const { $debug$, $libPath$, $workerMsgPort$, $backendMsgPort$ } = iframeEv.data[1];
+
+ if (useAtomics) {
+ initWebWorker($debug$, $libPath$, $workerMsgPort$);
+ } else {
+ if (!$backendMsgPort$) {
+ throw new Error('MessagePort for service worker is missing.');
+ }
+
+ initSwWorkers($debug$, $libPath$, $workerMsgPort$, $backendMsgPort$);
+ }
+ }
+ }
+);
+
+const initWebWorker = (debug: boolean, libPath: string, workerMessagePort: MessagePort) => {
+ const workerUrl = debug
+ ? libPath + WebWorkerUrl
+ : URL.createObjectURL(
+ new Blob([WebWorkerBlob], {
+ type: 'text/javascript',
+ })
+ );
+ const worker: PartytownWebWorker = createPartytownWorker(workerUrl + '&useMsgPort=true');
+
+ worker.postMessage([WorkerMessageType.SetMessagePort, workerMessagePort], [workerMessagePort]);
+};
+
+const initSwWorkers = (
+ debug: boolean,
+ libPath: string,
+ workerMessagePort: MessagePort,
+ backendMessagePort: MessagePort
+) => {
+ const onServiceWorkerCreated = () => {
+ const swContainer = window.navigator.serviceWorker;
+ swContainer.getRegistration().then((swReg) => {
+ const passMessagePortReq: PassMessagePortToServiceWorkerRequest = {
+ $msgPort$: backendMessagePort,
+ };
+ swReg!.active!.postMessage(passMessagePortReq, [backendMessagePort]);
+
+ initWebWorker(debug, libPath, workerMessagePort);
+ });
+ };
+
+ registerServiceWorker({
+ nav: navigator,
+ libPath,
+ onSuccess: onServiceWorkerCreated,
+ onError: console.error,
+ });
+};
diff --git a/src/lib/main/snippet.ts b/src/lib/main/snippet.ts
index 51d6cf81..6e7bc304 100644
--- a/src/lib/main/snippet.ts
+++ b/src/lib/main/snippet.ts
@@ -1,4 +1,4 @@
-import { debug } from '../utils';
+import { debug, registerServiceWorker } from '../utils';
import type { MainWindow, PartytownConfig } from '../types';
export function snippet(
@@ -9,6 +9,7 @@ export function snippet(
useAtomics: boolean,
config?: PartytownConfig,
libPath?: string,
+ customSandboxPath?: boolean,
timeout?: any,
scripts?: NodeListOf,
sandbox?: HTMLIFrameElement | HTMLScriptElement,
@@ -19,6 +20,7 @@ export function snippet(
function ready() {
if (!isReady) {
isReady = 1;
+ customSandboxPath = !!config!.sandboxLib;
if (debug) {
// default to use debug files
libPath = (config!.lib || '/~partytown/') + (config!.debug !== false ? 'debug/' : '');
@@ -27,7 +29,7 @@ export function snippet(
libPath = (config!.lib || '/~partytown/') + (config!.debug ? 'debug/' : '');
}
- if (libPath[0] == '/') {
+ if (libPath[0] == '/' || customSandboxPath) {
// grab all the partytown scripts
scripts = doc.querySelectorAll('script[type="text/partytown"]');
@@ -44,23 +46,17 @@ export function snippet(
loadSandbox(1);
} else if (nav.serviceWorker) {
// service worker support
- nav.serviceWorker
- .register(libPath + (config!.swPath || 'partytown-sw.js'), {
- scope: libPath,
- })
- .then(function (swRegistration) {
- if (swRegistration.active) {
- loadSandbox();
- } else if (swRegistration.installing) {
- swRegistration.installing.addEventListener('statechange', function (ev) {
- if ((ev.target as any as ServiceWorker).state == 'activated') {
- loadSandbox();
- }
- });
- } else if (debug) {
- console.warn(swRegistration);
- }
- }, console.error);
+ if (config!.sandboxLib) {
+ loadSandbox();
+ } else {
+ registerServiceWorker({
+ nav,
+ libPath,
+ swPath: config!.swPath,
+ onSuccess: loadSandbox,
+ onError: console.error,
+ });
+ }
} else {
// no support for atomics or service worker
fallback();
@@ -73,15 +69,20 @@ export function snippet(
}
function loadSandbox(isAtomics?: number) {
- sandbox = doc.createElement(isAtomics ? 'script' : 'iframe');
+ sandbox = doc.createElement(isAtomics || customSandboxPath ? 'script' : 'iframe');
if (!isAtomics) {
sandbox.setAttribute('style', 'display:block;width:0;height:0;border:0;visibility:hidden');
sandbox.setAttribute('aria-hidden', !0 as any);
}
- sandbox.src =
- libPath +
- 'partytown-' +
- (isAtomics ? 'atomics.js?v=_VERSION_' : 'sandbox-sw.html?' + Date.now());
+
+ let sandboxSrcBase = libPath + 'partytown-';
+ if (isAtomics) {
+ sandboxSrcBase += 'atomics.js?v=_VERSION_';
+ } else {
+ sandboxSrcBase += customSandboxPath ? 'sandbox-sw.js?v=_VERSION_' : 'sandbox-sw.html?';
+ }
+
+ sandbox.src = sandboxSrcBase + Date.now();
doc.body.appendChild(sandbox);
}
diff --git a/src/lib/sandbox/index.ts b/src/lib/sandbox/index.ts
index e62ed10a..cf98d2e6 100644
--- a/src/lib/sandbox/index.ts
+++ b/src/lib/sandbox/index.ts
@@ -1,6 +1,6 @@
-import { debug } from '../utils';
+import { createPartytownWorker, debug } from '../utils';
import { getAndSetInstanceId } from './main-instances';
-import { libPath, mainWindow } from './main-globals';
+import { mainWindow, libPath, sandboxLibPath } from './main-globals';
import { logMain } from '../log';
import { mainAccessHandler } from './main-access-handler';
import {
@@ -9,6 +9,7 @@ import {
PartytownWebWorker,
WorkerMessageType,
} from '../types';
+import { getIsolatedWorker } from './isolation';
import { registerWindow } from './main-register-window';
import syncCreateMessenger from '../build-modules/sync-create-messenger';
import WebWorkerBlob from '../build-modules/web-worker-blob';
@@ -22,16 +23,23 @@ const receiveMessage: MessengerRequestCallback = (accessReq, responseCallback) =
syncCreateMessenger(receiveMessage).then((onMessageHandler) => {
if (onMessageHandler) {
- worker = new Worker(
- debug
- ? libPath + WebWorkerUrl
- : URL.createObjectURL(
- new Blob([WebWorkerBlob], {
- type: 'text/javascript',
- })
- ),
- { name: `Partytown 🎉` }
- );
+ if (sandboxLibPath) {
+ worker = getIsolatedWorker(receiveMessage);
+ } else {
+ worker = createPartytownWorker(
+ debug
+ ? libPath + WebWorkerUrl
+ : URL.createObjectURL(
+ new Blob([WebWorkerBlob], {
+ type: 'text/javascript',
+ })
+ )
+ );
+
+ if (debug) {
+ worker.onerror = (ev) => console.error(`Web Worker Error`, ev);
+ }
+ }
worker.onmessage = (ev: MessageEvent) => {
const msg: MessageFromWorkerToSandbox = ev.data;
@@ -46,7 +54,6 @@ syncCreateMessenger(receiveMessage).then((onMessageHandler) => {
if (debug) {
logMain(`Created Partytown web worker (${VERSION})`);
- worker.onerror = (ev) => console.error(`Web Worker Error`, ev);
}
mainWindow.addEventListener('pt1', (ev: CustomEvent) =>
diff --git a/src/lib/sandbox/isolation.ts b/src/lib/sandbox/isolation.ts
new file mode 100644
index 00000000..5afb474d
--- /dev/null
+++ b/src/lib/sandbox/isolation.ts
@@ -0,0 +1,82 @@
+import type { MessageType } from 'scripts/utils';
+import {
+ InitIsolationIframeData,
+ IsolationIframeMessageType,
+ MainAccessRequest,
+ MessageFromSandboxToIsolationIframe,
+ MessengerRequestCallback,
+ PartytownWebWorker,
+} from '../types';
+import { libPath, sandboxLibPath } from './main-globals';
+import { debug } from '../utils';
+
+export const getIsolatedWorker = (receiveMessage: MessengerRequestCallback): PartytownWebWorker => {
+ const useAtomics = crossOriginIsolated;
+ return useAtomics ? getIsolatedWorkerAtomics() : getIsolatedWorkerSw(receiveMessage);
+};
+
+export const getIsolatedWorkerAtomics = (): PartytownWebWorker => {
+ const { port1: sandboxPort, port2: webWorkerPort } = new MessageChannel();
+
+ const iframe = initIsolationIframe('atomics', webWorkerPort);
+ document.body.appendChild(iframe);
+
+ return sandboxPort;
+};
+
+export const getIsolatedWorkerSw = (
+ receiveMessage: MessengerRequestCallback
+): PartytownWebWorker => {
+ const { port1: sandboxPort, port2: webWorkerPort } = new MessageChannel();
+ const { port1: sandboxSwPort, port2: serviceWorkerPort } = new MessageChannel();
+
+ sandboxSwPort.onmessage = (ev: MessageEvent) => {
+ receiveMessage(ev.data, (accessRsp) => {
+ sandboxSwPort.postMessage(accessRsp);
+ });
+ };
+
+ const iframe = initIsolationIframe('sw', webWorkerPort, serviceWorkerPort);
+ document.body.appendChild(iframe);
+
+ return sandboxPort;
+};
+
+const initIsolationIframe = (
+ msgType: MessageType,
+ workerMessagePort: MessagePort,
+ backendMessagePort?: MessagePort
+) => {
+ const iframe = document.createElement('iframe');
+
+ iframe.src = sandboxLibPath + `partytown-isolation-${msgType}.html`;
+ iframe.setAttribute('style', 'display:block;width:0;height:0;border:0;visibility:hidden');
+ iframe.setAttribute('aria-hidden', 'true');
+ iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts');
+
+ if (msgType === 'atomics') {
+ iframe.setAttribute('allow', 'cross-origin-isolated');
+ }
+
+ iframe.onload = () => {
+ const initData: InitIsolationIframeData = {
+ $debug$: debug,
+ $libPath$: libPath,
+ $workerMsgPort$: workerMessagePort,
+ $backendMsgPort$: backendMessagePort,
+ };
+
+ iframe.contentWindow!.postMessage(
+ [
+ IsolationIframeMessageType.InitializeIframe,
+ initData,
+ ] as MessageFromSandboxToIsolationIframe,
+ '*',
+ [workerMessagePort, backendMessagePort].filter(isMessagePort)
+ );
+ };
+
+ return iframe;
+};
+
+const isMessagePort = (port?: MessagePort): port is MessagePort => !!port;
diff --git a/src/lib/sandbox/main-globals.ts b/src/lib/sandbox/main-globals.ts
index f760db5d..e88bd25a 100644
--- a/src/lib/sandbox/main-globals.ts
+++ b/src/lib/sandbox/main-globals.ts
@@ -5,4 +5,7 @@ export const mainWindow: MainWindow = window.parent;
export const docImpl = document.implementation.createHTMLDocument();
export const config: PartytownConfig = mainWindow.partytown || {};
-export const libPath = (config.lib || '/~partytown/') + (debug ? 'debug/' : '');
+
+const libInnerPath = debug ? 'debug/' : '';
+export const libPath = (config.lib || '/~partytown/') + libInnerPath;
+export const sandboxLibPath = config.sandboxLib ? config.sandboxLib + libInnerPath : null;
diff --git a/src/lib/sandbox/read-main-platform.ts b/src/lib/sandbox/read-main-platform.ts
index a61e3b25..edee9e53 100644
--- a/src/lib/sandbox/read-main-platform.ts
+++ b/src/lib/sandbox/read-main-platform.ts
@@ -6,7 +6,7 @@ import {
len,
noop,
} from '../utils';
-import { config, docImpl, libPath, mainWindow } from './main-globals';
+import { config, docImpl, libPath, mainWindow, sandboxLibPath } from './main-globals';
import {
InterfaceType,
InterfaceInfo,
@@ -75,6 +75,7 @@ export const readMainPlatform = () => {
$config$,
$interfaces$: readImplementations(impls, initialInterfaces),
$libPath$: new URL(libPath, mainWindow.location as any) + '',
+ $sandboxLibPath$: sandboxLibPath || '',
$origin$: origin,
$localStorage$: readStorage('localStorage'),
$sessionStorage$: readStorage('sessionStorage'),
diff --git a/src/lib/service-worker/fetch.ts b/src/lib/service-worker/fetch.ts
index 327a153f..1477aab2 100644
--- a/src/lib/service-worker/fetch.ts
+++ b/src/lib/service-worker/fetch.ts
@@ -3,26 +3,32 @@ import type { MainAccessRequest, MainAccessResponse } from '../types';
import Sandbox from '@sandbox';
import SandboxDebug from '@sandbox-debug';
-export const onFetchServiceWorkerRequest = (ev: FetchEvent) => {
+const resolveTimeout = debug ? 120000 : 10000;
+
+type MessageResolve = [(data?: any) => void, any];
+
+export const onFetchServiceWorkerRequest = (ev: FetchEvent, messagePort?: MessagePort) => {
const req = ev.request;
const url = new URL(req.url);
const pathname = url.pathname;
- if (debug && pathname.endsWith('sw.html')) {
+ if (debug && pathname.endsWith('partytown-sandbox-sw.html')) {
// debug version (sandbox and web worker are not inlined)
ev.respondWith(response(SandboxDebug));
- } else if (!debug && pathname.endsWith('sw.html')) {
+ } else if (!debug && pathname.endsWith('partytown-sandbox-sw.html')) {
// sandbox and webworker, minified and inlined
ev.respondWith(response(Sandbox));
} else if (pathname.endsWith('proxytown')) {
// proxy request
- ev.respondWith(httpRequestFromWebWorker(req));
+ ev.respondWith(httpRequestFromWebWorker(req, messagePort));
}
};
const resolves = new Map();
-export const receiveMessageFromSandboxToServiceWorker = (ev: ExtendableMessageEvent) => {
+export const receiveMessageFromSandboxToServiceWorker = (
+ ev: ExtendableMessageEvent | MessageEvent
+) => {
const accessRsp: MainAccessResponse = ev.data;
const r = resolves.get(accessRsp.$msgId$);
@@ -43,14 +49,7 @@ const sendMessageToSandboxFromServiceWorker = (accessReq: MainAccessRequest) =>
})[0];
if (client) {
- const timeout = debug ? 120000 : 10000;
- const msgResolve: MessageResolve = [
- resolve,
- setTimeout(() => {
- resolves.delete(accessReq.$msgId$);
- resolve(swMessageError(accessReq, `Timeout`));
- }, timeout),
- ];
+ const msgResolve = buildMessageResolve({ accessReq, resolve, timeout: resolveTimeout });
resolves.set(accessReq.$msgId$, msgResolve);
client.postMessage(accessReq);
} else {
@@ -58,17 +57,24 @@ const sendMessageToSandboxFromServiceWorker = (accessReq: MainAccessRequest) =>
}
});
+const sendMessageToMessagePort = (accessReq: MainAccessRequest, messagePort: MessagePort) =>
+ new Promise((resolve) => {
+ const msgResolve = buildMessageResolve({ accessReq, resolve, timeout: resolveTimeout });
+ resolves.set(accessReq.$msgId$, msgResolve);
+ messagePort.postMessage(accessReq);
+ });
+
const swMessageError = (accessReq: MainAccessRequest, $error$: string): MainAccessResponse => ({
$msgId$: accessReq.$msgId$,
$error$,
});
-type MessageResolve = [(data?: any) => void, any];
-
-const httpRequestFromWebWorker = (req: Request) =>
+const httpRequestFromWebWorker = (req: Request, messagePort?: MessagePort) =>
new Promise(async (resolve) => {
const accessReq: MainAccessRequest = await req.clone().json();
- const responseData = await sendMessageToSandboxFromServiceWorker(accessReq);
+ const responseData = messagePort
+ ? await sendMessageToMessagePort(accessReq, messagePort)
+ : await sendMessageToSandboxFromServiceWorker(accessReq);
resolve(response(JSON.stringify(responseData), 'application/json'));
});
@@ -79,3 +85,19 @@ const response = (body: string, contentType?: string) =>
'Cache-Control': 'no-store',
},
});
+
+const buildMessageResolve = ({
+ accessReq,
+ timeout,
+ resolve,
+}: {
+ accessReq: MainAccessRequest;
+ timeout: number;
+ resolve: (res: MainAccessResponse) => void;
+}): MessageResolve => [
+ resolve,
+ setTimeout(() => {
+ resolves.delete(accessReq.$msgId$);
+ resolve(swMessageError(accessReq, `Timeout`));
+ }, timeout),
+];
diff --git a/src/lib/service-worker/index.ts b/src/lib/service-worker/index.ts
index 4841ab46..1c4eb6c9 100644
--- a/src/lib/service-worker/index.ts
+++ b/src/lib/service-worker/index.ts
@@ -1,3 +1,4 @@
+import type { MainAccessResponse, PassMessagePortToServiceWorkerRequest } from '../types';
import { onFetchServiceWorkerRequest, receiveMessageFromSandboxToServiceWorker } from './fetch';
(self as any as ServiceWorkerGlobalScope).oninstall = () =>
@@ -6,6 +7,18 @@ import { onFetchServiceWorkerRequest, receiveMessageFromSandboxToServiceWorker }
(self as any as ServiceWorkerGlobalScope).onactivate = () =>
(self as any as ServiceWorkerGlobalScope).clients.claim();
-(self as any as ServiceWorkerGlobalScope).onmessage = receiveMessageFromSandboxToServiceWorker;
+(self as any as ServiceWorkerGlobalScope).onmessage = (ev: ExtendableMessageEvent) => {
+ const data: MainAccessResponse | PassMessagePortToServiceWorkerRequest = ev.data;
+
+ if ('$msgPort$' in data) {
+ const messagePort = data.$msgPort$;
+ messagePort.onmessage = receiveMessageFromSandboxToServiceWorker;
+ (self as any as ServiceWorkerGlobalScope).onfetch = (ev: FetchEvent) =>
+ onFetchServiceWorkerRequest(ev, messagePort);
+ return;
+ }
+
+ return receiveMessageFromSandboxToServiceWorker(ev);
+};
(self as any as ServiceWorkerGlobalScope).onfetch = onFetchServiceWorkerRequest;
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 1b92079f..f750fb8d 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -52,6 +52,7 @@ export type MessageFromSandboxToWorker =
| [type: WorkerMessageType.ForwardMainTrigger, triggerData: ForwardMainTriggerData]
| [type: WorkerMessageType.LocationUpdate, locationChangeData: LocationUpdateData]
| [type: WorkerMessageType.DocumentVisibilityState, winId: WinId, visibilityState: string]
+ | [type: WorkerMessageType.SetMessagePort, msgPort: MessagePort]
| [
type: WorkerMessageType.CustomElementCallback,
winId: WinId,
@@ -77,6 +78,21 @@ export const enum WorkerMessageType {
LocationUpdate,
DocumentVisibilityState,
CustomElementCallback,
+ SetMessagePort,
+}
+
+export type InitIsolationIframeData = {
+ $debug$: boolean;
+ $libPath$: string;
+ $workerMsgPort$: MessagePort;
+ $backendMsgPort$?: MessagePort;
+};
+
+export type MessageFromSandboxToIsolationIframe =
+ | [type: IsolationIframeMessageType.InitializeIframe, initData: InitIsolationIframeData];
+
+export const enum IsolationIframeMessageType {
+ InitializeIframe,
}
export const enum LocationUpdateType {
@@ -109,8 +125,6 @@ export interface RefHandlerCallbackData {
$args$: SerializedTransfer | undefined;
}
-export type PostMessageToWorker = (msg: MessageFromSandboxToWorker) => void;
-
export interface MainWindowContext {
$winId$: WinId;
$isInitialized$?: number;
@@ -118,14 +132,16 @@ export interface MainWindowContext {
$window$: MainWindow;
}
-export interface PartytownWebWorker extends Worker {
- postMessage: PostMessageToWorker;
-}
+export type PartytownWebWorker = (Worker | MessagePort) & {
+ postMessage(msg: MessageFromSandboxToWorker): void;
+ postMessage(msg: MessageFromSandboxToWorker, transfer: Transferable[]): void;
+};
export interface InitWebWorkerData {
$config$: string;
$interfaces$: InterfaceInfo[];
$libPath$: string;
+ $sandboxLibPath$: string;
$sharedDataBuffer$?: SharedArrayBuffer;
$localStorage$: StorageItem[];
$sessionStorage$: StorageItem[];
@@ -164,6 +180,7 @@ export interface WebWorkerContext {
$indexedDB$: any;
$isInitialized$?: number;
$libPath$: string;
+ $sandboxLibPath$: string;
$origin$: string;
$postMessage$: (msg: MessageFromWorkerToSandbox, arr?: any[]) => void;
$sharedDataBuffer$?: SharedArrayBuffer;
@@ -261,6 +278,10 @@ export interface MainAccessResponse {
$isPromise$?: any;
}
+export interface PassMessagePortToServiceWorkerRequest {
+ $msgPort$: MessagePort;
+}
+
export const enum ApplyPathType {
SetValue,
GlobalConstructor,
@@ -483,6 +504,15 @@ export interface PartytownConfig {
* Path to the service worker file. Defaults to `partytown-sw.js`.
*/
swPath?: string;
+ /**
+ * An absolute path to the root directory which Partytown sandbox files
+ * can be found. This path might either start with `/` or lead
+ * to a different domain.
+ *
+ * When the path is on a different domain, 3rd party scripts will run on a
+ * different origin than the embedding document.
+ */
+ sandboxLib?: string;
}
/**
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 1515c7f2..a74b5102 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -136,4 +136,40 @@ export const isValidUrl = (url: any): boolean => {
} catch (_) {
return false;
}
-}
+};
+
+export const registerServiceWorker = ({
+ nav,
+ libPath,
+ swPath,
+ onSuccess,
+ onError,
+}: {
+ nav: Navigator;
+ libPath?: string;
+ swPath?: string;
+ onSuccess?: () => void;
+ onError?: (err: Error) => void;
+}) => {
+ nav.serviceWorker
+ .register(libPath + (swPath || 'partytown-sw.js'), {
+ scope: libPath,
+ })
+ .then(function (swRegistration) {
+ if (swRegistration.active) {
+ onSuccess?.();
+ } else if (swRegistration.installing) {
+ swRegistration.installing.addEventListener('statechange', function (ev) {
+ if ((ev.target as any as ServiceWorker).state == 'activated') {
+ onSuccess?.();
+ }
+ });
+ } else if (debug) {
+ console.warn(swRegistration);
+ }
+ }, onError);
+};
+
+export const createPartytownWorker = (url: string | URL) => {
+ return new Worker(url, { name: `Partytown 🎉` });
+};
diff --git a/src/lib/web-worker/index.ts b/src/lib/web-worker/index.ts
index 3afc7e10..462d479e 100644
--- a/src/lib/web-worker/index.ts
+++ b/src/lib/web-worker/index.ts
@@ -8,8 +8,9 @@ import { initNextScriptsInWebWorker } from './worker-exec';
import { initWebWorker } from './init-web-worker';
import { logWorker, normalizedWinId } from '../log';
import { workerForwardedTriggerHandle } from './worker-forwarded-trigger';
-import { forwardLocationChange } from "./worker-location";
+import { forwardLocationChange } from './worker-location';
+let messagePort: MessagePort | null = null; // Used for messages when web worker is created inside isolated iframe
const queuedEvents: MessageEvent[] = [];
const receiveMessageFromSandboxToWorker = (ev: MessageEvent) => {
@@ -50,10 +51,14 @@ const receiveMessageFromSandboxToWorker = (ev: MessageEvent {
+export const initWebWorker = (
+ initWebWorkerData: InitWebWorkerData,
+ messagePort?: MessagePort | null
+) => {
const config: PartytownConfig = (webWorkerCtx.$config$ = JSON.parse(initWebWorkerData.$config$));
const locOrigin = initWebWorkerData.$origin$;
webWorkerCtx.$importScripts$ = importScripts.bind(self);
webWorkerCtx.$interfaces$ = initWebWorkerData.$interfaces$;
webWorkerCtx.$libPath$ = initWebWorkerData.$libPath$;
+ webWorkerCtx.$sandboxLibPath$ = initWebWorkerData.$sandboxLibPath$;
webWorkerCtx.$origin$ = locOrigin;
- webWorkerCtx.$postMessage$ = (postMessage as any).bind(self);
+ webWorkerCtx.$postMessage$ = messagePort
+ ? messagePort.postMessage.bind(messagePort)
+ : (postMessage as any).bind(self);
webWorkerCtx.$sharedDataBuffer$ = initWebWorkerData.$sharedDataBuffer$;
webWorkerlocalStorage.set(locOrigin, initWebWorkerData.$localStorage$);
diff --git a/src/lib/web-worker/worker-constants.ts b/src/lib/web-worker/worker-constants.ts
index 10d77064..a9b40e6f 100644
--- a/src/lib/web-worker/worker-constants.ts
+++ b/src/lib/web-worker/worker-constants.ts
@@ -41,7 +41,7 @@ export const ABOUT_BLANK = 'about:blank';
export const commaSplit = (str: string) => str.split(',');
export const partytownLibUrl = (url: string) => {
- url = webWorkerCtx.$libPath$ + url;
+ url = (webWorkerCtx.$sandboxLibPath$ || webWorkerCtx.$libPath$) + url;
if (new URL(url).origin != location.origin) {
throw 'Invalid ' + url;
}
diff --git a/tests/index.html b/tests/index.html
index 9c713d4d..9c8473de 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -82,6 +82,7 @@ Platform Tests
Audio
Canvas
Custom Element
+ Different Origin Sandbox
Document
Document (Prod Build)
Element
diff --git a/tests/platform/different-origin/different-origin.spec.ts b/tests/platform/different-origin/different-origin.spec.ts
new file mode 100644
index 00000000..52b018fd
--- /dev/null
+++ b/tests/platform/different-origin/different-origin.spec.ts
@@ -0,0 +1,13 @@
+import { test, expect } from '@playwright/test';
+
+test('different-origin', async ({ page }) => {
+ await page.goto('/tests/platform/different-origin/');
+
+ await page.waitForSelector('.completed');
+
+ const testNoAccessToMainDocIndexedDb = page.locator('#testNoAccessToMainDocIndexedDb');
+ await expect(testNoAccessToMainDocIndexedDb).toHaveText('No databases found');
+
+ const testNoAccessToMainDocResources = page.locator('#testNoAccessToMainDocResources');
+ await expect(testNoAccessToMainDocResources).toHaveText('Request failed');
+});
diff --git a/tests/platform/different-origin/index.html b/tests/platform/different-origin/index.html
new file mode 100644
index 00000000..af2b3f55
--- /dev/null
+++ b/tests/platform/different-origin/index.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+ Different Origin Sandbox
+
+
+
+
+
+ Different Origin Sandbox
+
+ -
+ No access to main document IndexedDB
+
+
+
+
+
+ -
+ No access to main document resources
+
+
+
+
+
+
+
+
+ All Tests
+
+
diff --git a/tests/unit/utils.ts b/tests/unit/utils.ts
index a30d2656..9e173bbc 100644
--- a/tests/unit/utils.ts
+++ b/tests/unit/utils.ts
@@ -112,7 +112,7 @@ function getWorker(): TestWorker {
} as any;
}
-export interface TestWorker extends PartytownWebWorker {
+export type TestWorker = PartytownWebWorker & {
$messages: any[];
}