Skip to content

Commit be6bf52

Browse files
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: https://github.com/rrweb-io/rrweb/blame/cfd686d488a9b88dba6b6f8880b5e4375dd8062c/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>
1 parent b1f9daa commit be6bf52

28 files changed

+911
-184
lines changed

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@rrweb/record",
1515
"@rrweb/types",
1616
"@rrweb/packer",
17+
"@rrweb/utils",
1718
"@rrweb/web-extension",
1819
"rrvideo",
1920
"@rrweb/rrweb-plugin-console-record",

.changeset/unlucky-mirrors-invite.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"rrweb-snapshot": patch
3+
"rrweb": patch
4+
"@rrweb/utils": patch
5+
---
6+
7+
Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods).

.vscode/rrweb-monorepo.code-workspace

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"name": "@rrweb/types",
4141
"path": "../packages/types"
4242
},
43+
{
44+
"name": "@rrweb/utils",
45+
"path": "../packages/utils"
46+
},
4347
{
4448
"name": "@rrweb/packer",
4549
"path": "../packages/packer"
@@ -88,6 +92,7 @@
8892
"@rrweb/record",
8993
"@rrweb/replay",
9094
"@rrweb/types",
95+
"@rrweb/utils",
9196
"@rrweb/packer",
9297
"@rrweb/rrweb-plugin-console-record",
9398
"@rrweb/rrweb-plugin-console-replay",

guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Besides the `rrweb` and `@rrweb/record` packages, rrweb also provides other pack
4747
- [@rrweb/replay](packages/replay): A package for replaying rrweb sessions.
4848
- [@rrweb/packer](packages/packer): A package for packing and unpacking rrweb data.
4949
- [@rrweb/types](packages/types): Contains types shared across rrweb packages.
50+
- [@rrweb/utils](packages/utils): Contains utility functions shared across rrweb packages.
5051
- [web-extension](packages/web-extension): A web extension for rrweb.
5152
- [rrvideo](packages/rrvideo): A package for handling video operations in rrweb.
5253
- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs.

guide.zh_CN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ rrweb 代码分为录制和回放两部分,大多数时候用户在被录制
4343
- [@rrweb/record](packages/record):一个用于录制 rrweb 会话的包。
4444
- [@rrweb/replay](packages/replay):一个用于回放 rrweb 会话的包。
4545
- [@rrweb/packer](packages/packer):一个用于打包和解包 rrweb 数据的包。
46-
- [@rrweb/types](packages/types):包含 rrweb 中使用的类型定义。
46+
- [@rrweb/types](packages/types):包含 rrweb 包中共享的类型定义。
47+
- [@rrweb/utils](packages/utils):包含 rrweb 包中共享的工具函数。
4748
- [web-extension](packages/web-extension):rrweb 的网页扩展。
4849
- [rrvideo](packages/rrvideo):一个用于处理 rrweb 中视频操作的包。
4950
- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"cross-env": "^7.0.3",
3232
"esbuild-plugin-umd-wrapper": "^2.0.0",
3333
"eslint": "^8.53.0",
34-
"eslint-plugin-compat": "^4.2.0",
34+
"eslint-plugin-compat": "^5.0.0",
3535
"eslint-plugin-jest": "^27.6.0",
3636
"eslint-plugin-tsdoc": "^0.2.17",
3737
"markdownlint": "^0.25.1",
@@ -49,7 +49,7 @@
4949
"check-types": "yarn turbo run check-types --continue",
5050
"format": "yarn prettier --write '**/*.{ts,md}'",
5151
"format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write",
52-
"dev": "yarn turbo run dev --concurrency=17",
52+
"dev": "yarn turbo run dev --concurrency=18",
5353
"repl": "cd packages/rrweb && npm run repl",
5454
"live-stream": "cd packages/rrweb && yarn live-stream",
5555
"lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'",

packages/rrweb-snapshot/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
},
5555
"homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme",
5656
"devDependencies": {
57+
"@rrweb/utils": "^2.0.0-alpha.16",
5758
"@types/jsdom": "^20.0.0",
5859
"@types/node": "^18.15.11",
5960
"@types/puppeteer": "^5.4.4",

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
extractFileExtension,
2929
absolutifyURLs,
3030
} from './utils';
31+
import dom from '@rrweb/utils';
3132

3233
let _id = 1;
3334
const tagNameRegex = new RegExp('[^a-z0-9-_:]');
@@ -247,7 +248,7 @@ export function classMatchesRegex(
247248
if (!node) return false;
248249
if (node.nodeType !== node.ELEMENT_NODE) {
249250
if (!checkAncestors) return false;
250-
return classMatchesRegex(node.parentNode, regex, checkAncestors);
251+
return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);
251252
}
252253

253254
for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) {
@@ -257,7 +258,7 @@ export function classMatchesRegex(
257258
}
258259
}
259260
if (!checkAncestors) return false;
260-
return classMatchesRegex(node.parentNode, regex, checkAncestors);
261+
return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);
261262
}
262263

263264
export function needMaskingText(
@@ -269,16 +270,16 @@ export function needMaskingText(
269270
let el: Element;
270271
if (isElement(node)) {
271272
el = node;
272-
if (!el.childNodes.length) {
273+
if (!dom.childNodes(el).length) {
273274
// optimisation: we can avoid any of the below checks on leaf elements
274275
// as masking is applied to child text nodes only
275276
return false;
276277
}
277-
} else if (node.parentElement === null) {
278+
} else if (dom.parentElement(node) === null) {
278279
// should warn? maybe a text node isn't attached to a parent node yet?
279280
return false;
280281
} else {
281-
el = node.parentElement;
282+
el = dom.parentElement(node)!;
282283
}
283284
try {
284285
if (typeof maskTextClass === 'string') {
@@ -475,7 +476,7 @@ function serializeNode(
475476
case n.COMMENT_NODE:
476477
return {
477478
type: NodeType.Comment,
478-
textContent: (n as Comment).textContent || '',
479+
textContent: dom.textContent(n as Comment) || '',
479480
rootId,
480481
};
481482
default:
@@ -501,43 +502,42 @@ function serializeTextNode(
501502
const { needsMask, maskTextFn, rootId } = options;
502503
// The parent node may not be a html element which has a tagName attribute.
503504
// So just let it be undefined which is ok in this use case.
504-
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
505-
let textContent = n.textContent;
505+
const parent = dom.parentNode(n);
506+
const parentTagName = parent && (parent as HTMLElement).tagName;
507+
let text = dom.textContent(n);
506508
const isStyle = parentTagName === 'STYLE' ? true : undefined;
507509
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
508-
if (isStyle && textContent) {
510+
if (isStyle && text) {
509511
try {
510512
// try to read style sheet
511513
if (n.nextSibling || n.previousSibling) {
512514
// This is not the only child of the stylesheet.
513515
// We can't read all of the sheet's .cssRules and expect them
514516
// to _only_ include the current rule(s) added by the text node.
515517
// So we'll be conservative and keep textContent as-is.
516-
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
517-
textContent = stringifyStylesheet(
518-
(n.parentNode as HTMLStyleElement).sheet!,
519-
);
518+
} else if ((parent as HTMLStyleElement).sheet?.cssRules) {
519+
text = stringifyStylesheet((parent as HTMLStyleElement).sheet!);
520520
}
521521
} catch (err) {
522522
console.warn(
523523
`Cannot get CSS styles from text's parentNode. Error: ${err as string}`,
524524
n,
525525
);
526526
}
527-
textContent = absolutifyURLs(textContent, getHref(options.doc));
527+
text = absolutifyURLs(text, getHref(options.doc));
528528
}
529529
if (isScript) {
530-
textContent = 'SCRIPT_PLACEHOLDER';
530+
text = 'SCRIPT_PLACEHOLDER';
531531
}
532-
if (!isStyle && !isScript && textContent && needsMask) {
533-
textContent = maskTextFn
534-
? maskTextFn(textContent, n.parentElement)
535-
: textContent.replace(/[\S]/g, '*');
532+
if (!isStyle && !isScript && text && needsMask) {
533+
text = maskTextFn
534+
? maskTextFn(text, dom.parentElement(n))
535+
: text.replace(/[\S]/g, '*');
536536
}
537537

538538
return {
539539
type: NodeType.Text,
540-
textContent: textContent || '',
540+
textContent: text || '',
541541
isStyle,
542542
rootId,
543543
};
@@ -594,6 +594,7 @@ function serializeElementNode(
594594
}
595595
// remote css
596596
if (tagName === 'link' && inlineStylesheet) {
597+
//TODO: maybe replace this `.styleSheets` with original one
597598
const stylesheet = Array.from(doc.styleSheets).find((s) => {
598599
return s.href === (n as HTMLLinkElement).href;
599600
});
@@ -612,7 +613,7 @@ function serializeElementNode(
612613
tagName === 'style' &&
613614
(n as HTMLStyleElement).sheet &&
614615
// TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
615-
!(n.innerText || n.textContent || '').trim().length
616+
!(n.innerText || dom.textContent(n) || '').trim().length
616617
) {
617618
const cssText = stringifyStylesheet(
618619
(n as HTMLStyleElement).sheet as CSSStyleSheet,
@@ -1030,8 +1031,8 @@ export function serializeNodeWithId(
10301031
recordChild = recordChild && !serializedNode.needBlock;
10311032
// this property was not needed in replay side
10321033
delete serializedNode.needBlock;
1033-
const shadowRoot = (n as HTMLElement).shadowRoot;
1034-
if (shadowRoot && isNativeShadowDom(shadowRoot))
1034+
const shadowRootEl = dom.shadowRoot(n);
1035+
if (shadowRootEl && isNativeShadowDom(shadowRootEl))
10351036
serializedNode.isShadowHost = true;
10361037
}
10371038
if (
@@ -1080,31 +1081,29 @@ export function serializeNodeWithId(
10801081
) {
10811082
// value parameter in DOM reflects the correct value, so ignore childNode
10821083
} else {
1083-
for (const childN of Array.from(n.childNodes)) {
1084+
for (const childN of Array.from(dom.childNodes(n))) {
10841085
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
10851086
if (serializedChildNode) {
10861087
serializedNode.childNodes.push(serializedChildNode);
10871088
}
10881089
}
10891090
}
10901091

1091-
if (isElement(n) && n.shadowRoot) {
1092-
for (const childN of Array.from(n.shadowRoot.childNodes)) {
1092+
let shadowRootEl: ShadowRoot | null = null;
1093+
if (isElement(n) && (shadowRootEl = dom.shadowRoot(n))) {
1094+
for (const childN of Array.from(dom.childNodes(shadowRootEl))) {
10931095
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
10941096
if (serializedChildNode) {
1095-
isNativeShadowDom(n.shadowRoot) &&
1097+
isNativeShadowDom(shadowRootEl) &&
10961098
(serializedChildNode.isShadow = true);
10971099
serializedNode.childNodes.push(serializedChildNode);
10981100
}
10991101
}
11001102
}
11011103
}
11021104

1103-
if (
1104-
n.parentNode &&
1105-
isShadowRoot(n.parentNode) &&
1106-
isNativeShadowDom(n.parentNode)
1107-
) {
1105+
const parent = dom.parentNode(n);
1106+
if (parent && isShadowRoot(parent) && isNativeShadowDom(parent)) {
11081107
serializedNode.isShadow = true;
11091108
}
11101109

packages/rrweb-snapshot/src/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@ import type {
1111
textNode,
1212
elementNode,
1313
} from './types';
14+
import dom from '@rrweb/utils';
1415
import { NodeType } from './types';
1516

1617
export function isElement(n: Node): n is Element {
1718
return n.nodeType === n.ELEMENT_NODE;
1819
}
1920

2021
export function isShadowRoot(n: Node): n is ShadowRoot {
21-
const host: Element | null = (n as ShadowRoot)?.host;
22-
return Boolean(host?.shadowRoot === n);
22+
const hostEl: Element | null =
23+
// anchor and textarea elements also have a `host` property
24+
// but only shadow roots have a `mode` property
25+
(n && 'host' in n && 'mode' in n && dom.host(n as ShadowRoot)) || null;
26+
return Boolean(
27+
hostEl && 'shadowRoot' in hostEl && dom.shadowRoot(hostEl) === n,
28+
);
2329
}
2430

2531
/**

0 commit comments

Comments
 (0)