Reverse monkey patch built in methods to support LWC (#1509)
* Get around monkey patched Nodes
* inlineImages: Setting of `image.crossOrigin` is not always necessary (#1468)
Setting of the `crossorigin` attribute is not necessary for same-origin images, and causes an immediate image reload (albeit from cache) necessitating the use of a load event listener which subsequently mutates the snapshot. This change allows us to avoid the mutation of the snapshot for the same-origin case.
* Modify inlineImages test to remove delay and show that we can inline images without mutation
* Add an explicit test for when the `image.crossOrigin = 'anonymous';` method is necessary. Uses a combination of about:blank and our test server to simulate a cross-origin context
* Other test changes: there were some spurious rrweb mutations being generated by the addition of the crossorigin attribute that are now elimnated from the rrweb/__snapshots__/integration.test.ts.snap after this PR - this is good
* Move `childNodes` to @rrweb/utils
* Use non-monkey patched versions of the `childNodes`, `parentNode` `parentElement` `textContent` accessors
* Add getRootNode and contains, and add comprehensive todo list
* chore: Update turbo.json tasks for better build process
* Update caniuse-lite
* chore: Update eslint-plugin-compat to version 5.0.0
* chore: Bump @rrweb/utils version to 2.0.0-alpha.15
* delete unused yarn.lock files
* Set correct @rrweb/utils version in package.json
* Migrate over some accessors to reverse-monkey-patched version
* Add missing functions
* Fix illegal invocation error
* Revert closer to what it was.
This feels incorrect to me (Justin Halsall), but some of the tests break without it so I'm restoring this to be closer to its original here:
cfd686d488/packages/rrweb-snapshot/src/snapshot.ts (L1011)
* Reverse monkey patch all methods LWC hijacks
* Make tests more stable
* Safely handle rrdom nodes in hasShadowRoot
* Remove duplicated test
* Use variable `serverURL` in test
* Use monorepo default browserlist
* Fix typing issue for new typescript
* Remove unused package
* Remove unused code
* Add prefix to reverse-monkey-patched methods to make them more explicit
* Add default exports to @rrweb/utils
---------
Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
This commit is contained in:
221
packages/utils/src/index.ts
Normal file
221
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
type PrototypeOwner = Node | ShadowRoot | MutationObserver | Element;
|
||||
type TypeofPrototypeOwner =
|
||||
| typeof Node
|
||||
| typeof ShadowRoot
|
||||
| typeof MutationObserver
|
||||
| typeof Element;
|
||||
|
||||
type BasePrototypeCache = {
|
||||
Node: typeof Node.prototype;
|
||||
ShadowRoot: typeof ShadowRoot.prototype;
|
||||
MutationObserver: typeof MutationObserver.prototype;
|
||||
Element: typeof Element.prototype;
|
||||
};
|
||||
|
||||
const testableAccessors = {
|
||||
Node: ['childNodes', 'parentNode', 'parentElement', 'textContent'] as const,
|
||||
ShadowRoot: ['host', 'styleSheets'] as const,
|
||||
Element: ['shadowRoot', 'querySelector', 'querySelectorAll'] as const,
|
||||
MutationObserver: [] as const,
|
||||
} as const;
|
||||
|
||||
const testableMethods = {
|
||||
Node: ['contains', 'getRootNode'] as const,
|
||||
ShadowRoot: ['getSelection'],
|
||||
Element: [],
|
||||
MutationObserver: ['constructor'],
|
||||
} as const;
|
||||
|
||||
const untaintedBasePrototype: Partial<BasePrototypeCache> = {};
|
||||
|
||||
export function getUntaintedPrototype<T extends keyof BasePrototypeCache>(
|
||||
key: T,
|
||||
): BasePrototypeCache[T] {
|
||||
if (untaintedBasePrototype[key])
|
||||
return untaintedBasePrototype[key] as BasePrototypeCache[T];
|
||||
|
||||
const defaultObj = globalThis[key] as TypeofPrototypeOwner;
|
||||
const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T];
|
||||
|
||||
// use list of testable accessors to check if the prototype is tainted
|
||||
const accessorNames =
|
||||
key in testableAccessors ? testableAccessors[key] : undefined;
|
||||
const isUntaintedAccessors = Boolean(
|
||||
accessorNames &&
|
||||
// @ts-expect-error 2345
|
||||
accessorNames.every((accessor: keyof typeof defaultPrototype) =>
|
||||
Boolean(
|
||||
Object.getOwnPropertyDescriptor(defaultPrototype, accessor)
|
||||
?.get?.toString()
|
||||
.includes('[native code]'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const methodNames = key in testableMethods ? testableMethods[key] : undefined;
|
||||
const isUntaintedMethods = Boolean(
|
||||
methodNames &&
|
||||
methodNames.every(
|
||||
// @ts-expect-error 2345
|
||||
(method: keyof typeof defaultPrototype) =>
|
||||
typeof defaultPrototype[method] === 'function' &&
|
||||
defaultPrototype[method]?.toString().includes('[native code]'),
|
||||
),
|
||||
);
|
||||
|
||||
if (isUntaintedAccessors && isUntaintedMethods) {
|
||||
untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T];
|
||||
return defaultObj.prototype as BasePrototypeCache[T];
|
||||
}
|
||||
|
||||
try {
|
||||
const iframeEl = document.createElement('iframe');
|
||||
document.body.appendChild(iframeEl);
|
||||
const win = iframeEl.contentWindow;
|
||||
if (!win) return defaultObj.prototype as BasePrototypeCache[T];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
const untaintedObject = (win as any)[key]
|
||||
.prototype as BasePrototypeCache[T];
|
||||
// cleanup
|
||||
document.body.removeChild(iframeEl);
|
||||
|
||||
if (!untaintedObject) return defaultPrototype;
|
||||
|
||||
return (untaintedBasePrototype[key] = untaintedObject);
|
||||
} catch {
|
||||
return defaultPrototype;
|
||||
}
|
||||
}
|
||||
|
||||
const untaintedAccessorCache: Record<
|
||||
string,
|
||||
(this: PrototypeOwner, ...args: unknown[]) => unknown
|
||||
> = {};
|
||||
|
||||
export function getUntaintedAccessor<
|
||||
K extends keyof BasePrototypeCache,
|
||||
T extends keyof BasePrototypeCache[K],
|
||||
>(
|
||||
key: K,
|
||||
instance: BasePrototypeCache[K],
|
||||
accessor: T,
|
||||
): BasePrototypeCache[K][T] {
|
||||
const cacheKey = `${key}.${String(accessor)}`;
|
||||
if (untaintedAccessorCache[cacheKey])
|
||||
return untaintedAccessorCache[cacheKey].call(
|
||||
instance,
|
||||
) as BasePrototypeCache[K][T];
|
||||
|
||||
const untaintedPrototype = getUntaintedPrototype(key);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const untaintedAccessor = Object.getOwnPropertyDescriptor(
|
||||
untaintedPrototype,
|
||||
accessor,
|
||||
)?.get;
|
||||
|
||||
if (!untaintedAccessor) return instance[accessor];
|
||||
|
||||
untaintedAccessorCache[cacheKey] = untaintedAccessor;
|
||||
|
||||
return untaintedAccessor.call(instance) as BasePrototypeCache[K][T];
|
||||
}
|
||||
|
||||
type BaseMethod<K extends keyof BasePrototypeCache> = (
|
||||
this: BasePrototypeCache[K],
|
||||
...args: unknown[]
|
||||
) => unknown;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const untaintedMethodCache: Record<string, BaseMethod<any>> = {};
|
||||
export function getUntaintedMethod<
|
||||
K extends keyof BasePrototypeCache,
|
||||
T extends keyof BasePrototypeCache[K],
|
||||
>(
|
||||
key: K,
|
||||
instance: BasePrototypeCache[K],
|
||||
method: T,
|
||||
): BasePrototypeCache[K][T] {
|
||||
const cacheKey = `${key}.${String(method)}`;
|
||||
if (untaintedMethodCache[cacheKey])
|
||||
return untaintedMethodCache[cacheKey].bind(
|
||||
instance,
|
||||
) as BasePrototypeCache[K][T];
|
||||
|
||||
const untaintedPrototype = getUntaintedPrototype(key);
|
||||
const untaintedMethod = untaintedPrototype[method];
|
||||
|
||||
if (typeof untaintedMethod !== 'function') return instance[method];
|
||||
|
||||
untaintedMethodCache[cacheKey] = untaintedMethod as BaseMethod<K>;
|
||||
|
||||
return untaintedMethod.bind(instance) as BasePrototypeCache[K][T];
|
||||
}
|
||||
|
||||
export function childNodes(n: Node): NodeListOf<Node> {
|
||||
return getUntaintedAccessor('Node', n, 'childNodes');
|
||||
}
|
||||
|
||||
export function parentNode(n: Node): ParentNode | null {
|
||||
return getUntaintedAccessor('Node', n, 'parentNode');
|
||||
}
|
||||
|
||||
export function parentElement(n: Node): HTMLElement | null {
|
||||
return getUntaintedAccessor('Node', n, 'parentElement');
|
||||
}
|
||||
|
||||
export function textContent(n: Node): string | null {
|
||||
return getUntaintedAccessor('Node', n, 'textContent');
|
||||
}
|
||||
|
||||
export function contains(n: Node, other: Node): boolean {
|
||||
return getUntaintedMethod('Node', n, 'contains')(other);
|
||||
}
|
||||
|
||||
export function getRootNode(n: Node): Node {
|
||||
return getUntaintedMethod('Node', n, 'getRootNode')();
|
||||
}
|
||||
|
||||
export function host(n: ShadowRoot): Element | null {
|
||||
if (!n || !('host' in n)) return null;
|
||||
return getUntaintedAccessor('ShadowRoot', n, 'host');
|
||||
}
|
||||
|
||||
export function styleSheets(n: ShadowRoot): StyleSheetList {
|
||||
return n.styleSheets;
|
||||
}
|
||||
|
||||
export function shadowRoot(n: Node): ShadowRoot | null {
|
||||
if (!n || !('shadowRoot' in n)) return null;
|
||||
return getUntaintedAccessor('Element', n as Element, 'shadowRoot');
|
||||
}
|
||||
|
||||
export function querySelector(n: Element, selectors: string): Element | null {
|
||||
return getUntaintedAccessor('Element', n, 'querySelector')(selectors);
|
||||
}
|
||||
|
||||
export function querySelectorAll(
|
||||
n: Element,
|
||||
selectors: string,
|
||||
): NodeListOf<Element> {
|
||||
return getUntaintedAccessor('Element', n, 'querySelectorAll')(selectors);
|
||||
}
|
||||
|
||||
export function mutationObserverCtor(): (typeof MutationObserver)['prototype']['constructor'] {
|
||||
return getUntaintedPrototype('MutationObserver').constructor;
|
||||
}
|
||||
|
||||
export default {
|
||||
childNodes,
|
||||
parentNode,
|
||||
parentElement,
|
||||
textContent,
|
||||
contains,
|
||||
getRootNode,
|
||||
host,
|
||||
styleSheets,
|
||||
shadowRoot,
|
||||
querySelector,
|
||||
querySelectorAll,
|
||||
mutationObserver: mutationObserverCtor,
|
||||
};
|
||||
Reference in New Issue
Block a user