Skip to content

Commit

Permalink
Reverse monkey patch built in methods to support LWC #1509
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadman97 committed Jun 25, 2024
1 parent 6cf9dd1 commit 174cec0
Show file tree
Hide file tree
Showing 25 changed files with 892 additions and 169 deletions.
7 changes: 7 additions & 0 deletions .changeset/unlucky-mirrors-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"rrweb-snapshot": patch
"rrweb": patch
"@rrweb/utils": patch
---

Reverse monkey patch built in methods to support LWC (and other frameworks like angular which monkey patch built in methods).
5 changes: 5 additions & 0 deletions .vscode/rrweb-monorepo.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"name": "@rrweb/types",
"path": "../packages/types"
},
{
"name": "@rrweb/utils",
"path": "../packages/utils"
},
{
"name": "@rrweb/packer",
"path": "../packages/packer"
Expand Down Expand Up @@ -88,6 +92,7 @@
"@rrweb/record",
"@rrweb/replay",
"@rrweb/types",
"@rrweb/utils",
"@rrweb/packer",
"@rrweb/rrweb-plugin-console-record",
"@rrweb/rrweb-plugin-console-replay",
Expand Down
1 change: 1 addition & 0 deletions guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Besides the `rrweb` and `@rrweb/record` packages, rrweb also provides other pack
- [@rrweb/replay](packages/replay): A package for replaying rrweb sessions.
- [@rrweb/packer](packages/packer): A package for packing and unpacking rrweb data.
- [@rrweb/types](packages/types): Contains types shared across rrweb packages.
- [@rrweb/utils](packages/utils): Contains utility functions shared across rrweb packages.
- [web-extension](packages/web-extension): A web extension for rrweb.
- [rrvideo](packages/rrvideo): A package for handling video operations in rrweb.
- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record): A plugin for recording console logs.
Expand Down
3 changes: 2 additions & 1 deletion guide.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ rrweb 代码分为录制和回放两部分,大多数时候用户在被录制
- [@rrweb/record](packages/record):一个用于录制 rrweb 会话的包。
- [@rrweb/replay](packages/replay):一个用于回放 rrweb 会话的包。
- [@rrweb/packer](packages/packer):一个用于打包和解包 rrweb 数据的包。
- [@rrweb/types](packages/types):包含 rrweb 中使用的类型定义。
- [@rrweb/types](packages/types):包含 rrweb 包中共享的类型定义。
- [@rrweb/utils](packages/utils):包含 rrweb 包中共享的工具函数。
- [web-extension](packages/web-extension):rrweb 的网页扩展。
- [rrvideo](packages/rrvideo):一个用于处理 rrweb 中视频操作的包。
- [@rrweb/rrweb-plugin-console-record](packages/plugins/rrweb-plugin-console-record):一个用于记录控制台日志的插件。
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"cross-env": "^7.0.3",
"esbuild-plugin-umd-wrapper": "^2.0.0",
"eslint": "^8.53.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-compat": "^5.0.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-tsdoc": "^0.2.17",
"markdownlint": "^0.25.1",
Expand All @@ -49,7 +49,7 @@
"check-types": "yarn turbo run check-types --continue",
"format": "yarn prettier --write '**/*.{ts,md}'",
"format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write",
"dev": "yarn turbo run dev --concurrency=17",
"dev": "yarn turbo run dev --concurrency=18",
"repl": "cd packages/rrweb && npm run repl",
"live-stream": "cd packages/rrweb && yarn live-stream",
"lint": "yarn run concurrently --success=all -r -m=1 'yarn run markdownlint docs' 'yarn eslint packages/*/src --ext .ts,.tsx,.js,.jsx,.svelte'",
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb-snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
},
"homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme",
"devDependencies": {
"@rrweb/utils": "^2.0.0-alpha.15",
"@types/jsdom": "^20.0.0",
"@types/node": "^18.15.11",
"@types/puppeteer": "^5.4.4",
Expand Down
77 changes: 41 additions & 36 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import {
extractFileExtension,
isElementSrcBlocked,
} from './utils';
import {
childNodes,
parentNode,
parentElement,
textContent,
shadowRoot,
} from '@rrweb/utils';

let _id = 1;
const tagNameRegex = new RegExp('[^a-z0-9-_:]');
Expand Down Expand Up @@ -311,7 +318,7 @@ export function classMatchesRegex(
if (!node) return false;
if (node.nodeType !== node.ELEMENT_NODE) {
if (!checkAncestors) return false;
return classMatchesRegex(node.parentNode, regex, checkAncestors);
return classMatchesRegex(parentNode(node), regex, checkAncestors);
}

for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) {
Expand All @@ -321,7 +328,7 @@ export function classMatchesRegex(
}
}
if (!checkAncestors) return false;
return classMatchesRegex(node.parentNode, regex, checkAncestors);
return classMatchesRegex(parentNode(node), regex, checkAncestors);
}

export function needMaskingText(
Expand All @@ -333,16 +340,16 @@ export function needMaskingText(
let el: Element;
if (isElement(node)) {
el = node;
if (!el.childNodes.length) {
if (!childNodes(el).length) {
// optimisation: we can avoid any of the below checks on leaf elements
// as masking is applied to child text nodes only
return false;
}
} else if (node.parentElement === null) {
} else if (parentElement(node) === null) {
// should warn? maybe a text node isn't attached to a parent node yet?
return false;
} else {
el = node.parentElement;
el = parentElement(node)!;
}
try {
if (typeof maskTextClass === 'string') {
Expand Down Expand Up @@ -548,7 +555,7 @@ function serializeNode(
case n.COMMENT_NODE:
return {
type: NodeType.Comment,
textContent: (n as Comment).textContent || '',
textContent: textContent(n as Comment) || '',
rootId,
};
default:
Expand Down Expand Up @@ -580,45 +587,44 @@ function serializeTextNode(
} = options;
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = n.textContent;
const parent = parentNode(n);
const parentTagName = parent && (parent as HTMLElement).tagName;
let text = textContent(n);
const isStyle = parentTagName === 'STYLE' ? true : undefined;
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
/** Determines if this node has been handled already. */
let textContentHandled = false;
if (isStyle && textContent) {
if (isStyle && text) {
try {
// try to read style sheet
if (n.nextSibling || n.previousSibling) {
// This is not the only child of the stylesheet.
// We can't read all of the sheet's .cssRules and expect them
// to _only_ include the current rule(s) added by the text node.
// So we'll be conservative and keep textContent as-is.
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
textContent = stringifyStylesheet(
(n.parentNode as HTMLStyleElement).sheet!,
);
} else if ((parent as HTMLStyleElement).sheet?.cssRules) {
text = stringifyStylesheet((parent as HTMLStyleElement).sheet!);
}
} catch (err) {
console.warn(
`Cannot get CSS styles from text's parentNode. Error: ${err as string}`,
n,
);
}
textContent = absoluteToStylesheet(textContent, getHref(options.doc));
text = absoluteToStylesheet(text, getHref(options.doc));
textContentHandled = true;
}
if (isScript) {
textContent = 'SCRIPT_PLACEHOLDER';
text = 'SCRIPT_PLACEHOLDER';
textContentHandled = true;
} else if (parentTagName === 'NOSCRIPT') {
textContent = '';
text = '';
textContentHandled = true;
}
if (!isStyle && !isScript && textContent && needsMask) {
textContent = maskTextFn
? maskTextFn(textContent, n.parentElement)
: textContent.replace(/[\S]/g, '*');
if (!isStyle && !isScript && text && needsMask) {
text = maskTextFn
? maskTextFn(text, parentElement(n))
: text.replace(/[\S]/g, '*');
}

/* Start of Highlight */
Expand All @@ -627,7 +633,7 @@ function serializeTextNode(
const highlightOverwriteRecord =
n.parentElement?.getAttribute('data-hl-record');
const obfuscateDefaultPrivacy =
privacySetting === 'default' && shouldObfuscateTextByDefault(textContent);
privacySetting === 'default' && shouldObfuscateTextByDefault(text);
if (
(enableStrictPrivacy || obfuscateDefaultPrivacy) &&
!highlightOverwriteRecord &&
Expand All @@ -643,15 +649,15 @@ function serializeTextNode(
'BODY',
'NOSCRIPT',
]);
if (!IGNORE_TAG_NAMES.has(parentTagName) && textContent) {
textContent = obfuscateText(textContent);
if (!IGNORE_TAG_NAMES.has(parentTagName) && text) {
text = obfuscateText(text);
}
}
/* End of Highlight */

return {
type: NodeType.Text,
textContent: textContent || '',
textContent: text || '',
isStyle,
rootId,
};
Expand Down Expand Up @@ -714,6 +720,7 @@ function serializeElementNode(
}
// remote css
if (tagName === 'link' && inlineStylesheet) {
//TODO: maybe replace this `.styleSheets` with original one
const stylesheet = Array.from(doc.styleSheets).find((s) => {
return s.href === (n as HTMLLinkElement).href;
});
Expand All @@ -732,7 +739,7 @@ function serializeElementNode(
tagName === 'style' &&
(n as HTMLStyleElement).sheet &&
// TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
!(n.innerText || n.textContent || '').trim().length
!(n.innerText || textContent(n) || '').trim().length
) {
const cssText = stringifyStylesheet(
(n as HTMLStyleElement).sheet as CSSStyleSheet,
Expand Down Expand Up @@ -1191,8 +1198,8 @@ export function serializeNodeWithId(
// these properties was not needed in replay side
delete serializedNode.needBlock;
delete serializedNode.needMask;
const shadowRoot = (n as HTMLElement).shadowRoot;
if (shadowRoot && isNativeShadowDom(shadowRoot))
const shadowRootEl = shadowRoot(n);
if (shadowRootEl && isNativeShadowDom(shadowRootEl))
serializedNode.isShadowHost = true;
}
if (
Expand Down Expand Up @@ -1242,31 +1249,29 @@ export function serializeNodeWithId(
) {
// value parameter in DOM reflects the correct value, so ignore childNode
} else {
for (const childN of Array.from(n.childNodes)) {
for (const childN of Array.from(childNodes(n))) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
}
}

if (isElement(n) && n.shadowRoot) {
for (const childN of Array.from(n.shadowRoot.childNodes)) {
let shadowRootEl: ShadowRoot | null = null;
if (isElement(n) && (shadowRootEl = shadowRoot(n))) {
for (const childN of Array.from(childNodes(shadowRootEl))) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
isNativeShadowDom(n.shadowRoot) &&
isNativeShadowDom(shadowRootEl) &&
(serializedChildNode.isShadow = true);
serializedNode.childNodes.push(serializedChildNode);
}
}
}
}

if (
n.parentNode &&
isShadowRoot(n.parentNode) &&
isNativeShadowDom(n.parentNode)
) {
const parent = parentNode(n);
if (parent && isShadowRoot(parent) && isNativeShadowDom(parent)) {
serializedNode.isShadow = true;
}

Expand Down
8 changes: 6 additions & 2 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ import type {
textNode,
elementNode,
} from './types';
import { shadowRoot, host } from '@rrweb/utils';
import { NodeType } from './types';

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

export function isShadowRoot(n: Node): n is ShadowRoot {
const host: Element | null = (n as ShadowRoot)?.host;
return Boolean(host?.shadowRoot === n);
const hostEl: Element | null =
// anchor and textarea elements also have a `host` property
// but only shadow roots have a `mode` property
(n && 'host' in n && 'mode' in n && host(n as ShadowRoot)) || null;
return Boolean(hostEl && 'shadowRoot' in hostEl && shadowRoot(hostEl) === n);
}

/**
Expand Down
Loading

0 comments on commit 174cec0

Please sign in to comment.