├── tests
├── 404.html
├── platform
│ ├── navigator
│ │ ├── api.js
│ │ └── navigator.spec.ts
│ ├── script
│ │ ├── set-get-attr.js
│ │ ├── jsonp-a.js
│ │ ├── jsonp-b.js
│ │ ├── jsonp-c.js
│ │ ├── async-1.js
│ │ ├── async-2.js
│ │ ├── defer-1.js
│ │ ├── defer-2.js
│ │ └── source-mapping-url.js
│ ├── window
│ │ ├── text.txt
│ │ └── data.json
│ ├── iframe
│ │ ├── onload.js
│ │ ├── external.js
│ │ ├── location2.js
│ │ ├── no-global-share.js
│ │ ├── external-js.html
│ │ ├── location2.html
│ │ ├── no-global-share.html
│ │ ├── global-var.html
│ │ ├── content.html
│ │ ├── src-change1.html
│ │ ├── call-fn-on-parent.js
│ │ ├── no-deadlock.html
│ │ ├── current-script.js
│ │ ├── no-deadlock.js
│ │ ├── src-change2.html
│ │ ├── post-message.html
│ │ ├── fn-on-window.js
│ │ ├── location1.html
│ │ ├── parent-window.js
│ │ └── cross-origin.html
│ ├── image
│ │ ├── dot.gif
│ │ └── image.spec.ts
│ ├── audio
│ │ ├── submit.mp3
│ │ ├── audio-scripts.js
│ │ └── audio.spec.ts
│ ├── history
│ │ └── iframe-history.html
│ ├── document-prod
│ │ └── current-script-src.js
│ ├── document
│ │ └── current-script-src.js
│ ├── svg
│ │ └── svg.spec.ts
│ ├── screen
│ │ └── screen.spec.ts
│ ├── intersection-observer
│ │ └── intersection-observer.spec.ts
│ ├── video
│ │ └── video.spec.__ts
│ ├── element-style
│ │ └── element-style.spec.ts
│ ├── canvas
│ │ └── canvas.spec.ts
│ ├── style
│ │ └── style.spec.ts
│ ├── mutation-observer
│ │ └── mutation-observer.spec.ts
│ ├── resize-observer
│ │ └── resize-observer.spec.ts
│ ├── anchor
│ │ └── anchor.spec.ts
│ ├── storage
│ │ └── storage.spec.ts
│ ├── element-class
│ │ └── element-class.spec.ts
│ └── node
│ │ └── node.spec.ts
├── react-app
│ ├── src
│ │ ├── react-app-env.d.ts
│ │ ├── index.tsx
│ │ ├── index.css
│ │ ├── App.tsx
│ │ ├── App.css
│ │ └── logo.svg
│ ├── public
│ │ └── index.html
│ ├── tsconfig.json
│ └── package.json
├── nextjs
│ ├── next.config.js
│ ├── README.md
│ ├── pages
│ │ ├── _app.js
│ │ └── index.js
│ ├── playwright.nextjs.ts
│ ├── package.json
│ └── nextjs.spec.ts
├── integrations
│ ├── intercom
│ │ └── submit.3abafccd.mp3
│ ├── adobe-launch
│ │ ├── adobe-launch.spec.ts
│ │ └── standard.html
│ ├── gtm
│ │ └── gtm.spec.ts
│ ├── facebook-pixel
│ │ └── facebook-pixel.spec.ts
│ ├── event-forwarding
│ │ └── event-forwarding.spec.ts
│ ├── config
│ │ └── config.spec.ts
│ ├── mermaid
│ │ ├── standard.html
│ │ └── index.html
│ ├── hubspot
│ │ ├── forms-standard.html
│ │ ├── forms.html
│ │ ├── 20632911.js
│ │ └── scripts-20632911.js
│ ├── wistia
│ │ ├── standard.html
│ │ └── index.html
│ └── twitter
│ │ └── standard.html
├── unit
│ ├── snippet.spec.ts
│ └── integration.spec.ts
└── benchmarks
│ ├── run.cjs
│ ├── benchmark.js
│ └── index.html
├── docs
├── site
│ ├── .npmrc
│ ├── public
│ │ ├── 196x196.png
│ │ ├── 256x256.png
│ │ ├── 32x32.png
│ │ ├── favicon.ico
│ │ ├── partytown-logo.png
│ │ └── partytown-media.png
│ ├── tsconfig.json
│ ├── src
│ │ ├── config.ts
│ │ ├── components
│ │ │ ├── Header
│ │ │ │ ├── SkipToContent.astro
│ │ │ │ └── SidebarToggle.tsx
│ │ │ ├── RightSidebar
│ │ │ │ ├── RightSidebar.astro
│ │ │ │ ├── MoreMenu.astro
│ │ │ │ └── TableOfContents.tsx
│ │ │ ├── Footer
│ │ │ │ └── Footer.astro
│ │ │ ├── HeadCommon.astro
│ │ │ ├── HeadSEO.astro
│ │ │ └── PageContent
│ │ │ │ └── PageContent.astro
│ │ └── styles
│ │ │ └── code.css
│ ├── astro.config.mjs
│ ├── README.md
│ └── package.json
├── integrations.md
├── sandboxing.md
├── nuxt.md
├── debugging.md
├── getting-started.md
├── google-tag-manager.md
├── partytown-scripts.md
├── _table-of-contents.md
├── html.md
├── facebook-pixel.md
├── browser-support.md
├── astro.md
├── react.md
├── nextjs.md
├── forwarding-events.md
└── remix.md
├── .gitattributes
├── src
├── lib
│ ├── build-modules
│ │ ├── version.ts
│ │ ├── web-worker-blob.ts
│ │ ├── web-worker-url.ts
│ │ ├── sync-create-messenger.ts
│ │ └── sync-send-message-to-main.ts
│ ├── main
│ │ └── snippet-entry.ts
│ ├── web-worker
│ │ ├── worker-performance.ts
│ │ ├── worker-location.ts
│ │ ├── media
│ │ │ ├── bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── time-ranges.ts
│ │ │ ├── audio.ts
│ │ │ ├── utils.ts
│ │ │ ├── canvas
│ │ │ │ ├── index.ts
│ │ │ │ └── context-webgl.ts
│ │ │ ├── media-element.ts
│ │ │ └── audio-track.ts
│ │ ├── worker-element.ts
│ │ ├── worker-node-list.ts
│ │ ├── worker-forwarded-trigger.ts
│ │ ├── worker-state.ts
│ │ ├── worker-navigator.ts
│ │ ├── worker-css-style-declaration.ts
│ │ ├── worker-constructors.ts
│ │ ├── worker-environment.ts
│ │ ├── worker-media.ts
│ │ ├── worker-anchor.ts
│ │ ├── worker-image.ts
│ │ ├── worker-instance.ts
│ │ ├── worker-script.ts
│ │ ├── init-web-worker.ts
│ │ ├── worker-src-element.ts
│ │ └── worker-storage.ts
│ ├── sandbox
│ │ ├── main-globals.ts
│ │ ├── main-constants.ts
│ │ ├── main-forward-trigger.ts
│ │ ├── on-messenge-from-worker.ts
│ │ ├── index.ts
│ │ └── main-register-window.ts
│ ├── service-worker
│ │ ├── index.ts
│ │ ├── sync-send-message-to-main-sw.ts
│ │ └── sync-create-messenger-sw.ts
│ └── atomics
│ │ ├── sync-send-message-to-main-atomics.ts
│ │ └── sync-create-messenger-atomics.ts
├── react
│ ├── index.ts
│ ├── package.json
│ ├── api.md
│ └── api-extractor.json
├── services
│ ├── index.ts
│ ├── package.json
│ ├── google-tag-manager.ts
│ ├── facebook-pixel.ts
│ ├── freshpaint.ts
│ ├── api.md
│ └── api-extractor.json
├── utils
│ ├── index.ts
│ ├── package.json
│ ├── api.md
│ └── api-extractor.json
├── integration
│ ├── package.json
│ ├── index.ts
│ ├── snippet.ts
│ ├── api-extractor.json
│ └── api.md
└── modules.d.ts
├── .prettierignore
├── playwright.atomics.config.ts
├── scripts
├── serve.cjs
├── rollup.config.js
├── build-api.ts
├── build-utils.ts
├── build-services.ts
├── build-react.ts
├── copy-site.cjs
├── build-media-implementations.ts
├── build-main-snippet.ts
├── build-integration.ts
├── build-atomics.ts
└── build-web-worker.ts
├── .gitignore
├── playwright.config.ts
├── tsconfig.json
├── vercel.json
├── LICENSE
├── bin
└── partytown.cjs
└── DEVELOPER.md
/tests/404.html:
--------------------------------------------------------------------------------
1 | 404
2 |
--------------------------------------------------------------------------------
/tests/platform/navigator/api.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/site/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/tests/platform/script/set-get-attr.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/platform/window/text.txt:
--------------------------------------------------------------------------------
1 | text
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | tests/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/tests/platform/window/data.json:
--------------------------------------------------------------------------------
1 | { "mph": 88 }
2 |
--------------------------------------------------------------------------------
/tests/platform/iframe/onload.js:
--------------------------------------------------------------------------------
1 | (function () {})();
2 |
--------------------------------------------------------------------------------
/tests/platform/script/jsonp-a.js:
--------------------------------------------------------------------------------
1 | jsonpA({ mph: 88 });
2 |
--------------------------------------------------------------------------------
/tests/platform/script/jsonp-b.js:
--------------------------------------------------------------------------------
1 | jsonpB({ mph: 99 });
2 |
--------------------------------------------------------------------------------
/tests/platform/script/jsonp-c.js:
--------------------------------------------------------------------------------
1 | jsonpC({ mph: 77 });
2 |
--------------------------------------------------------------------------------
/src/lib/build-modules/version.ts:
--------------------------------------------------------------------------------
1 | export const VERSION = '_VERSION_';
2 |
--------------------------------------------------------------------------------
/src/react/index.ts:
--------------------------------------------------------------------------------
1 | export { Partytown, PartytownProps } from './snippet';
2 |
--------------------------------------------------------------------------------
/tests/react-app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tests/nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | };
4 |
--------------------------------------------------------------------------------
/src/lib/build-modules/web-worker-blob.ts:
--------------------------------------------------------------------------------
1 | const WEB_WORKER_BLOB = '';
2 | export default WEB_WORKER_BLOB;
3 |
--------------------------------------------------------------------------------
/src/lib/build-modules/web-worker-url.ts:
--------------------------------------------------------------------------------
1 | const WEB_WORKER_URL = '';
2 | export default WEB_WORKER_URL;
3 |
--------------------------------------------------------------------------------
/docs/site/public/196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/196x196.png
--------------------------------------------------------------------------------
/docs/site/public/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/256x256.png
--------------------------------------------------------------------------------
/docs/site/public/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/32x32.png
--------------------------------------------------------------------------------
/docs/site/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/favicon.ico
--------------------------------------------------------------------------------
/tests/platform/image/dot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/tests/platform/image/dot.gif
--------------------------------------------------------------------------------
/tests/nextjs/README.md:
--------------------------------------------------------------------------------
1 | ```bash
2 | npm run dev
3 | ```
4 |
5 | [http://localhost:3000](http://localhost:3000)
6 |
--------------------------------------------------------------------------------
/tests/platform/audio/submit.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/tests/platform/audio/submit.mp3
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | /lib
3 | /node_modules
4 | /react
5 | /tsc
6 | api.md
7 | tests/platform/script/source-mapping-url.js
--------------------------------------------------------------------------------
/docs/site/public/partytown-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/partytown-logo.png
--------------------------------------------------------------------------------
/docs/site/public/partytown-media.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/docs/site/public/partytown-media.png
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './facebook-pixel';
2 | export * from './freshpaint';
3 | export * from './google-tag-manager';
4 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { copyLibFiles, libDirPath } from './copy-lib-files';
2 | export type { CopyLibFilesOptions } from './copy-lib-files';
3 |
--------------------------------------------------------------------------------
/tests/integrations/intercom/submit.3abafccd.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/partytown/main/tests/integrations/intercom/submit.3abafccd.mp3
--------------------------------------------------------------------------------
/tests/nextjs/pages/_app.js:
--------------------------------------------------------------------------------
1 | function MyApp({ Component, pageProps }) {
2 | return ;
3 | }
4 |
5 | export default MyApp;
6 |
--------------------------------------------------------------------------------
/tests/platform/script/async-1.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const elm = document.getElementById('testAsync');
3 | elm.textContent += ' a1';
4 | })();
5 |
--------------------------------------------------------------------------------
/tests/platform/script/async-2.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const elm = document.getElementById('testAsync');
3 | elm.textContent += ' a2';
4 | })();
5 |
--------------------------------------------------------------------------------
/tests/platform/script/defer-1.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const elm = document.getElementById('testAsync');
3 | elm.textContent += ' d1';
4 | })();
5 |
--------------------------------------------------------------------------------
/src/lib/main/snippet-entry.ts:
--------------------------------------------------------------------------------
1 | import { snippet } from './snippet';
2 |
3 | snippet(window as any, document, navigator, top!, top!.crossOriginIsolated);
4 |
--------------------------------------------------------------------------------
/src/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@builder.io/partytown-react",
3 | "main": "index.cjs",
4 | "module": "index.mjs",
5 | "types": "index.d.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@builder.io/partytown-utils",
3 | "main": "index.cjs",
4 | "module": "index.mjs",
5 | "types": "index.d.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@builder.io/partytown-services",
3 | "main": "index.cjs",
4 | "module": "index.mjs",
5 | "types": "index.d.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/platform/iframe/external.js:
--------------------------------------------------------------------------------
1 | window.someWindowVar = {
2 | year: 1985,
3 | };
4 |
5 | document.getElementById('output').textContent = window.someWindowVar.year;
6 |
--------------------------------------------------------------------------------
/src/integration/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@builder.io/partytown-integration",
3 | "main": "index.cjs",
4 | "module": "index.mjs",
5 | "types": "index.d.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/docs/site/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "esnext",
5 | "jsx": "preserve"
6 | },
7 | "moduleResolution": "node"
8 | }
9 |
--------------------------------------------------------------------------------
/tests/platform/history/iframe-history.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/platform/script/defer-2.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const elm = document.getElementById('testAsync');
3 | elm.textContent += ' d2';
4 | setTimeout(() => {
5 | elm.className = 'testAsync';
6 | }, 50);
7 | })();
8 |
--------------------------------------------------------------------------------
/docs/site/src/config.ts:
--------------------------------------------------------------------------------
1 | export const SITE = {
2 | title: 'Partytown',
3 | description: 'Run scripts from a web worker.',
4 | defaultLanguage: 'en_US',
5 | };
6 |
7 | export const GITHUB_EDIT_URL = `https://github.com/BuilderIO/partytown/edit/main/docs`;
8 |
--------------------------------------------------------------------------------
/tests/platform/iframe/location2.js:
--------------------------------------------------------------------------------
1 | document.getElementById('output').textContent = location.pathname;
2 | parent.document.getElementById('testLocation').textContent = location.pathname;
3 | parent.document.getElementById('testLocation').className = 'testLocation';
4 |
--------------------------------------------------------------------------------
/tests/platform/iframe/no-global-share.js:
--------------------------------------------------------------------------------
1 | window.noGlobalShare = window.noGlobalShare || {
2 | value: Math.random(),
3 | };
4 |
5 | (function () {
6 | const elm = document.getElementById('testWWGlobalValue');
7 | elm.textContent = window.noGlobalShare.value;
8 | })();
9 |
--------------------------------------------------------------------------------
/tests/platform/iframe/external-js.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/react-app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/src/modules.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@sandbox' {
2 | const str: string;
3 | export default str;
4 | }
5 |
6 | declare module '@sandbox-debug' {
7 | const str: string;
8 | export default str;
9 | }
10 |
11 | declare module '@snippet' {
12 | const str: string;
13 | export default str;
14 | }
15 |
--------------------------------------------------------------------------------
/src/services/google-tag-manager.ts:
--------------------------------------------------------------------------------
1 | import type { PartytownForwardProperty } from '../lib/types';
2 |
3 | /**
4 | * Forwards Google Tag Manager main window calls to Partytown's worker thread.
5 | *
6 | * @public
7 | */
8 | export const googleTagManagerForward: PartytownForwardProperty[] = ['dataLayer.push'];
9 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-performance.ts:
--------------------------------------------------------------------------------
1 | import { WorkerInstance } from './worker-instance';
2 |
3 | export class Performance extends WorkerInstance {
4 | now() {
5 | // use the web worker's performance.now()
6 | // no need to go to main for this
7 | return performance.now();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/build-modules/sync-create-messenger.ts:
--------------------------------------------------------------------------------
1 | import type { Messenger } from '../types';
2 |
3 | const syncCreateMessenger: Messenger = async (receiveMessage) => {
4 | // dynamically replaced at build-time with
5 | receiveMessage;
6 | return null as any;
7 | };
8 |
9 | export default syncCreateMessenger;
10 |
--------------------------------------------------------------------------------
/tests/platform/iframe/location2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/platform/script/source-mapping-url.js:
--------------------------------------------------------------------------------
1 | /*! For license information please see LICENSE.txt */
2 | (function () {
3 | const elm = document.getElementById('testSourceMappingURL');
4 | elm.textContent = 'sourceMappingURL';
5 | elm.className = 'testSourceMappingURL';
6 | })();
7 | //# sourceMappingURL=source-mapping-url.js.map
--------------------------------------------------------------------------------
/tests/platform/iframe/no-global-share.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-location.ts:
--------------------------------------------------------------------------------
1 | import { logWorker } from '../log';
2 |
3 | export class Location extends URL {
4 | assign() {
5 | logWorker(`location.assign(), noop`);
6 | }
7 | reload() {
8 | logWorker(`location.reload(), noop`);
9 | }
10 | replace() {
11 | logWorker(`location.replace(), noop`);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/platform/iframe/global-var.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/playwright.atomics.config.ts:
--------------------------------------------------------------------------------
1 | import config from './playwright.config';
2 |
3 | config.use.launchOptions = {
4 | args: ['--enable-features=SharedArrayBuffer'],
5 | };
6 | config.use.contextOptions.recordVideo = undefined;
7 | config.webServer.command = 'npm run serve.atomics.test';
8 | config.webServer.port = 4003;
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/tests/platform/document-prod/current-script-src.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const elm = document.getElementById('testCurrentScriptSrc');
3 | elm.textContent = document.currentScript.dataset.currentScript;
4 |
5 | const loc = document.getElementById('testCurrentScriptSrcLocation');
6 | loc.textContent = new URL(document.currentScript.src).pathname;
7 | })();
8 |
--------------------------------------------------------------------------------
/tests/nextjs/playwright.nextjs.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | webServer: {
5 | command: 'npm run build && npm start',
6 | port: 3000,
7 | timeout: 120 * 1000,
8 | reuseExistingServer: !process.env.CI,
9 | },
10 | };
11 |
12 | export default config;
13 |
--------------------------------------------------------------------------------
/src/lib/build-modules/sync-send-message-to-main.ts:
--------------------------------------------------------------------------------
1 | import type { MainAccessResponse } from '../types';
2 |
3 | export default function syncSendMessageToMainServiceWorker(
4 | webWorkerCtx: any,
5 | accessReq: any
6 | ): MainAccessResponse {
7 | // dynamically replaced at build-time with
8 | webWorkerCtx;
9 | accessReq;
10 | return null as any;
11 | }
12 |
--------------------------------------------------------------------------------
/src/services/facebook-pixel.ts:
--------------------------------------------------------------------------------
1 | import type { PartytownForwardProperty } from '../lib/types';
2 |
3 | /**
4 | * Forwards Facebool Pixels main window calls to Partytown's worker thread.
5 | *
6 | * https://developers.facebook.com/docs/facebook-pixel/get-started
7 | *
8 | * @public
9 | */
10 | export const facebookPixelForward: PartytownForwardProperty[] = ['fbq'];
11 |
--------------------------------------------------------------------------------
/scripts/serve.cjs:
--------------------------------------------------------------------------------
1 | const { createServer } = require('./server.cjs');
2 |
3 | const port = parseInt(process.argv[2], 10);
4 | const enableAtomics = process.argv.includes('--atomics');
5 |
6 | (async () => {
7 | const server = await createServer(port, enableAtomics);
8 | console.log(`Serving${server.enableAtomics ? ` (atomics)` : ``}: ${server.address}tests/`);
9 | })();
10 |
--------------------------------------------------------------------------------
/src/lib/sandbox/main-globals.ts:
--------------------------------------------------------------------------------
1 | import type { MainWindow, PartytownConfig } from '../types';
2 | import { debug } from '../utils';
3 |
4 | export const mainWindow: MainWindow = window.parent;
5 | export const doc = document;
6 |
7 | export const config: PartytownConfig = mainWindow.partytown || {};
8 | export const libPath = (config.lib || '/~partytown/') + (debug ? 'debug/' : '');
9 |
--------------------------------------------------------------------------------
/tests/platform/iframe/content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/platform/document/current-script-src.js:
--------------------------------------------------------------------------------
1 | const elm = document.getElementById('testCurrentScriptSrc');
2 | elm.textContent = document.currentScript.dataset.currentScript;
3 |
4 | const loc = document.getElementById('testCurrentScriptSrcLocation');
5 |
6 | const currentScript = document.currentScript;
7 | const currentUrl = new URL(currentScript.src);
8 | loc.textContent = currentUrl.pathname;
9 |
--------------------------------------------------------------------------------
/tests/platform/iframe/src-change1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/scripts/rollup.config.js:
--------------------------------------------------------------------------------
1 | // build scripts are generated after tsc runs
2 | import { runBuild } from '../tsc/scripts/index.js';
3 | import { join } from 'path';
4 |
5 | export default function (cmdArgs) {
6 | const rootDir = join(__dirname, '..');
7 | const isDev = !!cmdArgs.configDev;
8 | const generateApi = !!cmdArgs.configApi;
9 | return runBuild(rootDir, isDev, generateApi);
10 | }
11 |
--------------------------------------------------------------------------------
/tests/platform/audio/audio-scripts.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const audio = new Audio('./submit.mp3?testNewAudio');
3 | document.getElementById('btnNewAudio').addEventListener('click', () => {
4 | audio.play().then(() => {
5 | const elm = document.getElementById('testNewAudio');
6 | elm.textContent = 'played';
7 | elm.className = 'testNewAudio';
8 | });
9 | });
10 | })();
11 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/bridge.ts:
--------------------------------------------------------------------------------
1 | import type { MediaSelf } from '../../types';
2 |
3 | export const [
4 | getter,
5 | setter,
6 | callMethod,
7 | constructGlobal,
8 | definePrototypePropertyDescriptor,
9 | randomId,
10 | WorkerProxy,
11 | WorkerEventTargetProxy,
12 | WinIdKey,
13 | InstanceIdKey,
14 | ApplyPathKey,
15 | ] = self.ptm!;
16 |
17 | declare const self: MediaSelf;
18 |
--------------------------------------------------------------------------------
/tests/platform/iframe/call-fn-on-parent.js:
--------------------------------------------------------------------------------
1 | const parentWindow = window.parent;
2 | const rtn = parentWindow.sum1985(30);
3 |
4 | const valElm = document.createElement('span');
5 | valElm.textContent = rtn;
6 | document.body.appendChild(valElm);
7 |
8 | const elm = parentWindow.document.getElementById('testCallWindowParentFn');
9 | elm.textContent = rtn;
10 | elm.className = 'testCallWindowParentFn';
11 |
--------------------------------------------------------------------------------
/tests/platform/iframe/no-deadlock.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/services/freshpaint.ts:
--------------------------------------------------------------------------------
1 | import type { PartytownForwardProperty } from '../lib/types';
2 |
3 | /**
4 | * Forwards Freshpaint.io main window calls to Partytown's worker thread.
5 | *
6 | * https://www.freshpaint.io/
7 | *
8 | * @public
9 | */
10 | export const freshpaintForward: PartytownForwardProperty[] = [
11 | 'freshpaint.addPageviewProperties',
12 | 'freshpaint.identify',
13 | 'freshpaint.track',
14 | ];
15 |
--------------------------------------------------------------------------------
/tests/platform/iframe/current-script.js:
--------------------------------------------------------------------------------
1 | const elm = window.parent.document.getElementById('testCurrentScript');
2 | elm.className = 'testCurrentScript';
3 |
4 | const currentScript = document.currentScript;
5 | const currentUrl = new URL(currentScript.src);
6 | elm.textContent = currentUrl.pathname;
7 |
8 | const div = document.createElement('div');
9 | div.textContent = elm.textContent;
10 | document.body.appendChild(div);
11 |
--------------------------------------------------------------------------------
/tests/platform/iframe/no-deadlock.js:
--------------------------------------------------------------------------------
1 | function fn(x) {
2 | const win = window;
3 | const winParent = win.parent;
4 | const winParentDocument = winParent.document;
5 |
6 | const iframe = winParentDocument.getElementById('deadlock-iframe');
7 |
8 | const iframeWin = iframe.contentWindow;
9 |
10 | const result = iframeWin.val + x;
11 |
12 | return result;
13 | }
14 |
15 | window.fn = fn;
16 | window.val = 88;
17 |
--------------------------------------------------------------------------------
/tests/platform/iframe/src-change2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/react-app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/docs/site/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // Full Astro Configuration API Documentation:
2 | // https://docs.astro.build/reference/configuration-reference
3 |
4 | // @ts-check
5 | export default /** @type {import('astro').AstroUserConfig} */ ({
6 | renderers: [
7 | // Enable the Preact renderer to support Preact JSX components.
8 | '@astrojs/renderer-preact',
9 | ],
10 | buildOptions: {
11 | site: 'https://partytown.builder.io',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/tests/platform/iframe/post-message.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | iframe origin:
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React App
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | .next/
3 | .DS_Store
4 | /dist/
5 | /docs/site/dist/
6 | /docs/site/public/~partytown/
7 | /docs/site/src/table-of-contents.ts
8 | /docs/site/src/pages/
9 | node_modules/
10 | /integration/
11 | /lib/
12 | /react/
13 | /tests/nextjs/public/
14 | /tests/benchmarks/screenshots/
15 | /tests/posts/
16 | /tests/react-app/public/~partytown/
17 | /tests/videos/
18 | /services/
19 | /tsc/
20 | /utils/
21 | /index.cjs
22 | /index.mjs
23 | /index.d.ts
--------------------------------------------------------------------------------
/tests/integrations/adobe-launch/adobe-launch.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('adobe-launch', async ({ page }) => {
4 | await page.goto('/tests/integrations/adobe-launch/');
5 |
6 | const btnTrack = page.locator('#track');
7 | await btnTrack.click();
8 |
9 | await page.waitForSelector('.completed');
10 |
11 | const result = page.locator('#result');
12 | await expect(result).toHaveText('tracked');
13 | });
14 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/index.ts:
--------------------------------------------------------------------------------
1 | import { createAudioConstructor } from './audio';
2 | import './media-element';
3 | import { createMediaSourceConstructor } from './media-source';
4 | import './canvas/index';
5 | import type { InitLazyMediaConstructors } from '../../types';
6 |
7 | const MediaCstrs: InitLazyMediaConstructors = {
8 | Audio: createAudioConstructor,
9 | MediaSource: createMediaSourceConstructor,
10 | };
11 |
12 | (self as any).ptm = MediaCstrs;
13 |
--------------------------------------------------------------------------------
/docs/site/README.md:
--------------------------------------------------------------------------------
1 | # Party Docs Site
2 |
3 | | Command | Action |
4 | | :---------------- | :------------------------------------------- |
5 | | `npm install` | Installs dependencies |
6 | | `npm run dev` | Starts local dev server at `localhost:3000` |
7 | | `npm run build` | Build your production site to `./dist/` |
8 | | `npm run preview` | Preview your build locally, before deploying |
9 |
--------------------------------------------------------------------------------
/tests/platform/iframe/fn-on-window.js:
--------------------------------------------------------------------------------
1 | const parentWindow = window.parent;
2 | parentWindow.sum1955 = (val) => 1955 + val;
3 |
4 | const fn = parentWindow.sum1955;
5 | const rtn = fn(30);
6 |
7 | const output = document.createElement('span');
8 | output.id = 'output';
9 | output.textContent = rtn;
10 | document.body.appendChild(output);
11 |
12 | const elm = parentWindow.document.getElementById('testSetCallWindowParentFn');
13 | elm.textContent = output.textContent;
14 |
--------------------------------------------------------------------------------
/tests/integrations/gtm/gtm.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('gtm', async ({ page }) => {
4 | await page.goto('/tests/integrations/gtm/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const buttonDataLayerPush = page.locator('#buttonDataLayerPush');
9 | await buttonDataLayerPush.click();
10 |
11 | const testDataLayer = page.locator('#testDataLayer');
12 | await expect(testDataLayer).toHaveText('pushed');
13 | });
14 |
--------------------------------------------------------------------------------
/tests/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-app",
3 | "private": true,
4 | "scripts": {
5 | "dev": "npm run partytown && next dev",
6 | "build": "npm run partytown && next build",
7 | "start": "next start",
8 | "partytown": "../../bin/partytown.cjs copylib public/~partytown"
9 | },
10 | "dependencies": {
11 | "@builder.io/partytown": "0.2.3",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "next": "12.0.10"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/integrations/facebook-pixel/facebook-pixel.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('facebook-pixel', async ({ page }) => {
4 | await page.goto('/tests/integrations/facebook-pixel/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const buttonSendEvent = page.locator('#buttonSendEvent');
9 | await buttonSendEvent.click();
10 |
11 | const testFbq = page.locator('#testFbq');
12 | await expect(testFbq).toHaveText('called');
13 | });
14 |
--------------------------------------------------------------------------------
/tests/platform/iframe/location1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/platform/svg/svg.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('svg', async ({ page }) => {
4 | await page.goto('/tests/platform/svg/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testCreateNS = page.locator('#testCreateNS');
9 | await expect(testCreateNS).toHaveText('svg 1');
10 |
11 | const testNamespaceURI = page.locator('#testNamespaceURI');
12 | await expect(testNamespaceURI).toHaveText('http://www.w3.org/2000/svg');
13 | });
14 |
--------------------------------------------------------------------------------
/src/lib/sandbox/main-constants.ts:
--------------------------------------------------------------------------------
1 | import type { MainWindow, MainWindowContext, WinId } from '../types';
2 |
3 | export const InstanceIdKey = /*#__PURE__*/ Symbol();
4 | export const CreatedKey = /*#__PURE__*/ Symbol();
5 | export const instances = /*#__PURE__*/ new Map();
6 | export const mainRefs = /*#__PURE__*/ new Map();
7 | export const winCtxs: { [winId: WinId]: MainWindowContext | undefined } = {};
8 | export const windowIds = /*#__PURE__*/ new WeakMap();
9 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/time-ranges.ts:
--------------------------------------------------------------------------------
1 | import { callMethod, getter, WorkerProxy } from './bridge';
2 | import { defineCstr } from './utils';
3 |
4 | export class TimeRanges extends WorkerProxy {
5 | start(...args: any[]) {
6 | return callMethod(this, ['start'], args);
7 | }
8 |
9 | end(...args: any[]) {
10 | return callMethod(this, ['end'], args);
11 | }
12 |
13 | get length() {
14 | return getter(this, ['length']);
15 | }
16 | }
17 |
18 | defineCstr('TimeRanges', TimeRanges);
19 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/audio.ts:
--------------------------------------------------------------------------------
1 | import type { InitLazyMediaConstructor, WebWorkerEnvironment } from '../../types';
2 | import { defineCstrName } from './utils';
3 |
4 | export const createAudioConstructor: InitLazyMediaConstructor = (env: WebWorkerEnvironment) =>
5 | defineCstrName(
6 | 'HTMLAudioElement',
7 | class {
8 | constructor(src?: string) {
9 | const audio = env.$document$.createElement('audio');
10 | audio.src = src!;
11 | return audio;
12 | }
13 | }
14 | );
15 |
--------------------------------------------------------------------------------
/tests/platform/screen/screen.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('screen', async ({ page }) => {
4 | await page.goto('/tests/platform/screen/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testWidthHeight = page.locator('#testWidthHeight');
9 | const dimensions = (await testWidthHeight.textContent()).split('x');
10 | const w = parseInt(dimensions[0], 10);
11 | const h = parseInt(dimensions[0], 10);
12 | expect(w > 9).toBe(true);
13 | expect(h > 9).toBe(true);
14 | });
15 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | use: {
5 | viewport: {
6 | width: 520,
7 | height: 600,
8 | },
9 | contextOptions: {
10 | recordVideo: {
11 | dir: 'tests/videos/',
12 | },
13 | },
14 | },
15 | webServer: {
16 | command: 'npm run serve.test',
17 | port: 4001,
18 | timeout: 120 * 1000,
19 | reuseExistingServer: !process.env.CI,
20 | },
21 | };
22 |
23 | export default config;
24 |
--------------------------------------------------------------------------------
/tests/platform/navigator/navigator.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('navigator', async ({ page }) => {
4 | await page.goto('/tests/platform/navigator/');
5 |
6 | const testUserAgent = await page.waitForSelector('.testUserAgent');
7 | const ua = await testUserAgent.textContent();
8 | expect(ua.startsWith('Mozilla/5.0')).toBe(true);
9 |
10 | await page.waitForSelector('.testSendBeacon');
11 | const testSendBeacon = page.locator('#testSendBeacon');
12 | await expect(testSendBeacon).toContainText('true');
13 | });
14 |
--------------------------------------------------------------------------------
/tests/nextjs/nextjs.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('nextjs', async ({ page }) => {
4 | await page.goto('/tests/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const h1 = page.locator('h1');
9 | await expect(h1).toHaveText('Next.js with 🎉');
10 |
11 | const outputNextScript = page.locator('#output-next-script');
12 | await expect(outputNextScript).toHaveText('passed');
13 |
14 | const outputScript = page.locator('#output-script');
15 | await expect(outputScript).toHaveText('passed');
16 | });
17 |
--------------------------------------------------------------------------------
/src/react/api.md:
--------------------------------------------------------------------------------
1 | ## API Report File for "@builder.io/partytown-react"
2 |
3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4 |
5 | ```ts
6 |
7 | // @public
8 | export const Partytown: (props?: PartytownProps) => any;
9 |
10 | // Warning: (ae-forgotten-export) The symbol "PartytownConfig" needs to be exported by the entry point index.d.ts
11 | //
12 | // @public
13 | export interface PartytownProps extends PartytownConfig {
14 | }
15 |
16 | // (No @packageDocumentation comment for this package)
17 |
18 | ```
19 |
--------------------------------------------------------------------------------
/src/lib/service-worker/index.ts:
--------------------------------------------------------------------------------
1 | import { onFetchServiceWorkerRequest, receiveMessageFromSandboxToServiceWorker } from './fetch';
2 |
3 | (self as any as ServiceWorkerGlobalScope).oninstall = () =>
4 | (self as any as ServiceWorkerGlobalScope).skipWaiting();
5 |
6 | (self as any as ServiceWorkerGlobalScope).onactivate = () =>
7 | (self as any as ServiceWorkerGlobalScope).clients.claim();
8 |
9 | (self as any as ServiceWorkerGlobalScope).onmessage = receiveMessageFromSandboxToServiceWorker;
10 |
11 | (self as any as ServiceWorkerGlobalScope).onfetch = onFetchServiceWorkerRequest;
12 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-element.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from './worker-node';
2 | import { NamespaceKey, NodeNameKey } from './worker-constants';
3 |
4 | export const ElementDescriptorMap: PropertyDescriptorMap & ThisType = {
5 | localName: {
6 | get() {
7 | return this[NodeNameKey]!.toLowerCase();
8 | },
9 | },
10 | namespaceURI: {
11 | get() {
12 | return this[NamespaceKey] || 'http://www.w3.org/1999/xhtml';
13 | },
14 | },
15 | nodeType: {
16 | value: 1,
17 | },
18 | tagName: {
19 | get() {
20 | return this[NodeNameKey];
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/docs/site/src/components/Header/SkipToContent.astro:
--------------------------------------------------------------------------------
1 | Skip to Content
2 |
3 |
23 |
--------------------------------------------------------------------------------
/src/utils/api.md:
--------------------------------------------------------------------------------
1 | ## API Report File for "@builder.io/partytown-utils"
2 |
3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4 |
5 | ```ts
6 |
7 | // @public
8 | export function copyLibFiles(destDir: string, opts?: CopyLibFilesOptions): Promise<{
9 | src: string;
10 | dest: string;
11 | }>;
12 |
13 | // @public (undocumented)
14 | export interface CopyLibFilesOptions {
15 | debugDir?: boolean;
16 | }
17 |
18 | // @public
19 | export function libDirPath(): string;
20 |
21 | // (No @packageDocumentation comment for this package)
22 |
23 | ```
24 |
--------------------------------------------------------------------------------
/tests/platform/iframe/parent-window.js:
--------------------------------------------------------------------------------
1 | const win = window;
2 | const winTop = win.top;
3 | const winTopTestScriptWindowParent = winTop.testScriptWindowParent;
4 | const topYear = winTopTestScriptWindowParent.year;
5 |
6 | const parentYear = window.parent.testScriptWindowParent.year;
7 | const total = topYear + parentYear;
8 |
9 | const div = document.createElement('div');
10 | div.textContent = total;
11 | document.body.appendChild(div);
12 |
13 | window.top.document.getElementById('testScriptWindowParent').textContent = total;
14 | window.parent.document
15 | .getElementById('testScriptWindowParent')
16 | .classList.add('testScriptWindowParent');
17 |
--------------------------------------------------------------------------------
/src/lib/service-worker/sync-send-message-to-main-sw.ts:
--------------------------------------------------------------------------------
1 | import type { MainAccessRequest, MainAccessResponse, WebWorkerContext } from '../types';
2 | import { partytownLibUrl } from '../web-worker/worker-constants';
3 |
4 | const syncSendMessageToMainServiceWorker = (
5 | webWorkerCtx: WebWorkerContext,
6 | accessReq: MainAccessRequest
7 | ): MainAccessResponse => {
8 | const xhr = new XMLHttpRequest();
9 | xhr.open('POST', partytownLibUrl('proxytown'), false);
10 | xhr.send(JSON.stringify(accessReq));
11 | // look ma, i'm synchronous (•‿•)
12 | return JSON.parse(xhr.responseText);
13 | };
14 |
15 | export default syncSendMessageToMainServiceWorker;
16 |
--------------------------------------------------------------------------------
/tests/platform/intersection-observer/intersection-observer.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('intersection-observer', async ({ page }) => {
4 | await page.goto('/tests/platform/intersection-observer/');
5 |
6 | const testIntersectionObserver = page.locator('#testIntersectionObserver');
7 | await expect(testIntersectionObserver).toHaveText('');
8 |
9 | const linkScrollDown = page.locator('#scrolldown');
10 | await linkScrollDown.click();
11 |
12 | await page.waitForSelector('.testIntersectionObserver');
13 |
14 | await expect(testIntersectionObserver).toHaveText('intersecting #testIntersectionObserver');
15 | });
16 |
--------------------------------------------------------------------------------
/tests/react-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/integration/index.ts:
--------------------------------------------------------------------------------
1 | import PartytownSnippet from '@snippet';
2 | import { createSnippet } from './snippet';
3 | import type { PartytownConfig } from '../lib/types';
4 |
5 | /**
6 | * Function that returns the Partytown snippet as a string, which can be
7 | * used as the innerHTML of the inlined Partytown script in the head.
8 | *
9 | * @public
10 | */
11 | export const partytownSnippet = (config?: PartytownConfig) =>
12 | createSnippet(config, PartytownSnippet);
13 |
14 | export { SCRIPT_TYPE } from '../lib/utils';
15 |
16 | export type {
17 | PartytownConfig,
18 | PartytownForwardProperty,
19 | ApplyHook,
20 | GetHook,
21 | SetHook,
22 | } from '../lib/types';
23 |
--------------------------------------------------------------------------------
/tests/platform/video/video.spec.__ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('video', async ({ page, context }) => {
4 | // TODO: figure out why playwright crashes
5 |
6 | await page.goto('/tests/platform/video/', { waitUntil: 'networkidle' });
7 |
8 | const btnPlay = page.locator('#btnPlay');
9 | await btnPlay.click();
10 | await page.waitForSelector('.testPlay');
11 | const testPlay = page.locator('#testPlay');
12 | await expect(testPlay).toHaveText('played');
13 | const testBufferedLength = page.locator('#testBufferedLength');
14 | const bufLen = await testBufferedLength.textContent();
15 | expect(!isNaN(bufLen as any)).toBe(true);
16 | });
17 |
--------------------------------------------------------------------------------
/tests/react-app/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo from './logo.svg';
3 | import './App.css';
4 | import { Partytown } from '@builder.io/partytown/react';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/src/services/api.md:
--------------------------------------------------------------------------------
1 | ## API Report File for "@builder.io/partytown-services"
2 |
3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4 |
5 | ```ts
6 |
7 | // Warning: (ae-forgotten-export) The symbol "PartytownForwardProperty" needs to be exported by the entry point index.d.ts
8 | //
9 | // @public
10 | export const facebookPixelForward: PartytownForwardProperty[];
11 |
12 | // @public
13 | export const freshpaintForward: PartytownForwardProperty[];
14 |
15 | // @public
16 | export const googleTagManagerForward: PartytownForwardProperty[];
17 |
18 | // (No @packageDocumentation comment for this package)
19 |
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@builder.io/partytown-docs",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "npm run partytown && astro dev",
7 | "start": "npm run partytown && astro dev",
8 | "build": "npm run partytown && npm run docs && astro build",
9 | "preview": "astro preview",
10 | "docs": "node ./load-docs.mjs",
11 | "partytown": "partytown copylib public/~partytown"
12 | },
13 | "devDependencies": {
14 | "@astrojs/renderer-preact": "^0.4.0",
15 | "@builder.io/partytown": "^0.3.0",
16 | "astro": "0.23.0-next.1",
17 | "domino": "^2.1.6",
18 | "front-matter": "^4.0.2",
19 | "marked": "^4.0.12"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/utils.ts:
--------------------------------------------------------------------------------
1 | export const ContextKey = Symbol();
2 | export const MediaSourceKey = Symbol();
3 | export const ReadyStateKey = Symbol();
4 | export const SourceBuffersKey = Symbol();
5 | export const SourceBufferTasksKey = Symbol();
6 | export const TimeRangesKey = Symbol();
7 |
8 | export const EMPTY_ARRAY = [];
9 |
10 | export const defineCstr = (cstrName: string, Cstr: any) =>
11 | ((self as any)[cstrName] = defineCstrName(cstrName, Cstr));
12 |
13 | export const defineCstrName = (cstrName: string, Cstr: any) =>
14 | Object.defineProperty(Cstr, 'name', {
15 | value: cstrName,
16 | });
17 |
18 | export const notImpl = (api: string) => console.warn(`${api} not implemented`);
19 |
--------------------------------------------------------------------------------
/tests/platform/element-style/element-style.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('element style', async ({ page }) => {
4 | await page.goto('/tests/platform/element-style/');
5 | await page.waitForSelector('.completed');
6 |
7 | const testColor = page.locator('#testColor');
8 | await expect(testColor).toHaveCSS('color', 'rgb(255, 0, 0)');
9 | await expect(testColor).toHaveText('red');
10 |
11 | const testBeforeAppend = page.locator('#testBeforeAppend');
12 | await expect(testBeforeAppend).toHaveCSS('color', 'rgb(0, 0, 255)');
13 | await expect(testBeforeAppend).toHaveCSS('fontWeight', '800');
14 | await expect(testBeforeAppend).toHaveText('blue');
15 | });
16 |
--------------------------------------------------------------------------------
/scripts/build-api.ts:
--------------------------------------------------------------------------------
1 | import type { BuildOptions } from './utils';
2 | import { join } from 'path';
3 | import { Extractor, ExtractorConfig } from '@microsoft/api-extractor';
4 |
5 | export function buildApi(opts: BuildOptions) {
6 | console.log('👑 Generate API types\n');
7 |
8 | const dirs = [opts.srcIntegrationDir, opts.srcServicesDir, opts.srcReactDir, opts.srcUtilsDir];
9 | dirs.forEach((dir) => {
10 | const extractorConfig = ExtractorConfig.loadFileAndPrepare(join(dir, 'api-extractor.json'));
11 | const result = Extractor.invoke(extractorConfig, {
12 | localBuild: true,
13 | showVerboseMessages: true,
14 | });
15 | if (!result.succeeded) {
16 | process.exit(1);
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/tests/react-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/docs/site/src/components/RightSidebar/RightSidebar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import TableOfContents from './TableOfContents.tsx';
3 | import MoreMenu from './MoreMenu.astro';
4 | const { content, githubEditUrl } = Astro.props;
5 | const headers = content.astro.headers;
6 | ---
7 |
8 |
14 |
15 |
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "target": "ES2018",
5 | "module": "ES2020",
6 | "lib": ["es2020", "DOM", "WebWorker"],
7 | "strict": true,
8 | "importsNotUsedAsValues": "error",
9 | "esModuleInterop": true,
10 | "moduleResolution": "node",
11 | "outDir": "tsc",
12 | "declaration": true,
13 | "skipLibCheck": true,
14 | "sourceMap": false,
15 | "jsx": "react",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@builder.io/partytown/integration": ["src/integration/index.ts"],
19 | "@builder.io/partytown/services": ["src/integration/services.ts"],
20 | "react": ["node_modules/react"]
21 | }
22 | },
23 | "include": ["src", "scripts", "tests/unit"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-node-list.ts:
--------------------------------------------------------------------------------
1 | import { len } from '../utils';
2 | import type { Node } from './worker-node';
3 |
4 | export class NodeList {
5 | private _: Node[];
6 |
7 | constructor(nodes: Node[]) {
8 | (this._ = nodes).map((node, index) => ((this as any)[index] = node));
9 | }
10 | entries() {
11 | return this._.entries();
12 | }
13 | forEach(cb: (value: Node, index: number) => void, thisArg?: any) {
14 | this._.map(cb, thisArg);
15 | }
16 | item(index: number) {
17 | return (this as any)[index];
18 | }
19 | keys() {
20 | return this._.keys();
21 | }
22 | get length() {
23 | return len(this._);
24 | }
25 | values() {
26 | return this._.values();
27 | }
28 | [Symbol.iterator]() {
29 | return this._[Symbol.iterator]();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/scripts/build-utils.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions, submodulePackageJson } from './utils';
2 | import { join } from 'path';
3 | import type { RollupOptions } from 'rollup';
4 |
5 | export function buildUtils(opts: BuildOptions): RollupOptions {
6 | return {
7 | input: join(opts.tscUtilsDir, 'index.js'),
8 | output: [
9 | {
10 | file: join(opts.distUtilsDir, 'index.cjs'),
11 | format: 'cjs',
12 | },
13 | {
14 | file: join(opts.distUtilsDir, 'index.mjs'),
15 | format: 'es',
16 | },
17 | ],
18 | external: ['fs', 'path', 'url', 'util'],
19 | plugins: [
20 | submodulePackageJson(
21 | '@builder.io/partytown/utils',
22 | opts.srcUtilsDir,
23 | opts.distUtilsDir,
24 | opts
25 | ),
26 | ],
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/scripts/build-services.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions, submodulePackageJson } from './utils';
2 | import { join } from 'path';
3 | import type { OutputOptions, RollupOptions } from 'rollup';
4 |
5 | export function buildServices(opts: BuildOptions): RollupOptions {
6 | const output: OutputOptions[] = [
7 | {
8 | file: join(opts.distServicesDir, 'index.cjs'),
9 | format: 'cjs',
10 | },
11 | {
12 | file: join(opts.distServicesDir, 'index.mjs'),
13 | format: 'es',
14 | },
15 | ];
16 |
17 | return {
18 | input: join(opts.tscServicesDir, 'index.js'),
19 | output,
20 | plugins: [
21 | submodulePackageJson(
22 | '@builder.io/partytown/services',
23 | opts.srcServicesDir,
24 | opts.distServicesDir,
25 | opts
26 | ),
27 | ],
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/docs/integrations.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Integration Guides
3 | ---
4 |
5 | Partytown can work with any HTML page, and doesn't require a specific framework. However, to make it easier to use in various frameworks or services, plugins/wrappers can be made for their ecosystem. Below is a current list of integrations. Please help us add more!
6 |
7 | - [Astro](/astro)
8 | - [HTML](/html)
9 | - [NextJS](/nextjs)
10 | - [Nuxt](/nuxt)
11 | - [React](/react)
12 | - [Remix](/remix)
13 | - [Shopify Hydrogen](/shopify-hydrogen)
14 |
15 | See something missing and would like to contribute? Please ping us on [Discord](https://discord.gg/tw5qMfgQ) and we'd love to work with you to get your integration up and running and listed here. It may be as simple as just writing a doc, similar to the ones above, detailing how to best implemented Partytown in the framework or service.
16 |
--------------------------------------------------------------------------------
/src/integration/snippet.ts:
--------------------------------------------------------------------------------
1 | import type { PartytownConfig } from '../lib/types';
2 |
3 | export const createSnippet = (config: PartytownConfig | undefined | null, snippetCode: string) => {
4 | const { forward = [], ...filteredConfig } = config || {};
5 |
6 | const configStr = JSON.stringify(filteredConfig, (k, v) => {
7 | if (typeof v === 'function') {
8 | v = String(v);
9 | if (v.startsWith(k + '(')) {
10 | v = 'function ' + v;
11 | }
12 | }
13 | return v;
14 | });
15 |
16 | return [
17 | `!(function(w,p,f,c){`,
18 | Object.keys(filteredConfig).length > 0
19 | ? `c=w[p]=Object.assign(w[p]||{},${configStr});`
20 | : `c=w[p]=w[p]||{};`,
21 | `c[f]=(c[f]||[])`,
22 | forward.length > 0 ? `.concat(${JSON.stringify(forward)})` : ``,
23 | `})(window,'partytown','forward');`,
24 | snippetCode,
25 | ].join('');
26 | };
27 |
--------------------------------------------------------------------------------
/scripts/build-react.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions, submodulePackageJson, submodulePath } from './utils';
2 | import { join } from 'path';
3 | import type { RollupOptions } from 'rollup';
4 |
5 | export function buildReact(opts: BuildOptions): RollupOptions {
6 | return {
7 | input: join(opts.tscReactDir, 'index.js'),
8 | output: [
9 | {
10 | file: join(opts.distReactDir, 'index.cjs'),
11 | format: 'cjs',
12 | },
13 | {
14 | file: join(opts.distReactDir, 'index.mjs'),
15 | format: 'es',
16 | },
17 | ],
18 | external: ['react'],
19 | plugins: [
20 | submodulePath('@builder.io/partytown/integration', '../integration/index'),
21 | submodulePackageJson(
22 | '@builder.io/partytown/react',
23 | opts.srcReactDir,
24 | opts.distReactDir,
25 | opts
26 | ),
27 | ],
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/canvas/index.ts:
--------------------------------------------------------------------------------
1 | import { definePrototypePropertyDescriptor } from '../bridge';
2 | import { ContextKey } from '../utils';
3 | import { createContext2D } from './context-2d';
4 | import { createContextWebGL } from './context-webgl';
5 |
6 | const HTMLCanvasDescriptorMap: PropertyDescriptorMap & ThisType = {
7 | getContext: {
8 | value(this: any, contextType: string, contextAttributes: any) {
9 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
10 | if (!this[ContextKey]) {
11 | this[ContextKey] = (contextType.includes('webgl') ? createContextWebGL : createContext2D)(
12 | this,
13 | contextType,
14 | contextAttributes
15 | );
16 | }
17 | return this[ContextKey];
18 | },
19 | },
20 | };
21 |
22 | definePrototypePropertyDescriptor(self.HTMLCanvasElement, HTMLCanvasDescriptorMap);
23 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-forwarded-trigger.ts:
--------------------------------------------------------------------------------
1 | import { deserializeFromMain } from './worker-serialization';
2 | import { environments } from './worker-constants';
3 | import { ForwardMainTriggerData, PlatformInstanceId } from '../types';
4 | import { len } from '../utils';
5 |
6 | export const workerForwardedTriggerHandle = ({
7 | $winId$,
8 | $forward$,
9 | $args$,
10 | }: ForwardMainTriggerData) => {
11 | // see src/lib/main/snippet.ts and src/lib/sandbox/main-forward-trigger.ts
12 | try {
13 | let target: any = environments[$winId$].$window$;
14 | let i = 0;
15 | let l = len($forward$);
16 |
17 | for (; i < l; i++) {
18 | if (i + 1 < l) {
19 | target = target[$forward$[i]];
20 | } else {
21 | target[$forward$[i]].apply(
22 | target,
23 | deserializeFromMain(null, PlatformInstanceId.window, [], $args$)
24 | );
25 | }
26 | }
27 | } catch (e) {
28 | console.error(e);
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/scripts/copy-site.cjs:
--------------------------------------------------------------------------------
1 | const { copy, emptyDir } = require('fs-extra');
2 | const { join } = require('path');
3 |
4 | async function copySiteDist() {
5 | const rootDir = join(__dirname, '..');
6 | const dist = join(rootDir, 'dist');
7 | const docsSrcDist = join(rootDir, 'docs', 'site', 'dist');
8 |
9 | await emptyDir(dist);
10 |
11 | console.log(docsSrcDist);
12 | await copy(docsSrcDist, dist, { overwrite: true });
13 |
14 | const distTests = join(dist, 'tests');
15 | await emptyDir(distTests);
16 |
17 | const tests = ['atomics', 'benchmarks', 'integrations', 'platform', '404.html', 'index.html'];
18 | await Promise.all(
19 | tests.map(async (f) => {
20 | const src = join(rootDir, 'tests', f);
21 | const dest = join(distTests, f);
22 | console.log(dest);
23 | await copy(src, dest, { overwrite: true });
24 | })
25 | );
26 |
27 | await copy(join(rootDir, 'lib'), join(dist, '~partytown'), { overwrite: true });
28 | }
29 |
30 | copySiteDist();
31 |
--------------------------------------------------------------------------------
/tests/platform/iframe/cross-origin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 | iframe origin:
13 |
14 |
15 |
16 | parent origin:
17 |
18 |
19 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/integrations/event-forwarding/event-forwarding.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('integration event forwarding', async ({ page }) => {
4 | await page.goto('/tests/integrations/event-forwarding/');
5 | await page.waitForSelector('.completed');
6 |
7 | const testFn = page.locator('#testFn');
8 | await expect(testFn).toHaveText('fnReady');
9 |
10 | const testArray = page.locator('#testArray');
11 | await expect(testArray).toHaveText('arrayReady');
12 |
13 | const buttonForwardEvent = page.locator('#buttonForwardEvent');
14 | await buttonForwardEvent.click();
15 | await expect(testFn).toHaveText('click1');
16 | await buttonForwardEvent.click();
17 | await expect(testFn).toHaveText('click2');
18 |
19 | const buttonArrayPush = page.locator('#buttonArrayPush');
20 | await buttonArrayPush.click();
21 | await expect(testArray).toHaveText(JSON.stringify({ mph: 88 }));
22 | await buttonArrayPush.click();
23 | await expect(testArray).toHaveText(JSON.stringify({ mph: 89 }));
24 | });
25 |
--------------------------------------------------------------------------------
/tests/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@builder.io/partytown": "file:../..",
7 | "@types/react": "^17.0.38",
8 | "@types/react-dom": "^17.0.11",
9 | "react": "^17.0.2",
10 | "react-dom": "^17.0.2",
11 | "react-scripts": "5.0.0",
12 | "typescript": "^4.5.4"
13 | },
14 | "scripts": {
15 | "start": "npm run partytown && react-scripts start",
16 | "build": "npm run partytown && react-scripts build",
17 | "test": "react-scripts test",
18 | "partytown": "../../bin/partytown.cjs copylib public/~partytown"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-state.ts:
--------------------------------------------------------------------------------
1 | import { InstanceStateKey, webWorkerRefIdsByRef, webWorkerRefsByRefId } from './worker-constants';
2 | import { randomId } from '../utils';
3 | import type { RefHandler } from '../types';
4 | import type { WorkerInstance } from './worker-instance';
5 |
6 | export const hasInstanceStateValue = (instance: WorkerInstance, stateKey: string | number) =>
7 | stateKey in instance[InstanceStateKey];
8 |
9 | export const getInstanceStateValue = (
10 | instance: WorkerInstance,
11 | stateKey: string | number
12 | ): T => instance[InstanceStateKey][stateKey];
13 |
14 | export const setInstanceStateValue = (
15 | instance: WorkerInstance,
16 | stateKey: string | number,
17 | stateValue: any
18 | ) => (instance[InstanceStateKey][stateKey] = stateValue);
19 |
20 | export const setWorkerRef = (ref: RefHandler, refId?: number) => {
21 | if (!(refId = webWorkerRefIdsByRef.get(ref))) {
22 | webWorkerRefIdsByRef.set(ref, (refId = randomId()));
23 | webWorkerRefsByRefId[refId] = ref;
24 | }
25 | return refId;
26 | };
27 |
--------------------------------------------------------------------------------
/docs/site/src/components/Footer/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { path } = Astro.props;
3 | ---
4 |
5 |
21 |
22 |
41 |
--------------------------------------------------------------------------------
/tests/platform/canvas/canvas.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('canvas', async ({ page }) => {
4 | await page.goto('/tests/platform/canvas/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testGetContext2dCanvas = page.locator('#testGetContext2dCanvas');
9 | await expect(testGetContext2dCanvas).toHaveText('CANVAS');
10 |
11 | const testMeasureText = page.locator('#testMeasureText');
12 | await expect(testMeasureText).not.toHaveText('');
13 |
14 | const testFillStyle = page.locator('#testFillStyle');
15 | await expect(testFillStyle).toHaveText('blue');
16 |
17 | const testCreateRadialGradient = page.locator('#testCreateRadialGradient');
18 | await expect(testCreateRadialGradient).toHaveText('CanvasGradient');
19 |
20 | const testGetContextWebGLCanvas = page.locator('#testGetContextWebGLCanvas');
21 | await expect(testGetContextWebGLCanvas).toHaveText('CANVAS');
22 |
23 | const testWebGLRenderInfo = page.locator('#testWebGLRenderInfo');
24 | await expect(testWebGLRenderInfo).not.toHaveText('');
25 | });
26 |
--------------------------------------------------------------------------------
/docs/site/src/components/RightSidebar/MoreMenu.astro:
--------------------------------------------------------------------------------
1 | ---
2 | const { githubEditUrl } = Astro.props;
3 | ---
4 |
5 |
29 |
30 |
38 |
--------------------------------------------------------------------------------
/src/lib/atomics/sync-send-message-to-main-atomics.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MainAccessRequest,
3 | MainAccessResponse,
4 | WebWorkerContext,
5 | WorkerMessageType,
6 | } from '../types';
7 |
8 | const syncSendMessageToMainAtomics = (
9 | webWorkerCtx: WebWorkerContext,
10 | accessReq: MainAccessRequest
11 | ): MainAccessResponse => {
12 | const sharedDataBuffer = webWorkerCtx.$sharedDataBuffer$!;
13 | const sharedData = new Int32Array(sharedDataBuffer);
14 |
15 | // Reset length before call
16 | Atomics.store(sharedData, 0, 0);
17 |
18 | // Asyncronously call main
19 | webWorkerCtx.$postMessage$([WorkerMessageType.ForwardWorkerAccessRequest, accessReq]);
20 |
21 | // Syncronously wait for response
22 | Atomics.wait(sharedData, 0, 0);
23 |
24 | let dataLength = Atomics.load(sharedData, 0);
25 | let accessRespStr = '';
26 | let i = 0;
27 |
28 | for (; i < dataLength; i++) {
29 | accessRespStr += String.fromCharCode(sharedData[i + 1]);
30 | }
31 |
32 | return JSON.parse(accessRespStr) as MainAccessResponse;
33 | };
34 |
35 | export default syncSendMessageToMainAtomics;
36 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "headers": [
3 | {
4 | "source": "/(.*)",
5 | "headers": [
6 | {
7 | "key": "Cross-Origin-Embedder-Policy",
8 | "value": "credentialless"
9 | },
10 | {
11 | "key": "Cross-Origin-Opener-Policy",
12 | "value": "same-origin"
13 | }
14 | ]
15 | },
16 | {
17 | "source": "/(.*)",
18 | "has": [
19 | {
20 | "type": "query",
21 | "key": "coep",
22 | "value": "require-corp"
23 | }
24 | ],
25 | "headers": [
26 | {
27 | "key": "Cross-Origin-Embedder-Policy",
28 | "value": "require-corp"
29 | }
30 | ]
31 | },
32 | {
33 | "source": "/(.*)",
34 | "has": [
35 | {
36 | "type": "query",
37 | "key": "coep",
38 | "value": "false"
39 | }
40 | ],
41 | "headers": [
42 | {
43 | "key": "Cross-Origin-Embedder-Policy",
44 | "value": "unsafe-none"
45 | }
46 | ]
47 | }
48 | ],
49 | "public": true
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-navigator.ts:
--------------------------------------------------------------------------------
1 | import type { WebWorkerEnvironment } from '../types';
2 | import { debug } from '../utils';
3 | import { logWorker } from '../log';
4 | import { resolveUrl } from './worker-exec';
5 | import { webWorkerCtx } from './worker-constants';
6 |
7 | export const createNavigator = (env: WebWorkerEnvironment) => {
8 | const navigator = self.navigator as any;
9 |
10 | navigator.sendBeacon = (url: string, body?: any) => {
11 | if (debug && webWorkerCtx.$config$.logSendBeaconRequests) {
12 | try {
13 | logWorker(
14 | `sendBeacon: ${resolveUrl(env, url, true)}${
15 | body ? ', data: ' + JSON.stringify(body) : ''
16 | }`
17 | );
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | }
22 | try {
23 | fetch(resolveUrl(env, url, true), {
24 | method: 'POST',
25 | body,
26 | mode: 'no-cors',
27 | keepalive: true,
28 | });
29 | return true;
30 | } catch (e) {
31 | console.error(e);
32 | return false;
33 | }
34 | };
35 |
36 | return navigator;
37 | };
38 |
--------------------------------------------------------------------------------
/docs/sandboxing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Sandboxing
3 | ---
4 |
5 | Third-party scripts are often a black-box with large amounts of code. What's buried within the obfuscated code is difficult to tell. It's minified for good reason, but regardless it becomes very difficult to understand what third-party scripts are executing on _your_ site and _your_ user's devices.
6 |
7 | Partytown on the other hand, is able to isolate and sandbox third-party scripts within a web worker, and allow, or deny, access to main thread APIs. This includes cookies, localStorage, or the entire document. Because the code is executed within the worker, and their access to the main thread _must_ go through the proxy, Partytown is able to give developers control over what the scripts that can execute.
8 |
9 | Essentially, Partytown lets you:
10 |
11 | - Isolate third-party scripts within a sandbox ([web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)).
12 | - Configure which browser APIs specific scripts can, and cannot, execute.
13 | - Option to log API calls and passed in arguments in order to give better insight as to what the scripts are doing.
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Builder.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-css-style-declaration.ts:
--------------------------------------------------------------------------------
1 | import type { ApplyPath } from '../types';
2 | import { cachedDimensions } from './worker-constants';
3 | import { logDimensionCacheClearStyle } from '../log';
4 | import { setter } from './worker-proxy';
5 | import { WorkerInstance } from './worker-instance';
6 |
7 | export class CSSStyleDeclaration extends WorkerInstance {
8 | constructor(winId: number, instanceId: number, applyPath: ApplyPath, styles: any) {
9 | super(winId, instanceId, applyPath);
10 |
11 | Object.assign(this, styles);
12 |
13 | return new Proxy(this, {
14 | get(target, propName) {
15 | return (target as any)[propName];
16 | },
17 | set(target, propName, propValue) {
18 | setter(target, [propName], propValue);
19 | logDimensionCacheClearStyle(target, propName);
20 | cachedDimensions.clear();
21 | return true;
22 | },
23 | });
24 | }
25 |
26 | getPropertyValue(propName: string) {
27 | return (this as any)[propName];
28 | }
29 |
30 | setProperty(propName: string, propValue: any) {
31 | (this as any)[propName] = propValue;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/platform/style/style.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('style', async ({ page }) => {
4 | await page.goto('/tests/platform/style/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testSheetType = page.locator('#testSheetType');
9 | await expect(testSheetType).toHaveText('text/css');
10 |
11 | const testCssRulesLength = page.locator('#testCssRulesLength');
12 | await expect(testCssRulesLength).toHaveText('1');
13 |
14 | const testOwnerNode = page.locator('#testOwnerNode');
15 | await expect(testOwnerNode).toHaveText('STYLE');
16 |
17 | const testCssRules0 = page.locator('#testCssRules0');
18 | await expect(testCssRules0).toHaveText('#testCssRules0 { color: red; }');
19 |
20 | const testCssRulesItem0 = page.locator('#testCssRulesItem0');
21 | await expect(testCssRulesItem0).toHaveText('#testCssRulesItem0');
22 |
23 | const testInsertRule = page.locator('#testInsertRule');
24 | await expect(testInsertRule).toHaveText('#testInsertRule');
25 |
26 | const testDeleteRule = page.locator('#testDeleteRule');
27 | await expect(testDeleteRule).toHaveText('rgb(0, 128, 0) 2');
28 | });
29 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-constructors.ts:
--------------------------------------------------------------------------------
1 | import { htmlMedia, lazyLoadMedia } from './worker-media';
2 | import type { Node } from './worker-node';
3 | import { nodeConstructors, webWorkerInstances } from './worker-constants';
4 |
5 | export const getOrCreateNodeInstance = (
6 | winId: number,
7 | instanceId: number,
8 | nodeName: string,
9 | namespace?: string,
10 | instance?: Node
11 | ) => {
12 | instance = webWorkerInstances.get(instanceId);
13 | if (!instance) {
14 | instance = createNodeInstance(winId, instanceId, nodeName, namespace);
15 | webWorkerInstances.set(instanceId, instance);
16 | }
17 | return instance;
18 | };
19 |
20 | export const createNodeInstance = (
21 | winId: number,
22 | instanceId: number,
23 | nodeName: string,
24 | namespace?: string
25 | ) => {
26 | if (htmlMedia.includes(nodeName)) {
27 | lazyLoadMedia();
28 | }
29 |
30 | const NodeCstr: typeof Node = nodeConstructors[nodeName]
31 | ? nodeConstructors[nodeName]
32 | : nodeName.includes('-')
33 | ? nodeConstructors.UNKNOWN
34 | : (self as any).HTMLElement;
35 |
36 | return new NodeCstr(winId, instanceId, [], nodeName, namespace);
37 | };
38 |
--------------------------------------------------------------------------------
/src/react/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "bundledPackages": ["react"],
4 | "compiler": {
5 | "tsconfigFilePath": "/tsconfig.json"
6 | },
7 | "mainEntryPointFilePath": "/tsc/src/react/index.d.ts",
8 | "apiReport": {
9 | "enabled": true,
10 | "reportFileName": "api.md",
11 | "reportFolder": "/src/react/",
12 | "reportTempFolder": "/.cache/api-extractor/react/"
13 | },
14 | "dtsRollup": {
15 | "enabled": true,
16 | "untrimmedFilePath": "/react/index.d.ts"
17 | },
18 | "docModel": {
19 | "enabled": false
20 | },
21 | "tsdocMetadata": {
22 | "enabled": false
23 | },
24 | "messages": {
25 | "compilerMessageReporting": {
26 | "default": {
27 | "logLevel": "none"
28 | }
29 | },
30 | "extractorMessageReporting": {
31 | "default": {
32 | "logLevel": "error"
33 | }
34 | },
35 | "tsdocMessageReporting": {
36 | "default": {
37 | "logLevel": "error"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "bundledPackages": ["react"],
4 | "compiler": {
5 | "tsconfigFilePath": "/tsconfig.json"
6 | },
7 | "mainEntryPointFilePath": "/tsc/src/utils/index.d.ts",
8 | "apiReport": {
9 | "enabled": true,
10 | "reportFileName": "api.md",
11 | "reportFolder": "/src/utils/",
12 | "reportTempFolder": "/.cache/api-extractor/utils/"
13 | },
14 | "dtsRollup": {
15 | "enabled": true,
16 | "untrimmedFilePath": "/utils/index.d.ts"
17 | },
18 | "docModel": {
19 | "enabled": false
20 | },
21 | "tsdocMetadata": {
22 | "enabled": false
23 | },
24 | "messages": {
25 | "compilerMessageReporting": {
26 | "default": {
27 | "logLevel": "none"
28 | }
29 | },
30 | "extractorMessageReporting": {
31 | "default": {
32 | "logLevel": "error"
33 | }
34 | },
35 | "tsdocMessageReporting": {
36 | "default": {
37 | "logLevel": "error"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/services/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "bundledPackages": ["partytown"],
4 | "compiler": {
5 | "tsconfigFilePath": "/tsconfig.json"
6 | },
7 | "mainEntryPointFilePath": "/tsc/src/services/index.d.ts",
8 | "apiReport": {
9 | "enabled": true,
10 | "reportFileName": "api.md",
11 | "reportFolder": "/src/services/",
12 | "reportTempFolder": "/.cache/api-extractor/services/"
13 | },
14 | "dtsRollup": {
15 | "enabled": true,
16 | "untrimmedFilePath": "/services/index.d.ts"
17 | },
18 | "docModel": {
19 | "enabled": false
20 | },
21 | "tsdocMetadata": {
22 | "enabled": false
23 | },
24 | "messages": {
25 | "compilerMessageReporting": {
26 | "default": {
27 | "logLevel": "none"
28 | }
29 | },
30 | "extractorMessageReporting": {
31 | "default": {
32 | "logLevel": "error"
33 | }
34 | },
35 | "tsdocMessageReporting": {
36 | "default": {
37 | "logLevel": "error"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/integration/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 | "bundledPackages": ["partytown"],
4 | "compiler": {
5 | "tsconfigFilePath": "/tsconfig.json"
6 | },
7 | "mainEntryPointFilePath": "/tsc/src/integration/index.d.ts",
8 | "apiReport": {
9 | "enabled": true,
10 | "reportFileName": "api.md",
11 | "reportFolder": "/src/integration/",
12 | "reportTempFolder": "/.cache/api-extractor/integration/"
13 | },
14 | "dtsRollup": {
15 | "enabled": true,
16 | "untrimmedFilePath": "/integration/index.d.ts"
17 | },
18 | "docModel": {
19 | "enabled": false
20 | },
21 | "tsdocMetadata": {
22 | "enabled": false
23 | },
24 | "messages": {
25 | "compilerMessageReporting": {
26 | "default": {
27 | "logLevel": "none"
28 | }
29 | },
30 | "extractorMessageReporting": {
31 | "default": {
32 | "logLevel": "error"
33 | }
34 | },
35 | "tsdocMessageReporting": {
36 | "default": {
37 | "logLevel": "error"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/docs/nuxt.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Nuxt
3 | ---
4 |
5 | There is a first-class [Nuxt integration for partytown](https://github.com/nuxt-community/partytown-module).
6 |
7 | ## Install
8 |
9 | Add `@nuxtjs/partytown` dependency to your project.
10 |
11 | ```bash
12 | yarn add @nuxtjs/partytown # or npm install @nuxtjs/partytown
13 | ```
14 |
15 | ## Configure
16 |
17 | Add `@nuxtjs/partytown` to the `modules` section of `nuxt.config.ts`. Use the `partytown` property for the [configuration](/configuration).
18 |
19 | ```js
20 | import { defineNuxtConfig } from 'nuxt3';
21 |
22 | export default defineNuxtConfig({
23 | modules: ['@nuxtjs/partytown'],
24 | partytown: {
25 | /* any partytown-specific configuration */
26 | },
27 | });
28 | ```
29 |
30 | ## Partytown Script
31 |
32 | Add `type: 'text/partytown'` [attribute](/partytown-scripts) to any scripts you want to be handled by partytown.
33 |
34 | ```html
35 |
36 |
37 |
38 |
39 |
40 | ```
41 |
42 | Note that the [Nuxt Partytown](https://github.com/nuxt-community/partytown-module) module already handles copying the library files to the correct location.
43 |
--------------------------------------------------------------------------------
/docs/debugging.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Debugging
3 | ---
4 |
5 | With `debug` mode and logging enabled, below is an example of the Partytown logs showing all calls, getters and setters:
6 |
7 | 
8 |
9 | The [debug config](/configuration) must be `true` in order to see more verbose logging messages. The config table below lists all of the possible log configurations.
10 |
11 | | Log Config Property | Description |
12 | | ----------------------- | ----------------------------------- |
13 | | `logCalls` | Log method calls |
14 | | `logGetters` | Log getter calls |
15 | | `logSetters` | Log setter calls |
16 | | `logImageRequests` | Log Image() src requests |
17 | | `logScriptExecution` | Log script executions |
18 | | `logSendBeaconRequests` | Log navigator.sendBeacon() requests |
19 | | `logStackTraces` | Log stack traces |
20 |
21 | Please see the [debug file docs](/distribution#libdebug) more information about the distribution.
22 |
--------------------------------------------------------------------------------
/tests/platform/audio/audio.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('audio', async ({ page }) => {
4 | if (process.env.CI) {
5 | // TODO: audio tests in CI
6 | return;
7 | }
8 |
9 | await page.goto('/tests/platform/audio/');
10 |
11 | page.on('console', (msg) => {
12 | if (msg.type() === 'error') {
13 | console.log(`Audio Test Error: ${msg.text()}`);
14 | }
15 | });
16 |
17 | // TODO: Doesn't always work
18 | // const ua: string = await page.evaluate('navigator.userAgent');
19 |
20 | // if (ua.includes('Chrome')) {
21 | // const testCreateElementAudio = page.locator('#testCreateElementAudio');
22 | // const btnCreateElementAudio = page.locator('#btnCreateElementAudio');
23 | // await btnCreateElementAudio.click();
24 | // await page.waitForSelector('.testCreateElementAudio');
25 | // await expect(testCreateElementAudio).toHaveText('played');
26 |
27 | // const testNewAudio = page.locator('#testNewAudio');
28 | // const btnNewAudio = page.locator('#btnNewAudio');
29 | // await btnNewAudio.click();
30 | // await page.waitForSelector('.testNewAudio');
31 | // await expect(testNewAudio).toHaveText('played');
32 | // }
33 | });
34 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | ---
4 |
5 | Partytown is fairly different from most web development libraries in mainly what’s required for its setup and configuration. At the lowest level, Partytown can work with just vanilla HTML, meaning it doesn’t need to be a part of a build process, doesn’t need a bundler, doesn’t require a specific framework, etc. Because it can integrate with any HTML page, it also makes it much easier to then create wrapper components or plugins for almost any ecosystem, such as Shopify, Wordpress, Nextjs, Gatsby and much more.
6 |
7 | What's different from most web development libraries is that Partytown does _not_ get bundled with your existing site's JavaScript. Instead it purposely wants to stay separate from your code so that it can run within a web worker, and allow your code to run on the main thread. If the two were bundled we've already lost the battle.
8 |
9 | ## Install NPM package
10 |
11 | ```
12 | npm install @builder.io/partytown
13 | ```
14 |
15 | ## Next Steps
16 |
17 | 1. [Add Partytown type attribute to Third-Party Scripts](/partytown-scripts)
18 | 2. [Add Partytown snippet to the ``](/integrations)
19 | 3. [Copy Partytown library files](/copy-library-files)
20 |
--------------------------------------------------------------------------------
/scripts/build-media-implementations.ts:
--------------------------------------------------------------------------------
1 | import type { OutputOptions, RollupOptions } from 'rollup';
2 | import { BuildOptions, fileSize, jsBannerPlugin, versionPlugin, watchDir } from './utils';
3 | import { join } from 'path';
4 | import { minifyPlugin } from './minify';
5 |
6 | export function buildMediaImplementation(opts: BuildOptions): RollupOptions {
7 | const debugOutput: OutputOptions = {
8 | file: join(opts.distLibDebugDir, `partytown-media.js`),
9 | format: 'es',
10 | exports: 'none',
11 | intro: `((self) => {`,
12 | outro: `})(self);`,
13 | plugins: [...minifyPlugin(opts, true), versionPlugin(opts), fileSize()],
14 | };
15 |
16 | const output: OutputOptions[] = [debugOutput];
17 | if (!opts.isDev) {
18 | output.push({
19 | file: join(opts.distLibDir, `partytown-media.js`),
20 | format: 'es',
21 | exports: 'none',
22 | intro: `((self) => {`,
23 | outro: `})(self);`,
24 | plugins: [...minifyPlugin(opts, false), versionPlugin(opts), fileSize()],
25 | });
26 | }
27 |
28 | return {
29 | input: join(opts.tscLibDir, 'web-worker', 'media', 'index.js'),
30 | output,
31 | plugins: [watchDir(opts, join(opts.tscLibDir, 'web-worker', 'media')), jsBannerPlugin(opts)],
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/docs/site/src/components/HeadCommon.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { partytownSnippet } from '@builder.io/partytown/integration';
3 | const partytownSnippetHtml = partytownSnippet({
4 | debug: true
5 | });
6 | ---
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-environment.ts:
--------------------------------------------------------------------------------
1 | import { debug } from '../utils';
2 | import { environments, webWorkerCtx, WinIdKey } from './worker-constants';
3 | import { InitializeEnvironmentData, WorkerMessageType } from '../types';
4 | import { logWorker, normalizedWinId } from '../log';
5 | import { Window } from './worker-window';
6 |
7 | export const createEnvironment = (
8 | { $winId$, $parentWinId$, $url$ }: InitializeEnvironmentData,
9 | isIframeWindow?: boolean
10 | ) => {
11 | if (!environments[$winId$]) {
12 | // create a simulated global environment for this window
13 | // if it hasn't already been created (like an iframe)
14 | new Window($winId$, $parentWinId$, $url$, isIframeWindow);
15 |
16 | if (debug) {
17 | const winType = $winId$ === $parentWinId$ ? 'top' : 'iframe';
18 | logWorker(
19 | `Created ${winType} window ${normalizedWinId($winId$)} environment (${$winId$})`,
20 | $winId$
21 | );
22 | }
23 | }
24 |
25 | webWorkerCtx.$postMessage$([WorkerMessageType.InitializeNextScript, $winId$]);
26 |
27 | return environments[$winId$];
28 | };
29 |
30 | export const getEnv = (instance: { [WinIdKey]: number }) => environments[instance[WinIdKey]];
31 |
32 | export const getEnvWindow = (instance: { [WinIdKey]: number }) => getEnv(instance).$window$;
33 |
--------------------------------------------------------------------------------
/docs/site/src/components/Header/SidebarToggle.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionalComponent } from 'preact';
2 | import { h, Fragment } from 'preact';
3 | import { useState, useEffect } from 'preact/hooks';
4 |
5 | const MenuToggle: FunctionalComponent = () => {
6 | const [sidebarShown, setSidebarShown] = useState(false);
7 |
8 | useEffect(() => {
9 | const body = document.getElementsByTagName('body')[0];
10 | if (sidebarShown) {
11 | body.classList.add('mobile-sidebar-toggle');
12 | } else {
13 | body.classList.remove('mobile-sidebar-toggle');
14 | }
15 | }, [sidebarShown]);
16 |
17 | return (
18 |
41 | );
42 | };
43 |
44 | export default MenuToggle;
45 |
--------------------------------------------------------------------------------
/tests/platform/mutation-observer/mutation-observer.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('mutation-observer', async ({ page }) => {
4 | await page.goto('/tests/platform/mutation-observer/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const buttonObserve = page.locator('#buttonObserve');
9 | const buttonAttr = page.locator('#buttonAttr');
10 | const buttonChildList = page.locator('#buttonChildList');
11 |
12 | const testMutationObserver = page.locator('#testMutationObserver');
13 |
14 | await buttonAttr.click();
15 | await expect(testMutationObserver).toHaveText('');
16 |
17 | await buttonChildList.click();
18 | await expect(testMutationObserver).toHaveText('');
19 |
20 | await buttonObserve.click();
21 |
22 | await buttonAttr.click();
23 | await page.waitForSelector('.step1');
24 | await expect(testMutationObserver).toHaveText('attributes');
25 |
26 | await buttonChildList.click();
27 | await page.waitForSelector('.step2');
28 | await expect(testMutationObserver).toHaveText('childList');
29 |
30 | await buttonObserve.click();
31 | await expect(testMutationObserver).toHaveText('');
32 |
33 | await buttonAttr.click();
34 | await expect(testMutationObserver).toHaveText('');
35 |
36 | await buttonChildList.click();
37 | await expect(testMutationObserver).toHaveText('');
38 | });
39 |
--------------------------------------------------------------------------------
/tests/platform/resize-observer/resize-observer.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('resize-observer', async ({ page }) => {
4 | await page.goto('/tests/platform/resize-observer/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const buttonObserve = page.locator('#buttonObserve');
9 | const buttonIncrease = page.locator('#buttonIncrease');
10 | const buttonDecrease = page.locator('#buttonDecrease');
11 |
12 | const testResizeObserver = page.locator('#testResizeObserver');
13 |
14 | await buttonIncrease.click();
15 | await page.waitForSelector('.step1');
16 | await expect(testResizeObserver).toHaveText('');
17 |
18 | await buttonObserve.click();
19 | await page.waitForSelector('.step2');
20 | await expect(testResizeObserver).toHaveText('Height: 50px');
21 |
22 | await buttonIncrease.click();
23 | await page.waitForSelector('.step3');
24 | await expect(testResizeObserver).toHaveText('Height: 60px');
25 |
26 | await buttonDecrease.click();
27 | await page.waitForSelector('.step4');
28 | await expect(testResizeObserver).toHaveText('Height: 50px');
29 |
30 | await buttonObserve.click();
31 | await page.waitForSelector('.step5');
32 |
33 | await buttonDecrease.click();
34 | await page.waitForSelector('.step6');
35 | await expect(testResizeObserver).toHaveText('Height: 50px');
36 | });
37 |
--------------------------------------------------------------------------------
/src/lib/sandbox/main-forward-trigger.ts:
--------------------------------------------------------------------------------
1 | import { len } from '../utils';
2 | import { MainWindow, PartytownWebWorker, WorkerMessageType } from '../types';
3 | import { serializeForWorker } from './main-serialization';
4 |
5 | export const mainForwardTrigger = (
6 | worker: PartytownWebWorker,
7 | $winId$: number,
8 | win: MainWindow
9 | ) => {
10 | let queuedForwardCalls = win._ptf;
11 | let forwards = (win.partytown || {}).forward || [];
12 | let i: number;
13 | let mainForwardFn: any;
14 |
15 | let forwardCall = ($forward$: string[], args: any) =>
16 | worker.postMessage([
17 | WorkerMessageType.ForwardMainTrigger,
18 | {
19 | $winId$,
20 | $forward$,
21 | $args$: serializeForWorker($winId$, Array.from(args)),
22 | },
23 | ]);
24 |
25 | win._ptf = undefined;
26 |
27 | forwards.map((forwardProps) => {
28 | mainForwardFn = win;
29 | forwardProps.split('.').map((_, i, arr) => {
30 | mainForwardFn = mainForwardFn[arr[i]] =
31 | i + 1 < len(arr)
32 | ? mainForwardFn[arr[i]] || (arr[i + 1] === 'push' ? [] : {})
33 | : (...args: any) => forwardCall(arr, args);
34 | });
35 | });
36 |
37 | if (queuedForwardCalls) {
38 | for (i = 0; i < len(queuedForwardCalls); i += 2) {
39 | forwardCall(queuedForwardCalls[i], queuedForwardCalls[i + 1]);
40 | }
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/tests/nextjs/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import Script from 'next/script';
3 | import { Partytown } from '@builder.io/partytown/react';
4 |
5 | export default function Home() {
6 | return (
7 | <>
8 |
9 | Next.js
10 |
15 |
16 |
17 |
18 |
19 | Next.js with 🎉
20 |
21 |
22 |
23 |
24 |
25 |
33 |
34 |
43 |
44 | >
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/build-main-snippet.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions, fileSize, jsBannerPlugin, versionPlugin } from './utils';
2 | import { join } from 'path';
3 | import { minifyPlugin } from './minify';
4 | import type { OutputOptions, RollupOptions } from 'rollup';
5 |
6 | export function buildMainSnippet(opts: BuildOptions): RollupOptions {
7 | const partytownDebug: OutputOptions = {
8 | file: join(opts.distLibDebugDir, 'partytown.js'),
9 | format: 'es',
10 | exports: 'none',
11 | plugins: [versionPlugin(opts), ...minifyPlugin(opts, true)],
12 | };
13 |
14 | const partytownMin: OutputOptions = {
15 | file: join(opts.distLibDir, 'partytown.js'),
16 | format: 'es',
17 | exports: 'none',
18 | plugins: [
19 | ...minifyPlugin(opts, false),
20 | {
21 | name: 'snippetClosure',
22 | generateBundle(opts, bundle) {
23 | for (const f in bundle) {
24 | const b = bundle[f];
25 | if (b.type === 'chunk') {
26 | b.code = b.code.replace(`"use strict";`, ``).trim();
27 | }
28 | }
29 | },
30 | },
31 | fileSize(),
32 | ],
33 | };
34 |
35 | const output = [partytownDebug];
36 | if (!opts.isDev) {
37 | output.push(partytownMin);
38 | }
39 |
40 | return {
41 | input: join(opts.tscLibDir, 'main', 'snippet-entry.js'),
42 | output,
43 | plugins: [jsBannerPlugin(opts)],
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/media-element.ts:
--------------------------------------------------------------------------------
1 | import { AudioTrackList, hasAudioTracks } from './audio-track';
2 | import { definePrototypePropertyDescriptor, getter, InstanceIdKey, WinIdKey } from './bridge';
3 | import { ReadyStateKey, TimeRangesKey } from './utils';
4 | import { TimeRanges } from './time-ranges';
5 |
6 | const HTMLMediaDescriptorMap: PropertyDescriptorMap & ThisType = {
7 | buffered: {
8 | get() {
9 | if (!this[TimeRangesKey]) {
10 | this[TimeRangesKey] = new TimeRanges(this[WinIdKey], this[InstanceIdKey], ['buffered']);
11 |
12 | setTimeout(() => {
13 | this[TimeRangesKey] = undefined;
14 | }, 5000);
15 | }
16 | return this[TimeRangesKey];
17 | },
18 | },
19 |
20 | readyState: {
21 | get() {
22 | if (this[ReadyStateKey] === 4) {
23 | return 4;
24 | }
25 | if (typeof this[ReadyStateKey] !== 'number') {
26 | this[ReadyStateKey] = getter(this, ['readyState']);
27 |
28 | setTimeout(() => {
29 | this[ReadyStateKey] = undefined;
30 | }, 1000);
31 | }
32 | return this[ReadyStateKey];
33 | },
34 | },
35 | };
36 |
37 | if (hasAudioTracks) {
38 | HTMLMediaDescriptorMap.audioTracks = {
39 | get(this: any) {
40 | return new AudioTrackList(this);
41 | },
42 | };
43 | }
44 |
45 | definePrototypePropertyDescriptor(self.HTMLMediaElement, HTMLMediaDescriptorMap);
46 |
--------------------------------------------------------------------------------
/src/lib/service-worker/sync-create-messenger-sw.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MainAccessRequest,
3 | MessageFromWorkerToSandbox,
4 | Messenger,
5 | PartytownWebWorker,
6 | WorkerMessageType,
7 | } from '../types';
8 | import { onMessageFromWebWorker } from '../sandbox/on-messenge-from-worker';
9 | import { readMainPlatform } from '../sandbox/read-main-platform';
10 |
11 | const createMessengerServiceWorker: Messenger = (receiveMessage) => {
12 | const swContainer = window.navigator.serviceWorker;
13 | return swContainer.getRegistration().then((swRegistration) => {
14 | swContainer.addEventListener('message', (ev: MessageEvent) =>
15 | receiveMessage(
16 | ev.data,
17 | (accessRsp) => swRegistration!.active && swRegistration!.active.postMessage(accessRsp)
18 | )
19 | );
20 |
21 | return (worker: PartytownWebWorker, msg: MessageFromWorkerToSandbox) => {
22 | if (msg[0] === WorkerMessageType.MainDataRequestFromWorker) {
23 | // web worker has requested data from the main thread
24 | // collect up all the info about the main thread interfaces
25 | // send the main thread interface data to the web worker
26 | worker.postMessage([WorkerMessageType.MainDataResponseToWorker, readMainPlatform()]);
27 | } else {
28 | onMessageFromWebWorker(worker, msg);
29 | }
30 | };
31 | });
32 | };
33 |
34 | export default createMessengerServiceWorker;
35 |
--------------------------------------------------------------------------------
/tests/integrations/config/config.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('integration config', async ({ page }) => {
4 | await page.goto('/tests/integrations/config/');
5 | await page.waitForSelector('.completed');
6 |
7 | const testGetContinue = page.locator('#testGetContinue');
8 | await expect(testGetContinue).toHaveText('Integration Config');
9 |
10 | const testGetHook = page.locator('#testGetHook');
11 | await expect(testGetHook).toHaveText('hooked-getter');
12 |
13 | const testGetWindowHook = page.locator('#testGetWindowHook');
14 | await expect(testGetWindowHook).toHaveText('999');
15 |
16 | const testSetContinue = page.locator('#testSetContinue');
17 | await expect(testSetContinue).toHaveText('Continue Title Update');
18 |
19 | const testSetPrevent = page.locator('#testSetPrevent');
20 | await expect(testSetPrevent).toHaveText('prevented body.innerHTML');
21 |
22 | const testSetHook = page.locator('#testSetHook');
23 | await expect(testSetHook).toHaveText('hooked-SPAN-innerText-value');
24 |
25 | const testApplyContinue = page.locator('#testApplyContinue');
26 | await expect(testApplyContinue).toHaveText('88');
27 |
28 | const testApplyHook = page.locator('#testApplyHook');
29 | await expect(testApplyHook).toHaveText('prevented document.write()');
30 |
31 | const testApplyArgs = page.locator('#testApplyArgs');
32 | await expect(testApplyArgs).toHaveText('1.21');
33 | });
34 |
--------------------------------------------------------------------------------
/docs/google-tag-manager.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Google Tag Manager
3 | ---
4 |
5 | ## Add the Google Tag Manager Script
6 |
7 | - [Install Google Tag Manager for web pages](https://developers.google.com/tag-platform/tag-manager/web)
8 |
9 | ## Partytown Script
10 |
11 | Set the script element's `type` attribute to `text/partytown`. For example:
12 |
13 | ```html
14 |
17 | ```
18 |
19 | ## Google Analytics 4 (GA4)
20 |
21 | When using Google Analytics, it's recommended to use [Google Analytics 4](https://support.google.com/analytics/answer/10089681?hl=en). Not only is GA4 Google's official recommendation, but it's also because the GA4 responses come with the correct CORS headers. Simply put, if you ensure you're using GA4, you will not have to [proxy](/proxying-requests) the requests to `www.google-analytics.com`.
22 |
23 | ## Forward Events
24 |
25 | Google Tag Manager uses the [dataLayer](https://developers.google.com/tag-platform/tag-manager/web/datalayer) array to send events. In order for Partytown to forward the calls to `window.dataLayer.push({..})`, the forward config should add `"dataLayer.push"`. Please see [forwarding events and triggers](/forwarding-events) for more information.
26 |
27 | ## Example Config
28 |
29 | ```json
30 | {
31 | "forward": ["dataLayer.push"]
32 | }
33 | ```
34 |
35 | Please see the [integration docs](/integrations) for framework specific configuration.
36 |
--------------------------------------------------------------------------------
/docs/partytown-scripts.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Partytown Scripts
3 | ---
4 |
5 | ## Partytown Script Type
6 |
7 | Add the `type="text/partytown"` attribute to each individual third-party script to run from a web worker. Note that each script is opt-in, meaning that the updated `type` attribute should only be added to scripts that should run with Partytown. Partytown will _not_ automatically upgrade any scripts unless this attribute is added.
8 |
9 | ```diff
10 | -
11 | +
12 | ```
13 |
14 | ## Why `type="text/partytown"`?
15 |
16 | The `type="text/partytown"` attribute does two things:
17 |
18 | 1. Informs the browser to _not_ process the script. By giving the script a [type attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) which the browser does not recognize: "The embedded content is treated as a data block which won't be processed by the browser."
19 | 2. Provides a query selector so Partytown can find all the scripts to run from within the web worker. When the document is ready and Partytown has initialized, Partytown will then query selector for all of the `script[type="text/partytown"]` script elements. You'll notice that after a script has been executed in the web worker, it'll then get an updated type attribute of `type="text/partytown-x"`.
20 |
21 | ## Integrations
22 |
23 | Please see the [integration guides](/integrations) for more information on how to setup Partytown.
24 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-media.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApplyPathKey,
3 | commaSplit,
4 | InstanceIdKey,
5 | partytownLibUrl,
6 | webWorkerCtx,
7 | WinIdKey,
8 | } from './worker-constants';
9 | import { callMethod, constructGlobal, getter, setter } from './worker-proxy';
10 | import { definePrototypePropertyDescriptor, randomId } from '../utils';
11 | import type { InitLazyMediaConstructors, MediaSelf } from '../types';
12 | import { WorkerEventTargetProxy, WorkerInstance } from './worker-instance';
13 |
14 | export const lazyLoadMedia = (): InitLazyMediaConstructors => {
15 | if (!self.ptm) {
16 | // assign functions the lazy loaded code will need
17 | self.ptm = [
18 | getter,
19 | setter,
20 | callMethod,
21 | constructGlobal,
22 | definePrototypePropertyDescriptor,
23 | randomId,
24 | WorkerInstance,
25 | WorkerEventTargetProxy,
26 | WinIdKey,
27 | InstanceIdKey,
28 | ApplyPathKey,
29 | ];
30 |
31 | // sync load partytown-media, which will reset self.ptm
32 | // to Window media constructors, like Audio and MediaSource
33 | webWorkerCtx.$importScripts$(partytownLibUrl('partytown-media.js'));
34 | }
35 |
36 | // return the map of Window media constructors
37 | return self.ptm as any;
38 | };
39 |
40 | export const htmlMedia = commaSplit('AUDIO,CANVAS,VIDEO');
41 |
42 | export const windowMediaConstructors = commaSplit('Audio,MediaSource');
43 |
44 | declare const self: MediaSelf;
45 |
--------------------------------------------------------------------------------
/src/lib/sandbox/on-messenge-from-worker.ts:
--------------------------------------------------------------------------------
1 | import { initializedWorkerScript, readNextScript } from './read-main-scripts';
2 | import { mainWindow } from './main-globals';
3 | import {
4 | MainWindowContext,
5 | MessageFromWorkerToSandbox,
6 | PartytownWebWorker,
7 | WinId,
8 | WorkerMessageType,
9 | } from '../types';
10 | import { randomId } from '../utils';
11 | import { registerWindow } from './main-register-window';
12 | import { winCtxs } from './main-constants';
13 |
14 | export const onMessageFromWebWorker = (
15 | worker: PartytownWebWorker,
16 | msg: MessageFromWorkerToSandbox,
17 | winCtx?: MainWindowContext
18 | ) => {
19 | if (msg[0] === WorkerMessageType.InitializedWebWorker) {
20 | // web worker has finished initializing and ready to run scripts
21 | registerWindow(worker, randomId(), mainWindow);
22 | } else {
23 | winCtx = winCtxs[msg[1] as WinId]!;
24 | if (winCtx) {
25 | if (msg[0] === WorkerMessageType.InitializeNextScript) {
26 | // web worker has been initialized with the main data
27 | requestAnimationFrame(() => readNextScript(worker, winCtx!));
28 | } else if (msg[0] === WorkerMessageType.InitializedEnvironmentScript) {
29 | // web worker has finished initializing the script, and has another one to do
30 | // doing this postMessage back-and-forth so we don't have long running tasks
31 | initializedWorkerScript(worker, winCtx, msg[2], msg[3]);
32 | }
33 | }
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/docs/_table-of-contents.md:
--------------------------------------------------------------------------------
1 | # Docs Table Of Contents
2 |
3 | - [Introduction](/)
4 | - [How Does It Work?](/how-does-partytown-work)
5 | - [Trade-Offs](/trade-offs)
6 | - [Browser Support](/browser-support)
7 | - [Getting Started](/getting-started)
8 | - [Overview](/getting-started)
9 | - [Partytown Scripts](/partytown-scripts)
10 | - [Copy Library Files](/copy-library-files)
11 | - [Atomics](/atomics)
12 | - [Distribution](/distribution)
13 | - [Integrations](/integrations)
14 | - [Astro](/astro)
15 | - [HTML](/html)
16 | - [NextJS](/nextjs)
17 | - [Nuxt](/nuxt)
18 | - [React](/react)
19 | - [Remix](/remix)
20 | - [Shopify Hydrogen](/shopify-hydrogen)
21 | - [Configuration](/configuration)
22 | - [Overview](/configuration)
23 | - [Proxying Requests](/proxying-requests)
24 | - [Forwarding Events](/forwarding-events)
25 | - [Sandboxing](/sandboxing)
26 | - [Debugging](/debugging)
27 | - [Third-Party Services](/common-services)
28 | - [Common Services](/common-services)
29 | - [Facebook Pixel](/facebook-pixel)
30 | - [Google Tag Manager](/google-tag-manager)
31 | - [Tests](/tests/)
32 | - [End-to-end Tests](/tests/)
33 | - [Services](/tests/benchmarks/services/)
34 | - [Benchmark](/tests/benchmarks/)
35 | - [Community](https://github.com/BuilderIO/partytown)
36 | - [Github](https://github.com/BuilderIO/partytown)
37 | - [Discord](https://discord.gg/tw5qMfgQ)
38 | - [@QwikDev](https://twitter.com/QwikDev)
39 | - [@Builderio](https://twitter.com/builderio)
40 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-anchor.ts:
--------------------------------------------------------------------------------
1 | import { commaSplit } from './worker-constants';
2 | import { getEnv } from './worker-environment';
3 | import { getInstanceStateValue, setInstanceStateValue } from './worker-state';
4 | import { getter, setter } from './worker-proxy';
5 | import type { Node } from './worker-node';
6 | import { resolveToUrl } from './worker-exec';
7 | import { StateProp } from '../types';
8 |
9 | export const HTMLAnchorDescriptorMap: PropertyDescriptorMap & ThisType = {};
10 |
11 | const anchorProps = commaSplit('hash,host,hostname,href,origin,pathname,port,protocol,search');
12 |
13 | anchorProps.map((anchorProp) => {
14 | HTMLAnchorDescriptorMap[anchorProp] = {
15 | get(this: any) {
16 | let env = getEnv(this);
17 | let value = getInstanceStateValue(this, StateProp.url);
18 | let href: string;
19 |
20 | if (typeof value !== 'string') {
21 | href = getter(this, ['href']);
22 | setInstanceStateValue(this, StateProp.url, href);
23 | value = (new URL(href) as any)[anchorProp];
24 | }
25 |
26 | return (resolveToUrl(env, value) as any)[anchorProp];
27 | },
28 |
29 | set(this: any, value) {
30 | let env = getEnv(this);
31 | let href = getInstanceStateValue(this, StateProp.url);
32 | let url: any = resolveToUrl(env, href);
33 |
34 | url[anchorProp] = new URL(value + '', url.href);
35 |
36 | setInstanceStateValue(this, StateProp.url, url.href);
37 |
38 | setter(this, ['href'], url.href);
39 | },
40 | };
41 | });
42 |
--------------------------------------------------------------------------------
/docs/site/src/components/HeadSEO.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { SITE, } from '../config.ts';
3 | export interface Props {
4 | content: any;
5 | site: any;
6 | canonicalURL: URL | string;
7 | }
8 | const { content = {}, canonicalURL } = Astro.props;
9 | const formattedContentTitle = content.title ? `${content.title} ${SITE.title}` : SITE.title;
10 | const imageSrc = '';
11 | const canonicalImageSrc = new URL(imageSrc, Astro.site);
12 | const imageAlt = '';
13 | ---
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/bin/partytown.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | async function run() {
4 | const task = process.argv.slice(2).filter((a) => !a.startsWith('-'))[0];
5 | const args = process.argv.slice(2).filter((a) => a !== task);
6 |
7 | if (task === 'help' || args.includes('--help') || args.includes('-h')) {
8 | help();
9 | } else if (task === 'version' || args.includes('--version') || args.includes('-v')) {
10 | console.log(version());
11 | } else if (task === 'copylib') {
12 | await copyLibTask(args);
13 | } else {
14 | panic('Unknown partytown task: ' + task);
15 | }
16 | }
17 |
18 | async function copyLibTask(args) {
19 | try {
20 | const utils = require('../utils/index.cjs');
21 | const destDir = args.filter((a) => !a.startsWith('-'))[0];
22 | const logResult = !args.includes('--silent');
23 | const includeDebugDir = !args.includes('--no-debug');
24 | const result = await utils.copyLibFiles(destDir, {
25 | debugDir: includeDebugDir,
26 | });
27 |
28 | if (logResult) {
29 | console.log('Partytown lib copied to: ' + result.dest);
30 | }
31 | } catch (e) {
32 | panic(String(e.message || e));
33 | }
34 | }
35 |
36 | function help() {
37 | console.log(``);
38 | console.log(`Partytown (${version()}):`);
39 | console.log(``);
40 | console.log(` copylib [--no-debug | --silent]`);
41 | console.log(``);
42 | }
43 |
44 | function version() {
45 | return require('../package.json').version;
46 | }
47 |
48 | function panic(msg) {
49 | console.error('\n❌ ' + msg);
50 | help();
51 | process.exit(1);
52 | }
53 |
54 | run();
55 |
--------------------------------------------------------------------------------
/tests/platform/anchor/anchor.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('anchor', async ({ page }) => {
4 | await page.goto('/tests/platform/anchor/');
5 |
6 | await page.waitForSelector('.testAnchor');
7 | const testAnchor = page.locator('#testAnchor');
8 | await expect(testAnchor).toHaveText('/tests/platform/anchor/some/other/path');
9 |
10 | await page.waitForSelector('.testAnchorConstructor');
11 | const testAnchorConstructor = page.locator('#testAnchorConstructor');
12 | await expect(testAnchorConstructor).toHaveText('HTMLAnchorElement HTMLAnchorElement');
13 |
14 | await page.waitForSelector('.testCreateAnchorNoAppend');
15 | const testCreateAnchorNoAppend = page.locator('#testCreateAnchorNoAppend');
16 | await expect(testCreateAnchorNoAppend).toHaveText('/tests/platform/anchor/no-append-child');
17 |
18 | await page.waitForSelector('.testCreateAnchorAppend');
19 | const testCreateAnchorAppend = page.locator('#testCreateAnchorAppend');
20 | await expect(testCreateAnchorAppend).toHaveText('/tests/platform/anchor/append-child');
21 |
22 | await page.waitForSelector('.testInnerHtmlFirstChild');
23 | const testInnerHtmlFirstChild = page.locator('#testInnerHtmlFirstChild');
24 | await expect(testInnerHtmlFirstChild).toHaveText('#');
25 |
26 | await page.waitForSelector('.testGetHref');
27 | const testGetHref = page.locator('#testGetHref');
28 | await expect(testGetHref).toHaveText('https://builder.io/');
29 |
30 | await page.waitForSelector('.testSetHref');
31 | const testSetHref = page.locator('#testSetHref');
32 | await expect(testSetHref).toHaveText('/pathname');
33 | });
34 |
--------------------------------------------------------------------------------
/scripts/build-integration.ts:
--------------------------------------------------------------------------------
1 | import { BuildOptions, submodulePackageJson } from './utils';
2 | import { join } from 'path';
3 | import type { OutputOptions, RollupOptions } from 'rollup';
4 | import { readFile } from 'fs-extra';
5 |
6 | export function buildIntegration(opts: BuildOptions): RollupOptions {
7 | const output: OutputOptions[] = [
8 | {
9 | file: join(opts.distIntegrationDir, 'index.cjs'),
10 | format: 'cjs',
11 | },
12 | {
13 | file: join(opts.distIntegrationDir, 'index.mjs'),
14 | format: 'es',
15 | },
16 | ];
17 |
18 | return {
19 | input: join(opts.tscIntegrationDir, 'index.js'),
20 | output,
21 | plugins: [
22 | {
23 | name: 'snippet',
24 | resolveId(id) {
25 | if (id === '@snippet') {
26 | return id;
27 | }
28 | },
29 | async load(id) {
30 | if (id === '@snippet') {
31 | const codeFileName = 'partytown.js';
32 |
33 | let codeFilePath: string;
34 | if (opts.isDev) {
35 | codeFilePath = join(opts.distLibDebugDir, codeFileName);
36 | } else {
37 | codeFilePath = join(opts.distLibDir, codeFileName);
38 | }
39 |
40 | const code = JSON.stringify((await readFile(codeFilePath, 'utf-8')).trim());
41 | return `const PartytownSnippet = ${code}; export default PartytownSnippet;`;
42 | }
43 | },
44 | },
45 | submodulePackageJson(
46 | '@builder.io/partytown/integration',
47 | opts.srcIntegrationDir,
48 | opts.distIntegrationDir,
49 | opts
50 | ),
51 | ],
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/tests/integrations/mermaid/standard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Mermaid
8 |
46 |
47 |
48 | Standard Mermaid
49 |
50 |
51 |
52 | graph TD; A-->B; A-->C; B-->D; C-->D;
53 |
54 |
57 |
58 |
59 | Partytown Mermaid
60 | All Tests
61 |
62 |
63 |
--------------------------------------------------------------------------------
/tests/platform/image/image.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('image', async ({ page }) => {
4 | await page.goto('/tests/platform/image/');
5 |
6 | await page.waitForSelector('.testImageOnLoad');
7 | const testImageOnLoad = page.locator('#testImageOnLoad');
8 | await expect(testImageOnLoad).toHaveText('load');
9 |
10 | await page.waitForSelector('.testImageOnError');
11 | const testImageOnError = page.locator('#testImageOnError');
12 | await expect(testImageOnError).toHaveText('error');
13 |
14 | await page.waitForSelector('.testImageListenerLoad');
15 | const testImageListenerLoad = page.locator('#testImageListenerLoad');
16 | await expect(testImageListenerLoad).toHaveText('load');
17 |
18 | await page.waitForSelector('.testImageListenerError');
19 | const testImageListenerError = page.locator('#testImageListenerError');
20 | await expect(testImageListenerError).toHaveText('error');
21 |
22 | await page.waitForSelector('.testImgOnLoad');
23 | const testImgOnLoad = page.locator('#testImgOnLoad');
24 | await expect(testImgOnLoad).toHaveText('load');
25 |
26 | await page.waitForSelector('.testImgOnError');
27 | const testImgOnError = page.locator('#testImgOnError');
28 | await expect(testImgOnError).toHaveText('error');
29 |
30 | await page.waitForSelector('.testImgListenerLoad');
31 | const testImgListenerLoad = page.locator('#testImgListenerLoad');
32 | await expect(testImgListenerLoad).toHaveText('load');
33 |
34 | await page.waitForSelector('.testImgListenerError');
35 | const testImgListenerError = page.locator('#testImgListenerError');
36 | await expect(testImgListenerError).toHaveText('error');
37 | });
38 |
--------------------------------------------------------------------------------
/docs/html.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: HTML
3 | ---
4 |
5 | At the lowest level, Partytown is not tied to one specific framework or build tool. Because of this, Partytown can be used in any webpage as long as the HTML is updated, and the [library scripts](/copy-library-files) are hosted from the origin.
6 |
7 | While the `partytown.js` file _could_ be an external request, it's recommended to inline the script instead.
8 |
9 | ```html
10 |
11 |
14 |
15 |
16 | ```
17 |
18 | ## Configure
19 |
20 | The [configuration](/configuration) should be added to `window` using the `partytown` global object.
21 |
22 | Below is an HTML example of setting up the [forwarding](/forwarding-events) for [Google Tag Manager](/google-tag-manager). Note that the config is before the inlined partytown script.
23 |
24 | ```html
25 |
26 |
31 |
34 |
35 | ```
36 |
37 | ## Partytown Script
38 |
39 | Add the `type="text/partytown"` [attribute](/partytown-scripts) for each script that should run from a web worker.
40 |
41 | ```html
42 |
45 | ```
46 |
47 | ## Copy Library Files
48 |
49 | How the files are copied or served from your site is up each site's setup. A `partytown copylib` CLI [copy task](/copy-library-files) has been provided for convenience which helps copy the Partytown library files to the public directory.
50 |
--------------------------------------------------------------------------------
/docs/facebook-pixel.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Facebook Pixel
3 | ---
4 |
5 | ## Add the Pixel Script
6 |
7 | - [Meta (Facebook) Pixel](https://www.facebook.com/business/learn/facebook-ads-pixel)
8 | - [How to Set Up and Install a Meta Pixel](https://www.facebook.com/business/help/952192354843755?id=1205376682832142)
9 |
10 | ## Partytown Script
11 |
12 | Set the script element's `type` attribute to `text/partytown`. For example:
13 |
14 | ```html
15 |
18 | ```
19 |
20 | ## Proxy Requests
21 |
22 | The `connect.facebook.net` response does not provide the correct CORS header, and a reverse proxy should be used. Below is an example of setting the `resolveUrl` config to proxy the `connect.facebook.net` requests. Please see [Proxying Requests](/proxying-requests) for more information.
23 |
24 | ## Forward Events
25 |
26 | Facebook Pixel uses the [fbq()](https://www.facebook.com/business/help/402791146561655?id=1205376682832142) function to send events. In order for Partytown to forward the calls to `window.fbq({..})`, the forward config should add `"fbq"`. Please see [forwarding events and triggers](/forwarding-events) for more information.
27 |
28 | ## Example Config
29 |
30 | ```js
31 | // https://partytown.builder.io/configuration
32 | {
33 | resolveUrl={function(url) {
34 | if (url.hostname === "connect.facebook.net") {
35 | var proxyUrl = new URL('https://my-reverse-proxy.com/');
36 | proxyUrl.searchParams.append('url', url);
37 | return proxyUrl;
38 | }
39 | },
40 | forward: [
41 | "fbq"
42 | ]
43 | }
44 | ```
45 |
46 | Please see the [integration docs](/integrations) for framework specific configuration.
47 |
--------------------------------------------------------------------------------
/tests/unit/snippet.spec.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'uvu/assert';
2 | import { snippet } from '../../src/lib/main/snippet';
3 | import { suite } from './utils';
4 |
5 | const test = suite();
6 |
7 | test('service worker iframe, lib and debug config', ({ win, document, navigator, top }) => {
8 | win.partytown = {
9 | lib: '/my-custom-location/',
10 | debug: true,
11 | };
12 |
13 | const script = document.createElement('script');
14 | script.type = 'text/partytown';
15 | document.body.appendChild(script);
16 |
17 | snippet(win, document, navigator, top, false);
18 |
19 | assert.equal(navigator.$serviceWorkerUrl, '/my-custom-location/debug/partytown-sw.js');
20 | assert.equal(navigator.$serviceWorkerOptions, { scope: '/my-custom-location/debug/' });
21 |
22 | const iframe = document.body.querySelector('iframe')!;
23 | const iframeUrl = new URL(iframe.src, 'http://builder.io/');
24 | assert.equal(iframeUrl.pathname, '/my-custom-location/debug/partytown-sandbox-sw.html');
25 | });
26 |
27 | test('service worker iframe, defaults', ({ win, document, navigator, top }) => {
28 | const script = document.createElement('script');
29 | script.type = 'text/partytown';
30 | document.body.appendChild(script);
31 |
32 | snippet(win, document, navigator, top, false);
33 |
34 | assert.equal(navigator.$serviceWorkerUrl, '/~partytown/partytown-sw.js');
35 | assert.equal(navigator.$serviceWorkerOptions, { scope: '/~partytown/' });
36 |
37 | const iframe = document.body.querySelector('iframe')!;
38 | const iframeUrl = new URL(iframe.src, 'http://builder.io/');
39 | assert.equal(iframeUrl.pathname, '/~partytown/partytown-sandbox-sw.html');
40 | assert.not.equal(iframeUrl.search, '');
41 | });
42 |
43 | test.run();
44 |
--------------------------------------------------------------------------------
/tests/platform/storage/storage.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('storage', async ({ page }) => {
4 | await page.goto('/tests/platform/storage/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testWinEquals = page.locator('#testWinEquals');
9 | await expect(testWinEquals).toHaveText('true');
10 |
11 | const testLocalStorageGet = page.locator('#testLocalStorageGet');
12 | await expect(testLocalStorageGet).toHaveText('12');
13 |
14 | const testLocalStorageSetGet = page.locator('#testLocalStorageSetGet');
15 | await expect(testLocalStorageSetGet).toHaveText('88');
16 |
17 | const testLocalStorageLen = page.locator('#testLocalStorageLen');
18 | await expect(testLocalStorageLen).toHaveText('1');
19 |
20 | const testLocalStorageClear = page.locator('#testLocalStorageClear');
21 | await expect(testLocalStorageClear).toHaveText('0');
22 |
23 | const testLocalStorageKey = page.locator('#testLocalStorageKey');
24 | await expect(testLocalStorageKey).toHaveText('gw');
25 |
26 | const testLocalStorageKeyGet = page.locator('#testLocalStorageKeyGet');
27 | await expect(testLocalStorageKeyGet).toHaveText('1.21');
28 |
29 | const testLocalStorageRemove = page.locator('#testLocalStorageRemove');
30 | await expect(testLocalStorageRemove).toHaveText('0');
31 |
32 | const testSessionStorageSetGet = page.locator('#testSessionStorageSetGet');
33 | await expect(testSessionStorageSetGet).toHaveText('88');
34 |
35 | const testSessionStorageLen = page.locator('#testSessionStorageLen');
36 | await expect(testSessionStorageLen).toHaveText('1');
37 |
38 | const testWinSessionStorage = page.locator('#testWinSessionStorage');
39 | await expect(testWinSessionStorage).toHaveText('1');
40 | });
41 |
--------------------------------------------------------------------------------
/tests/benchmarks/run.cjs:
--------------------------------------------------------------------------------
1 | const { chromium, webkit } = require('playwright');
2 | const { createServer } = require('../../scripts/server.cjs');
3 | const { join } = require('path');
4 | const { cpus } = require('os');
5 |
6 | const screenshotPath = join(__dirname, 'screenshots');
7 |
8 | (async () => {
9 | let title = `Benchmark: node ${process.version}, ${process.platform}, ${process.arch}`;
10 | title += `, ${cpus()[0].model}`;
11 | console.log(title);
12 | console.log(''.padStart(title.length, '-'));
13 |
14 | const baseline = await runBenchmark(chromium, true, 'Chromium Baseline');
15 | const atomics = await runBenchmark(chromium, false, 'Chromium Atomics');
16 |
17 | console.log(`Baseline:`.padStart(12, ' '), `${baseline}ms`);
18 | console.log(`Atomics:`.padStart(12, ' '), `${atomics}ms`);
19 | })();
20 |
21 | async function runBenchmark(browserType, isBaseline, label) {
22 | const server = await createServer(4005, true);
23 |
24 | const browser = await browserType.launch();
25 | const page = await browser.newPage();
26 | await page.setViewportSize({ width: 360, height: 360 });
27 |
28 | const url = new URL('/tests/benchmarks/', server.address);
29 | if (isBaseline) {
30 | url.searchParams.set('baseline', '');
31 | }
32 | await page.goto(url.href);
33 |
34 | await page.waitForSelector('.completed', { timeout: 180000 });
35 |
36 | const avgElement = page.locator('#result');
37 | const avgResultText = await avgElement.textContent();
38 | const avgResult = parseFloat(avgResultText.replace('ms', ''));
39 |
40 | await page.screenshot({
41 | path: join(screenshotPath, `${label.replace(/ /g, '-').toLowerCase()}.png`),
42 | });
43 | await browser.close();
44 | await server.close();
45 |
46 | return avgResult;
47 | }
48 |
--------------------------------------------------------------------------------
/tests/integrations/hubspot/forms-standard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Hubspot Forms (Standard)
8 |
46 |
47 |
48 | Hubspot Forms (Standard)
49 |
50 |
51 |
58 | Hubspot Forms (Partytown)
59 | All Tests
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docs/site/src/components/RightSidebar/TableOfContents.tsx:
--------------------------------------------------------------------------------
1 | import type { FunctionalComponent } from "preact";
2 | import { h, Fragment } from "preact";
3 | import { useState, useEffect, useRef } from "preact/hooks";
4 |
5 | const TableOfContents: FunctionalComponent<{ headers: any[] }> = ({
6 | headers = [],
7 | }) => {
8 | const itemOffsets = useRef([]);
9 | const [activeId, setActiveId] = useState(undefined);
10 |
11 | useEffect(() => {
12 | const getItemOffsets = () => {
13 | const titles = document.querySelectorAll("article :is(h1, h2, h3, h4)");
14 | itemOffsets.current = Array.from(titles).map((title) => ({
15 | id: title.id,
16 | topOffset: title.getBoundingClientRect().top + window.scrollY,
17 | }));
18 | };
19 |
20 | getItemOffsets();
21 | window.addEventListener("resize", getItemOffsets);
22 |
23 | return () => {
24 | window.removeEventListener("resize", getItemOffsets);
25 | };
26 | }, []);
27 |
28 | return (
29 | <>
30 | On this page
31 |
32 |
39 | {headers
40 | .filter(({ depth }) => depth > 1 && depth < 4)
41 | .map((header) => (
42 |
49 | ))}
50 |
51 | >
52 | );
53 | };
54 |
55 | export default TableOfContents;
56 |
--------------------------------------------------------------------------------
/tests/platform/element-class/element-class.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('element class', async ({ page }) => {
4 | await page.goto('/tests/platform/element-class/');
5 | await page.waitForSelector('.completed');
6 |
7 | const testClassname = page.locator('#testClassname');
8 | await expect(testClassname).toHaveClass('testClassname');
9 | await expect(testClassname).toHaveText('testClassname');
10 |
11 | const testClassList = page.locator('#testClassList');
12 | await expect(testClassList).toHaveClass('testClassList');
13 | await expect(testClassList).toHaveText('testClassList');
14 |
15 | const testClassListRemove = page.locator('#testClassListRemove');
16 | await expect(testClassListRemove).toHaveClass('testClassListRemove');
17 | await expect(testClassListRemove).toHaveText('testClassListRemove');
18 |
19 | const testToggleOn = page.locator('#testToggleOn');
20 | await expect(testToggleOn).toHaveClass('testToggleOn');
21 | await expect(testToggleOn).toHaveText('testToggleOn');
22 |
23 | const testToggleOff = page.locator('#testToggleOff');
24 | await expect(testToggleOff).toHaveClass('testToggleOff');
25 | await expect(testToggleOff).toHaveText('testToggleOff');
26 |
27 | const testClassListValue = page.locator('#testClassListValue');
28 | await expect(testClassListValue).toHaveClass('testClassListValue');
29 | await expect(testClassListValue).toHaveText('testClassListValue');
30 |
31 | const testClassListLength = page.locator('#testClassListLength');
32 | await expect(testClassListLength).toHaveText('3');
33 |
34 | const testImmediate = page.locator('#testImmediate');
35 | await expect(testImmediate).toHaveText('add');
36 | await expect(testImmediate).toHaveClass('testImmediate1 testImmediate2');
37 | });
38 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-image.ts:
--------------------------------------------------------------------------------
1 | import { debug } from '../utils';
2 | import type { EventHandler, WebWorkerEnvironment } from '../types';
3 | import { logWorker } from '../log';
4 | import { resolveUrl } from './worker-exec';
5 | import { webWorkerCtx } from './worker-constants';
6 |
7 | export const createImageConstructor = (env: WebWorkerEnvironment) =>
8 | class HTMLImageElement {
9 | s: string;
10 | l: EventHandler[];
11 | e: EventHandler[];
12 |
13 | constructor() {
14 | this.s = '';
15 | this.l = [];
16 | this.e = [];
17 | }
18 |
19 | get src() {
20 | return this.s;
21 | }
22 | set src(src: string) {
23 | if (debug && webWorkerCtx.$config$.logImageRequests) {
24 | logWorker(`Image() request: ${resolveUrl(env, src)}`, env.$winId$);
25 | }
26 |
27 | fetch(resolveUrl(env, src, true), {
28 | mode: 'no-cors',
29 | keepalive: true,
30 | }).then(
31 | (rsp) => {
32 | if (rsp.ok || rsp.status === 0) {
33 | this.l.map((cb) => cb({ type: 'load' }));
34 | } else {
35 | this.e.map((cb) => cb({ type: 'error' }));
36 | }
37 | },
38 | () => this.e.forEach((cb) => cb({ type: 'error' }))
39 | );
40 | }
41 |
42 | addEventListener(eventName: 'load' | 'error', cb: EventHandler) {
43 | if (eventName === 'load') {
44 | this.l.push(cb);
45 | }
46 | if (eventName === 'error') {
47 | this.e.push(cb);
48 | }
49 | }
50 |
51 | get onload() {
52 | return this.l[0];
53 | }
54 | set onload(cb: EventHandler) {
55 | this.l = [cb];
56 | }
57 |
58 | get onerror() {
59 | return this.e[0];
60 | }
61 | set onerror(cb: EventHandler) {
62 | this.e = [cb];
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/docs/browser-support.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Browser Support
3 | ---
4 |
5 | Partytown works to ensure that all browsers can still run third-party scripts, whether they have support for service workers, atomics, or neither. This means if you're supporting legacy browsers such as IE11, the scripts should continue to work as if Partytown wasn't being used.
6 |
7 | At its core, Partytown uses Service Workers or Atomics to do its synchronous communication from the web worker to the main thread. However, when a browser does not support either of those, it'll fallback to run the scripts the traditional way (the way it works today without Partytown).
8 |
9 | ## How Feature Support Works
10 |
11 | 1. Browser checks to see if it has support for [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) using the [crossOriginIsolated](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated) boolean found on `window`.
12 | 1. If the browser and site do not have support for Atomics, it'll then check to see if the browser has support for [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). Currently this is the most commonly used one.
13 | 1. If the browser does not have support for Service Workers or Atomics, it'll then find all the `type="text/partytown"` scripts and reset them to be traditional scripts and execute normally.
14 |
15 | > Note: Atomics are still experimental, and also require specific server-side response headers for them to work. Please see the [Enabling Atomics](/atomics) section for more info.
16 |
17 | ## Web API Feature Support
18 |
19 | - [Atomics](https://caniuse.com/mdn-javascript_builtins_atomics)
20 | - [JavaScript Proxy](https://caniuse.com/proxy)
21 | - [Service Workers](https://caniuse.com/serviceworkers)
22 | - [Web Workers](https://caniuse.com/webworkers)
23 |
--------------------------------------------------------------------------------
/docs/astro.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Astro
3 | ---
4 |
5 | The easiest way to add Partytown to an [Astro](https://astro.build/) site is adding the snippet to a `
35 |
36 |
39 | ```
40 |
41 | ## Copy Library Files
42 |
43 | Copy library files to `public/~partytown`. How the files are copied or served from your site is up each site's setup. A `partytown copylib` CLI [copy task](/copy-library-files) has been provided for convenience which helps copy the Partytown library files to the public directory. Below is an example of creating a "partytown" NPM script which runs before the `astro build` command:
44 |
45 | ```json
46 | "scripts": {
47 | "build": "npm run partytown && astro build",
48 | "partytown": "partytown copylib public/~partytown"
49 | }
50 | ```
51 |
--------------------------------------------------------------------------------
/src/lib/atomics/sync-create-messenger-atomics.ts:
--------------------------------------------------------------------------------
1 | import { onMessageFromWebWorker } from '../sandbox/on-messenge-from-worker';
2 | import { readMainPlatform } from '../sandbox/read-main-platform';
3 | import {
4 | MainAccessRequest,
5 | MessageFromWorkerToSandbox,
6 | Messenger,
7 | PartytownWebWorker,
8 | WorkerMessageType,
9 | } from '../types';
10 |
11 | const createMessengerAtomics: Messenger = async (receiveMessage) => {
12 | const size = 1024 * 1024 * 1024;
13 | const sharedDataBuffer = new SharedArrayBuffer(size);
14 | const sharedData = new Int32Array(sharedDataBuffer);
15 |
16 | return (worker: PartytownWebWorker, msg: MessageFromWorkerToSandbox) => {
17 | const msgType = msg[0];
18 | const accessReq = msg[1] as MainAccessRequest;
19 |
20 | if (msgType === WorkerMessageType.MainDataRequestFromWorker) {
21 | // web worker has requested data from the main thread
22 | // collect up all the info about the main thread interfaces
23 | // send the main thread interface data to the web worker
24 | const initData = readMainPlatform();
25 | initData.$sharedDataBuffer$ = sharedDataBuffer;
26 | worker.postMessage([WorkerMessageType.MainDataResponseToWorker, initData]);
27 | } else if (msgType === WorkerMessageType.ForwardWorkerAccessRequest) {
28 | receiveMessage(accessReq, (accessRsp) => {
29 | const stringifiedData = JSON.stringify(accessRsp);
30 | const stringifiedDataLength = stringifiedData.length;
31 | for (let i = 0; i < stringifiedDataLength; i++) {
32 | sharedData[i + 1] = stringifiedData.charCodeAt(i);
33 | }
34 | sharedData[0] = stringifiedDataLength;
35 | Atomics.notify(sharedData, 0);
36 | });
37 | } else {
38 | onMessageFromWebWorker(worker, msg);
39 | }
40 | };
41 | };
42 | export default createMessengerAtomics;
43 |
--------------------------------------------------------------------------------
/docs/react.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: React
3 | ---
4 |
5 | The Partytown NPM package already comes with a React component, which is a thin wrapper to the Partytown snippet. This React component should be usable from most React/Preact projects.
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm install @builder.io/partytown
11 | ```
12 |
13 | ## Configure
14 |
15 | The ` ` component is imported from the `@builder.io/partytown/react` submodule. The [config properties](/configuration) are JSX props.
16 |
17 | Below is an example of setting the `debug` config to `true`, and [forward](/forwarding-events) config for [Google Tag Manager](/google-tag-manager).
18 |
19 | ```jsx
20 | import { Partytown } from '@builder.io/partytown/react';
21 |
22 | export function Head() {
23 | return (
24 | <>
25 |
26 | >
27 | );
28 | }
29 | ```
30 |
31 | ## Partytown Script
32 |
33 | Add the `type="text/partytown"` [prop](/partytown-scripts) for each script that should run from a web worker. The example below is using the React specific way of inlining a script with [dangerouslySetInnerHTML](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml).
34 |
35 | ```jsx
36 |
42 | ```
43 |
44 | ## Copy Library Files
45 |
46 | Copy library files to `public/~partytown`. How the files are copied or served from your site is up each site's setup. A `partytown copylib` CLI [copy task](/copy-library-files) has been provided for convenience which helps copy the Partytown library files to the public directory. Below is an example of creating a "partytown" NPM script which could run before the build:
47 |
48 | ```json
49 | "scripts": {
50 | "partytown": "partytown copylib public/~partytown"
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-instance.ts:
--------------------------------------------------------------------------------
1 | import { ApplyPath, CallType } from '../types';
2 | import {
3 | ApplyPathKey,
4 | InstanceIdKey,
5 | NodeNameKey,
6 | InstanceStateKey,
7 | WinIdKey,
8 | NamespaceKey,
9 | eventTargetMethods,
10 | } from './worker-constants';
11 | import { callMethod, getter, setter } from './worker-proxy';
12 |
13 | export class WorkerInstance {
14 | [WinIdKey]: number;
15 | [InstanceIdKey]: number;
16 | [ApplyPathKey]: string[];
17 | [NodeNameKey]: string | undefined;
18 | [NamespaceKey]: string | undefined;
19 | [InstanceStateKey]: { [key: string]: any };
20 |
21 | constructor(
22 | winId: number,
23 | instanceId: number,
24 | applyPath?: ApplyPath,
25 | nodeName?: string,
26 | namespace?: string
27 | ) {
28 | this[WinIdKey] = winId!;
29 | this[InstanceIdKey] = instanceId!;
30 | this[ApplyPathKey] = applyPath || [];
31 | this[NodeNameKey] = nodeName;
32 | this[InstanceStateKey] = {};
33 | if (namespace) {
34 | this[NamespaceKey] = namespace;
35 | }
36 | }
37 | }
38 |
39 | export class WorkerEventTargetProxy extends WorkerInstance {}
40 | eventTargetMethods.map(
41 | (methodName) =>
42 | ((WorkerEventTargetProxy as any).prototype[methodName] = function (...args: any[]) {
43 | return callMethod(this, [methodName], args, CallType.NonBlocking);
44 | })
45 | );
46 |
47 | export class WorkerTrapProxy extends WorkerInstance {
48 | constructor(winId: number, instanceId: number, applyPath?: ApplyPath, nodeName?: string) {
49 | super(winId, instanceId, applyPath, nodeName);
50 |
51 | return new Proxy(this, {
52 | get(instance, propName) {
53 | return getter(instance, [propName]);
54 | },
55 | set(instance, propName, propValue) {
56 | setter(instance, [propName], propValue);
57 | return true;
58 | },
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-script.ts:
--------------------------------------------------------------------------------
1 | import { getEnv } from './worker-environment';
2 | import { getInstanceStateValue, setInstanceStateValue } from './worker-state';
3 | import { getter, setter } from './worker-proxy';
4 | import { HTMLSrcElementDescriptorMap } from './worker-src-element';
5 | import type { Node } from './worker-node';
6 | import { resolveUrl } from './worker-exec';
7 | import { StateProp } from '../types';
8 |
9 | const innerHTMLDescriptor: PropertyDescriptor & ThisType = {
10 | get() {
11 | return getInstanceStateValue(this, StateProp.innerHTML) || '';
12 | },
13 | set(scriptContent: string) {
14 | setInstanceStateValue(this, StateProp.innerHTML, scriptContent);
15 | },
16 | };
17 |
18 | export const HTMLScriptDescriptorMap: PropertyDescriptorMap & ThisType = {
19 | innerHTML: innerHTMLDescriptor,
20 | innerText: innerHTMLDescriptor,
21 |
22 | src: {
23 | get() {
24 | return getInstanceStateValue(this, StateProp.url) || '';
25 | },
26 | set(url: string) {
27 | const env = getEnv(this);
28 | const orgUrl = resolveUrl(env, url, true);
29 | url = resolveUrl(env, url);
30 | setInstanceStateValue(this, StateProp.url, url);
31 | setter(this, ['src'], url);
32 | if (orgUrl !== url) {
33 | setter(this, ['dataset', 'ptsrc'], orgUrl);
34 | }
35 | },
36 | },
37 |
38 | textContent: innerHTMLDescriptor,
39 |
40 | type: {
41 | get() {
42 | return getter(this, ['type']);
43 | },
44 | set(type: string) {
45 | if (!isScriptJsType(type)) {
46 | setInstanceStateValue(this, StateProp.type, type);
47 | setter(this, ['type'], type);
48 | }
49 | },
50 | },
51 |
52 | ...HTMLSrcElementDescriptorMap,
53 | };
54 |
55 | export const isScriptJsType = (scriptType: string) =>
56 | !scriptType || scriptType === 'text/javascript';
57 |
--------------------------------------------------------------------------------
/src/integration/api.md:
--------------------------------------------------------------------------------
1 | ## API Report File for "@builder.io/partytown-integration"
2 |
3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4 |
5 | ```ts
6 |
7 | // Warning: (ae-forgotten-export) The symbol "ApplyHookOptions" needs to be exported by the entry point index.d.ts
8 | //
9 | // @public (undocumented)
10 | export type ApplyHook = (opts: ApplyHookOptions) => any;
11 |
12 | // Warning: (ae-forgotten-export) The symbol "GetHookOptions" needs to be exported by the entry point index.d.ts
13 | //
14 | // @public (undocumented)
15 | export type GetHook = (opts: GetHookOptions) => any;
16 |
17 | // @public
18 | export interface PartytownConfig {
19 | // (undocumented)
20 | apply?: ApplyHook;
21 | debug?: boolean;
22 | forward?: PartytownForwardProperty[];
23 | // (undocumented)
24 | get?: GetHook;
25 | globalFns?: string[];
26 | lib?: string;
27 | logCalls?: boolean;
28 | logGetters?: boolean;
29 | logImageRequests?: boolean;
30 | logMainAccess?: boolean;
31 | logScriptExecution?: boolean;
32 | logSendBeaconRequests?: boolean;
33 | logSetters?: boolean;
34 | logStackTraces?: boolean;
35 | resolveUrl?(url: URL, location: Location): URL | undefined | null;
36 | // (undocumented)
37 | set?: SetHook;
38 | }
39 |
40 | // @public
41 | export type PartytownForwardProperty = string;
42 |
43 | // @public
44 | export const partytownSnippet: (config?: PartytownConfig | undefined) => string;
45 |
46 | // @public
47 | export const SCRIPT_TYPE = "text/partytown";
48 |
49 | // Warning: (ae-forgotten-export) The symbol "SetHookOptions" needs to be exported by the entry point index.d.ts
50 | //
51 | // @public (undocumented)
52 | export type SetHook = (opts: SetHookOptions) => any;
53 |
54 | // (No @packageDocumentation comment for this package)
55 |
56 | ```
57 |
--------------------------------------------------------------------------------
/tests/platform/node/node.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('node', async ({ page }) => {
4 | await page.goto('/tests/platform/node/');
5 |
6 | await page.waitForSelector('.completed');
7 |
8 | const testNodeNameType = page.locator('#testNodeNameType');
9 | await expect(testNodeNameType).toHaveText('#text 3');
10 |
11 | const testCheckbox = page.locator('#testCheckbox');
12 | await expect(testCheckbox).toBeChecked();
13 |
14 | const testText = page.locator('#testText');
15 | await expect(testText).toHaveText('Hello World');
16 |
17 | const testRemove = page.locator('#testRemove');
18 | await expect(testRemove).toHaveText('This is awesome');
19 |
20 | const testHrefProp = page.locator('#testHrefProp');
21 | await expect(testHrefProp).toHaveText('undefined');
22 |
23 | const testParentNode = page.locator('#testParentNode');
24 | await expect(testParentNode).toHaveText('hasParentNode');
25 |
26 | const testComment = page.locator('#testComment');
27 | await expect(testComment).toHaveText('8 #comment 1.21 false');
28 |
29 | const testFragmentNodeType = page.locator('#testFragmentNodeType');
30 | await expect(testFragmentNodeType).toHaveText('11 #document-fragment');
31 |
32 | const testFragmentChildNodes = page.locator('#testFragmentChildNodes');
33 | await expect(testFragmentChildNodes).toHaveText('1 DIV SPAN fragment');
34 |
35 | const testCompareDocumentPosition = page.locator('#testCompareDocumentPosition');
36 | const docPosition = parseInt(await testCompareDocumentPosition.textContent(), 10);
37 | expect(docPosition).toBeGreaterThanOrEqual(35);
38 |
39 | const testBodyParentNode = page.locator('#testBodyParentNode');
40 | await expect(testBodyParentNode).toHaveText('BODY');
41 |
42 | const testCachedParentNode = page.locator('#testCachedParentNode');
43 | await expect(testCachedParentNode).toHaveText('cached-parent-node');
44 | });
45 |
--------------------------------------------------------------------------------
/src/lib/web-worker/init-web-worker.ts:
--------------------------------------------------------------------------------
1 | import { CSSStyleSheet } from './worker-style';
2 | import { defineWorkerInterface, patchPrototypes } from './worker-define-constructors';
3 | import { logWorker } from '../log';
4 | import type { InitWebWorkerData } from '../types';
5 | import { Node } from './worker-node';
6 | import type { PartytownConfig } from '@builder.io/partytown/integration';
7 | import { Performance } from './worker-performance';
8 | import { webWorkerCtx, webWorkerlocalStorage, webWorkerSessionStorage } from './worker-constants';
9 | import { Window } from './worker-window';
10 |
11 | export const initWebWorker = (initWebWorkerData: InitWebWorkerData) => {
12 | const config: PartytownConfig = (webWorkerCtx.$config$ = JSON.parse(initWebWorkerData.$config$));
13 | webWorkerCtx.$importScripts$ = importScripts.bind(self);
14 | webWorkerCtx.$libPath$ = initWebWorkerData.$libPath$;
15 | webWorkerCtx.$postMessage$ = (postMessage as any).bind(self);
16 | webWorkerCtx.$sharedDataBuffer$ = initWebWorkerData.$sharedDataBuffer$;
17 |
18 | webWorkerlocalStorage.set(origin, initWebWorkerData.$localStorage$);
19 | webWorkerSessionStorage.set(origin, initWebWorkerData.$sessionStorage$);
20 |
21 | delete (self as any).postMessage;
22 | delete (self as any).importScripts;
23 |
24 | (self as any).Node = Node;
25 | (self as any).Window = Window;
26 | (self as any).CSSStyleSheet = CSSStyleSheet;
27 | (self as any).Performance = Performance;
28 |
29 | initWebWorkerData.$interfaces$.map(defineWorkerInterface);
30 |
31 | patchPrototypes();
32 |
33 | const fnConfigs: (keyof PartytownConfig)[] = ['resolveUrl', 'get', 'set', 'apply'];
34 | fnConfigs.map((configName: keyof PartytownConfig) => {
35 | if (config[configName]) {
36 | config[configName] = new Function('return ' + config[configName])();
37 | }
38 | });
39 |
40 | webWorkerCtx.$isInitialized$ = 1;
41 |
42 | logWorker(`Initialized web worker`);
43 | };
44 |
--------------------------------------------------------------------------------
/docs/site/src/styles/code.css:
--------------------------------------------------------------------------------
1 | .language-css > code,
2 | .language-sass > code,
3 | .language-scss > code {
4 | color: #fd9170;
5 | }
6 |
7 | [class*='language-'] .namespace {
8 | opacity: 0.7;
9 | }
10 |
11 | .token.plain-text,
12 | [class*='language-bash'] span.token,
13 | [class*='language-shell'] span.token {
14 | color: hsla(var(--color-gray-90), 1);
15 | }
16 |
17 | [class*='language-bash'] span.token,
18 | [class*='language-shell'] span.token {
19 | font-style: bold;
20 | }
21 |
22 | .token.prolog,
23 | .token.comment,
24 | [class*='language-bash'] span.token.comment,
25 | [class*='language-shell'] span.token.comment {
26 | color: hsla(var(--color-gray-70), 1);
27 | }
28 |
29 | .token.selector,
30 | .token.tag,
31 | .token.unit,
32 | .token.url,
33 | .token.variable,
34 | .token.entity,
35 | .token.deleted {
36 | color: #fa5e5b;
37 | }
38 |
39 | .token.boolean,
40 | .token.constant,
41 | .token.doctype,
42 | .token.number,
43 | .token.regex,
44 | .token.builtin,
45 | .token.class,
46 | .token.hexcode,
47 | .token.class-name,
48 | .token.attr-name {
49 | color: hsla(var(--color-yellow), 1);
50 | }
51 |
52 | .token.atrule,
53 | .token.attribute,
54 | .token.attr-value .token.punctuation,
55 | .token.attr-value,
56 | .token.pseudo-class,
57 | .token.pseudo-element,
58 | .token.string {
59 | color: hsla(var(--color-green), 1);
60 | }
61 |
62 | .token.symbol,
63 | .token.function,
64 | .token.id,
65 | .token.important {
66 | color: hsla(var(--color-blue), 1);
67 | }
68 |
69 | .token.important,
70 | .token.id {
71 | font-weight: bold;
72 | }
73 |
74 | .token.cdata,
75 | .token.char,
76 | .token.property {
77 | color: #23b1af;
78 | }
79 |
80 | .token.inserted {
81 | color: hsla(var(--color-green), 1);
82 | }
83 |
84 | .token.keyword {
85 | color: #ff657c;
86 | font-style: italic;
87 | }
88 |
89 | .token.operator {
90 | color: hsla(var(--color-gray-70), 1);
91 | }
92 |
93 | .token.attr-value .token.attr-equals,
94 | .token.punctuation {
95 | color: hsla(var(--color-gray-80), 1);
96 | }
97 |
--------------------------------------------------------------------------------
/tests/unit/integration.spec.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'uvu/assert';
2 | import { suite } from './utils';
3 | import { createSnippet } from '../../src/integration/snippet';
4 |
5 | const test = suite();
6 |
7 | test('partytownSnippet overwrite existing window.partytown', ({ snippetCode, run, win }) => {
8 | win.partytown = { lib: 'libpath-1', forward: ['a'] };
9 | const code = createSnippet(
10 | {
11 | lib: 'libpath-2',
12 | forward: ['b'],
13 | },
14 | snippetCode
15 | );
16 | run(code);
17 | assert.equal(win.partytown, { lib: 'libpath-2', forward: ['a', 'b'] });
18 | });
19 |
20 | test('partytownSnippet merge existing window.partytown', ({ snippetCode, run, win }) => {
21 | win.partytown = { lib: 'libpath', forward: ['a'] };
22 | const code = createSnippet(
23 | {
24 | forward: ['b'],
25 | },
26 | snippetCode
27 | );
28 | run(code);
29 | assert.equal(win.partytown, { lib: 'libpath', forward: ['a', 'b'] });
30 | });
31 |
32 | test('partytownSnippet w/ existing window.partytown.forward', ({ snippetCode, run, win }) => {
33 | win.partytown = { forward: ['a'] };
34 | const code = createSnippet(
35 | {
36 | forward: ['b'],
37 | },
38 | snippetCode
39 | );
40 | run(code);
41 | assert.equal(win.partytown, { forward: ['a', 'b'] });
42 | });
43 |
44 | test('partytownSnippet w/ config.forward', ({ snippetCode, run, win }) => {
45 | const code = createSnippet(
46 | {
47 | forward: ['a'],
48 | },
49 | snippetCode
50 | );
51 | run(code);
52 | assert.equal(win.partytown, { forward: ['a'] });
53 | });
54 |
55 | test('partytownSnippet w/ config', ({ snippetCode, run, win }) => {
56 | const code = createSnippet({}, snippetCode);
57 | run(code);
58 | assert.equal(win.partytown, { forward: [] });
59 | });
60 |
61 | test('partytownSnippet w/out config', ({ snippetCode, run, win }) => {
62 | const code = (createSnippet as any)(null, snippetCode);
63 | run(code);
64 | assert.equal(win.partytown, { forward: [] });
65 | });
66 |
67 | test.run();
68 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-src-element.ts:
--------------------------------------------------------------------------------
1 | import { callMethod } from './worker-proxy';
2 | import { commaSplit } from './worker-constants';
3 | import { EventHandler, StateProp } from '../types';
4 | import { getInstanceStateValue, setInstanceStateValue } from './worker-state';
5 | import type { Node } from './worker-node';
6 | import { noop } from '../utils';
7 |
8 | export const HTMLSrcElementDescriptorMap: PropertyDescriptorMap & ThisType = {
9 | addEventListener: {
10 | value(...args: any[]) {
11 | const eventName = args[0];
12 | const callbacks = getInstanceStateValue(this, eventName) || [];
13 | callbacks.push(args[1]);
14 | setInstanceStateValue(this, eventName, callbacks);
15 | },
16 | },
17 | async: {
18 | get: noop,
19 | set: noop,
20 | },
21 | defer: {
22 | get: noop,
23 | set: noop,
24 | },
25 | onload: {
26 | get() {
27 | let callbacks = getInstanceStateValue(this, StateProp.loadHandlers);
28 | return (callbacks && callbacks[0]) || null;
29 | },
30 | set(cb) {
31 | setInstanceStateValue(this, StateProp.loadHandlers, cb ? [cb] : null);
32 | },
33 | },
34 | onerror: {
35 | get() {
36 | let callbacks = getInstanceStateValue(this, StateProp.errorHandlers);
37 | return (callbacks && callbacks[0]) || null;
38 | },
39 | set(cb) {
40 | setInstanceStateValue(this, StateProp.errorHandlers, cb ? [cb] : null);
41 | },
42 | },
43 |
44 | getAttribute: {
45 | value(attrName: string) {
46 | if (attrName === 'src') {
47 | return (this as any).src;
48 | }
49 | return callMethod(this, ['getAttribute'], [attrName]);
50 | },
51 | },
52 |
53 | setAttribute: {
54 | value(attrName: string, attrValue: any) {
55 | if (scriptAttrPropNames.includes(attrName)) {
56 | (this as any)[attrName] = attrValue;
57 | } else {
58 | callMethod(this, ['setAttribute'], [attrName, attrValue]);
59 | }
60 | },
61 | },
62 | };
63 |
64 | const scriptAttrPropNames = commaSplit('src,type');
65 |
--------------------------------------------------------------------------------
/src/lib/web-worker/worker-storage.ts:
--------------------------------------------------------------------------------
1 | import { callMethod } from './worker-proxy';
2 | import { CallType, StorageItem } from '../types';
3 | import { EMPTY_ARRAY } from '../utils';
4 |
5 | export const addStorageApi = (
6 | win: any,
7 | storageName: 'localStorage' | 'sessionStorage',
8 | storages: Map
9 | ) => {
10 | let isOrigin = () => self.origin === win.origin;
11 | let getItems = (items?: StorageItem[]) => {
12 | items = storages.get(win.origin);
13 | if (!items) {
14 | storages.set(win.origin, (items = []));
15 | }
16 | return items;
17 | };
18 | let getIndexByKey = (key: string) => getItems().findIndex((i) => i[STORAGE_KEY] === key);
19 | let index: number;
20 | let item: StorageItem;
21 |
22 | let storage: Storage = {
23 | getItem(key) {
24 | index = getIndexByKey(key);
25 | return index > -1 ? getItems()[index][STORAGE_VALUE] : null;
26 | },
27 |
28 | setItem(key, value) {
29 | index = getIndexByKey(key);
30 | if (index > -1) {
31 | getItems()[index][STORAGE_VALUE] = value;
32 | } else {
33 | getItems().push([key, value]);
34 | }
35 | if (isOrigin()) {
36 | callMethod(win, [storageName, 'setItem'], [key, value], CallType.NonBlocking);
37 | }
38 | },
39 |
40 | removeItem(key) {
41 | index = getIndexByKey(key);
42 | if (index > -1) {
43 | getItems().splice(index, 1);
44 | }
45 | if (isOrigin()) {
46 | callMethod(win, [storageName, 'removeItem'], [key], CallType.NonBlocking);
47 | }
48 | },
49 |
50 | key(index) {
51 | item = getItems()[index];
52 | return item ? item[STORAGE_KEY] : null;
53 | },
54 |
55 | clear() {
56 | getItems().length = 0;
57 | if (isOrigin()) {
58 | callMethod(win, [storageName, 'clear'], EMPTY_ARRAY, CallType.NonBlocking);
59 | }
60 | },
61 |
62 | get length() {
63 | return getItems().length;
64 | },
65 | };
66 |
67 | win[storageName] = storage;
68 | };
69 |
70 | const STORAGE_KEY = 0;
71 | const STORAGE_VALUE = 1;
72 |
--------------------------------------------------------------------------------
/tests/integrations/mermaid/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Mermaid 🎉
9 |
10 |
22 |
23 |
24 |
62 |
63 |
64 | Mermaid 🎉
65 |
66 |
67 |
68 | graph TD; A-->B; A-->C; B-->D; C-->D;
69 |
70 |
73 |
74 | Standard Mermaid
75 | All Tests
76 |
77 |
78 |
--------------------------------------------------------------------------------
/tests/integrations/wistia/standard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Standard Wistia
8 |
46 |
47 |
48 | Standard Wistia
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
70 |
71 |
72 | Partytown Wistia
73 | All Tests
74 |
75 |
76 |
--------------------------------------------------------------------------------
/tests/benchmarks/benchmark.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const h1 = document.getElementById('h1');
3 | document.title = h1.textContent;
4 |
5 | const resultElm = document.getElementById('result');
6 | resultElm.hidden = false;
7 | const output = document.getElementById('output');
8 | const results = document.getElementById('results');
9 |
10 | const timeBetween = 20;
11 | const iterateCount = 1000;
12 | const runCount = 10;
13 | const runs = [];
14 |
15 | const test = (i) => {
16 | const outer = document.createElement('div');
17 | outer.setAttribute('attr', i);
18 | outer.removeAttribute('attr');
19 | outer.className = 'c' + i;
20 | output.appendChild(outer);
21 |
22 | const inner = document.createElement('span');
23 | inner.textContent = i;
24 | inner.id = 'i' + i;
25 | inner.classList.add('a');
26 | inner.style.color = 'red';
27 | inner.tabIndex = i;
28 | outer.appendChild(inner);
29 |
30 | inner.style.color;
31 | };
32 |
33 | const run = () => {
34 | const start = performance.now();
35 | for (let i = 0; i < iterateCount; i++) {
36 | test(i);
37 | }
38 | const duration = performance.now() - start;
39 | runs.push(duration);
40 |
41 | const runId = runs.length;
42 |
43 | output.textContent = '';
44 |
45 | const resultTr = document.createElement('tr');
46 | const resultTh = document.createElement('th');
47 | resultTh.textContent = runId;
48 | const resultTd = document.createElement('td');
49 | resultTd.textContent = `${duration.toFixed(1)}ms`;
50 | resultTr.appendChild(resultTh);
51 | resultTr.appendChild(resultTd);
52 | results.appendChild(resultTr);
53 |
54 | if (runId < runCount) {
55 | setTimeout(() => run(), timeBetween);
56 | } else {
57 | const total = runs.reduce((t, dur) => {
58 | t += dur;
59 | return t;
60 | }, 0);
61 | const ave = total / runCount;
62 | resultElm.textContent = `${ave.toFixed(1)}ms`;
63 | resultElm.classList.add('completed');
64 | document.title = h1.textContent;
65 | }
66 | };
67 |
68 | setTimeout(() => run(), timeBetween);
69 | })();
70 |
--------------------------------------------------------------------------------
/src/lib/sandbox/index.ts:
--------------------------------------------------------------------------------
1 | import { debug, PT_IFRAME_APPENDED } from '../utils';
2 | import { getAndSetInstanceId } from './main-instances';
3 | import { libPath, mainWindow } from './main-globals';
4 | import { logMain } from '../log';
5 | import { mainAccessHandler } from './main-access-handler';
6 | import {
7 | MessageFromWorkerToSandbox,
8 | MessengerRequestCallback,
9 | PartytownWebWorker,
10 | WorkerMessageType,
11 | } from '../types';
12 | import { registerWindow } from './main-register-window';
13 | import syncCreateMessenger from '../build-modules/sync-create-messenger';
14 | import WebWorkerBlob from '../build-modules/web-worker-blob';
15 | import WebWorkerUrl from '../build-modules/web-worker-url';
16 | import { VERSION } from '../build-modules/version';
17 |
18 | let worker: PartytownWebWorker;
19 |
20 | const receiveMessage: MessengerRequestCallback = (accessReq, responseCallback) =>
21 | mainAccessHandler(worker, accessReq).then(responseCallback);
22 |
23 | syncCreateMessenger(receiveMessage).then((onMessageHandler) => {
24 | if (onMessageHandler) {
25 | worker = new Worker(
26 | debug
27 | ? libPath + WebWorkerUrl
28 | : URL.createObjectURL(
29 | new Blob([WebWorkerBlob], {
30 | type: 'text/javascript',
31 | })
32 | ),
33 | { name: `Partytown 🎉` }
34 | );
35 |
36 | worker.onmessage = (ev: MessageEvent) => {
37 | const msg: MessageFromWorkerToSandbox = ev.data;
38 | if (msg[0] === WorkerMessageType.AsyncAccessRequest) {
39 | // fire and forget async call within web worker
40 | mainAccessHandler(worker, msg[1]);
41 | } else {
42 | // blocking call within web worker
43 | onMessageHandler(worker, msg);
44 | }
45 | };
46 |
47 | if (debug) {
48 | logMain(`Created Partytown web worker (${VERSION})`);
49 | worker.onerror = (ev) => console.error(`Web Worker Error`, ev);
50 | }
51 |
52 | mainWindow.addEventListener(PT_IFRAME_APPENDED, (ev: CustomEvent) =>
53 | registerWindow(worker, getAndSetInstanceId(ev.detail.frameElement), ev.detail)
54 | );
55 | }
56 | });
57 |
--------------------------------------------------------------------------------
/scripts/build-atomics.ts:
--------------------------------------------------------------------------------
1 | import type { RollupOptions } from 'rollup';
2 | import {
3 | BuildOptions,
4 | fileSize,
5 | jsBannerPlugin,
6 | syncCommunicationModulesPlugin,
7 | versionPlugin,
8 | watchDir,
9 | } from './utils';
10 | import { join } from 'path';
11 | import { minifyPlugin } from './minify';
12 | import { webWorkerBlobUrlPlugin } from './build-web-worker';
13 |
14 | export function buildAtomics(opts: BuildOptions): RollupOptions[] {
15 | const rollups: RollupOptions[] = [];
16 |
17 | rollups.push(buildAtomicsDebug(opts));
18 | if (!opts.isDev) {
19 | rollups.push(buildAtomicsMin(opts));
20 | }
21 |
22 | return rollups;
23 | }
24 |
25 | function buildAtomicsDebug(opts: BuildOptions): RollupOptions {
26 | return {
27 | input: join(opts.tscLibDir, 'sandbox', 'index.js'),
28 | output: {
29 | file: join(opts.distLibDebugDir, 'partytown-atomics.js'),
30 | format: 'es',
31 | exports: 'none',
32 | intro: `((window)=>{`,
33 | outro: `})(window);`,
34 | plugins: [versionPlugin(opts), ...minifyPlugin(opts, true)],
35 | },
36 | plugins: [
37 | versionPlugin(opts),
38 | syncCommunicationModulesPlugin(opts, 'atomics'),
39 | webWorkerBlobUrlPlugin(opts, 'atomics', true),
40 | watchDir(opts, join(opts.tscLibDir, 'atomics')),
41 | watchDir(opts, join(opts.tscLibDir, 'web-worker')),
42 | jsBannerPlugin(opts),
43 | ],
44 | };
45 | }
46 |
47 | function buildAtomicsMin(opts: BuildOptions): RollupOptions {
48 | return {
49 | input: join(opts.tscLibDir, 'sandbox', 'index.js'),
50 | output: {
51 | file: join(opts.distLibDir, 'partytown-atomics.js'),
52 | format: 'es',
53 | exports: 'none',
54 | intro: `((window)=>{`,
55 | outro: `})(window);`,
56 | plugins: [...minifyPlugin(opts, false), fileSize()],
57 | },
58 | plugins: [
59 | syncCommunicationModulesPlugin(opts, 'atomics'),
60 | webWorkerBlobUrlPlugin(opts, 'atomics', false),
61 | watchDir(opts, join(opts.tscLibDir, 'atomics')),
62 | watchDir(opts, join(opts.tscLibDir, 'web-worker')),
63 | versionPlugin(opts),
64 | jsBannerPlugin(opts),
65 | ],
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/src/lib/sandbox/main-register-window.ts:
--------------------------------------------------------------------------------
1 | import { debug } from '../utils';
2 | import {
3 | InitializeEnvironmentData,
4 | MainWindow,
5 | PartytownWebWorker,
6 | WorkerMessageType,
7 | } from '../types';
8 | import { logMain, normalizedWinId } from '../log';
9 | import { winCtxs, windowIds } from './main-constants';
10 |
11 | export const registerWindow = (
12 | worker: PartytownWebWorker,
13 | $winId$: number,
14 | $window$: MainWindow
15 | ) => {
16 | if (!windowIds.has($window$)) {
17 | windowIds.set($window$, $winId$);
18 |
19 | const doc = $window$.document;
20 | const history = $window$.history;
21 | const $parentWinId$ = windowIds.get($window$.parent)!;
22 |
23 | const sendInitEnvData = () =>
24 | worker.postMessage([
25 | WorkerMessageType.InitializeEnvironment,
26 | {
27 | $winId$,
28 | $parentWinId$,
29 | $url$: doc.baseURI,
30 | },
31 | ]);
32 |
33 | const pushState = history.pushState.bind(history);
34 | const replaceState = history.replaceState.bind(history);
35 |
36 | const onLocationChange = () =>
37 | setTimeout(() =>
38 | worker.postMessage([WorkerMessageType.LocationUpdate, $winId$, doc.baseURI])
39 | );
40 |
41 | history.pushState = (data, _, url) => {
42 | pushState(data, _, url);
43 | onLocationChange();
44 | };
45 |
46 | history.replaceState = (data, _, url) => {
47 | replaceState(data, _, url);
48 | onLocationChange();
49 | };
50 |
51 | $window$.addEventListener('popstate', onLocationChange);
52 | $window$.addEventListener('hashchange', onLocationChange);
53 |
54 | winCtxs[$winId$] = {
55 | $winId$,
56 | $window$,
57 | };
58 |
59 | if (debug) {
60 | winCtxs[$winId$]!.$startTime$ = performance.now();
61 | }
62 |
63 | if (debug) {
64 | const winType = $winId$ === $parentWinId$ ? 'top' : 'iframe';
65 | logMain(`Registered ${winType} window ${normalizedWinId($winId$)} (${$winId$})`);
66 | }
67 |
68 | if (doc.readyState === 'complete') {
69 | sendInitEnvData();
70 | } else {
71 | $window$.addEventListener('load', sendInitEnvData);
72 | }
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/docs/nextjs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: NextJS
3 | ---
4 |
5 | The Next.js setup is largely the same as the [React integration guide](/react), except it goes further into how to add to the [NextJS](https://nextjs.org/) framework's ` ` component.
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm install @builder.io/partytown
11 | ```
12 |
13 | ## Configure
14 |
15 | The ` ` component is imported from the `@builder.io/partytown/react` submodule. The [config properties](/configuration) are JSX props.
16 |
17 | The following is an example of including the ` ` component in a Nextjs page. Notice the ` ` component is in the ``, and the example analytics script has the `type="text/partytown"` attribute.
18 |
19 | ```jsx
20 | import Head from 'next/head';
21 | import { Partytown } from '@builder.io/partytown/react';
22 |
23 | const Home = () => {
24 | return (
25 | <>
26 |
27 | My App
28 |
29 |
65 |
66 |
76 |
77 |
78 | Partytown Adobe Launch
79 | All Tests
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/canvas/context-webgl.ts:
--------------------------------------------------------------------------------
1 | import { ApplyPathKey, callMethod, InstanceIdKey, randomId, setter, WinIdKey } from '../bridge';
2 | import { CallType } from '../../../types';
3 | import type { WorkerInstance } from '../../worker-instance';
4 |
5 | export const createContextWebGL = (
6 | canvasInstance: WorkerInstance,
7 | contextType: string,
8 | contextAttributes: any
9 | ) => {
10 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext
11 | const winId: number = (canvasInstance as any)[WinIdKey];
12 | const ctxInstanceId = randomId();
13 | const ctxInstance = { [WinIdKey]: winId, [InstanceIdKey]: ctxInstanceId, [ApplyPathKey]: [] };
14 |
15 | const ctx = callMethod(
16 | canvasInstance,
17 | ['getContext'],
18 | [contextType, contextAttributes],
19 | CallType.Blocking,
20 | ctxInstanceId
21 | );
22 |
23 | const WebGLRenderingContextHandler = {
24 | get(target: any, propName: string | symbol) {
25 | if (typeof propName === 'string') {
26 | if (typeof ctx[propName] !== 'function') {
27 | // context prop getter
28 | return ctx[propName];
29 | }
30 |
31 | // context method
32 | return (...args: any[]) => {
33 | return callMethod(ctxInstance, [propName], args, getWebGlMethodCallType(propName));
34 | };
35 | }
36 |
37 | // symbol prop getter
38 | return target[propName];
39 | },
40 |
41 | set(target: any, propName: string | symbol, value: any) {
42 | if (typeof propName === 'string' && propName in ctx) {
43 | // context prop setter
44 | if (ctx[propName] !== value && typeof value !== 'function') {
45 | setter(ctxInstance, [propName], value);
46 | }
47 | ctx[propName] = value;
48 | } else {
49 | target[propName] = value;
50 | }
51 | return true;
52 | },
53 | };
54 |
55 | return new Proxy(ctx, WebGLRenderingContextHandler);
56 | };
57 |
58 | const ctxWebGLGetterMethods = 'checkFramebufferStatus,makeXRCompatible'.split(',');
59 |
60 | const getWebGlMethodCallType = (methodName: string) =>
61 | methodName.startsWith('create') ||
62 | methodName.startsWith('get') ||
63 | methodName.startsWith('is') ||
64 | ctxWebGLGetterMethods.includes(methodName)
65 | ? CallType.Blocking
66 | : CallType.NonBlocking;
67 |
--------------------------------------------------------------------------------
/DEVELOPER.md:
--------------------------------------------------------------------------------
1 | # Local Development
2 |
3 | Welcome 🎉!! If you've found a bug, or have an idea to add a feature we'd love to hear from you. It may save time to first ping the group on [Partytown's Discord channel](https://discord.gg/hbuEtxdEZ3) to talk through any ideas or any issues that may be a bug.
4 |
5 | ## Installation
6 |
7 | ```
8 | npm install
9 | npm run dev
10 | ```
11 |
12 | See the [distribution](https://github.com/BuilderIO/partytown#distribution) section about the various files created. Note that both the root directory, and the `tests` directory receive a copy of the build files, such as `tests/~partytown/partytown.js`.
13 |
14 | ## Submitting Issues And Writing Tests
15 |
16 | We need your help! If you found a bug, it's best to create a [Pull Request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) that replicates the issue using a test page. In the [tests directory](https://github.com/BuilderIO/partytown/tree/main/tests), copy one of the directories, such as `tests/document`, and recreate the issue you've found.
17 |
18 | Follow the [manual testing](#manual-testing) directions on how to start the local dev server. Next, the more you can describe the debug and pin-point the issue the better, and any fixes to the runtime to solve the problem would be awesome. 🎉
19 |
20 | If the PR fixes the issue, then creating an [end-to-end test](#e2e-testing) would help ensure no regressions happen over time.
21 |
22 | ## Manual Testing
23 |
24 | Tests to be manually ran on a browser are located in the `tests` directory. These pages can help to test out DOM apis and individual services.
25 |
26 | ```
27 | npm run serve
28 | ```
29 |
30 | http://localhost:4000/
31 |
32 | ## E2E Testing
33 |
34 | E2E tests use [@playwright/test](https://playwright.dev/docs/intro#writing-assertions), which allows us to test on Chromium, Firefox and WebKit browsers.
35 | These pages are also tested on every commit within the project's [CI Workflow](https://github.com/BuilderIO/partytown/actions/workflows/ci.yml).
36 |
37 | ```
38 | npm test
39 | ```
40 |
41 | [Test Results](https://github.com/BuilderIO/partytown/actions/workflows/ci.yml)
42 |
43 | ## Deployed Tests
44 |
45 | The same pages found in `tests` are deployed to:
46 |
47 | https://partytown.builder.io/
48 |
49 | ## Publishing
50 |
51 | ```
52 | npm run release
53 | ```
54 |
--------------------------------------------------------------------------------
/tests/integrations/twitter/standard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Standard Twitter Embed
9 |
10 |
15 |
53 |
54 |
55 | Standard Twitter Embed
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Partytown Twitter Embed
64 | All Tests
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/scripts/build-web-worker.ts:
--------------------------------------------------------------------------------
1 | import { OutputOptions, Plugin, rollup } from 'rollup';
2 | import {
3 | BuildOptions,
4 | getJsBanner,
5 | MessageType,
6 | onwarn,
7 | syncCommunicationModulesPlugin,
8 | versionPlugin,
9 | } from './utils';
10 | import { join } from 'path';
11 | import { writeFile } from 'fs-extra';
12 | import { minifyPlugin } from './minify';
13 |
14 | export async function buildWebWorker(opts: BuildOptions, msgType: MessageType, debug: boolean) {
15 | const build = await rollup({
16 | input: join(opts.tscLibDir, 'web-worker', 'index.js'),
17 | plugins: [syncCommunicationModulesPlugin(opts, msgType), versionPlugin(opts)],
18 | onwarn,
19 | });
20 |
21 | const output: OutputOptions = {
22 | format: 'es',
23 | exports: 'none',
24 | intro: `((self)=>{`,
25 | outro: `})(self);`,
26 | plugins: [...minifyPlugin(opts, debug)],
27 | };
28 |
29 | const generated = await build.generate(output);
30 |
31 | const webWorkerCode = getJsBanner(opts, generated.output[0].code);
32 | if (debug) {
33 | const outName = `partytown-ww-${msgType}.js`;
34 | await writeFile(join(opts.distLibDebugDir, outName), webWorkerCode);
35 | }
36 |
37 | return webWorkerCode;
38 | }
39 |
40 | export function webWorkerBlobUrlPlugin(
41 | opts: BuildOptions,
42 | msgType: MessageType,
43 | debug: boolean
44 | ): Plugin {
45 | return {
46 | name: 'webWorkerBlobUrlPlugin',
47 | resolveId(id) {
48 | if (id.endsWith('web-worker-blob') || id.endsWith('web-worker-url')) {
49 | return id;
50 | }
51 | return null;
52 | },
53 | async load(id) {
54 | if (id.endsWith('web-worker-blob')) {
55 | let code = `""`;
56 | if (!opts.isDev) {
57 | code = JSON.stringify(await buildWebWorker(opts, msgType, debug));
58 | }
59 | return `const WEB_WORKER_BLOB = ${code}; export default WEB_WORKER_BLOB;`;
60 | }
61 | if (id.endsWith('web-worker-url')) {
62 | return `const WEB_WORKER_URL = "partytown-ww-${msgType}.js"; export default WEB_WORKER_URL;`;
63 | }
64 | return null;
65 | },
66 | async generateBundle() {
67 | if (opts.isDev) {
68 | const wwCode = await buildWebWorker(opts, msgType, debug);
69 | const wwDebugFilePath = join(opts.distLibDebugDir, `partytown-ww-${msgType}.js`);
70 | await writeFile(wwDebugFilePath, wwCode);
71 | }
72 | },
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/src/lib/web-worker/media/audio-track.ts:
--------------------------------------------------------------------------------
1 | import { callMethod, getter, InstanceIdKey, setter, WinIdKey, WorkerProxy } from './bridge';
2 | import { CallType } from '../../types';
3 | import { defineCstr } from './utils';
4 | import { SourceBuffer } from './source-buffer';
5 |
6 | export const AudioTrackList = class {
7 | constructor(mediaElm: any) {
8 | const audioTracks = 'audioTracks';
9 | const winId = mediaElm[WinIdKey];
10 | const instanceId = mediaElm[InstanceIdKey];
11 |
12 | const instance = {
13 | addEventListener(...args: any[]) {
14 | callMethod(
15 | mediaElm,
16 | [audioTracks, 'addEventListener'],
17 | args,
18 | CallType.NonBlockingNoSideEffect
19 | );
20 | },
21 | getTrackById(...args: any[]) {
22 | return callMethod(mediaElm, [audioTracks, 'getTrackById'], args);
23 | },
24 | get length(): number {
25 | return getter(mediaElm, [audioTracks, 'length']);
26 | },
27 | removeEventListener(...args: any[]) {
28 | callMethod(
29 | mediaElm,
30 | [audioTracks, 'removeEventListener'],
31 | args,
32 | CallType.NonBlockingNoSideEffect
33 | );
34 | },
35 | };
36 |
37 | return new Proxy(instance, {
38 | get(target: any, propName) {
39 | if (typeof propName === 'number') {
40 | return new AudioTrack(winId, instanceId, [audioTracks, propName]);
41 | }
42 | return target[propName];
43 | },
44 | }) as any;
45 | }
46 | };
47 |
48 | const AudioTrack = class extends WorkerProxy {
49 | get enabled() {
50 | return getter(this, ['enabled']);
51 | }
52 | set enabled(value: any) {
53 | setter(this, ['enabled'], value);
54 | }
55 |
56 | get id() {
57 | return getter(this, ['id']);
58 | }
59 |
60 | get kind() {
61 | return getter(this, ['kind']);
62 | }
63 |
64 | get label() {
65 | return getter(this, ['label']);
66 | }
67 |
68 | get language() {
69 | return getter(this, ['language']);
70 | }
71 |
72 | get sourceBuffer() {
73 | return new SourceBuffer(this);
74 | }
75 | };
76 |
77 | export const hasAudioTracks = 'audioTracks' in (self.HTMLMediaElement.prototype as any);
78 | if (hasAudioTracks) {
79 | // not all browsers have audioTracks, only add if it's found
80 | defineCstr('AudioTrackList', AudioTrackList);
81 | defineCstr('AudioTrack', AudioTrack);
82 | }
83 |
--------------------------------------------------------------------------------
/docs/remix.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Remix
3 | ---
4 |
5 | The Remix setup is largely the same as the [React integration guide](/react), except it goes further into how to add to the [Remix](https://remix.run/) framework's app root component.
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm install @builder.io/partytown
11 | ```
12 |
13 | ## Configure
14 |
15 | The ` ` component is imported from the `@builder.io/partytown/react` submodule. The [config properties](/configuration) are JSX props.
16 |
17 | The following is an example of including the ` ` component in the app's root component. Notice the ` ` component is in the `` and before the ` ` component. The example below is setting the [forward](/forwarding-events) config for [Google Tag Manager](/google-tag-manager).
18 |
19 | ```jsx
20 | import { Partytown } from '@builder.io/partytown/react';
21 | import { Links, Meta, Outlet } from 'remix';
22 |
23 | export default function App() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ...
34 |
35 | );
36 | }
37 | ```
38 |
39 | ## Partytown Script
40 |
41 | Add the `type="text/partytown"` [prop](/partytown-scripts) for each script that should run from a web worker. The example below is using the React specific way of inlining a script with [dangerouslySetInnerHTML](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml).
42 |
43 | ```jsx
44 |
45 |
51 | ```
52 |
53 | ## Copy Library Files
54 |
55 | Copy library files to `public/~partytown`. How the files are copied or served from your site is up each site's setup. A `partytown copylib` CLI [copy task](/copy-library-files) has been provided for convenience which helps copy the Partytown library files to the public directory. Below is an example of creating a "partytown" NPM script which could run before the build:
56 |
57 | ```json
58 | "scripts": {
59 | "build": "npm run partytown && cross-env NODE_ENV=production remix build",
60 | "partytown": "partytown copylib public/~partytown"
61 | }
62 | ```
63 |
--------------------------------------------------------------------------------
/tests/integrations/wistia/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Partytown Wistia 🎉
8 |
46 |
59 |
60 |
61 |
62 | Partytown Wistia 🎉
63 |
64 |
65 |
66 |
67 |
71 |
72 |
81 |
82 |
83 | Standard Wistia
84 | All Tests
85 |
86 |
87 |
--------------------------------------------------------------------------------
/docs/site/src/components/PageContent/PageContent.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import MoreMenu from '../RightSidebar/MoreMenu.astro';
3 | import TableOfContents from '../RightSidebar/TableOfContents.tsx';
4 |
5 | const { content, githubEditUrl } = Astro.props;
6 | const title = content.title;
7 | const headers = content.astro.headers;
8 | const currentPage = Astro.request.url.pathname;
9 | ---
10 |
11 |
12 |
13 | {currentPage === '/' ? (
14 | Run Third-Party Scripts From A Web Worker
15 | ) : (
16 | {title}
17 | )}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
50 |
51 |
--------------------------------------------------------------------------------
/tests/integrations/hubspot/forms.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Hubspot Forms 🎉
8 |
28 |
29 |
67 |
68 |
69 | Hubspot Forms 🎉
70 |
71 |
72 |
79 | Hubspot Forms (Standard)
80 | All Tests
81 |
82 |
83 |
--------------------------------------------------------------------------------
/tests/integrations/hubspot/20632911.js:
--------------------------------------------------------------------------------
1 | // HubSpot Script Loader. Please do not block this resource. See more: http://hubs.ly/H0702_H0
2 |
3 | !(function (t, e, r) {
4 | if (!document.getElementById(t)) {
5 | var n = document.createElement('script');
6 | for (var a in ((n.src = './collectedforms.js'), (n.type = 'text/javascript'), (n.id = t), r))
7 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
8 | var i = document.getElementsByTagName('script')[0];
9 | i.parentNode.insertBefore(n, i);
10 | }
11 | })('CollectedForms-20632911', 0, {
12 | 'crossorigin': 'anonymous',
13 | 'data-leadin-portal-id': 20632911,
14 | 'data-leadin-env': 'prod',
15 | 'data-loader': 'hs-scriptloader',
16 | 'data-hsjs-portal': 20632911,
17 | 'data-hsjs-env': 'prod',
18 | 'data-hsjs-hublet': 'na1',
19 | });
20 | var _hsp = (window._hsp = window._hsp || []);
21 | _hsp.push(['addEnabledFeatureGates', ['CookieBanner:Reporting']]);
22 | !(function (t, e, r) {
23 | if (!document.getElementById(t)) {
24 | var n = document.createElement('script');
25 | for (var a in ((n.src = './banner-20632911.js'), (n.type = 'text/javascript'), (n.id = t), r))
26 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
27 | var i = document.getElementsByTagName('script')[0];
28 | i.parentNode.insertBefore(n, i);
29 | }
30 | })('cookieBanner-20632911', 0, {
31 | 'data-cookieconsent': 'ignore',
32 | 'data-hs-ignore': true,
33 | 'data-loader': 'hs-scriptloader',
34 | 'data-hsjs-portal': 20632911,
35 | 'data-hsjs-env': 'prod',
36 | 'data-hsjs-hublet': 'na1',
37 | });
38 | !(function (e, t) {
39 | if (!document.getElementById(e)) {
40 | var c = document.createElement('script');
41 | (c.src = './analytics-20632911.js'), (c.type = 'text/javascript'), (c.id = e);
42 | var n = document.getElementsByTagName('script')[0];
43 | n.parentNode.insertBefore(c, n);
44 | }
45 | })('hs-analytics');
46 | !(function (t, e, r) {
47 | if (!document.getElementById(t)) {
48 | var n = document.createElement('script');
49 | for (var a in ((n.src = './leadflows.js'), (n.type = 'text/javascript'), (n.id = t), r))
50 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
51 | var i = document.getElementsByTagName('script')[0];
52 | i.parentNode.insertBefore(n, i);
53 | }
54 | })('LeadFlows-20632911', 0, {
55 | 'crossorigin': 'anonymous',
56 | 'data-leadin-portal-id': 20632911,
57 | 'data-leadin-env': 'prod',
58 | 'data-loader': 'hs-scriptloader',
59 | 'data-hsjs-portal': 20632911,
60 | 'data-hsjs-env': 'prod',
61 | 'data-hsjs-hublet': 'na1',
62 | });
63 |
--------------------------------------------------------------------------------
/tests/integrations/hubspot/scripts-20632911.js:
--------------------------------------------------------------------------------
1 | // HubSpot Script Loader. Please do not block this resource. See more: http://hubs.ly/H0702_H0
2 |
3 | !(function (t, e, r) {
4 | if (!document.getElementById(t)) {
5 | var n = document.createElement('script');
6 | for (var a in ((n.src = './leadflows.js'), (n.type = 'text/javascript'), (n.id = t), r))
7 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
8 | var i = document.getElementsByTagName('script')[0];
9 | i.parentNode.insertBefore(n, i);
10 | }
11 | })('LeadFlows-20632911', 0, {
12 | 'crossorigin': 'anonymous',
13 | 'data-leadin-portal-id': 20632911,
14 | 'data-leadin-env': 'prod',
15 | 'data-loader': 'hs-scriptloader',
16 | 'data-hsjs-portal': 20632911,
17 | 'data-hsjs-env': 'prod',
18 | 'data-hsjs-hublet': 'na1',
19 | });
20 | var _hsp = (window._hsp = window._hsp || []);
21 | _hsp.push(['addEnabledFeatureGates', ['CookieBanner:Reporting']]);
22 | !(function (t, e, r) {
23 | if (!document.getElementById(t)) {
24 | var n = document.createElement('script');
25 | for (var a in ((n.src = './20632911.js'), (n.type = 'text/javascript'), (n.id = t), r))
26 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
27 | var i = document.getElementsByTagName('script')[0];
28 | i.parentNode.insertBefore(n, i);
29 | }
30 | })('cookieBanner-20632911', 0, {
31 | 'data-cookieconsent': 'ignore',
32 | 'data-hs-ignore': true,
33 | 'data-loader': 'hs-scriptloader',
34 | 'data-hsjs-portal': 20632911,
35 | 'data-hsjs-env': 'prod',
36 | 'data-hsjs-hublet': 'na1',
37 | });
38 | !(function (t, e, r) {
39 | if (!document.getElementById(t)) {
40 | var n = document.createElement('script');
41 | for (var a in ((n.src = './collectedforms.js'), (n.type = 'text/javascript'), (n.id = t), r))
42 | r.hasOwnProperty(a) && n.setAttribute(a, r[a]);
43 | var i = document.getElementsByTagName('script')[0];
44 | i.parentNode.insertBefore(n, i);
45 | }
46 | })('CollectedForms-20632911', 0, {
47 | 'crossorigin': 'anonymous',
48 | 'data-leadin-portal-id': 20632911,
49 | 'data-leadin-env': 'prod',
50 | 'data-loader': 'hs-scriptloader',
51 | 'data-hsjs-portal': 20632911,
52 | 'data-hsjs-env': 'prod',
53 | 'data-hsjs-hublet': 'na1',
54 | });
55 | !(function (e, t) {
56 | if (!document.getElementById(e)) {
57 | var c = document.createElement('script');
58 | (c.src = './analytics-20632911.js'), (c.type = 'text/javascript'), (c.id = e);
59 | var n = document.getElementsByTagName('script')[0];
60 | n.parentNode.insertBefore(c, n);
61 | }
62 | })('hs-analytics');
63 |
--------------------------------------------------------------------------------
/tests/react-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/benchmarks/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Benchmark
8 |
42 |
43 |
44 |
45 | Partytown
46 | Baseline
47 |
48 |
49 |
50 |
51 | running...
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
83 |
84 |
85 |
--------------------------------------------------------------------------------