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[]; }