diff --git a/src/lib/sandbox/main-access-handler.ts b/src/lib/sandbox/main-access-handler.ts index 87df7900..3bb28f2d 100644 --- a/src/lib/sandbox/main-access-handler.ts +++ b/src/lib/sandbox/main-access-handler.ts @@ -7,7 +7,8 @@ import { PartytownWebWorker, WinId, } from '../types'; -import { debug, isPromise, len } from '../utils'; +import { debug, getConstructorName, isPromise, len } from '../utils'; +import { defineCustomElement } from './main-custom-element'; import { deserializeFromWorker, serializeForWorker } from './main-serialization'; import { getInstance, setInstanceId } from './main-instances'; import { normalizedWinId } from '../log'; @@ -64,7 +65,14 @@ export const mainAccessHandler = async ( // get the existing instance instance = getInstance(winId, task.$instanceId$); if (instance) { - rtnValue = applyToInstance(worker, instance, applyPath, isLast, task.$groupedGetters$); + rtnValue = applyToInstance( + worker, + winId, + instance, + applyPath, + isLast, + task.$groupedGetters$ + ); if (task.$assignInstanceId$) { if (typeof task.$assignInstanceId$ === 'string') { @@ -81,9 +89,13 @@ export const mainAccessHandler = async ( if (isPromise(rtnValue)) { rtnValue = await rtnValue; - accessRsp.$isPromise$ = true; + if (isLast) { + accessRsp.$isPromise$ = true; + } + } + if (isLast) { + accessRsp.$rtnValue$ = serializeForWorker(winId, rtnValue); } - accessRsp.$rtnValue$ = serializeForWorker(winId, rtnValue); } else { if (debug) { accessRsp.$error$ = `Error finding instance "${ @@ -112,6 +124,7 @@ export const mainAccessHandler = async ( const applyToInstance = ( worker: PartytownWebWorker, + winId: WinId, instance: any, applyPath: ApplyPath, isLast: boolean, @@ -159,6 +172,10 @@ const applyToInstance = ( // previous is the method name args = deserializeFromWorker(worker, current); + if (previous === 'define' && getConstructorName(instance) === 'CustomElementRegistry') { + args[1] = defineCustomElement(winId, worker, args[1]); + } + if (previous === 'insertRule') { // possible that the async insertRule has thrown an error // and the subsequent async insertRule's have bad indexes diff --git a/src/lib/sandbox/main-custom-element.ts b/src/lib/sandbox/main-custom-element.ts new file mode 100644 index 00000000..4bbbc477 --- /dev/null +++ b/src/lib/sandbox/main-custom-element.ts @@ -0,0 +1,35 @@ +import { CustomElementData, PartytownWebWorker, WinId, WorkerMessageType } from '../types'; +import { defineConstructorName } from '../utils'; +import { getAndSetInstanceId } from './main-instances'; +import { winCtxs } from './main-constants'; + +export const defineCustomElement = ( + winId: WinId, + worker: PartytownWebWorker, + ceData: CustomElementData +) => { + const Cstr = defineConstructorName( + class extends (winCtxs[winId]!.$window$ as any).HTMLElement {}, + ceData[0] + ); + + const ceCallbackMethods = + 'connectedCallback,disconnectedCallback,attributeChangedCallback,adoptedCallback'.split(','); + + ceCallbackMethods.map( + (callbackMethodName) => + (Cstr.prototype[callbackMethodName] = function (...args: any) { + worker.postMessage([ + WorkerMessageType.CustomElementCallback, + winId, + getAndSetInstanceId(this)!, + callbackMethodName, + args, + ]); + }) + ); + + Cstr.observedAttributes = ceData[1]; + + return Cstr; +}; diff --git a/src/lib/sandbox/main-serialization.ts b/src/lib/sandbox/main-serialization.ts index 6b2ff162..befc90d8 100644 --- a/src/lib/sandbox/main-serialization.ts +++ b/src/lib/sandbox/main-serialization.ts @@ -1,4 +1,4 @@ -import { getConstructorName, isValidMemberName, startsWith } from '../utils'; +import { getConstructorName, getNodeName, isValidMemberName, startsWith } from '../utils'; import { getInstance, getAndSetInstanceId } from './main-instances'; import { mainRefs } from './main-constants'; import { @@ -61,7 +61,10 @@ export const serializeForWorker = ( } else if (cstrName === 'Attr') { return [SerializedType.Attr, [(value as Attr).name, (value as Attr).value]]; } else if (value.nodeType) { - return [SerializedType.Instance, [$winId$, getAndSetInstanceId(value)!, value.nodeName]]; + return [ + SerializedType.Instance, + [$winId$, getAndSetInstanceId(value)!, getNodeName(value)], + ]; } else { return [SerializedType.Object, serializeObjectForWorker($winId$, value, added, true, true)]; } diff --git a/src/lib/sandbox/read-main-platform.ts b/src/lib/sandbox/read-main-platform.ts index 8539b843..544ace73 100644 --- a/src/lib/sandbox/read-main-platform.ts +++ b/src/lib/sandbox/read-main-platform.ts @@ -2,6 +2,7 @@ import { createElementFromConstructor, debug, getConstructorName, + getNodeName, isValidMemberName, len, noop, @@ -24,6 +25,7 @@ export const readMainPlatform = () => { const textNode = docImpl.createTextNode(''); const comment = docImpl.createComment(''); const frag = docImpl.createDocumentFragment(); + const shadowRoot = docImpl.createElement('p').attachShadow({ mode: 'open' }); const intersectionObserver = getGlobalConstructor(mainWindow, 'IntersectionObserver'); const mutationObserver = getGlobalConstructor(mainWindow, 'MutationObserver'); const resizeObserver = getGlobalConstructor(mainWindow, 'ResizeObserver'); @@ -56,6 +58,7 @@ export const readMainPlatform = () => { [textNode], [comment], [frag], + [shadowRoot], [elm], [elm.attributes], [elm.classList], @@ -140,13 +143,7 @@ const readOwnImplementation = ( readImplementationMember(interfaceMembers, impl, memberName) ); - interfaces.push([ - cstrName, - superCstrName, - interfaceMembers, - interfaceType, - (impl as Node).nodeName, - ]); + interfaces.push([cstrName, superCstrName, interfaceMembers, interfaceType, getNodeName(impl)]); } }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 04c8b609..1dd37c23 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -49,7 +49,14 @@ export type MessageFromSandboxToWorker = | [type: WorkerMessageType.RefHandlerCallback, callbackData: RefHandlerCallbackData] | [type: WorkerMessageType.ForwardMainTrigger, triggerData: ForwardMainTriggerData] | [type: WorkerMessageType.LocationUpdate, winId: WinId, documentBaseURI: string] - | [type: WorkerMessageType.DocumentVisibilityState, winId: WinId, visibilityState: string]; + | [type: WorkerMessageType.DocumentVisibilityState, winId: WinId, visibilityState: string] + | [ + type: WorkerMessageType.CustomElementCallback, + winId: WinId, + instanceId: InstanceId, + callbackName: string, + args: any[] + ]; export const enum WorkerMessageType { MainDataRequestFromWorker, @@ -65,6 +72,7 @@ export const enum WorkerMessageType { AsyncAccessRequest, LocationUpdate, DocumentVisibilityState, + CustomElementCallback, } export interface ForwardMainTriggerData { @@ -599,8 +607,8 @@ export interface PostMessageData { export interface WorkerConstructor { new ( - instanceId: InstanceId, - winId: WinId, + winId?: WinId, + instanceId?: InstanceId, applyPath?: ApplyPath, instanceData?: any, namespace?: string @@ -621,3 +629,9 @@ export interface WorkerNode extends WorkerInstance, Node {} export interface WorkerWindow extends WorkerInstance { [key: string]: any; } + +export interface WorkerNodeConstructors { + [tagName: string]: WorkerConstructor; +} + +export type CustomElementData = [cstrName: string, observedAttributes: string[]]; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f363bddb..308c0cd6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -35,6 +35,9 @@ export const getLastMemberName = (applyPath: ApplyPath, i?: number) => { return applyPath[0] as string; }; +export const getNodeName = (node: Node) => + node.nodeType === 11 && (node as any).host ? '#s' : node.nodeName; + export const EMPTY_ARRAY = []; if (debug) { /*#__PURE__*/ Object.freeze(EMPTY_ARRAY); diff --git a/src/lib/web-worker/index.ts b/src/lib/web-worker/index.ts index fc82b19e..8e64c8c9 100644 --- a/src/lib/web-worker/index.ts +++ b/src/lib/web-worker/index.ts @@ -1,3 +1,4 @@ +import { callCustomElementCallback } from './worker-custom-elements'; import { callWorkerRefHandler } from './worker-serialization'; import { createEnvironment } from './worker-environment'; import { debug } from '../utils'; @@ -40,6 +41,8 @@ const receiveMessageFromSandboxToWorker = (ev: MessageEvent { + const customElements = 'customElements'; + const registry = new Map(); + + win[customElements] = { + define(tagName: string, Cstr: any, opts: any) { + registry.set(tagName, Cstr); + nodeCstrs[tagName.toUpperCase()] = Cstr; + const ceData: CustomElementData = [Cstr.name, Cstr.observedAttributes]; + callMethod(win, [customElements, 'define'], [tagName, ceData, opts]); + }, + + get: (tagName: string) => registry.get(tagName), + + whenDefined: (tagName: string) => + registry.has(tagName) + ? Promise.resolve() + : callMethod(win, [customElements, 'whenDefined'], [tagName]), + + upgrade: (elm: any) => callMethod(win, [customElements, 'upgrade'], [elm]), + }; +}; + +export const callCustomElementCallback = ( + _type: WorkerMessageType.CustomElementCallback, + winId: WinId, + instanceId: InstanceId, + callbackName: string, + args: any[] +) => { + const elm = getOrCreateNodeInstance(winId, instanceId) as any; + if (elm && typeof elm[callbackName] === 'function') { + elm[callbackName].apply(elm, args); + } +}; diff --git a/src/lib/web-worker/worker-document.ts b/src/lib/web-worker/worker-document.ts index 2343ecb2..40aca462 100644 --- a/src/lib/web-worker/worker-document.ts +++ b/src/lib/web-worker/worker-document.ts @@ -62,10 +62,11 @@ export const patchDocument = ( const isIframe = tagName === NodeName.IFrame; const winId = this[WinIdKey]; const instanceId = (isIframe ? 'f_' : '') + randomId(); - const elm = getOrCreateNodeInstance(winId, instanceId, tagName); callMethod(this, ['createElement'], [tagName], CallType.NonBlocking, instanceId); + const elm = getOrCreateNodeInstance(winId, instanceId, tagName); + if (isIframe) { // an iframe element's instanceId is the same as its contentWindow's winId // and the contentWindow's parentWinId is the iframe element's winId diff --git a/src/lib/web-worker/worker-node.ts b/src/lib/web-worker/worker-node.ts index ebba780f..405b5520 100644 --- a/src/lib/web-worker/worker-node.ts +++ b/src/lib/web-worker/worker-node.ts @@ -80,7 +80,7 @@ export const createNodeCstr = ( } get nodeName() { - return this[InstanceDataKey]; + return this[InstanceDataKey] === '#s' ? '#document-fragment' : this[InstanceDataKey]; } get nodeType() { diff --git a/src/lib/web-worker/worker-window.ts b/src/lib/web-worker/worker-window.ts index 6ecf909e..51b3c8de 100644 --- a/src/lib/web-worker/worker-window.ts +++ b/src/lib/web-worker/worker-window.ts @@ -8,9 +8,9 @@ import { WebWorkerEnvironment, WinDocId, WinId, - WorkerConstructor, WorkerInstance, WorkerNode, + WorkerNodeConstructors, WorkerWindow, } from '../types'; import { @@ -29,6 +29,7 @@ import { webWorkerSessionStorage, WinIdKey, } from './worker-constants'; +import { createCustomElementRegistry } from './worker-custom-elements'; import { cachedDimensionMethods, cachedDimensionProps, @@ -49,7 +50,6 @@ import { defineProperty, definePrototypeProperty, definePrototypeValue, - EMPTY_ARRAY, getConstructorName, len, randomId, @@ -83,6 +83,10 @@ export const createWindow = ( isIframeWindow?: boolean, isDocumentImplementation?: boolean ) => { + let cstrInstanceId: InstanceId | undefined; + let cstrNodeName: string | undefined; + let cstrNamespace: string | undefined; + // base class all Nodes/Elements/Global Constructors will extend const WorkerBase = class implements WorkerInstance { [WinIdKey]: WinId; @@ -93,20 +97,19 @@ export const createWindow = ( [InstanceStateKey]: { [key: string]: any }; constructor( - winId: WinId, - instanceId: InstanceId, + winId?: WinId, + instanceId?: InstanceId, applyPath?: ApplyPath, instanceData?: any, namespace?: string ) { - this[WinIdKey] = winId; - this[InstanceIdKey] = instanceId!; + this[WinIdKey] = winId || $winId$; + this[InstanceIdKey] = instanceId || cstrInstanceId || randomId(); this[ApplyPathKey] = applyPath || []; - this[InstanceDataKey] = instanceData; + this[InstanceDataKey] = instanceData || cstrNodeName; + this[NamespaceKey] = namespace || cstrNamespace; this[InstanceStateKey] = {}; - if (namespace) { - this[NamespaceKey] = namespace; - } + cstrInstanceId = cstrNodeName = cstrNamespace = undefined; } }; @@ -155,7 +158,7 @@ export const createWindow = ( } }; - let nodeCstrs: { [nodeName: string]: WorkerConstructor } = {}; + let nodeCstrs: WorkerNodeConstructors = {}; let $createNode$ = ( nodeName: string, instanceId: InstanceId, @@ -168,8 +171,12 @@ export const createWindow = ( ? nodeCstrs[nodeName] : nodeName.includes('-') ? nodeCstrs.UNKNOWN - : nodeCstrs.DIV; - return new NodeCstr($winId$, instanceId, EMPTY_ARRAY, nodeName, namespace) as any; + : nodeCstrs.I; + + cstrInstanceId = instanceId; + cstrNodeName = nodeName; + cstrNamespace = namespace; + return new NodeCstr() as any; }; win.Window = WorkerWindow; @@ -177,6 +184,7 @@ export const createWindow = ( createNodeCstr(win, env, WorkerBase); createCSSStyleDeclarationCstr(win, WorkerBase, 'CSSStyleDeclaration'); createPerformanceConstructor(win, WorkerBase, 'Performance'); + createCustomElementRegistry(win, nodeCstrs); // define all of the global constructors that should live on window webWorkerCtx.$interfaces$.map( @@ -194,7 +202,7 @@ export const createWindow = ( ? class extends WorkerBase { // create the constructor and set as a prop on window constructor(...args: any[]) { - super($winId$, randomId()); + super(); constructGlobal(this, cstrName, args); } } diff --git a/tests/index.html b/tests/index.html index b3168246..0807c150 100644 --- a/tests/index.html +++ b/tests/index.html @@ -71,6 +71,7 @@

Platform Tests

  • Anchor
  • Audio
  • Canvas
  • +
  • Custom Element
  • Document
  • Document (Prod Build)
  • Element
  • diff --git a/tests/platform/custom-element/custom-element.spec.ts b/tests/platform/custom-element/custom-element.spec.ts new file mode 100644 index 00000000..ac255ad7 --- /dev/null +++ b/tests/platform/custom-element/custom-element.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; + +test('custom-element', async ({ page }) => { + await page.goto('/tests/platform/custom-element/'); + + await page.waitForSelector('.testDefine'); + const testDefine = page.locator('#testDefine'); + await expect(testDefine).toHaveText('TestDefineElement'); + + await page.waitForSelector('.testConnectedCallback'); + const testConnectedCallback = page.locator('#testConnectedCallback'); + await expect(testConnectedCallback).toHaveText('test-connected-callback'); + + await page.waitForSelector('.testDisconnectedCallback'); + const testDisconnectedCallback = page.locator('#testDisconnectedCallback'); + await expect(testDisconnectedCallback).toHaveText('test-disconnected-callback'); + + await page.waitForSelector('.testAttributeChangedCallback'); + const testAttributeChangedCallback = page.locator('#testAttributeChangedCallback'); + await expect(testAttributeChangedCallback).toHaveText('mph 87 88'); + + await page.waitForSelector('.testConstructor'); + const testConstructor = page.locator('#testConstructor'); + await expect(testConstructor).toHaveText('test-constructor'); + + await page.waitForSelector('.testShadowRoot'); + const testShadowRoot = page.locator('#testShadowRoot'); + await expect(testShadowRoot).toHaveText('shadow #document-fragment'); +}); diff --git a/tests/platform/custom-element/index.html b/tests/platform/custom-element/index.html new file mode 100644 index 00000000..24a137cb --- /dev/null +++ b/tests/platform/custom-element/index.html @@ -0,0 +1,214 @@ + + + + + + + Custom Element + + + + + +

    Custom Element

    + + + +
    +

    All Tests

    + +