Skip to content

Commit

Permalink
feat: support custom elements
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley committed Mar 11, 2022
1 parent 389a0b2 commit 59d1104
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 34 deletions.
25 changes: 21 additions & 4 deletions src/lib/sandbox/main-access-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand All @@ -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 "${
Expand Down Expand Up @@ -112,6 +124,7 @@ export const mainAccessHandler = async (

const applyToInstance = (
worker: PartytownWebWorker,
winId: WinId,
instance: any,
applyPath: ApplyPath,
isLast: boolean,
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions src/lib/sandbox/main-custom-element.ts
Original file line number Diff line number Diff line change
@@ -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;
};
7 changes: 5 additions & 2 deletions src/lib/sandbox/main-serialization.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)];
}
Expand Down
11 changes: 4 additions & 7 deletions src/lib/sandbox/read-main-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
createElementFromConstructor,
debug,
getConstructorName,
getNodeName,
isValidMemberName,
len,
noop,
Expand All @@ -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');
Expand Down Expand Up @@ -56,6 +58,7 @@ export const readMainPlatform = () => {
[textNode],
[comment],
[frag],
[shadowRoot],
[elm],
[elm.attributes],
[elm.classList],
Expand Down Expand Up @@ -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)]);
}
};

Expand Down
20 changes: 17 additions & 3 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -65,6 +72,7 @@ export const enum WorkerMessageType {
AsyncAccessRequest,
LocationUpdate,
DocumentVisibilityState,
CustomElementCallback,
}

export interface ForwardMainTriggerData {
Expand Down Expand Up @@ -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
Expand All @@ -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[]];
3 changes: 3 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/web-worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { callCustomElementCallback } from './worker-custom-elements';
import { callWorkerRefHandler } from './worker-serialization';
import { createEnvironment } from './worker-environment';
import { debug } from '../utils';
Expand Down Expand Up @@ -40,6 +41,8 @@ const receiveMessageFromSandboxToWorker = (ev: MessageEvent<MessageFromSandboxTo
environments[msgValue].$visibilityState$ = msg[2];
} else if (msgType === WorkerMessageType.LocationUpdate) {
environments[msgValue].$location$.href = msg[2];
} else if (msgType === WorkerMessageType.CustomElementCallback) {
callCustomElementCallback(...msg);
}
} else if (msgType === WorkerMessageType.MainDataResponseToWorker) {
// received initial main data
Expand Down
4 changes: 2 additions & 2 deletions src/lib/web-worker/media/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,14 @@ export const initMedia = (
},
};

const WorkerMediaSource = defineCstr(
defineCstr(
win,
'MediaSource',
class extends WorkerEventTargetProxy {
[SourceBuffersKey]: typeof WorkerSourceBufferList;

constructor() {
super(env.$winId$, randomId());
super(env.$winId$);
this[SourceBuffersKey] = new WorkerSourceBufferList(this as any);
constructGlobal(this, 'MediaSource', EMPTY_ARRAY);
}
Expand Down
45 changes: 45 additions & 0 deletions src/lib/web-worker/worker-custom-elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { callMethod } from './worker-proxy';
import type {
CustomElementData,
InstanceId,
WinId,
WorkerMessageType,
WorkerNodeConstructors,
} from '../types';
import { getOrCreateNodeInstance } from './worker-constructors';

export const createCustomElementRegistry = (win: any, nodeCstrs: WorkerNodeConstructors) => {
const customElements = 'customElements';
const registry = new Map<string, any>();

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);
}
};
3 changes: 2 additions & 1 deletion src/lib/web-worker/worker-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/lib/web-worker/worker-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const createNodeCstr = (
}

get nodeName() {
return this[InstanceDataKey];
return this[InstanceDataKey] === '#s' ? '#document-fragment' : this[InstanceDataKey];
}

get nodeType() {
Expand Down
Loading

1 comment on commit 59d1104

@vercel
Copy link

@vercel vercel bot commented on 59d1104 Mar 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.