From 5068664d518051612132fb7ebc9f836e6b59255c Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 29 Dec 2021 19:13:23 +1100 Subject: [PATCH 01/79] rrdom: add a diff function for properties --- packages/rrdom/src/diff.ts | 58 ++++++++++++++++ packages/rrdom/test/diff.test.ts | 112 +++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 packages/rrdom/src/diff.ts create mode 100644 packages/rrdom/test/diff.test.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts new file mode 100644 index 0000000000..f5b3f25aad --- /dev/null +++ b/packages/rrdom/src/diff.ts @@ -0,0 +1,58 @@ +import { INode, NodeType } from 'rrweb-snapshot'; +import { RRElement, RRNode } from './document-browser'; + +export function diff(oldTree: INode, newTree: RRNode) { + if (oldTree && newTree) { + if (oldTree.__sn?.id === newTree.__sn?.id) { + switch (newTree.nodeType) { + case NodeType.Element: + diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); + break; + } + const oldChildren = oldTree.childNodes; + const newChildren = newTree.childNodes; + if (oldChildren.length > 0 && newChildren.length > 0) { + diffChildren( + (Array.from(oldChildren) as unknown) as INode[], + newChildren, + ); + } else if (oldChildren.length > 0) { + // TODO Remove all children. + } else if (newChildren.length > 0) { + // TODO Add all new children. + } + } else if (oldTree.__sn?.id !== newTree.__sn?.id) { + // Replace the old node with the new node + } else if (oldTree.__sn?.id) { + } else if (newTree.__sn?.id) { + } + } else if (oldTree) { + if (oldTree.parentNode) oldTree.parentNode.removeChild(oldTree); + } else if (newTree) { + // TODO Create a new node. + } +} + +function diffProps(oldTree: HTMLElement, newTree: RRElement) { + const oldAttributes = oldTree.attributes; + const newAttributes = newTree.attributes; + + for (const { name, value } of Array.from(oldAttributes)) { + if (!(name in newAttributes)) oldTree.removeAttribute(name); + const newValue = newAttributes[name]; + if (value === newValue) continue; + else oldTree.setAttribute(name, newValue as string); + } + + for (let attribute in newAttributes) { + const newValue = newAttributes[attribute]; + if (oldAttributes.hasOwnProperty(attribute)) continue; + if (typeof newValue === 'boolean' || typeof newValue === 'number') { + // TODO Some special cases for some kinds of elements. e.g. checked, rr_scrollLeft + } else oldTree.setAttribute(attribute, newValue); + } +} + +function diffChildren(oldChildren: INode[], newChildren: RRNode[]) { + // TODO +} diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts new file mode 100644 index 0000000000..353bec34d7 --- /dev/null +++ b/packages/rrdom/test/diff.test.ts @@ -0,0 +1,112 @@ +/** + * @jest-environment jsdom + */ +import { RRDocument } from '../src/document-browser'; +import { diff } from '../src/diff'; +import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; + +describe('diff algorithm for rrdom', () => { + it('diff a node', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + let sn = { + type: NodeType.Element, + tagName: tagName, + attributes: {}, + childNodes: [], + id: 1, + } as serializedNodeWithId; + node.__sn = sn; + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = sn; + diff(node, rrNode); + expect(node).toBeInstanceOf(HTMLElement); + expect(((node as unknown) as HTMLElement).tagName).toBe(tagName); + }); + + it('diff properties (add new properties)', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + let sn = { + type: NodeType.Element, + tagName: tagName, + attributes: {}, + childNodes: [], + id: 1, + } as serializedNodeWithId; + node.__sn = sn; + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = sn; + rrNode.attributes = { id: 'node1', class: 'node' }; + diff(node, rrNode); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('node'); + }); + + it('diff properties (update exist properties)', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + let sn = { + type: NodeType.Element, + tagName: tagName, + attributes: {}, + childNodes: [], + id: 1, + } as serializedNodeWithId; + node.__sn = sn; + ((node as unknown) as HTMLElement).id = 'element1'; + ((node as unknown) as HTMLElement).className = 'element'; + ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = sn; + rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; + diff(node, rrNode); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('node'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'color: white', + ); + + rrNode.attributes = { id: 'node2' }; + diff(node, rrNode); + expect(((node as unknown) as HTMLElement).id).toBe('node2'); + expect(((node as unknown) as HTMLElement).className).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'undefined', + ); + }); + + it('diff properties (delete old properties)', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + let sn = { + type: NodeType.Element, + tagName: tagName, + attributes: {}, + childNodes: [], + id: 1, + } as serializedNodeWithId; + node.__sn = sn; + ((node as unknown) as HTMLElement).id = 'element1'; + ((node as unknown) as HTMLElement).className = 'element'; + ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = sn; + rrNode.attributes = { id: 'node1' }; + diff(node, rrNode); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'undefined', + ); + + rrNode.attributes = { src: 'link' }; + diff(node, rrNode); + expect(((node as unknown) as HTMLElement).id).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe('link'); + }); +}); From b3879274ad04d7d23aa72707f7e4dde6d6e5a3cc Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 6 Jan 2022 23:25:14 +1100 Subject: [PATCH 02/79] implement diffChildren function and unit tests --- packages/rrdom/src/diff.ts | 73 ++- packages/rrdom/src/document-browser.ts | 648 +++++++++++++++++++++++++ packages/rrdom/test/diff.test.ts | 581 ++++++++++++++++++++-- 3 files changed, 1258 insertions(+), 44 deletions(-) create mode 100644 packages/rrdom/src/document-browser.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index f5b3f25aad..9dd808ba6e 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -11,15 +11,12 @@ export function diff(oldTree: INode, newTree: RRNode) { } const oldChildren = oldTree.childNodes; const newChildren = newTree.childNodes; - if (oldChildren.length > 0 && newChildren.length > 0) { + if (oldChildren.length > 0 || newChildren.length > 0) { diffChildren( (Array.from(oldChildren) as unknown) as INode[], newChildren, + oldTree, ); - } else if (oldChildren.length > 0) { - // TODO Remove all children. - } else if (newChildren.length > 0) { - // TODO Add all new children. } } else if (oldTree.__sn?.id !== newTree.__sn?.id) { // Replace the old node with the new node @@ -53,6 +50,68 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { } } -function diffChildren(oldChildren: INode[], newChildren: RRNode[]) { - // TODO +function diffChildren( + oldChildren: (INode | undefined)[], + newChildren: RRNode[], + parentNode: INode, +) { + let oldStartIndex = 0, + oldEndIndex = oldChildren.length - 1, + newStartIndex = 0, + newEndIndex = newChildren.length - 1; + let oldStartNode = oldChildren[oldStartIndex], + oldEndNode = oldChildren[oldEndIndex], + newStartNode = newChildren[newStartIndex], + newEndNode = newChildren[newEndIndex]; + let oldIdToIndex: Record | undefined = undefined, + indexInOld; + while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { + if (oldStartNode === undefined) { + oldStartNode = oldChildren[++oldStartIndex]; + } else if (oldEndNode === undefined) { + oldEndNode = oldChildren[--oldEndIndex]; + } else if (oldStartNode.__sn.id === newStartNode.__sn.id) { + diff(oldStartNode, newStartNode); + oldStartNode = oldChildren[++oldStartIndex]; + newStartNode = newChildren[++newStartIndex]; + } else if (oldEndNode.__sn.id === newEndNode.__sn.id) { + diff(oldEndNode, newEndNode); + oldEndNode = oldChildren[--oldEndIndex]; + newEndNode = newChildren[--newEndIndex]; + } else if (oldStartNode.__sn.id === newEndNode.__sn.id) { + parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); + diff(oldStartNode, newEndNode); + oldStartNode = oldChildren[++oldStartIndex]; + newEndNode = newChildren[--newEndIndex]; + } else if (oldEndNode.__sn.id === newStartNode.__sn.id) { + parentNode.insertBefore(oldEndNode, oldStartNode); + diff(oldEndNode, newStartNode); + oldEndNode = oldChildren[--oldEndIndex]; + newStartNode = newChildren[++newStartIndex]; + } else { + if (!oldIdToIndex) { + oldIdToIndex = {}; + for (let i = oldStartIndex; i <= oldEndIndex; i++) + oldIdToIndex[oldChildren[i]!.__sn.id] = i; + } + indexInOld = oldIdToIndex[newStartNode.__sn.id]; + if (indexInOld) { + const nodeToMove = oldChildren[indexInOld]!; + parentNode.insertBefore(nodeToMove, oldStartNode); + diff(nodeToMove, newStartNode); + oldChildren[indexInOld] = undefined; + } else { + // TODO Create a new node. + } + newStartNode = newChildren[++newStartIndex]; + } + } + if (oldStartIndex > oldEndIndex) { + // TODO Create several new nodes. + } else if (newStartIndex > newEndIndex) { + for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { + const node = oldChildren[oldStartIndex]; + node && parentNode.removeChild(node); + } + } } diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts new file mode 100644 index 0000000000..99cccc1b8a --- /dev/null +++ b/packages/rrdom/src/document-browser.ts @@ -0,0 +1,648 @@ +import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { parseCSSText, camelize, toCSSText } from './style'; + +export abstract class RRNode { + __sn: serializedNodeWithId; + children: Array = []; + parentElement: RRElement | null = null; + parentNode: RRNode | null = null; + ownerDocument: RRDocument | null = null; + ELEMENT_NODE = 1; + TEXT_NODE = 3; + + get nodeType() { + if (this instanceof RRDocument) return NodeType.Document; + if (this instanceof RRDocumentType) return NodeType.DocumentType; + if (this instanceof RRElement) return NodeType.Element; + if (this instanceof RRText) return NodeType.Text; + if (this instanceof RRCDATASection) return NodeType.CDATA; + if (this instanceof RRComment) return NodeType.Comment; + } + + get childNodes() { + return this.children; + } + + contains(node: RRNode) { + if (node === this) return true; + for (const child of this.children) { + if (child.contains(node)) return true; + } + return false; + } + + removeChild(node: RRNode) { + const indexOfChild = this.children.indexOf(node); + if (indexOfChild !== -1) { + this.children.splice(indexOfChild, 1); + node.parentElement = null; + node.parentNode = null; + } + } + + toString(nodeName?: string) { + return `${JSON.stringify(this.__sn?.id) || ''} ${nodeName}`; + } +} + +export class RRWindow { + scrollLeft = 0; + scrollTop = 0; + scrollTo(options?: ScrollToOptions) { + if (!options) return; + if (typeof options.left === 'number') this.scrollLeft = options.left; + if (typeof options.top === 'number') this.scrollTop = options.top; + } +} + +export class RRDocument extends RRNode { + private mirror: Map = new Map(); + + get documentElement(): RRElement { + return this.children.find( + (node) => node instanceof RRElement && node.tagName === 'HTML', + ) as RRElement; + } + + get body() { + return ( + this.documentElement?.children.find( + (node) => node instanceof RRElement && node.tagName === 'BODY', + ) || null + ); + } + + get head() { + return ( + this.documentElement?.children.find( + (node) => node instanceof RRElement && node.tagName === 'HEAD', + ) || null + ); + } + + get implementation() { + return this; + } + + get firstElementChild() { + return this.documentElement; + } + + appendChild(childNode: RRNode) { + const nodeType = childNode.nodeType; + if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { + if (this.children.some((s) => s.nodeType === nodeType)) { + throw new Error( + `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ + nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' + } on RRDocument allowed.`, + ); + } + } + childNode.parentElement = null; + childNode.parentNode = this; + childNode.ownerDocument = this; + this.children.push(childNode); + return childNode; + } + + insertBefore(newChild: RRNode, refChild: RRNode | null) { + if (refChild === null) return this.appendChild(newChild); + const childIndex = this.children.indexOf(refChild); + if (childIndex == -1) + throw new Error( + "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", + ); + this.children.splice(childIndex, 0, newChild); + newChild.parentElement = null; + newChild.parentNode = this; + newChild.ownerDocument = this; + return newChild; + } + + createDocument( + _namespace: string | null, + _qualifiedName: string | null, + _doctype?: DocumentType | null, + ) { + return new RRDocument(); + } + + createDocumentType( + qualifiedName: string, + publicId: string, + systemId: string, + ) { + const documentTypeNode = new RRDocumentType( + qualifiedName, + publicId, + systemId, + ); + documentTypeNode.ownerDocument = this; + return documentTypeNode; + } + + createElement( + tagName: K, + ): RRElementType; + createElement(tagName: string): RRElement; + createElement(tagName: string) { + const upperTagName = tagName.toUpperCase(); + let element; + switch (upperTagName) { + case 'AUDIO': + case 'VIDEO': + element = new RRMediaElement(upperTagName); + break; + case 'IFRAME': + element = new RRIframeElement(upperTagName); + break; + case 'IMG': + element = new RRImageElement('IMG'); + break; + case 'CANVAS': + element = new RRCanvasElement('CANVAS'); + break; + case 'STYLE': + element = new RRStyleElement('STYLE'); + break; + default: + element = new RRElement(upperTagName); + break; + } + element.ownerDocument = this; + return element; + } + + createElementNS( + _namespaceURI: 'http://www.w3.org/2000/svg', + qualifiedName: string, + ) { + return this.createElement(qualifiedName as keyof HTMLElementTagNameMap); + } + + createComment(data: string) { + const commentNode = new RRComment(data); + commentNode.ownerDocument = this; + return commentNode; + } + + createCDATASection(data: string) { + const sectionNode = new RRCDATASection(data); + sectionNode.ownerDocument = this; + return sectionNode; + } + + createTextNode(data: string) { + const textNode = new RRText(data); + textNode.ownerDocument = this; + return textNode; + } + + /** + * This does come with some side effects. For example: + * 1. All event listeners currently registered on the document, nodes inside the document, or the document's window are removed. + * 2. All existing nodes are removed from the document. + */ + open() { + this.children = []; + } + + close() {} + + buildFromDom(dom: Document) { + let notSerializedId = -1; + const NodeTypeMap: Record = {}; + NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; + NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; + NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; + NodeTypeMap[document.TEXT_NODE] = NodeType.Text; + NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; + NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; + + function getValidTagName(element: HTMLElement): string { + if (element instanceof HTMLFormElement) { + return 'FORM'; + } + return element.tagName.toUpperCase().trim(); + } + + const walk = function (node: INode) { + let serializedNodeWithId = node.__sn; + let rrNode: RRNode; + if (!serializedNodeWithId) { + serializedNodeWithId = { + type: NodeTypeMap[node.nodeType], + textContent: '', + id: notSerializedId, + }; + notSerializedId -= 1; + node.__sn = serializedNodeWithId; + } + if (!this.mirror.has(serializedNodeWithId.id)) { + switch (node.nodeType) { + case node.DOCUMENT_NODE: + if ( + serializedNodeWithId.rootId && + serializedNodeWithId.rootId !== serializedNodeWithId.id + ) + rrNode = this.createDocument(); + else rrNode = this; + break; + case node.DOCUMENT_TYPE_NODE: + const documentType = (node as unknown) as DocumentType; + rrNode = this.createDocumentType( + documentType.name, + documentType.publicId, + documentType.systemId, + ); + break; + case node.ELEMENT_NODE: + const elementNode = (node as unknown) as HTMLElement; + const tagName = getValidTagName(elementNode); + rrNode = this.createElement(tagName); + const rrElement = rrNode as RRElement; + for (const { name, value } of Array.from(elementNode.attributes)) { + rrElement.attributes[name] = value; + } + // form fields + if ( + tagName === 'INPUT' || + tagName === 'TEXTAREA' || + tagName === 'SELECT' + ) { + const value = (elementNode as + | HTMLInputElement + | HTMLTextAreaElement).value; + if ( + ['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes( + rrElement.attributes.type as string, + ) && + value + ) { + rrElement.attributes.value = value; + } else if ((elementNode as HTMLInputElement).checked) { + rrElement.attributes.checked = (elementNode as HTMLInputElement).checked; + } + } + if (tagName === 'OPTION') { + const selectValue = (elementNode as HTMLOptionElement) + .parentElement; + if ( + rrElement.attributes.value === + (selectValue as HTMLSelectElement).value + ) { + rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected; + } + } + // canvas image data + if (tagName === 'CANVAS') { + rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); + } + // media elements + if (tagName === 'AUDIO' || tagName === 'VIDEO') { + const rrMediaElement = rrElement as RRMediaElement; + rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; + rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; + } + // scroll + if (elementNode.scrollLeft) { + rrElement.scrollLeft = elementNode.scrollLeft; + } + if (elementNode.scrollTop) { + rrElement.scrollTop = elementNode.scrollTop; + } + break; + case node.TEXT_NODE: + rrNode = this.createTextNode( + ((node as unknown) as Text).textContent, + ); + break; + case node.CDATA_SECTION_NODE: + rrNode = this.createCDATASection(); + break; + case node.COMMENT_NODE: + rrNode = this.createComment( + ((node as unknown) as Comment).textContent || '', + ); + break; + default: + return; + } + rrNode.__sn = serializedNodeWithId; + this.mirror.set(serializedNodeWithId.id, rrNode); + } else { + rrNode = this.mirror.get(serializedNodeWithId.id); + rrNode.parentElement = null; + rrNode.parentNode = null; + rrNode.children = []; + } + const parentNode = node.parentElement || node.parentNode; + if (parentNode) { + const parentSN = ((parentNode as unknown) as INode).__sn; + const parentRRNode = this.mirror.get(parentSN.id); + parentRRNode.appendChild(rrNode); + rrNode.parentNode = parentRRNode; + rrNode.parentElement = + parentRRNode instanceof RRElement ? parentRRNode : null; + } + + if ( + serializedNodeWithId.type === NodeType.Document || + serializedNodeWithId.type === NodeType.Element + ) { + node.childNodes.forEach((node) => walk((node as unknown) as INode)); + } + }.bind(this); + + if (dom) { + this.destroyTree(); + walk((dom as unknown) as INode); + } + } + + destroyTree() { + this.children = []; + this.mirror.clear(); + } + + toString() { + return super.toString('RRDocument'); + } +} + +export class RRDocumentType extends RRNode { + readonly name: string; + readonly publicId: string; + readonly systemId: string; + + constructor(qualifiedName: string, publicId: string, systemId: string) { + super(); + this.name = qualifiedName; + this.publicId = publicId; + this.systemId = systemId; + } + + toString() { + return super.toString('RRDocumentType'); + } +} + +export class RRElement extends RRNode { + tagName: string; + attributes: Record = {}; + scrollLeft: number = 0; + scrollTop: number = 0; + shadowRoot: RRElement | null = null; + + constructor(tagName: string) { + super(); + this.tagName = tagName; + } + + get classList() { + return new ClassList( + this.attributes.class as string | undefined, + (newClassName) => { + this.attributes.class = newClassName; + }, + ); + } + + get id() { + return this.attributes.id; + } + + get className() { + return this.attributes.class || ''; + } + + get textContent() { + return ''; + } + + set textContent(newText: string) {} + + get style() { + const style = (this.attributes.style + ? parseCSSText(this.attributes.style as string) + : {}) as Record & { + setProperty: ( + name: string, + value: string | null, + priority?: string | null, + ) => void; + }; + style.setProperty = (name: string, value: string | null) => { + const normalizedName = camelize(name); + if (!value) delete style[normalizedName]; + else style[normalizedName] = value; + this.attributes.style = toCSSText(style); + }; + // This is used to bypass the smoothscroll polyfill in rrweb player. + style.scrollBehavior = ''; + return style; + } + + getAttribute(name: string) { + let upperName = name && name.toLowerCase(); + if (upperName in this.attributes) return this.attributes[upperName]; + return null; + } + + setAttribute(name: string, attribute: string) { + this.attributes[name.toLowerCase()] = attribute; + } + + hasAttribute(name: string) { + return (name && name.toLowerCase()) in this.attributes; + } + + setAttributeNS( + _namespace: string | null, + qualifiedName: string, + value: string, + ): void { + this.setAttribute(qualifiedName, value); + } + + removeAttribute(name: string) { + delete this.attributes[name]; + } + + appendChild(newChild: RRNode): RRNode { + this.children.push(newChild); + newChild.parentNode = this; + newChild.parentElement = this; + newChild.ownerDocument = this.ownerDocument; + return newChild; + } + + insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { + if (refChild === null) return this.appendChild(newChild); + const childIndex = this.children.indexOf(refChild); + if (childIndex == -1) + throw new Error( + "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", + ); + this.children.splice(childIndex, 0, newChild); + newChild.parentElement = null; + newChild.parentNode = this; + newChild.ownerDocument = this.ownerDocument; + return newChild; + } + + dispatchEvent(_event: Event) { + return true; + } + + /** + * Creates a shadow root for element and returns it. + */ + attachShadow(init: ShadowRootInit): RRElement { + this.shadowRoot = init.mode === 'open' ? this : null; + return this; + } + + toString() { + let attributeString = ''; + for (let attribute in this.attributes) { + attributeString += `${attribute}="${this.attributes[attribute]}" `; + } + return `${super.toString(this.tagName)} ${attributeString}`; + } +} + +export class RRImageElement extends RRElement { + src: string; + width: number; + height: number; + onload: ((this: GlobalEventHandlers, ev: Event) => any) | null; +} + +export class RRMediaElement extends RRElement { + currentTime: number = 0; + paused: boolean = true; + async play() { + this.paused = false; + } + async pause() { + this.paused = true; + } +} + +export class RRCanvasElement extends RRElement { + /** + * This is just a dummy implementation to prevent rrweb replayer from drawing mouse tail. If further analysis of canvas is needed, we may implement it with node-canvas. + */ + getContext(): CanvasRenderingContext2D | null { + return null; + } +} + +export class RRStyleElement extends RRElement { + // TODO record change of rules like virtual style element. +} + +export class RRIframeElement extends RRElement { + width: string = ''; + height: string = ''; + src: string = ''; + contentDocument: RRDocument = new RRDocument(); + contentWindow: RRWindow = new RRWindow(); + + constructor(tagName: string) { + super(tagName); + const htmlElement = this.contentDocument.createElement('HTML'); + this.contentDocument.appendChild(htmlElement); + htmlElement.appendChild(this.contentDocument.createElement('HEAD')); + htmlElement.appendChild(this.contentDocument.createElement('BODY')); + } +} + +export class RRText extends RRNode { + textContent: string; + + constructor(data: string) { + super(); + this.textContent = data; + } + + toString() { + return `${super.toString('RRText')} text=${JSON.stringify( + this.textContent, + )}`; + } +} + +export class RRComment extends RRNode { + data: string; + + constructor(data: string) { + super(); + this.data = data; + } + + toString() { + return `${super.toString('RRComment')} data=${JSON.stringify(this.data)}`; + } +} +export class RRCDATASection extends RRNode { + data: string; + + constructor(data: string) { + super(); + this.data = data; + } + + toString() { + return `${super.toString('RRCDATASection')} data=${JSON.stringify( + this.data, + )}`; + } +} + +interface RRElementTagNameMap { + img: RRImageElement; + audio: RRMediaElement; + video: RRMediaElement; +} + +type RRElementType< + K extends keyof HTMLElementTagNameMap +> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; + +class ClassList extends Array { + private onChange: ((newClassText: string) => void) | undefined; + + constructor( + classText?: string, + onChange?: ((newClassText: string) => void) | undefined, + ) { + super(); + if (classText) { + const classes = classText.trim().split(/\s+/); + super.push(...classes); + } + this.onChange = onChange; + } + + add = (...classNames: string[]) => { + for (const item of classNames) { + const className = String(item); + if (super.indexOf(className) >= 0) continue; + super.push(className); + } + this.onChange && this.onChange(super.join(' ')); + }; + + remove = (...classNames: string[]) => { + for (const item of classNames) { + const className = String(item); + const index = super.indexOf(className); + if (index < 0) continue; + super.splice(index, 1); + } + this.onChange && this.onChange(super.join(' ')); + }; +} diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 353bec34d7..fd9368a52b 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -1,25 +1,71 @@ /** * @jest-environment jsdom */ -import { RRDocument } from '../src/document-browser'; +import { RRDocument, RRElement, RRNode } from '../src/document-browser'; import { diff } from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; +const elementSn = { + type: NodeType.Element, + tagName: 'DIV', + attributes: {}, + childNodes: [], + id: 1, +} as serializedNodeWithId; + +type ElementType = { + tagName: keyof HTMLElementTagNameMap; + id: number; + children?: ElementType[]; +}; + +/** + * Create a document tree or a RRDom tree according to the given ElementType data. + * + * @param treeNode the given data structure + * @param rrDocument determine to generate a RRDom tree. + */ +function createTree( + treeNode: ElementType, + rrDocument?: RRDocument, +): INode | RRNode { + let root: INode | RRNode; + root = rrDocument + ? rrDocument.createElement(treeNode.tagName) + : ((document.createElement(treeNode.tagName) as unknown) as INode); + root.__sn = Object.assign({}, elementSn, { + tagName: treeNode.tagName, + id: treeNode.id, + }); + if (treeNode.children) + for (let child of treeNode.children) { + const childNode = createTree(child, rrDocument); + if (rrDocument) (root as RRElement).appendChild(childNode as RRNode); + else (root as INode).appendChild(childNode as Node); + } + return root; +} + +function shuffle(list: number[]) { + let currentIndex = list.length - 1; + while (currentIndex > 0) { + const randomIndex = Math.floor(Math.random() * (currentIndex - 1)); + const temp = list[randomIndex]; + list[randomIndex] = list[currentIndex]; + list[currentIndex] = temp; + currentIndex--; + } + return list; +} + describe('diff algorithm for rrdom', () => { it('diff a node', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; - let sn = { - type: NodeType.Element, - tagName: tagName, - attributes: {}, - childNodes: [], - id: 1, - } as serializedNodeWithId; - node.__sn = sn; + node.__sn = Object.assign({}, elementSn, { tagName }); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = sn; + rrNode.__sn = Object.assign({}, elementSn, { tagName }); diff(node, rrNode); expect(node).toBeInstanceOf(HTMLElement); expect(((node as unknown) as HTMLElement).tagName).toBe(tagName); @@ -28,17 +74,10 @@ describe('diff algorithm for rrdom', () => { it('diff properties (add new properties)', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; - let sn = { - type: NodeType.Element, - tagName: tagName, - attributes: {}, - childNodes: [], - id: 1, - } as serializedNodeWithId; - node.__sn = sn; + node.__sn = Object.assign({}, elementSn, { tagName }); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = sn; + rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node' }; diff(node, rrNode); expect(((node as unknown) as HTMLElement).id).toBe('node1'); @@ -48,20 +87,13 @@ describe('diff algorithm for rrdom', () => { it('diff properties (update exist properties)', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; - let sn = { - type: NodeType.Element, - tagName: tagName, - attributes: {}, - childNodes: [], - id: 1, - } as serializedNodeWithId; - node.__sn = sn; + node.__sn = Object.assign({}, elementSn, { tagName }); ((node as unknown) as HTMLElement).id = 'element1'; ((node as unknown) as HTMLElement).className = 'element'; ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = sn; + rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; diff(node, rrNode); expect(((node as unknown) as HTMLElement).id).toBe('node1'); @@ -82,20 +114,13 @@ describe('diff algorithm for rrdom', () => { it('diff properties (delete old properties)', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; - let sn = { - type: NodeType.Element, - tagName: tagName, - attributes: {}, - childNodes: [], - id: 1, - } as serializedNodeWithId; - node.__sn = sn; + node.__sn = Object.assign({}, elementSn, { tagName }); ((node as unknown) as HTMLElement).id = 'element1'; ((node as unknown) as HTMLElement).className = 'element'; ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = sn; + rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1' }; diff(node, rrNode); expect(((node as unknown) as HTMLElement).id).toBe('node1'); @@ -109,4 +134,486 @@ describe('diff algorithm for rrdom', () => { expect(((node as unknown) as HTMLElement).id).toBe('undefined'); expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe('link'); }); + + it('diff children (append elements)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(1); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3]); + }); + + it('diff children (prepends elements)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(2); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + }); + + it('diff children (add elements in the middle)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + }); + + it('diff children (add elements at begin and end)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + }); + + it('diff children (add children to parent with no children)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + }) as INode; + expect(node.childNodes.length).toEqual(0); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3]); + }); + + it('diff children (remove all children from parent)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(0); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(0); + expect(rrNode.childNodes.length).toEqual(0); + }); + + it('diff children (remove elements from the beginning)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([3, 4, 5]); + }); + + it('diff children (remove elements from end)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3]); + }); + + it('diff children (remove elements from the middle)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 4, 5]); + }); + + it('diff children (moves element forward)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [2, 3, 4, 1, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([2, 3, 4, 1, 5]); + }); + + it('diff children (move elements to end)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 2, 3]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [2, 3, 1].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([2, 3, 1]); + }); + + it('diff children (move element backwards)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 4, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([1, 4, 2, 3]); + }); + + it('diff children (swap first and last)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 2, 3, 1].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([4, 2, 3, 1]); + }); + + it('diff children (move to left and replace)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 1, 2, 3, 6].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([4, 1, 2, 3, 6]); + }); + + it('diff children (move to left and leaves hold)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 6].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(2); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(2); + expect(rrNode.childNodes.length).toEqual(2); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([4, 6]); + }); + + it('diff children (reverse elements)', () => { + const node = createTree({ + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5, 6, 7, 8].map((c) => ({ + tagName: 'span', + id: c, + })), + }) as INode; + expect(node.childNodes.length).toEqual(8); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [8, 7, 6, 5, 4, 3, 2, 1].map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(8); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(8); + expect(rrNode.childNodes.length).toEqual(8); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual([8, 7, 6, 5, 4, 3, 2, 1]); + }); + + it('diff children (handle random shuffle 1)', () => { + /* Number of elements remains the same and no element will be added or removed. */ + let oldElementsNum = 15, + newElementsNum = 15; + let oldElementsIds = [], + newElementsIds = []; + for (let i = 1; i <= oldElementsNum; i++) { + oldElementsIds.push(i); + newElementsIds.push(i); + } + shuffle(oldElementsIds); + shuffle(newElementsIds); + const node = createTree({ + tagName: 'p', + id: 0, + children: oldElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }) as INode; + expect(node.childNodes.length).toEqual(oldElementsNum); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: newElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(newElementsNum); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(newElementsNum); + expect(rrNode.childNodes.length).toEqual(newElementsNum); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual(newElementsIds); + }); + + it('diff children (handle random shuffle 2)', () => { + /* May need to add or remove some elements. */ + let oldElementsNum = 20, + newElementsNum = 30; + let oldElementsIds = [], + newElementsIds = []; + for (let i = 1; i <= oldElementsNum + 10; i++) oldElementsIds.push(i); + for (let i = 1; i <= newElementsNum + 10; i++) newElementsIds.push(i); + shuffle(oldElementsIds); + shuffle(newElementsIds); + oldElementsIds = oldElementsIds.slice(0, oldElementsNum); + newElementsIds = newElementsIds.slice(0, newElementsNum); + const node = createTree({ + tagName: 'p', + id: 0, + children: oldElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }) as INode; + expect(node.childNodes.length).toEqual(oldElementsNum); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: newElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(newElementsNum); + diff(node, rrNode); + expect(node.childNodes.length).toEqual(newElementsNum); + expect(rrNode.childNodes.length).toEqual(newElementsNum); + expect( + Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), + ).toEqual(newElementsIds); + }); }); From 4b1421f0c5253f77ea4ee31187ed182f8dbbe176 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 7 Jan 2022 01:38:29 +1100 Subject: [PATCH 03/79] finish basic functions of diff algorithm --- packages/rrdom/src/diff.ts | 95 +++++++++++++++++++---------- packages/rrdom/test/diff.test.ts | 100 ++++++++++++++++++++++--------- 2 files changed, 135 insertions(+), 60 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 9dd808ba6e..c91df42982 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,32 +1,32 @@ import { INode, NodeType } from 'rrweb-snapshot'; -import { RRElement, RRNode } from './document-browser'; +import type { Mirror } from 'rrweb/typings/types'; +import { + RRCDATASection, + RRComment, + RRElement, + RRNode, + RRText, +} from './document-browser'; -export function diff(oldTree: INode, newTree: RRNode) { - if (oldTree && newTree) { - if (oldTree.__sn?.id === newTree.__sn?.id) { - switch (newTree.nodeType) { - case NodeType.Element: - diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); - break; - } - const oldChildren = oldTree.childNodes; - const newChildren = newTree.childNodes; - if (oldChildren.length > 0 || newChildren.length > 0) { - diffChildren( - (Array.from(oldChildren) as unknown) as INode[], - newChildren, - oldTree, - ); - } - } else if (oldTree.__sn?.id !== newTree.__sn?.id) { - // Replace the old node with the new node - } else if (oldTree.__sn?.id) { - } else if (newTree.__sn?.id) { +export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { + if (oldTree.__sn?.id === newTree.__sn?.id) { + switch (newTree.nodeType) { + case NodeType.Element: + diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); + break; + // TODO: Diff other kinds of nodes. + default: + } + const oldChildren = oldTree.childNodes; + const newChildren = newTree.childNodes; + if (oldChildren.length > 0 || newChildren.length > 0) { + diffChildren( + (Array.from(oldChildren) as unknown) as INode[], + newChildren, + oldTree, + mirror, + ); } - } else if (oldTree) { - if (oldTree.parentNode) oldTree.parentNode.removeChild(oldTree); - } else if (newTree) { - // TODO Create a new node. } } @@ -54,6 +54,7 @@ function diffChildren( oldChildren: (INode | undefined)[], newChildren: RRNode[], parentNode: INode, + mirror: Mirror, ) { let oldStartIndex = 0, oldEndIndex = oldChildren.length - 1, @@ -71,21 +72,21 @@ function diffChildren( } else if (oldEndNode === undefined) { oldEndNode = oldChildren[--oldEndIndex]; } else if (oldStartNode.__sn.id === newStartNode.__sn.id) { - diff(oldStartNode, newStartNode); + diff(oldStartNode, newStartNode, mirror); oldStartNode = oldChildren[++oldStartIndex]; newStartNode = newChildren[++newStartIndex]; } else if (oldEndNode.__sn.id === newEndNode.__sn.id) { - diff(oldEndNode, newEndNode); + diff(oldEndNode, newEndNode, mirror); oldEndNode = oldChildren[--oldEndIndex]; newEndNode = newChildren[--newEndIndex]; } else if (oldStartNode.__sn.id === newEndNode.__sn.id) { parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); - diff(oldStartNode, newEndNode); + diff(oldStartNode, newEndNode, mirror); oldStartNode = oldChildren[++oldStartIndex]; newEndNode = newChildren[--newEndIndex]; } else if (oldEndNode.__sn.id === newStartNode.__sn.id) { parentNode.insertBefore(oldEndNode, oldStartNode); - diff(oldEndNode, newStartNode); + diff(oldEndNode, newStartNode, mirror); oldEndNode = oldChildren[--oldEndIndex]; newStartNode = newChildren[++newStartIndex]; } else { @@ -98,16 +99,28 @@ function diffChildren( if (indexInOld) { const nodeToMove = oldChildren[indexInOld]!; parentNode.insertBefore(nodeToMove, oldStartNode); - diff(nodeToMove, newStartNode); + diff(nodeToMove, newStartNode, mirror); oldChildren[indexInOld] = undefined; } else { - // TODO Create a new node. + const newNode = createOrGetNode(newStartNode, mirror); + parentNode.insertBefore(newNode, oldStartNode); } newStartNode = newChildren[++newStartIndex]; } } if (oldStartIndex > oldEndIndex) { - // TODO Create several new nodes. + const referenceRRNode = newChildren[newEndIndex + 1]; + let referenceNode = null; + if (referenceRRNode) + parentNode.childNodes.forEach((child) => { + if (((child as unknown) as INode).__sn.id === referenceRRNode.__sn.id) + referenceNode = child; + }); + for (; newStartIndex <= newEndIndex; ++newStartIndex) + parentNode.insertBefore( + createOrGetNode(newChildren[newStartIndex], mirror), + referenceNode, + ); } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { const node = oldChildren[oldStartIndex]; @@ -115,3 +128,19 @@ function diffChildren( } } } + +export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { + let node = mirror.getNode(rrNode.__sn.id); + if (node !== null) return node; + if (rrNode instanceof RRElement) { + node = (document.createElement(rrNode.tagName) as unknown) as INode; + } else if (rrNode instanceof RRText) { + node = (document.createTextNode(rrNode.textContent) as unknown) as INode; + } else if (rrNode instanceof RRComment) { + node = (document.createComment(rrNode.data) as unknown) as INode; + } else if (rrNode instanceof RRCDATASection) { + node = (document.createCDATASection(rrNode.data) as unknown) as INode; + } else throw new Error('Unknown rrNode type ' + rrNode.toString()); + node.__sn = { ...rrNode.__sn }; + return node; +} diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index fd9368a52b..7961b12ffa 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ import { RRDocument, RRElement, RRNode } from '../src/document-browser'; -import { diff } from '../src/diff'; +import { createOrGetNode, diff } from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; const elementSn = { @@ -21,9 +21,9 @@ type ElementType = { /** * Create a document tree or a RRDom tree according to the given ElementType data. - * + * * @param treeNode the given data structure - * @param rrDocument determine to generate a RRDom tree. + * @param rrDocument determine to generate a RRDom tree. */ function createTree( treeNode: ElementType, @@ -59,6 +59,24 @@ function shuffle(list: number[]) { } describe('diff algorithm for rrdom', () => { + // An adhoc mirror just for unit tests. + const mirror = { + map: {}, + getId() { + return 0; + }, + getNode() { + return null; + }, + removeNodeFromMap() {}, + has(id: number) { + return this.map.hasOwnProperty(id); + }, + reset() { + this.map = {}; + }, + }; + it('diff a node', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; @@ -66,7 +84,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node).toBeInstanceOf(HTMLElement); expect(((node as unknown) as HTMLElement).tagName).toBe(tagName); }); @@ -79,7 +97,7 @@ describe('diff algorithm for rrdom', () => { const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node' }; - diff(node, rrNode); + diff(node, rrNode, mirror); expect(((node as unknown) as HTMLElement).id).toBe('node1'); expect(((node as unknown) as HTMLElement).className).toBe('node'); }); @@ -95,7 +113,7 @@ describe('diff algorithm for rrdom', () => { const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; - diff(node, rrNode); + diff(node, rrNode, mirror); expect(((node as unknown) as HTMLElement).id).toBe('node1'); expect(((node as unknown) as HTMLElement).className).toBe('node'); expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( @@ -103,7 +121,7 @@ describe('diff algorithm for rrdom', () => { ); rrNode.attributes = { id: 'node2' }; - diff(node, rrNode); + diff(node, rrNode, mirror); expect(((node as unknown) as HTMLElement).id).toBe('node2'); expect(((node as unknown) as HTMLElement).className).toBe('undefined'); expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( @@ -122,7 +140,7 @@ describe('diff algorithm for rrdom', () => { const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1' }; - diff(node, rrNode); + diff(node, rrNode, mirror); expect(((node as unknown) as HTMLElement).id).toBe('node1'); expect(((node as unknown) as HTMLElement).className).toBe('undefined'); expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( @@ -130,7 +148,7 @@ describe('diff algorithm for rrdom', () => { ); rrNode.attributes = { src: 'link' }; - diff(node, rrNode); + diff(node, rrNode, mirror); expect(((node as unknown) as HTMLElement).id).toBe('undefined'); expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe('link'); }); @@ -151,7 +169,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -175,7 +193,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -199,7 +217,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -223,7 +241,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -246,7 +264,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -269,7 +287,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(0); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(0); expect(rrNode.childNodes.length).toEqual(0); }); @@ -293,7 +311,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -320,7 +338,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -347,7 +365,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -374,7 +392,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -401,7 +419,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -425,7 +443,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -449,7 +467,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -473,7 +491,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -497,7 +515,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(2); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(2); expect(rrNode.childNodes.length).toEqual(2); expect( @@ -527,7 +545,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(8); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(8); expect(rrNode.childNodes.length).toEqual(8); expect( @@ -568,7 +586,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(newElementsNum); expect(rrNode.childNodes.length).toEqual(newElementsNum); expect( @@ -609,11 +627,39 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode); + diff(node, rrNode, mirror); expect(node.childNodes.length).toEqual(newElementsNum); expect(rrNode.childNodes.length).toEqual(newElementsNum); expect( Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), ).toEqual(newElementsIds); }); + + it('create a real HTML element from RRElement', () => { + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement('DIV'); + rrNode.__sn = Object.assign({}, elementSn, { id: 0 }); + let result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(HTMLElement); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as HTMLElement).tagName).toBe('DIV'); + }); + + it('create a node from RRNode', () => { + const rrDocument = new RRDocument(); + const textContent = 'Text Content'; + let rrNode: RRNode = rrDocument.createTextNode(textContent); + rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; + let result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(Text); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as Text).textContent).toBe(textContent); + + rrNode = rrDocument.createComment(textContent); + rrNode.__sn = { id: 0, type: NodeType.Comment, textContent }; + result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(Comment); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as Comment).textContent).toBe(textContent); + }); }); From d0dd136aff85c1b9a554319f2ade4f5c9214b6d5 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 13 Jan 2022 16:56:28 +1100 Subject: [PATCH 04/79] fix several bugs in the diff algorithm --- packages/rrdom/src/diff.ts | 68 +++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index c91df42982..b981c4d55a 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,4 +1,4 @@ -import { INode, NodeType } from 'rrweb-snapshot'; +import { elementNode, INode, NodeType } from 'rrweb-snapshot'; import type { Mirror } from 'rrweb/typings/types'; import { RRCDATASection, @@ -9,24 +9,22 @@ import { } from './document-browser'; export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { - if (oldTree.__sn?.id === newTree.__sn?.id) { - switch (newTree.nodeType) { - case NodeType.Element: - diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); - break; - // TODO: Diff other kinds of nodes. - default: - } - const oldChildren = oldTree.childNodes; - const newChildren = newTree.childNodes; - if (oldChildren.length > 0 || newChildren.length > 0) { - diffChildren( - (Array.from(oldChildren) as unknown) as INode[], - newChildren, - oldTree, - mirror, - ); - } + switch (newTree.nodeType) { + case NodeType.Element: + diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); + break; + // TODO: Diff other kinds of nodes. + default: + } + const oldChildren = oldTree.childNodes; + const newChildren = newTree.childNodes; + if (oldChildren.length > 0 || newChildren.length > 0) { + diffChildren( + (Array.from(oldChildren) as unknown) as INode[], + newChildren, + oldTree, + mirror, + ); } } @@ -46,7 +44,15 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { if (oldAttributes.hasOwnProperty(attribute)) continue; if (typeof newValue === 'boolean' || typeof newValue === 'number') { // TODO Some special cases for some kinds of elements. e.g. checked, rr_scrollLeft - } else oldTree.setAttribute(attribute, newValue); + } else { + if ((newTree.__sn as elementNode).isSVG && attribute === 'xlink:href') + oldTree.setAttributeNS( + 'http://www.w3.org/1999/xlink', + attribute, + newValue, + ); + else oldTree.setAttribute(attribute, newValue); + } } } @@ -71,20 +77,20 @@ function diffChildren( oldStartNode = oldChildren[++oldStartIndex]; } else if (oldEndNode === undefined) { oldEndNode = oldChildren[--oldEndIndex]; - } else if (oldStartNode.__sn.id === newStartNode.__sn.id) { + } else if (oldStartNode.__sn?.id === newStartNode.__sn.id) { diff(oldStartNode, newStartNode, mirror); oldStartNode = oldChildren[++oldStartIndex]; newStartNode = newChildren[++newStartIndex]; - } else if (oldEndNode.__sn.id === newEndNode.__sn.id) { + } else if (oldEndNode.__sn?.id === newEndNode.__sn.id) { diff(oldEndNode, newEndNode, mirror); oldEndNode = oldChildren[--oldEndIndex]; newEndNode = newChildren[--newEndIndex]; - } else if (oldStartNode.__sn.id === newEndNode.__sn.id) { + } else if (oldStartNode.__sn?.id === newEndNode.__sn.id) { parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); diff(oldStartNode, newEndNode, mirror); oldStartNode = oldChildren[++oldStartIndex]; newEndNode = newChildren[--newEndIndex]; - } else if (oldEndNode.__sn.id === newStartNode.__sn.id) { + } else if (oldEndNode.__sn?.id === newStartNode.__sn.id) { parentNode.insertBefore(oldEndNode, oldStartNode); diff(oldEndNode, newStartNode, mirror); oldEndNode = oldChildren[--oldEndIndex]; @@ -92,8 +98,10 @@ function diffChildren( } else { if (!oldIdToIndex) { oldIdToIndex = {}; - for (let i = oldStartIndex; i <= oldEndIndex; i++) - oldIdToIndex[oldChildren[i]!.__sn.id] = i; + for (let i = oldStartIndex; i <= oldEndIndex; i++) { + const oldChild = oldChildren[i]; + if (oldChild?.__sn) oldIdToIndex[oldChild.__sn.id] = i; + } } indexInOld = oldIdToIndex[newStartNode.__sn.id]; if (indexInOld) { @@ -133,7 +141,12 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { let node = mirror.getNode(rrNode.__sn.id); if (node !== null) return node; if (rrNode instanceof RRElement) { - node = (document.createElement(rrNode.tagName) as unknown) as INode; + if ((rrNode.__sn as elementNode).isSVG) + node = (document.createElementNS( + 'http://www.w3.org/2000/svg', + rrNode.tagName.toLowerCase(), + ) as unknown) as INode; + else node = (document.createElement(rrNode.tagName) as unknown) as INode; } else if (rrNode instanceof RRText) { node = (document.createTextNode(rrNode.textContent) as unknown) as INode; } else if (rrNode instanceof RRComment) { @@ -142,5 +155,6 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { node = (document.createCDATASection(rrNode.data) as unknown) as INode; } else throw new Error('Unknown rrNode type ' + rrNode.toString()); node.__sn = { ...rrNode.__sn }; + diff(node, rrNode, mirror); return node; } From 110b0bad0cec09b9d4f61f0e19785383eb69c378 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 15 Jan 2022 21:10:35 +1100 Subject: [PATCH 05/79] replace the virtual parent optimization in applyMutation() --- packages/rrdom/src/diff.ts | 6 +- packages/rrdom/src/document-browser.ts | 91 +++++++++-- packages/rrweb/package.json | 1 + packages/rrweb/src/replay/index.ts | 207 +++++++++++-------------- packages/rrweb/src/utils.ts | 3 +- 5 files changed, 173 insertions(+), 135 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index b981c4d55a..da6bb50a58 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -132,7 +132,10 @@ function diffChildren( } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { const node = oldChildren[oldStartIndex]; - node && parentNode.removeChild(node); + if (node) { + parentNode.removeChild(node); + mirror.removeNodeFromMap(node); + } } } } @@ -155,6 +158,7 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { node = (document.createCDATASection(rrNode.data) as unknown) as INode; } else throw new Error('Unknown rrNode type ' + rrNode.toString()); node.__sn = { ...rrNode.__sn }; + mirror.map[rrNode.__sn.id] = node; diff(node, rrNode, mirror); return node; } diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 99cccc1b8a..eeda1eab88 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -23,6 +23,26 @@ export abstract class RRNode { return this.children; } + get firstChild(): RRNode | null { + return this.childNodes[0] ?? null; + } + + get nextSibling(): RRNode | null { + let parentNode = this.parentNode; + if (!parentNode) return null; + const siblings = parentNode.children; + let index = siblings.indexOf(this); + return siblings[index + 1]; + } + + set textContent(textContent: string) { + if (this instanceof RRText) this.textContent = textContent; + else if (this instanceof RRElement) { + if (this.childNodes[0] instanceof RRText) + this.childNodes[0].textContent = textContent; + } + } + contains(node: RRNode) { if (node === this) return true; for (const child of this.children) { @@ -31,6 +51,18 @@ export abstract class RRNode { return false; } + appendChild(newChild: RRNode): RRNode { + throw new Error( + `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, + ); + } + + insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { + throw new Error( + `RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`, + ); + } + removeChild(node: RRNode) { const indexOfChild = this.children.indexOf(node); if (indexOfChild !== -1) { @@ -56,7 +88,30 @@ export class RRWindow { } export class RRDocument extends RRNode { - private mirror: Map = new Map(); + public mirror = { + map: {}, + getId(n: RRNode) { + return n.__sn.id >= 0 ? n.__sn.id : -1; + }, + getNode(id: number): RRNode | null { + return this.map[id] || null; + }, + removeNodeFromMap(n: RRNode) { + const id = n.__sn.id; + delete this.map[id]; + if (n.childNodes) { + n.childNodes.forEach((child) => + this.removeNodeFromMap(child as RRNode), + ); + } + }, + has(id: number) { + return this.map.hasOwnProperty(id); + }, + reset() { + this.map = {}; + }, + }; get documentElement(): RRElement { return this.children.find( @@ -330,9 +385,9 @@ export class RRDocument extends RRNode { return; } rrNode.__sn = serializedNodeWithId; - this.mirror.set(serializedNodeWithId.id, rrNode); + this.mirror.map[serializedNodeWithId.id] = rrNode; } else { - rrNode = this.mirror.get(serializedNodeWithId.id); + rrNode = this.mirror.getNode(serializedNodeWithId.id); rrNode.parentElement = null; rrNode.parentNode = null; rrNode.children = []; @@ -340,7 +395,7 @@ export class RRDocument extends RRNode { const parentNode = node.parentElement || node.parentNode; if (parentNode) { const parentSN = ((parentNode as unknown) as INode).__sn; - const parentRRNode = this.mirror.get(parentSN.id); + const parentRRNode = this.mirror.getNode(parentSN.id); parentRRNode.appendChild(rrNode); rrNode.parentNode = parentRRNode; rrNode.parentElement = @@ -363,7 +418,7 @@ export class RRDocument extends RRNode { destroyTree() { this.children = []; - this.mirror.clear(); + this.mirror.reset(); } toString() { @@ -417,12 +472,6 @@ export class RRElement extends RRNode { return this.attributes.class || ''; } - get textContent() { - return ''; - } - - set textContent(newText: string) {} - get style() { const style = (this.attributes.style ? parseCSSText(this.attributes.style as string) @@ -432,6 +481,7 @@ export class RRElement extends RRNode { value: string | null, priority?: string | null, ) => void; + removeProperty: (name: string) => string; }; style.setProperty = (name: string, value: string | null) => { const normalizedName = camelize(name); @@ -439,8 +489,12 @@ export class RRElement extends RRNode { else style[normalizedName] = value; this.attributes.style = toCSSText(style); }; - // This is used to bypass the smoothscroll polyfill in rrweb player. - style.scrollBehavior = ''; + style.removeProperty = (name: string) => { + const normalizedName = camelize(name); + const value = style[normalizedName] ?? ''; + delete style[normalizedName]; + return value; + }; return style; } @@ -561,11 +615,17 @@ export class RRIframeElement extends RRElement { } export class RRText extends RRNode { - textContent: string; + private _textContent: string; + public get textContent(): string { + return this._textContent; + } + public set textContent(value: string) { + this._textContent = value; + } constructor(data: string) { super(); - this.textContent = data; + this._textContent = data; } toString() { @@ -646,3 +706,4 @@ class ClassList extends Array { this.onChange && this.onChange(super.join(' ')); }; } +export { diff } from './diff'; diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 4a4be3615e..204825cd27 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -75,6 +75,7 @@ "@xstate/fsm": "^1.4.0", "fflate": "^0.4.4", "mitt": "^1.1.3", + "rrdom": "^0.0.0", "rrweb-snapshot": "^1.1.12" } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 3b1a9a7e8c..efea5169b2 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -6,6 +6,7 @@ import { BuildCache, createCache, } from 'rrweb-snapshot'; +import { RRDocument, RRNode, RRElement, diff } from 'rrdom/es/document-browser'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -130,6 +131,8 @@ export class Replayer { private mousePos: mouseMovePos | null = null; private touchActive: boolean | null = null; + private usingRRDom = false; + private rrdom: RRDocument = new RRDocument(); constructor( events: Array, @@ -169,24 +172,33 @@ export class Replayer { this.virtualStyleRulesMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { - const { scrollMap, inputMap } = this.treeIndex.flush(); - - this.fragmentParentMap.forEach((parent, frag) => - this.restoreRealParent(frag, parent), - ); - for (const node of this.virtualStyleRulesMap.keys()) { - // restore css rules of style elements after they are mounted - this.restoreNodeSheet(node); - } - this.fragmentParentMap.clear(); - this.elementStateMap.clear(); - this.virtualStyleRulesMap.clear(); - - for (const d of scrollMap.values()) { - this.applyScroll(d); - } - for (const d of inputMap.values()) { - this.applyInput(d); + // const { scrollMap, inputMap } = this.treeIndex.flush(); + + // this.fragmentParentMap.forEach((parent, frag) => + // this.restoreRealParent(frag, parent), + // ); + // for (const node of this.virtualStyleRulesMap.keys()) { + // // restore css rules of style elements after they are mounted + // this.restoreNodeSheet(node); + // } + // this.fragmentParentMap.clear(); + // this.elementStateMap.clear(); + // this.virtualStyleRulesMap.clear(); + + // for (const d of scrollMap.values()) { + // this.applyScroll(d); + // } + // for (const d of inputMap.values()) { + // this.applyInput(d); + // } + if (this.usingRRDom) { + diff( + (this.iframe.contentDocument! as unknown) as INode, + this.rrdom, + this.mirror, + ); + this.rrdom.destroyTree(); + this.usingRRDom = false; } }); this.emitter.on(ReplayerEvents.PlayBack, () => { @@ -1261,8 +1273,13 @@ export class Replayer { } private applyMutation(d: mutationData, useVirtualParent: boolean) { + if (!this.usingRRDom && useVirtualParent) { + this.usingRRDom = true; + this.rrdom.buildFromDom(this.iframe.contentDocument!); + } + const mirror = useVirtualParent ? this.rrdom.mirror : this.mirror; d.removes.forEach((mutation) => { - let target = this.mirror.getNode(mutation.id); + let target = mirror.getNode(mutation.id); if (!target) { if (d.removes.find((r) => r.id === mutation.parentId)) { // no need to warn, parent was already removed @@ -1270,52 +1287,36 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } - if (this.virtualStyleRulesMap.has(target)) { - this.virtualStyleRulesMap.delete(target); - } - let parent: INode | null | ShadowRoot = this.mirror.getNode( + // TODO modify virtual style rules + // if (this.virtualStyleRulesMap.has(target)) { + // this.virtualStyleRulesMap.delete(target); + // } + let parent: INode | null | ShadowRoot | RRNode = mirror.getNode( mutation.parentId, ); if (!parent) { return this.warnNodeNotFound(d, mutation.parentId); } - if (mutation.isShadow && hasShadowRoot(parent)) { - parent = parent.shadowRoot; + if (mutation.isShadow && hasShadowRoot(parent as Node)) { + parent = (parent as Element | RRElement).shadowRoot; } // target may be removed with its parents before - this.mirror.removeNodeFromMap(target); - if (parent) { - let realTarget = null; - const realParent = - '__sn' in parent ? this.fragmentParentMap.get(parent) : undefined; - if (realParent && realParent.contains(target)) { - parent = realParent; - } else if (this.fragmentParentMap.has(target)) { - /** - * the target itself is a fragment document and it's not in the dom - * so we should remove the real target from its parent - */ - realTarget = this.fragmentParentMap.get(target)!; - this.fragmentParentMap.delete(target); - target = realTarget; - } + mirror.removeNodeFromMap(target as INode & RRNode); + if (parent) try { - parent.removeChild(target); + parent.removeChild(target as INode & RRNode); } catch (error) { if (error instanceof DOMException) { this.warn( 'parent could not remove child in mutation', parent, - realParent, target, - realTarget, d, ); } else { throw error; } } - } }); // tslint:disable-next-line: variable-name @@ -1328,7 +1329,7 @@ export class Replayer { const nextNotInDOM = (mutation: addedNodeMutation) => { let next: Node | null = null; if (mutation.nextId) { - next = this.mirror.getNode(mutation.nextId) as Node; + next = mirror.getNode(mutation.nextId) as Node; } // next not present at this moment if ( @@ -1346,7 +1347,7 @@ export class Replayer { if (!this.iframe.contentDocument) { return console.warn('Looks like your replayer has been destroyed.'); } - let parent: INode | null | ShadowRoot = this.mirror.getNode( + let parent: INode | null | ShadowRoot | RRNode = mirror.getNode( mutation.parentId, ); if (!parent) { @@ -1357,63 +1358,30 @@ export class Replayer { return queue.push(mutation); } - let parentInDocument = null; - if (this.iframe.contentDocument.contains) { - parentInDocument = this.iframe.contentDocument.contains(parent); - } else if (this.iframe.contentDocument.body.contains) { - // fix for IE - // refer 'Internet Explorer notes' at https://developer.mozilla.org/zh-CN/docs/Web/API/Document - parentInDocument = this.iframe.contentDocument.body.contains(parent); + if (mutation.node.isShadow && hasShadowRoot(parent as Node)) { + parent = (parent as Element | RRElement).shadowRoot as ShadowRoot; } - const hasIframeChild = - ((parent as unknown) as HTMLElement).getElementsByTagName?.('iframe') - .length > 0; - /** - * Why !isIframeINode(parent)? If parent element is an iframe, iframe document can't be appended to virtual parent. - * Why !hasIframeChild? If we move iframe elements from dom to fragment document, we will lose the contentDocument of iframe. So we need to disable the virtual dom optimization if a parent node contains iframe elements. - */ - if ( - useVirtualParent && - parentInDocument && - !isIframeINode(parent) && - !hasIframeChild - ) { - const virtualParent = (document.createDocumentFragment() as unknown) as INode; - this.mirror.map[mutation.parentId] = virtualParent; - this.fragmentParentMap.set(virtualParent, parent); - - // store the state, like scroll position, of child nodes before they are unmounted from dom - this.storeState(parent); - - while (parent.firstChild) { - virtualParent.appendChild(parent.firstChild); - } - parent = virtualParent; - } - - if (mutation.node.isShadow && hasShadowRoot(parent)) { - parent = parent.shadowRoot; - } - - let previous: Node | null = null; - let next: Node | null = null; + let previous: Node | RRNode | null = null; + let next: Node | RRNode | null = null; if (mutation.previousId) { - previous = this.mirror.getNode(mutation.previousId) as Node; + previous = mirror.getNode(mutation.previousId) as Node | RRNode; } if (mutation.nextId) { - next = this.mirror.getNode(mutation.nextId) as Node; + next = mirror.getNode(mutation.nextId) as Node | RRNode; } if (nextNotInDOM(mutation)) { return queue.push(mutation); } - if (mutation.node.rootId && !this.mirror.getNode(mutation.node.rootId)) { + if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { return; } const targetDoc = mutation.node.rootId - ? this.mirror.getNode(mutation.node.rootId) + ? mirror.getNode(mutation.node.rootId) + : useVirtualParent + ? this.rrdom : this.iframe.contentDocument; if (isIframeINode(parent)) { this.attachDocumentToIframe(mutation, parent); @@ -1421,7 +1389,7 @@ export class Replayer { } const target = buildNodeWithSN(mutation.node, { doc: targetDoc as Document, - map: this.mirror.map, + map: mirror.map, skipChild: true, hackCss: true, cache: this.cache, @@ -1444,32 +1412,38 @@ export class Replayer { ) { // https://github.com/rrweb-io/rrweb/issues/745 // parent is textarea, will only keep one child node as the value - for (const c of Array.from(parent.childNodes)) { + for (const c of Array.from(parent.childNodes as Iterable)) { if (c.nodeType === parent.TEXT_NODE) { - parent.removeChild(c); + parent.removeChild(c as Node & RRNode); } } } if (previous && previous.nextSibling && previous.nextSibling.parentNode) { - parent.insertBefore(target, previous.nextSibling); + parent.insertBefore( + target as INode & RRNode, + previous.nextSibling as INode & RRNode, + ); } else if (next && next.parentNode) { // making sure the parent contains the reference nodes // before we insert target before next. - parent.contains(next) - ? parent.insertBefore(target, next) - : parent.insertBefore(target, null); + parent.contains(next as Node & RRNode) + ? parent.insertBefore( + target as INode & RRNode, + next as INode & RRNode, + ) + : parent.insertBefore(target as INode & RRNode, null); } else { /** * Sometimes the document changes and the MutationObserver is disconnected, so the removal of child elements can't be detected and recorded. After the change of document, we may get another mutation which adds a new html element, while the old html element still exists in the dom, and we need to remove the old html element first to avoid collision. */ if (parent === targetDoc) { while (targetDoc.firstChild) { - targetDoc.removeChild(targetDoc.firstChild); + targetDoc.removeChild(targetDoc.firstChild as Node & RRNode); } } - parent.appendChild(target); + parent.appendChild(target as Node & RRNode); } if (isIframeINode(target)) { @@ -1515,7 +1489,7 @@ export class Replayer { break; } for (const tree of resolveTrees) { - let parent = this.mirror.getNode(tree.value.parentId); + let parent = mirror.getNode(tree.value.parentId); if (!parent) { this.debug( 'Drop resolve tree since there is no parent for the root node.', @@ -1534,7 +1508,7 @@ export class Replayer { } d.texts.forEach((mutation) => { - let target = this.mirror.getNode(mutation.id); + let target = mirror.getNode(mutation.id); if (!target) { if (d.removes.find((r) => r.id === mutation.id)) { // no need to warn, element was already removed @@ -1542,16 +1516,10 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } - /** - * apply text content to real parent directly - */ - if (this.fragmentParentMap.has(target)) { - target = this.fragmentParentMap.get(target)!; - } target.textContent = mutation.value; }); d.attributes.forEach((mutation) => { - let target = this.mirror.getNode(mutation.id); + let target = mirror.getNode(mutation.id); if (!target) { if (d.removes.find((r) => r.id === mutation.id)) { // no need to warn, element was already removed @@ -1559,17 +1527,17 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } - if (this.fragmentParentMap.has(target)) { - target = this.fragmentParentMap.get(target)!; - } for (const attributeName in mutation.attributes) { if (typeof attributeName === 'string') { const value = mutation.attributes[attributeName]; if (value === null) { - ((target as Node) as Element).removeAttribute(attributeName); + (target as Element | RRElement).removeAttribute(attributeName); } else if (typeof value === 'string') { try { - ((target as Node) as Element).setAttribute(attributeName, value); + (target as Element | RRElement).setAttribute( + attributeName, + value, + ); } catch (error) { if (this.config.showWarning) { console.warn( @@ -1580,7 +1548,7 @@ export class Replayer { } } else if (attributeName === 'style') { let styleValues = value as styleAttributeValue; - const targetEl = (target as Node) as HTMLElement; + const targetEl = target as HTMLElement | RRElement; for (var s in styleValues) { if (styleValues[s] === false) { targetEl.style.removeProperty(s); @@ -1644,8 +1612,8 @@ export class Replayer { private legacy_resolveMissingNode( map: missingNodeMap, - parent: Node, - target: Node, + parent: Node | RRNode, + target: Node | RRNode, targetMutation: addedNodeMutation, ) { const { previousId, nextId } = targetMutation; @@ -1653,7 +1621,7 @@ export class Replayer { const nextInMap = nextId && map[nextId]; if (previousInMap) { const { node, mutation } = previousInMap as missingNode; - parent.insertBefore(node, target); + parent.insertBefore(node as Node & RRNode, target as Node & RRNode); delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; if (mutation.previousId || mutation.nextId) { @@ -1662,7 +1630,10 @@ export class Replayer { } if (nextInMap) { const { node, mutation } = nextInMap as missingNode; - parent.insertBefore(node, target.nextSibling); + parent.insertBefore( + node as Node & RRNode, + target.nextSibling as Node & RRNode, + ); delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; if (mutation.previousId || mutation.nextId) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 8ae1635fe6..c26fd9959f 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -22,6 +22,7 @@ import { NodeType, isShadowRoot, } from 'rrweb-snapshot'; +import { RRNode } from 'rrdom/es/document-browser'; export function on( type: string, @@ -581,7 +582,7 @@ export type AppendedIframe = { }; export function isIframeINode( - node: INode | ShadowRoot, + node: INode | ShadowRoot | RRNode, ): node is HTMLIFrameINode { if ('__sn' in node) { return ( From d396cbd2d6b6e339beeecb3cbbd59dfdc3883cee Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 17 Jan 2022 18:37:57 +1100 Subject: [PATCH 06/79] fix: moveAndHover after the diff algorithm is executed --- packages/rrdom/rollup.config.js | 5 +++ packages/rrdom/src/document-browser.ts | 22 +++++++++- packages/rrdom/src/document-nodejs.ts | 2 +- packages/rrweb/src/replay/index.ts | 58 ++++++++++++++------------ 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index 80baed3473..ee8e668c5e 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -27,6 +27,11 @@ const baseConfigs = [ name: 'RRDocument', path: 'document-nodejs', }, + { + input: './src/document-browser.ts', + name: 'RRDocument', + path: 'document-browser', + }, ]; let configs = []; diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index eeda1eab88..269ce368b1 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -32,7 +32,7 @@ export abstract class RRNode { if (!parentNode) return null; const siblings = parentNode.children; let index = siblings.indexOf(this); - return siblings[index + 1]; + return siblings[index + 1] ?? null; } set textContent(textContent: string) { @@ -540,7 +540,7 @@ export class RRElement extends RRNode { "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", ); this.children.splice(childIndex, 0, newChild); - newChild.parentElement = null; + newChild.parentElement = this; newChild.parentNode = this; newChild.ownerDocument = this.ownerDocument; return newChild; @@ -706,4 +706,22 @@ class ClassList extends Array { this.onChange && this.onChange(super.join(' ')); }; } + export { diff } from './diff'; +/** + * Print the RRDom as a string. + * @param rootNode the root node of the RRDom tree + * @returns printed string + */ +export function printRRDom(rootNode: RRNode) { + return walk(rootNode, ''); +} + +function walk(node: RRNode, blankSpace: string) { + let printText = `${blankSpace}${node.toString()}\n`; + for (const child of node.childNodes) + printText += walk(child, blankSpace + ' '); + if (node instanceof RRIframeElement) + printText += walk(node.contentDocument, blankSpace + ' '); + return printText; +} diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index aff8d90512..2689a07f17 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -563,7 +563,7 @@ export class RRElement extends RRNode { "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", ); this.children.splice(childIndex, 0, newChild); - newChild.parentElement = null; + newChild.parentElement = this; newChild.parentNode = this; newChild.ownerDocument = this.ownerDocument; return newChild; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index efea5169b2..eb6d40302f 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -172,7 +172,28 @@ export class Replayer { this.virtualStyleRulesMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { - // const { scrollMap, inputMap } = this.treeIndex.flush(); + if (this.usingRRDom) { + diff( + (this.iframe.contentDocument! as unknown) as INode, + this.rrdom, + this.mirror, + ); + this.rrdom.destroyTree(); + this.usingRRDom = false; + } + + if (this.mousePos) { + this.moveAndHover( + this.mousePos.x, + this.mousePos.y, + this.mousePos.id, + true, + this.mousePos.debugData, + ); + } + this.mousePos = null; + + const { scrollMap, inputMap } = this.treeIndex.flush(); // this.fragmentParentMap.forEach((parent, frag) => // this.restoreRealParent(frag, parent), @@ -185,20 +206,11 @@ export class Replayer { // this.elementStateMap.clear(); // this.virtualStyleRulesMap.clear(); - // for (const d of scrollMap.values()) { - // this.applyScroll(d); - // } - // for (const d of inputMap.values()) { - // this.applyInput(d); - // } - if (this.usingRRDom) { - diff( - (this.iframe.contentDocument! as unknown) as INode, - this.rrdom, - this.mirror, - ); - this.rrdom.destroyTree(); - this.usingRRDom = false; + for (const d of scrollMap.values()) { + this.applyScroll(d); + } + for (const d of inputMap.values()) { + this.applyInput(d); } }); this.emitter.on(ReplayerEvents.PlayBack, () => { @@ -502,16 +514,6 @@ export class Replayer { const castFn = this.getCastFn(event, true); castFn(); } - if (this.mousePos) { - this.moveAndHover( - this.mousePos.x, - this.mousePos.y, - this.mousePos.id, - true, - this.mousePos.debugData, - ); - } - this.mousePos = null; if (this.touchActive === true) { this.mouse.classList.add('touch-active'); } else if (this.touchActive === false) { @@ -906,7 +908,7 @@ export class Replayer { /** * Same as the situation of missing input target. */ - if (d.id === -1) { + if (d.id === -1 || isSync) { break; } const event = new Event(MouseInteractions[d.type].toLowerCase()); @@ -1024,6 +1026,7 @@ export class Replayer { break; } case IncrementalSource.MediaInteraction: { + // TODO what if the media element doesn't exist const target = this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); @@ -1059,6 +1062,7 @@ export class Replayer { break; } case IncrementalSource.StyleSheetRule: { + // TODO adopt rrdom here const target = this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); @@ -1160,6 +1164,7 @@ export class Replayer { break; } case IncrementalSource.StyleDeclaration: { + // TODO adopt rrdom here // same with StyleSheetRule const target = this.mirror.getNode(d.id); if (!target) { @@ -1216,6 +1221,7 @@ export class Replayer { break; } case IncrementalSource.CanvasMutation: { + // TODO adopt rrdom here if (!this.config.UNSAFE_replayCanvas) { return; } From 3c1d37d2fe7377542c9593a3aaba78808d41776d Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 18 Jan 2022 19:56:11 +1100 Subject: [PATCH 07/79] replace virtual style map with rrdom cssom version has to be above 0.5.0 to pass virtual style tests --- package.json | 3 + packages/rrdom/src/diff.ts | 97 +- packages/rrdom/src/document-browser.ts | 35 +- packages/rrdom/test/diff.test.ts | 1191 ++++++++++------- packages/rrweb/package.json | 3 - packages/rrweb/src/replay/index.ts | 326 ++--- packages/rrweb/src/replay/virtual-styles.ts | 188 --- packages/rrweb/src/types.ts | 6 - packages/rrweb/src/utils.ts | 22 + .../rrweb/test/replay/virtual-styles.test.ts | 165 --- packages/rrweb/typings/replay/index.d.ts | 7 +- .../rrweb/typings/replay/virtual-styles.d.ts | 43 - packages/rrweb/typings/types.d.ts | 3 - packages/rrweb/typings/utils.d.ts | 8 +- yarn.lock | 82 +- 15 files changed, 936 insertions(+), 1243 deletions(-) delete mode 100644 packages/rrweb/src/replay/virtual-styles.ts delete mode 100644 packages/rrweb/test/replay/virtual-styles.test.ts delete mode 100644 packages/rrweb/typings/replay/virtual-styles.d.ts diff --git a/package.json b/package.json index a9298f4111..342273e6c3 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ "test:watch": "yarn lerna run test:watch --parallel", "dev": "yarn lerna run dev --parallel", "repl": "cd packages/rrweb && npm run repl" + }, + "resolutions": { + "**/jsdom/cssom": "^0.5.0" } } diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index da6bb50a58..d3239bd2a8 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,17 +1,25 @@ import { elementNode, INode, NodeType } from 'rrweb-snapshot'; -import type { Mirror } from 'rrweb/typings/types'; +import type { Mirror } from 'rrweb/src/types'; import { RRCDATASection, RRComment, RRElement, + RRStyleElement, RRNode, RRText, + VirtualStyleRules, + StyleRuleType, } from './document-browser'; export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { switch (newTree.nodeType) { case NodeType.Element: diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); + if ( + oldTree instanceof HTMLStyleElement && + newTree instanceof RRStyleElement + ) + applyVirtualStyleRulesToNode(oldTree, newTree.rules); break; // TODO: Diff other kinds of nodes. default: @@ -112,6 +120,7 @@ function diffChildren( } else { const newNode = createOrGetNode(newStartNode, mirror); parentNode.insertBefore(newNode, oldStartNode); + diff(newNode, newStartNode, mirror); } newStartNode = newChildren[++newStartIndex]; } @@ -124,11 +133,11 @@ function diffChildren( if (((child as unknown) as INode).__sn.id === referenceRRNode.__sn.id) referenceNode = child; }); - for (; newStartIndex <= newEndIndex; ++newStartIndex) - parentNode.insertBefore( - createOrGetNode(newChildren[newStartIndex], mirror), - referenceNode, - ); + for (; newStartIndex <= newEndIndex; ++newStartIndex) { + const newNode = createOrGetNode(newChildren[newStartIndex], mirror); + parentNode.insertBefore(newNode, referenceNode); + diff(newNode, newChildren[newStartIndex], mirror); + } } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { const node = oldChildren[oldStartIndex]; @@ -159,6 +168,80 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { } else throw new Error('Unknown rrNode type ' + rrNode.toString()); node.__sn = { ...rrNode.__sn }; mirror.map[rrNode.__sn.id] = node; - diff(node, rrNode, mirror); return node; } + +export function getNestedRule( + rules: CSSRuleList, + position: number[], +): CSSGroupingRule { + const rule = rules[position[0]] as CSSGroupingRule; + if (position.length === 1) { + return rule; + } else { + return getNestedRule( + ((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule) + .cssRules, + position.slice(2), + ); + } +} + +export function getPositionsAndIndex(nestedIndex: number[]) { + const positions = [...nestedIndex]; + const index = positions.pop(); + return { positions, index }; +} + +export function applyVirtualStyleRulesToNode( + styleNode: HTMLStyleElement, + virtualStyleRules: VirtualStyleRules, +) { + const sheet = styleNode.sheet!; + + virtualStyleRules.forEach((rule) => { + if (rule.type === StyleRuleType.Insert) { + try { + if (Array.isArray(rule.index)) { + const { positions, index } = getPositionsAndIndex(rule.index); + const nestedRule = getNestedRule(sheet.cssRules, positions); + nestedRule.insertRule(rule.cssText, index); + } else { + sheet.insertRule(rule.cssText, rule.index); + } + } catch (e) { + /** + * sometimes we may capture rules with browser prefix + * insert rule with prefixs in other browsers may cause Error + */ + } + } else if (rule.type === StyleRuleType.Remove) { + try { + if (Array.isArray(rule.index)) { + const { positions, index } = getPositionsAndIndex(rule.index); + const nestedRule = getNestedRule(sheet.cssRules, positions); + nestedRule.deleteRule(index || 0); + } else { + sheet.deleteRule(rule.index); + } + } catch (e) { + /** + * accessing styleSheet rules may cause SecurityError + * for specific access control settings + */ + } + } else if (rule.type === StyleRuleType.SetProperty) { + const nativeRule = (getNestedRule( + sheet.cssRules, + rule.index, + ) as unknown) as CSSStyleRule; + nativeRule.style.setProperty(rule.property, rule.value, rule.priority); + } else if (rule.type === StyleRuleType.RemoveProperty) { + const nativeRule = (getNestedRule( + sheet.cssRules, + rule.index, + ) as unknown) as CSSStyleRule; + nativeRule.style.removeProperty(rule.property); + } + }); +} diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 269ce368b1..1b2bce7f6a 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -595,7 +595,7 @@ export class RRCanvasElement extends RRElement { } export class RRStyleElement extends RRElement { - // TODO record change of rules like virtual style element. + public rules: VirtualStyleRules = []; } export class RRIframeElement extends RRElement { @@ -707,6 +707,39 @@ class ClassList extends Array { }; } +export enum StyleRuleType { + Insert, + Remove, + Snapshot, + SetProperty, + RemoveProperty, +} +type InsertRule = { + cssText: string; + type: StyleRuleType.Insert; + index?: number | number[]; +}; +type RemoveRule = { + type: StyleRuleType.Remove; + index: number | number[]; +}; +type SetPropertyRule = { + type: StyleRuleType.SetProperty; + index: number[]; + property: string; + value: string | null; + priority: string | undefined; +}; +type RemovePropertyRule = { + type: StyleRuleType.RemoveProperty; + index: number[]; + property: string; +}; + +export type VirtualStyleRules = Array< + InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule +>; + export { diff } from './diff'; /** * Print the RRDom as a string. diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 7961b12ffa..affa7e089f 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -1,8 +1,18 @@ /** * @jest-environment jsdom */ -import { RRDocument, RRElement, RRNode } from '../src/document-browser'; -import { createOrGetNode, diff } from '../src/diff'; +import { + RRDocument, + RRElement, + RRNode, + StyleRuleType, + VirtualStyleRules, +} from '../src/document-browser'; +import { + applyVirtualStyleRulesToNode, + createOrGetNode, + diff, +} from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; const elementSn = { @@ -76,590 +86,739 @@ describe('diff algorithm for rrdom', () => { this.map = {}; }, }; - - it('diff a node', () => { - const tagName = 'DIV'; - const node = (document.createElement(tagName) as unknown) as INode; - node.__sn = Object.assign({}, elementSn, { tagName }); - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = Object.assign({}, elementSn, { tagName }); - diff(node, rrNode, mirror); - expect(node).toBeInstanceOf(HTMLElement); - expect(((node as unknown) as HTMLElement).tagName).toBe(tagName); - }); - it('diff properties (add new properties)', () => { - const tagName = 'DIV'; - const node = (document.createElement(tagName) as unknown) as INode; - node.__sn = Object.assign({}, elementSn, { tagName }); - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = Object.assign({}, elementSn, { tagName }); - rrNode.attributes = { id: 'node1', class: 'node' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('node'); - }); + describe('diff properties', () => { + it('add new properties', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + node.__sn = Object.assign({}, elementSn, { tagName }); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = Object.assign({}, elementSn, { tagName }); + rrNode.attributes = { id: 'node1', class: 'node' }; + diff(node, rrNode, mirror); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('node'); + }); - it('diff properties (update exist properties)', () => { - const tagName = 'DIV'; - const node = (document.createElement(tagName) as unknown) as INode; - node.__sn = Object.assign({}, elementSn, { tagName }); - ((node as unknown) as HTMLElement).id = 'element1'; - ((node as unknown) as HTMLElement).className = 'element'; - ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = Object.assign({}, elementSn, { tagName }); - rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('node'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( - 'color: white', - ); - - rrNode.attributes = { id: 'node2' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node2'); - expect(((node as unknown) as HTMLElement).className).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( - 'undefined', - ); - }); + it('update exist properties', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + node.__sn = Object.assign({}, elementSn, { tagName }); + ((node as unknown) as HTMLElement).id = 'element1'; + ((node as unknown) as HTMLElement).className = 'element'; + ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = Object.assign({}, elementSn, { tagName }); + rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; + diff(node, rrNode, mirror); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('node'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'color: white', + ); + + rrNode.attributes = { id: 'node2' }; + diff(node, rrNode, mirror); + expect(((node as unknown) as HTMLElement).id).toBe('node2'); + expect(((node as unknown) as HTMLElement).className).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'undefined', + ); + }); - it('diff properties (delete old properties)', () => { - const tagName = 'DIV'; - const node = (document.createElement(tagName) as unknown) as INode; - node.__sn = Object.assign({}, elementSn, { tagName }); - ((node as unknown) as HTMLElement).id = 'element1'; - ((node as unknown) as HTMLElement).className = 'element'; - ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement(tagName); - rrNode.__sn = Object.assign({}, elementSn, { tagName }); - rrNode.attributes = { id: 'node1' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( - 'undefined', - ); - - rrNode.attributes = { src: 'link' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe('link'); + it('delete old properties', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + node.__sn = Object.assign({}, elementSn, { tagName }); + ((node as unknown) as HTMLElement).id = 'element1'; + ((node as unknown) as HTMLElement).className = 'element'; + ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = Object.assign({}, elementSn, { tagName }); + rrNode.attributes = { id: 'node1' }; + diff(node, rrNode, mirror); + expect(((node as unknown) as HTMLElement).id).toBe('node1'); + expect(((node as unknown) as HTMLElement).className).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + 'undefined', + ); + + rrNode.attributes = { src: 'link' }; + diff(node, rrNode, mirror); + expect(((node as unknown) as HTMLElement).id).toBe('undefined'); + expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe( + 'link', + ); + }); }); - it('diff children (append elements)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(1); - const rrNode = createTree( - { + describe('diff children', () => { + it('append elements', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(3); - expect(rrNode.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3]); - }); + children: [1].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(1); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3]); + }); - it('diff children (prepends elements)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(2); - const rrNode = createTree( - { + it('prepends elements', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(5); - expect(rrNode.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - }); + children: [4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(2); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + }); - it('diff children (add elements in the middle)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(4); - const rrNode = createTree( - { + it('add elements in the middle', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(5); - expect(rrNode.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - }); + children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + }); - it('diff children (add elements at begin and end)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [2, 3, 4].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(3); - const rrNode = createTree( - { + it('add elements at begin and end', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(5); - expect(rrNode.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - }); + children: [2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + }); - it('diff children (add children to parent with no children)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - }) as INode; - expect(node.childNodes.length).toEqual(0); - const rrNode = createTree( - { + it('add children to parent with no children', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(3); - expect(rrNode.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3]); - }); + }) as INode; + expect(node.childNodes.length).toEqual(0); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3]); + }); - it('diff children (remove all children from parent)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(4); - const rrNode = createTree( - { + it('remove all children from parent', () => { + const node = createTree({ tagName: 'p', id: 0, - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(0); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(0); - expect(rrNode.childNodes.length).toEqual(0); - }); + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(0); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(0); + expect(rrNode.childNodes.length).toEqual(0); + }); - it('diff children (remove elements from the beginning)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - const rrNode = createTree( - { + it('remove elements from the beginning', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(3); - expect(rrNode.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([3, 4, 5]); - }); + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([3, 4, 5]); + }); - it('diff children (remove elements from end)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - const rrNode = createTree( - { + it('remove elements from end', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(3); - expect(rrNode.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3]); - }); + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3]); + }); - it('diff children (remove elements from the middle)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - const rrNode = createTree( - { + it('remove elements from the middle', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(4); - expect(rrNode.childNodes.length).toEqual(4); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 4, 5]); - }); + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 2, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 4, 5]); + }); - it('diff children (moves element forward)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3, 4, 5]); - const rrNode = createTree( - { + it('moves element forward', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [2, 3, 4, 1, 5].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(5); - expect(rrNode.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([2, 3, 4, 1, 5]); - }); + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3, 4, 5]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [2, 3, 4, 1, 5].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([2, 3, 4, 1, 5]); + }); - it('diff children (move elements to end)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 2, 3]); - const rrNode = createTree( - { + it('move elements to end', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [2, 3, 1].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(3); - expect(rrNode.childNodes.length).toEqual(3); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([2, 3, 1]); - }); + children: [1, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 2, 3]); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [2, 3, 1].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(3); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(3); + expect(rrNode.childNodes.length).toEqual(3); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([2, 3, 1]); + }); - it('diff children (move element backwards)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(4); - const rrNode = createTree( - { + it('move element backwards', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [1, 4, 2, 3].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(4); - expect(rrNode.childNodes.length).toEqual(4); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([1, 4, 2, 3]); - }); + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [1, 4, 2, 3].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([1, 4, 2, 3]); + }); - it('diff children (swap first and last)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(4); - const rrNode = createTree( - { + it('swap first and last', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [4, 2, 3, 1].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(4); - expect(rrNode.childNodes.length).toEqual(4); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([4, 2, 3, 1]); - }); + children: [1, 2, 3, 4].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(4); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 2, 3, 1].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(4); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(4); + expect(rrNode.childNodes.length).toEqual(4); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([4, 2, 3, 1]); + }); - it('diff children (move to left and replace)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(5); - const rrNode = createTree( - { + it('move to left and replace', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [4, 1, 2, 3, 6].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(5); - expect(rrNode.childNodes.length).toEqual(5); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([4, 1, 2, 3, 6]); - }); + children: [1, 2, 3, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(5); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 1, 2, 3, 6].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(5); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(5); + expect(rrNode.childNodes.length).toEqual(5); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([4, 1, 2, 3, 6]); + }); - it('diff children (move to left and leaves hold)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 4, 5].map((c) => ({ tagName: 'span', id: c })), - }) as INode; - expect(node.childNodes.length).toEqual(3); - const rrNode = createTree( - { + it('move to left and leaves hold', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [4, 6].map((c) => ({ tagName: 'span', id: c })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(2); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(2); - expect(rrNode.childNodes.length).toEqual(2); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([4, 6]); - }); + children: [1, 4, 5].map((c) => ({ tagName: 'span', id: c })), + }) as INode; + expect(node.childNodes.length).toEqual(3); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [4, 6].map((c) => ({ tagName: 'span', id: c })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(2); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(2); + expect(rrNode.childNodes.length).toEqual(2); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([4, 6]); + }); - it('diff children (reverse elements)', () => { - const node = createTree({ - tagName: 'p', - id: 0, - children: [1, 2, 3, 4, 5, 6, 7, 8].map((c) => ({ - tagName: 'span', - id: c, - })), - }) as INode; - expect(node.childNodes.length).toEqual(8); - const rrNode = createTree( - { + it('reverse elements', () => { + const node = createTree({ tagName: 'p', id: 0, - children: [8, 7, 6, 5, 4, 3, 2, 1].map((c) => ({ + children: [1, 2, 3, 4, 5, 6, 7, 8].map((c) => ({ tagName: 'span', id: c, })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(8); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(8); - expect(rrNode.childNodes.length).toEqual(8); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual([8, 7, 6, 5, 4, 3, 2, 1]); - }); + }) as INode; + expect(node.childNodes.length).toEqual(8); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: [8, 7, 6, 5, 4, 3, 2, 1].map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(8); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(8); + expect(rrNode.childNodes.length).toEqual(8); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual([8, 7, 6, 5, 4, 3, 2, 1]); + }); - it('diff children (handle random shuffle 1)', () => { - /* Number of elements remains the same and no element will be added or removed. */ - let oldElementsNum = 15, - newElementsNum = 15; - let oldElementsIds = [], - newElementsIds = []; - for (let i = 1; i <= oldElementsNum; i++) { - oldElementsIds.push(i); - newElementsIds.push(i); - } - shuffle(oldElementsIds); - shuffle(newElementsIds); - const node = createTree({ - tagName: 'p', - id: 0, - children: oldElementsIds.map((c) => ({ - tagName: 'span', - id: c, - })), - }) as INode; - expect(node.childNodes.length).toEqual(oldElementsNum); - const rrNode = createTree( - { + it('handle random shuffle 1', () => { + /* Number of elements remains the same and no element will be added or removed. */ + let oldElementsNum = 15, + newElementsNum = 15; + let oldElementsIds = [], + newElementsIds = []; + for (let i = 1; i <= oldElementsNum; i++) { + oldElementsIds.push(i); + newElementsIds.push(i); + } + shuffle(oldElementsIds); + shuffle(newElementsIds); + const node = createTree({ tagName: 'p', id: 0, - children: newElementsIds.map((c) => ({ + children: oldElementsIds.map((c) => ({ tagName: 'span', id: c, })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(newElementsNum); - expect(rrNode.childNodes.length).toEqual(newElementsNum); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual(newElementsIds); - }); + }) as INode; + expect(node.childNodes.length).toEqual(oldElementsNum); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: newElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(newElementsNum); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(newElementsNum); + expect(rrNode.childNodes.length).toEqual(newElementsNum); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual(newElementsIds); + }); - it('diff children (handle random shuffle 2)', () => { - /* May need to add or remove some elements. */ - let oldElementsNum = 20, - newElementsNum = 30; - let oldElementsIds = [], - newElementsIds = []; - for (let i = 1; i <= oldElementsNum + 10; i++) oldElementsIds.push(i); - for (let i = 1; i <= newElementsNum + 10; i++) newElementsIds.push(i); - shuffle(oldElementsIds); - shuffle(newElementsIds); - oldElementsIds = oldElementsIds.slice(0, oldElementsNum); - newElementsIds = newElementsIds.slice(0, newElementsNum); - const node = createTree({ - tagName: 'p', - id: 0, - children: oldElementsIds.map((c) => ({ - tagName: 'span', - id: c, - })), - }) as INode; - expect(node.childNodes.length).toEqual(oldElementsNum); - const rrNode = createTree( - { + it('handle random shuffle 2', () => { + /* May need to add or remove some elements. */ + let oldElementsNum = 20, + newElementsNum = 30; + let oldElementsIds = [], + newElementsIds = []; + for (let i = 1; i <= oldElementsNum + 10; i++) oldElementsIds.push(i); + for (let i = 1; i <= newElementsNum + 10; i++) newElementsIds.push(i); + shuffle(oldElementsIds); + shuffle(newElementsIds); + oldElementsIds = oldElementsIds.slice(0, oldElementsNum); + newElementsIds = newElementsIds.slice(0, newElementsNum); + const node = createTree({ tagName: 'p', id: 0, - children: newElementsIds.map((c) => ({ + children: oldElementsIds.map((c) => ({ tagName: 'span', id: c, })), - }, - new RRDocument(), - ) as RRNode; - expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode, mirror); - expect(node.childNodes.length).toEqual(newElementsNum); - expect(rrNode.childNodes.length).toEqual(newElementsNum); - expect( - Array.from(node.childNodes).map((c) => ((c as unknown) as INode).__sn.id), - ).toEqual(newElementsIds); + }) as INode; + expect(node.childNodes.length).toEqual(oldElementsNum); + const rrNode = createTree( + { + tagName: 'p', + id: 0, + children: newElementsIds.map((c) => ({ + tagName: 'span', + id: c, + })), + }, + new RRDocument(), + ) as RRNode; + expect(rrNode.childNodes.length).toEqual(newElementsNum); + diff(node, rrNode, mirror); + expect(node.childNodes.length).toEqual(newElementsNum); + expect(rrNode.childNodes.length).toEqual(newElementsNum); + expect( + Array.from(node.childNodes).map( + (c) => ((c as unknown) as INode).__sn.id, + ), + ).toEqual(newElementsIds); + }); }); - it('create a real HTML element from RRElement', () => { - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement('DIV'); - rrNode.__sn = Object.assign({}, elementSn, { id: 0 }); - let result = createOrGetNode(rrNode, mirror); - expect(result).toBeInstanceOf(HTMLElement); - expect(result.__sn.id).toBe(0); - expect(((result as unknown) as HTMLElement).tagName).toBe('DIV'); + describe('create or get a Node', () => { + it('create a real HTML element from RRElement', () => { + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement('DIV'); + rrNode.__sn = Object.assign({}, elementSn, { id: 0 }); + let result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(HTMLElement); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as HTMLElement).tagName).toBe('DIV'); + }); + + it('create a node from RRNode', () => { + const rrDocument = new RRDocument(); + const textContent = 'Text Content'; + let rrNode: RRNode = rrDocument.createTextNode(textContent); + rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; + let result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(Text); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as Text).textContent).toBe(textContent); + + rrNode = rrDocument.createComment(textContent); + rrNode.__sn = { id: 0, type: NodeType.Comment, textContent }; + result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(Comment); + expect(result.__sn.id).toBe(0); + expect(((result as unknown) as Comment).textContent).toBe(textContent); + }); }); - it('create a node from RRNode', () => { - const rrDocument = new RRDocument(); - const textContent = 'Text Content'; - let rrNode: RRNode = rrDocument.createTextNode(textContent); - rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; - let result = createOrGetNode(rrNode, mirror); - expect(result).toBeInstanceOf(Text); - expect(result.__sn.id).toBe(0); - expect(((result as unknown) as Text).textContent).toBe(textContent); - - rrNode = rrDocument.createComment(textContent); - rrNode.__sn = { id: 0, type: NodeType.Comment, textContent }; - result = createOrGetNode(rrNode, mirror); - expect(result).toBeInstanceOf(Comment); - expect(result.__sn.id).toBe(0); - expect(((result as unknown) as Comment).textContent).toBe(textContent); + describe('apply virtual style rules to node', () => { + it('should insert rule at index 0 in empty sheet', () => { + document.write(''); + const styleEl = document.getElementsByTagName('style')[0]; + + const cssText = '.added-rule {border: 1px solid yellow;}'; + + const virtualStyleRules: VirtualStyleRules = [ + { cssText, index: 0, type: StyleRuleType.Insert }, + ]; + applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); + + expect(styleEl.sheet?.cssRules?.length).toEqual(1); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); + }); + + it('should insert rule at index 0 and keep exsisting rules', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const cssText = '.added-rule {border: 1px solid yellow;}'; + const virtualStyleRules: VirtualStyleRules = [ + { cssText, index: 0, type: StyleRuleType.Insert }, + ]; + applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); + + expect(styleEl.sheet?.cssRules?.length).toEqual(3); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); + }); + + it('should delete rule at index 0', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const virtualStyleRules: VirtualStyleRules = [ + { index: 0, type: StyleRuleType.Remove }, + ]; + applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); + + expect(styleEl.sheet?.cssRules?.length).toEqual(1); + expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}'); + }); + + // JSDOM/CSSOM is currently broken for this test + // remove '.skip' once https://github.com/NV/CSSOM/pull/113#issue-712485075 is merged + it.skip('should insert rule at index [0,0] and keep existing rules', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const cssText = '.added-rule {border: 1px solid yellow;}'; + const virtualStyleRules: VirtualStyleRules = [ + { cssText, index: [0, 0], type: StyleRuleType.Insert }, + ]; + applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); + + console.log( + Array.from((styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules), + ); + + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, + ).toEqual(3); + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, + ).toEqual(cssText); + }); + + it('should delete rule at index [0,1]', () => { + document.write(` + + `); + const styleEl = document.getElementsByTagName('style')[0]; + + const virtualStyleRules: VirtualStyleRules = [ + { index: [0, 1], type: StyleRuleType.Remove }, + ]; + applyVirtualStyleRulesToNode(styleEl, virtualStyleRules); + + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, + ).toEqual(1); + expect( + (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, + ).toEqual('a {color: blue;}'); + }); }); }); diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 204825cd27..ce63ff2222 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -46,7 +46,6 @@ "@types/chai": "^4.1.6", "@types/inquirer": "0.0.43", "@types/jest": "^27.0.2", - "@types/jsdom": "^16.2.12", "@types/node": "^12.20.16", "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.3", @@ -56,8 +55,6 @@ "inquirer": "^6.2.1", "jest": "^27.2.4", "jest-snapshot": "^23.6.0", - "jsdom": "^17.0.0", - "jsdom-global": "^3.0.2", "prettier": "2.2.1", "puppeteer": "^9.1.1", "rollup": "^2.45.2", diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index eb6d40302f..846ecfcd5b 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -6,7 +6,15 @@ import { BuildCache, createCache, } from 'rrweb-snapshot'; -import { RRDocument, RRNode, RRElement, diff } from 'rrdom/es/document-browser'; +import { + RRNode, + RRDocument, + RRElement, + RRStyleElement, + StyleRuleType, + VirtualStyleRules, + diff, +} from 'rrdom/es/document-browser'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -35,7 +43,6 @@ import { inputData, canvasMutationData, Mirror, - ElementState, styleAttributeValue, styleValueWithPriority, mouseMovePos, @@ -51,18 +58,11 @@ import { isIframeINode, getBaseDimension, hasShadowRoot, + getNestedRule, + getPositionsAndIndex, } from '../utils'; import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; -import { - applyVirtualStyleRulesToNode, - storeCSSRules, - StyleRuleType, - VirtualStyleRules, - VirtualStyleRulesMap, - getNestedRule, - getPositionsAndIndex, -} from './virtual-styles'; const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -114,9 +114,6 @@ export class Replayer { private treeIndex!: TreeIndex; private fragmentParentMap!: Map; - private elementStateMap!: Map; - // Hold the list of CSSRules for in-memory state restoration - private virtualStyleRulesMap!: VirtualStyleRulesMap; // The replayer uses the cache to speed up replay and scrubbing. private cache: BuildCache = createCache(); @@ -168,8 +165,6 @@ export class Replayer { this.treeIndex = new TreeIndex(); this.fragmentParentMap = new Map(); - this.elementStateMap = new Map(); - this.virtualStyleRulesMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { if (this.usingRRDom) { @@ -195,17 +190,6 @@ export class Replayer { const { scrollMap, inputMap } = this.treeIndex.flush(); - // this.fragmentParentMap.forEach((parent, frag) => - // this.restoreRealParent(frag, parent), - // ); - // for (const node of this.virtualStyleRulesMap.keys()) { - // // restore css rules of style elements after they are mounted - // this.restoreNodeSheet(node); - // } - // this.fragmentParentMap.clear(); - // this.elementStateMap.clear(); - // this.virtualStyleRulesMap.clear(); - for (const d of scrollMap.values()) { this.applyScroll(d); } @@ -718,6 +702,7 @@ export class Replayer { mutation: addedNodeMutation, iframeEl: HTMLIFrameElement, ) { + // TODO adopt rrdom here const collected: AppendedIframe[] = []; // If iframeEl is detached from dom, iframeEl.contentDocument is null. if (!iframeEl.contentDocument) { @@ -1062,160 +1047,118 @@ export class Replayer { break; } case IncrementalSource.StyleSheetRule: { - // TODO adopt rrdom here - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - - const styleEl = (target as Node) as HTMLStyleElement; - const parent = (target.parentNode as unknown) as INode; - const usingVirtualParent = this.fragmentParentMap.has(parent); - - /** - * Always use existing DOM node, when it's there. - * In in-memory replay, there is virtual node, but it's `sheet` is inaccessible. - * Hence, we buffer all style changes in virtualStyleRulesMap. - */ - const styleSheet = usingVirtualParent ? null : styleEl.sheet; - let rules: VirtualStyleRules; - - if (!styleSheet) { - /** - * styleEl.sheet is only accessible if the styleEl is part of the - * dom. This doesn't work on DocumentFragments so we have to add the - * style mutations to the virtualStyleRulesMap. - */ - - if (this.virtualStyleRulesMap.has(target)) { - rules = this.virtualStyleRulesMap.get(target) as VirtualStyleRules; - } else { - rules = []; - this.virtualStyleRulesMap.set(target, rules); + if (this.usingRRDom) { + const target = this.rrdom.mirror.getNode(d.id) as RRStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); } - } - - if (d.adds) { - d.adds.forEach(({ rule, index: nestedIndex }) => { - if (styleSheet) { - try { - if (Array.isArray(nestedIndex)) { - const { positions, index } = getPositionsAndIndex( - nestedIndex, - ); - const nestedRule = getNestedRule( - styleSheet.cssRules, - positions, - ); - nestedRule.insertRule(rule, index); - } else { - const index = - nestedIndex === undefined - ? undefined - : Math.min(nestedIndex, styleSheet.cssRules.length); - styleSheet.insertRule(rule, index); - } - } catch (e) { - /** - * sometimes we may capture rules with browser prefix - * insert rule with prefixs in other browsers may cause Error - */ - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ + const rules: VirtualStyleRules = target.rules; + d.adds?.forEach(({ rule, index: nestedIndex }) => + rules?.push({ + cssText: rule, + index: nestedIndex, + type: StyleRuleType.Insert, + }), + ); + d.removes?.forEach(({ index: nestedIndex }) => + rules?.push({ index: nestedIndex, type: StyleRuleType.Remove }), + ); + } else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const styleSheet = ((target as Node) as HTMLStyleElement).sheet!; + d.adds?.forEach(({ rule, index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule( + styleSheet.cssRules, + positions, + ); + nestedRule.insertRule(rule, index); + } else { + const index = + nestedIndex === undefined + ? undefined + : Math.min(nestedIndex, styleSheet.cssRules.length); + styleSheet.insertRule(rule, index); } - } else { - rules?.push({ - cssText: rule, - index: nestedIndex, - type: StyleRuleType.Insert, - }); + } catch (e) { + /** + * sometimes we may capture rules with browser prefix + * insert rule with prefixs in other browsers may cause Error + */ + /** + * accessing styleSheet rules may cause SecurityError + * for specific access control settings + */ } }); - } - if (d.removes) { - d.removes.forEach(({ index: nestedIndex }) => { - if (usingVirtualParent) { - rules?.push({ index: nestedIndex, type: StyleRuleType.Remove }); - } else { - try { - if (Array.isArray(nestedIndex)) { - const { positions, index } = getPositionsAndIndex( - nestedIndex, - ); - const nestedRule = getNestedRule( - styleSheet!.cssRules, - positions, - ); - nestedRule.deleteRule(index || 0); - } else { - styleSheet?.deleteRule(nestedIndex); - } - } catch (e) { - /** - * same as insertRule - */ + d.removes?.forEach(({ index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule( + styleSheet.cssRules, + positions, + ); + nestedRule.deleteRule(index || 0); + } else { + styleSheet?.deleteRule(nestedIndex); } + } catch (e) { + /** + * same as insertRule + */ } }); } break; } case IncrementalSource.StyleDeclaration: { - // TODO adopt rrdom here - // same with StyleSheetRule - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - - const styleEl = (target as Node) as HTMLStyleElement; - const parent = (target.parentNode as unknown) as INode; - const usingVirtualParent = this.fragmentParentMap.has(parent); - - const styleSheet = usingVirtualParent ? null : styleEl.sheet; - let rules: VirtualStyleRules = []; - - if (!styleSheet) { - if (this.virtualStyleRulesMap.has(target)) { - rules = this.virtualStyleRulesMap.get(target) as VirtualStyleRules; - } else { - rules = []; - this.virtualStyleRulesMap.set(target, rules); + if (this.usingRRDom) { + const target = this.rrdom.mirror.getNode(d.id) as RRStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); } - } - - if (d.set) { - if (styleSheet) { - const rule = (getNestedRule( - styleSheet.rules, - d.index, - ) as unknown) as CSSStyleRule; - rule.style.setProperty(d.set.property, d.set.value, d.set.priority); - } else { + const rules: VirtualStyleRules = target.rules; + d.set && rules.push({ type: StyleRuleType.SetProperty, index: d.index, ...d.set, }); + d.remove && + rules.push({ + type: StyleRuleType.RemoveProperty, + index: d.index, + ...d.remove, + }); + } else { + const target = (this.mirror.getNode( + d.id, + ) as Node) as HTMLStyleElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const styleSheet = target.sheet!; + if (d.set) { + const rule = (getNestedRule( + styleSheet.rules, + d.index, + ) as unknown) as CSSStyleRule; + rule.style.setProperty(d.set.property, d.set.value, d.set.priority); } - } - if (d.remove) { - if (styleSheet) { + if (d.remove) { const rule = (getNestedRule( styleSheet.rules, d.index, ) as unknown) as CSSStyleRule; rule.style.removeProperty(d.remove.property); - } else { - rules.push({ - type: StyleRuleType.RemoveProperty, - index: d.index, - ...d.remove, - }); } } break; @@ -1293,10 +1236,6 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } - // TODO modify virtual style rules - // if (this.virtualStyleRulesMap.has(target)) { - // this.virtualStyleRulesMap.delete(target); - // } let parent: INode | null | ShadowRoot | RRNode = mirror.getNode( mutation.parentId, ); @@ -1764,73 +1703,6 @@ export class Replayer { } parent.appendChild(frag); // restore state of elements after they are mounted - this.restoreState(parent); - } - - /** - * store state of elements before unmounted from dom recursively - * the state should be restored in the handler of event ReplayerEvents.Flush - * e.g. browser would lose scroll position after the process that we add children of parent node to Fragment Document as virtual dom - */ - private storeState(parent: INode) { - if (parent) { - if (parent.nodeType === parent.ELEMENT_NODE) { - const parentElement = (parent as unknown) as HTMLElement; - if (parentElement.scrollLeft || parentElement.scrollTop) { - // store scroll position state - this.elementStateMap.set(parent, { - scroll: [parentElement.scrollLeft, parentElement.scrollTop], - }); - } - if (parentElement.tagName === 'STYLE') - storeCSSRules( - parentElement as HTMLStyleElement, - this.virtualStyleRulesMap, - ); - const children = parentElement.children; - for (const child of Array.from(children)) { - this.storeState((child as unknown) as INode); - } - } - } - } - - /** - * restore the state of elements recursively, which was stored before elements were unmounted from dom in virtual parent mode - * this function corresponds to function storeState - */ - private restoreState(parent: INode) { - if (parent.nodeType === parent.ELEMENT_NODE) { - const parentElement = (parent as unknown) as HTMLElement; - if (this.elementStateMap.has(parent)) { - const storedState = this.elementStateMap.get(parent)!; - // restore scroll position - if (storedState.scroll) { - parentElement.scrollLeft = storedState.scroll[0]; - parentElement.scrollTop = storedState.scroll[1]; - } - this.elementStateMap.delete(parent); - } - const children = parentElement.children; - for (const child of Array.from(children)) { - this.restoreState((child as unknown) as INode); - } - } - } - - private restoreNodeSheet(node: INode) { - const storedRules = this.virtualStyleRulesMap.get(node); - if (node.nodeName !== 'STYLE') { - return; - } - - if (!storedRules) { - return; - } - - const styleNode = (node as unknown) as HTMLStyleElement; - - applyVirtualStyleRulesToNode(storedRules, styleNode); } private warnNodeNotFound(d: incrementalData, id: number) { diff --git a/packages/rrweb/src/replay/virtual-styles.ts b/packages/rrweb/src/replay/virtual-styles.ts deleted file mode 100644 index f850df27c9..0000000000 --- a/packages/rrweb/src/replay/virtual-styles.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { INode } from 'rrweb-snapshot'; - -export enum StyleRuleType { - Insert, - Remove, - Snapshot, - SetProperty, - RemoveProperty, -} - -type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -type SnapshotRule = { - type: StyleRuleType.Snapshot; - cssTexts: string[]; -}; -type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; - -export type VirtualStyleRules = Array< - InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule ->; -export type VirtualStyleRulesMap = Map; - -export function getNestedRule( - rules: CSSRuleList, - position: number[], -): CSSGroupingRule { - const rule = rules[position[0]] as CSSGroupingRule; - if (position.length === 1) { - return rule; - } else { - return getNestedRule( - ((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule) - .cssRules, - position.slice(2), - ); - } -} - -export function getPositionsAndIndex(nestedIndex: number[]) { - const positions = [...nestedIndex]; - const index = positions.pop(); - return { positions, index }; -} - -export function applyVirtualStyleRulesToNode( - storedRules: VirtualStyleRules, - styleNode: HTMLStyleElement, -) { - const { sheet } = styleNode; - if (!sheet) { - // styleNode without sheet means the DOM has been removed - // so the rules no longer need to be applied - return; - } - - storedRules.forEach((rule) => { - if (rule.type === StyleRuleType.Insert) { - try { - if (Array.isArray(rule.index)) { - const { positions, index } = getPositionsAndIndex(rule.index); - const nestedRule = getNestedRule(sheet.cssRules, positions); - nestedRule.insertRule(rule.cssText, index); - } else { - sheet.insertRule(rule.cssText, rule.index); - } - } catch (e) { - /** - * sometimes we may capture rules with browser prefix - * insert rule with prefixs in other browsers may cause Error - */ - } - } else if (rule.type === StyleRuleType.Remove) { - try { - if (Array.isArray(rule.index)) { - const { positions, index } = getPositionsAndIndex(rule.index); - const nestedRule = getNestedRule(sheet.cssRules, positions); - nestedRule.deleteRule(index || 0); - } else { - sheet.deleteRule(rule.index); - } - } catch (e) { - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } - } else if (rule.type === StyleRuleType.Snapshot) { - restoreSnapshotOfStyleRulesToNode(rule.cssTexts, styleNode); - } else if (rule.type === StyleRuleType.SetProperty) { - const nativeRule = (getNestedRule( - sheet.cssRules, - rule.index, - ) as unknown) as CSSStyleRule; - nativeRule.style.setProperty(rule.property, rule.value, rule.priority); - } else if (rule.type === StyleRuleType.RemoveProperty) { - const nativeRule = (getNestedRule( - sheet.cssRules, - rule.index, - ) as unknown) as CSSStyleRule; - nativeRule.style.removeProperty(rule.property); - } - }); -} - -function restoreSnapshotOfStyleRulesToNode( - cssTexts: string[], - styleNode: HTMLStyleElement, -) { - try { - const existingRules = Array.from(styleNode.sheet?.cssRules || []).map( - (rule) => rule.cssText, - ); - const existingRulesReversed = Object.entries(existingRules).reverse(); - let lastMatch = existingRules.length; - existingRulesReversed.forEach(([index, rule]) => { - const indexOf = cssTexts.indexOf(rule); - if (indexOf === -1 || indexOf > lastMatch) { - try { - styleNode.sheet?.deleteRule(Number(index)); - } catch (e) { - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } - } - lastMatch = indexOf; - }); - cssTexts.forEach((cssText, index) => { - try { - if (styleNode.sheet?.cssRules[index]?.cssText !== cssText) { - styleNode.sheet?.insertRule(cssText, index); - } - } catch (e) { - /** - * sometimes we may capture rules with browser prefix - * insert rule with prefixs in other browsers may cause Error - */ - } - }); - } catch (e) { - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } -} - -export function storeCSSRules( - parentElement: HTMLStyleElement, - virtualStyleRulesMap: VirtualStyleRulesMap, -) { - try { - const cssTexts = Array.from( - (parentElement as HTMLStyleElement).sheet?.cssRules || [], - ).map((rule) => rule.cssText); - virtualStyleRulesMap.set((parentElement as unknown) as INode, [ - { - type: StyleRuleType.Snapshot, - cssTexts, - }, - ]); - } catch (e) { - /** - * accessing styleSheet rules may cause SecurityError - * for specific access control settings - */ - } -} diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 78ed5f8c44..e87bf57419 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -599,12 +599,6 @@ export enum ReplayerEvents { PlayBack = 'play-back', } -// store the state that would be changed during the process(unmount from dom and mount again) -export type ElementState = { - // [scrollLeft,scrollTop] - scroll?: [number, number]; -}; - export type KeepIframeSrcFn = (src: string) => boolean; declare global { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index c26fd9959f..4c26f5288e 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -628,3 +628,25 @@ export function hasShadowRoot( ): n is T & { shadowRoot: ShadowRoot } { return Boolean(((n as unknown) as Element)?.shadowRoot); } + +export function getNestedRule( + rules: CSSRuleList, + position: number[], +): CSSGroupingRule { + const rule = rules[position[0]] as CSSGroupingRule; + if (position.length === 1) { + return rule; + } else { + return getNestedRule( + ((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule) + .cssRules, + position.slice(2), + ); + } +} + +export function getPositionsAndIndex(nestedIndex: number[]) { + const positions = [...nestedIndex]; + const index = positions.pop(); + return { positions, index }; +} diff --git a/packages/rrweb/test/replay/virtual-styles.test.ts b/packages/rrweb/test/replay/virtual-styles.test.ts deleted file mode 100644 index 44d27bd925..0000000000 --- a/packages/rrweb/test/replay/virtual-styles.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { - applyVirtualStyleRulesToNode, - StyleRuleType, - VirtualStyleRules, -} from '../../src/replay/virtual-styles'; - -describe('virtual styles', () => { - describe('applyVirtualStyleRulesToNode', () => { - it('should insert rule at index 0 in empty sheet', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: 0, type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect(styleEl.sheet?.cssRules?.length).toEqual(1); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); - }); - - it('should insert rule at index 0 and keep exsisting rules', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: 0, type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect(styleEl.sheet?.cssRules?.length).toEqual(3); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual(cssText); - }); - - it('should delete rule at index 0', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const virtualStyleRules: VirtualStyleRules = [ - { index: 0, type: StyleRuleType.Remove }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect(styleEl.sheet?.cssRules?.length).toEqual(1); - expect(styleEl.sheet?.cssRules[0].cssText).toEqual('div {color: black;}'); - }); - - it('should restore a snapshot by inserting missing rules', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const virtualStyleRules: VirtualStyleRules = [ - { - cssTexts: ['a {color: blue;}', 'div {color: black;}'], - type: StyleRuleType.Snapshot, - }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect(styleEl.sheet?.cssRules?.length).toEqual(2); - }); - - it('should restore a snapshot by fixing order of rules', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const cssTexts = ['a {color: blue;}', 'div {color: black;}']; - - const virtualStyleRules: VirtualStyleRules = [ - { - cssTexts, - type: StyleRuleType.Snapshot, - }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect(styleEl.sheet?.cssRules?.length).toEqual(2); - expect( - Array.from(styleEl.sheet?.cssRules || []).map((rule) => rule.cssText), - ).toEqual(cssTexts); - }); - - // JSDOM/CSSOM is currently broken for this test - // remove '.skip' once https://github.com/NV/CSSOM/pull/113#issue-712485075 is merged - it.skip('should insert rule at index [0,0] and keep exsisting rules', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const cssText = '.added-rule {border: 1px solid yellow;}'; - const virtualStyleRules: VirtualStyleRules = [ - { cssText, index: [0, 0], type: StyleRuleType.Insert }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - console.log( - Array.from((styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules), - ); - - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, - ).toEqual(3); - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, - ).toEqual(cssText); - }); - - it('should delete rule at index [0,1]', () => { - const dom = new JSDOM(` - - `); - const styleEl = dom.window.document.getElementsByTagName('style')[0]; - - const virtualStyleRules: VirtualStyleRules = [ - { index: [0, 1], type: StyleRuleType.Remove }, - ]; - applyVirtualStyleRulesToNode(virtualStyleRules, styleEl); - - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules?.length, - ).toEqual(1); - expect( - (styleEl.sheet?.cssRules[0] as CSSMediaRule).cssRules[0].cssText, - ).toEqual('a {color: blue;}'); - }); - }); -}); diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index 2c49091b4b..b9c6e12ff8 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -17,8 +17,6 @@ export declare class Replayer { private legacy_missingNodeRetryMap; private treeIndex; private fragmentParentMap; - private elementStateMap; - private virtualStyleRulesMap; private cache; private imageMap; private mirror; @@ -26,6 +24,8 @@ export declare class Replayer { private newDocumentQueue; private mousePos; private touchActive; + private usingRRDom; + private rrdom; constructor(events: Array, config?: Partial); on(event: string, handler: Handler): this; off(event: string, handler: Handler): this; @@ -63,9 +63,6 @@ export declare class Replayer { private isUserInteraction; private backToNormal; private restoreRealParent; - private storeState; - private restoreState; - private restoreNodeSheet; private warnNodeNotFound; private warnCanvasMutationFailed; private debugNodeNotFound; diff --git a/packages/rrweb/typings/replay/virtual-styles.d.ts b/packages/rrweb/typings/replay/virtual-styles.d.ts deleted file mode 100644 index 11ebf52d1b..0000000000 --- a/packages/rrweb/typings/replay/virtual-styles.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { INode } from 'rrweb-snapshot'; -export declare enum StyleRuleType { - Insert = 0, - Remove = 1, - Snapshot = 2, - SetProperty = 3, - RemoveProperty = 4 -} -declare type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -declare type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -declare type SnapshotRule = { - type: StyleRuleType.Snapshot; - cssTexts: string[]; -}; -declare type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -declare type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; -export declare type VirtualStyleRules = Array; -export declare type VirtualStyleRulesMap = Map; -export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule; -export declare function getPositionsAndIndex(nestedIndex: number[]): { - positions: number[]; - index: number | undefined; -}; -export declare function applyVirtualStyleRulesToNode(storedRules: VirtualStyleRules, styleNode: HTMLStyleElement): void; -export declare function storeCSSRules(parentElement: HTMLStyleElement, virtualStyleRulesMap: VirtualStyleRulesMap): void; -export {}; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 6cf36aea4a..be1eb836dd 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -451,9 +451,6 @@ export declare enum ReplayerEvents { StateChange = "state-change", PlayBack = "play-back" } -export declare type ElementState = { - scroll?: [number, number]; -}; export declare type KeepIframeSrcFn = (src: string) => boolean; declare global { interface Window { diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index bdd5d741d2..0ad030b147 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,5 +1,6 @@ import { Mirror, throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, removedNodeMutation, textMutation, attributeMutation, mutationData, scrollData, inputData, DocumentDimension, IWindow } from './types'; import { INode, serializedNodeWithId } from 'rrweb-snapshot'; +import { RRNode } from 'rrdom/es/document-browser'; export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler; export declare function createMirror(): Mirror; export declare let _mirror: Mirror; @@ -61,9 +62,14 @@ export declare type AppendedIframe = { mutationInQueue: addedNodeMutation; builtNode: HTMLIFrameINode; }; -export declare function isIframeINode(node: INode | ShadowRoot): node is HTMLIFrameINode; +export declare function isIframeINode(node: INode | ShadowRoot | RRNode): node is HTMLIFrameINode; export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension; export declare function hasShadowRoot(n: T): n is T & { shadowRoot: ShadowRoot; }; +export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule; +export declare function getPositionsAndIndex(nestedIndex: number[]): { + positions: number[]; + index: number | undefined; +}; export {}; diff --git a/yarn.lock b/yarn.lock index 2ac3e4a726..b29b363748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1964,7 +1964,7 @@ jest-diff "^27.0.0" pretty-format "^27.0.0" -"@types/jsdom@^16.2.12", "@types/jsdom@^16.2.4": +"@types/jsdom@^16.2.4": version "16.2.13" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f" integrity sha512-8JQCjdeAidptSsOcRWk2iTm9wCcwn9l+kRG6k5bzUacrnm1ezV4forq0kWjUih/tumAeoG+OspOvQEbbRucBTw== @@ -2329,7 +2329,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.4.1: +acorn@^8.2.4: version "8.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== @@ -3767,12 +3767,7 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@^0.5.0: +cssom@^0.4.4, cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== @@ -3810,15 +3805,6 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -data-urls@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.0.tgz#3ff551c986d7c6234a0ac4bbf20a269e1cd6b378" - integrity sha512-4AefxbTTdFtxDUdh0BuMBs2qJVL25Mow2zlcuuePegQwgD6GEmQao42LLEeksOui8nL4RcNEugIpFP7eRd33xg== - dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^9.0.0" - dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -3863,7 +3849,7 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@^10.2.1, decimal.js@^10.3.1: +decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== @@ -4781,15 +4767,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -6849,11 +6826,6 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom-global@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-3.0.2.tgz#6bd299c13b0c4626b2da2c0393cd4385d606acb9" - integrity sha1-a9KZwTsMRiay2iwDk81DhdYGrLk= - jsdom@^16.4.0: version "16.6.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.6.0.tgz#f79b3786682065492a3da6a60a4695da983805ac" @@ -6920,39 +6892,6 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" -jsdom@^17.0.0: - version "17.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-17.0.0.tgz#3ec82d1d30030649c8defedc45fff6aa3e5d06ae" - integrity sha512-MUq4XdqwtNurZDVeKScENMPHnkgmdIvMzZ1r1NSwHkDuaqI6BouPjr+17COo4/19oLNnmdpFDPOHVpgIZmZ+VA== - dependencies: - abab "^2.0.5" - acorn "^8.4.1" - acorn-globals "^6.0.0" - cssom "^0.5.0" - cssstyle "^2.3.0" - data-urls "^3.0.0" - decimal.js "^10.3.1" - domexception "^2.0.1" - escodegen "^2.0.0" - form-data "^4.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^9.0.0" - ws "^8.0.0" - xml-name-validator "^3.0.0" - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -10853,14 +10792,6 @@ whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" -whatwg-url@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-9.1.0.tgz#1b112cf237d72cd64fa7882b9c3f6234a1c3050d" - integrity sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA== - dependencies: - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -10998,11 +10929,6 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== -ws@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.1.0.tgz#75e5ec608f66d3d3934ec6dbc4ebc8a34a68638c" - integrity sha512-0UWlCD2s3RSclw8FN+D0zDTUyMO+1kHwJQQJzkgUh16S8d3NYON0AKCEQPffE0ez4JyRFu76QDA9KR5bOG/7jw== - xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" From 37c3ec2a584c33fd8595b0ec3f372e7c461b36ad Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 19 Jan 2022 11:30:28 +1100 Subject: [PATCH 08/79] fix: failed virtual style tests in replayer.test.ts --- packages/rrdom/src/diff.ts | 10 +++++----- packages/rrdom/src/document-browser.ts | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index d3239bd2a8..fb72a1ce10 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -15,11 +15,11 @@ export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { switch (newTree.nodeType) { case NodeType.Element: diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); - if ( - oldTree instanceof HTMLStyleElement && - newTree instanceof RRStyleElement - ) - applyVirtualStyleRulesToNode(oldTree, newTree.rules); + if (newTree instanceof RRStyleElement && newTree.rules.length > 0) + applyVirtualStyleRulesToNode( + (oldTree as Node) as HTMLStyleElement, + newTree.rules, + ); break; // TODO: Diff other kinds of nodes. default: diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 1b2bce7f6a..5157530a2a 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -276,6 +276,7 @@ export class RRDocument extends RRNode { NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; function getValidTagName(element: HTMLElement): string { + // https://github.com/rrweb-io/rrweb-snapshot/issues/56 if (element instanceof HTMLFormElement) { return 'FORM'; } From 553583d834e67ea82ec50a74905e2c2d1238fb1f Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 19 Jan 2022 16:09:47 +1100 Subject: [PATCH 09/79] fix: failed polyfill tests caused by nodejs compatibility of different versions --- packages/rrdom/package.json | 1 + packages/rrdom/src/polyfill.ts | 2 ++ packages/rrdom/test/polyfill.test.ts | 7 ++++++- yarn.lock | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index ab09add661..f5dc85a26e 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -29,6 +29,7 @@ "@types/cssom": "^0.4.1", "@types/jest": "^27.0.1", "@types/nwsapi": "^2.2.2", + "compare-versions": "^4.1.3", "jest": "^27.1.1", "rollup": "^2.56.3", "rollup-plugin-terser": "^7.0.2", diff --git a/packages/rrdom/src/polyfill.ts b/packages/rrdom/src/polyfill.ts index 271af35f56..4114e19c1a 100644 --- a/packages/rrdom/src/polyfill.ts +++ b/packages/rrdom/src/polyfill.ts @@ -2,6 +2,8 @@ import { RRDocument, RRNode } from './document-nodejs'; /** * Polyfill the performance for nodejs. + * Note: The performance api is available through the global object from nodejs v16.0.0. + * https://github.com/nodejs/node/pull/37970 */ export function polyfillPerformance() { if (typeof window !== 'undefined' || 'performance' in global) return; diff --git a/packages/rrdom/test/polyfill.test.ts b/packages/rrdom/test/polyfill.test.ts index 6222f7de3f..429cdb484e 100644 --- a/packages/rrdom/test/polyfill.test.ts +++ b/packages/rrdom/test/polyfill.test.ts @@ -1,3 +1,4 @@ +import { compare } from 'compare-versions'; import { RRDocument, RRNode } from '../src/document-nodejs'; import { polyfillPerformance, @@ -9,7 +10,8 @@ import { describe('polyfill for nodejs', () => { it('should polyfill performance api', () => { - expect(global.performance).toBeUndefined(); + if (compare(process.version, 'v16.0.0', '<')) + expect(global.performance).toBeUndefined(); polyfillPerformance(); expect(global.performance).toBeDefined(); expect(performance).toBeDefined(); @@ -60,6 +62,9 @@ describe('polyfill for nodejs', () => { }); it('should polyfill Event type', () => { + // if the second version is greater + if (compare(process.version, 'v15.0.0', '<')) + expect(global.Event).toBeUndefined(); polyfillEvent(); expect(global.Event).toBeDefined(); expect(Event).toBeDefined(); diff --git a/yarn.lock b/yarn.lock index b29b363748..50b87ccb8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3349,6 +3349,11 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compare-versions@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.3.tgz#8f7b8966aef7dc4282b45dfa6be98434fc18a1a4" + integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" From ff1bc52930d1621342c38133b1637742189c73a8 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 19 Jan 2022 17:27:27 +1100 Subject: [PATCH 10/79] fix: svg viewBox attribute doesn't work Cause the attribute viewBox is case sensitive, set value for viewbox doesn't work --- packages/rrdom/src/diff.ts | 62 +++- packages/rrdom/src/document-browser.ts | 339 ++++++++++---------- packages/rrdom/src/document-nodejs.ts | 159 +-------- packages/rrdom/test/document-nodejs.test.ts | 9 +- packages/rrweb/src/replay/index.ts | 6 +- 5 files changed, 236 insertions(+), 339 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index fb72a1ce10..d48ef7f254 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -11,6 +11,52 @@ import { StyleRuleType, } from './document-browser'; +const NAMESPACES: Record = { + svg: 'http://www.w3.org/2000/svg', + 'xlink:href': 'http://www.w3.org/1999/xlink', + xmlns: 'http://www.w3.org/2000/xmlns/', +}; + +// camel case svg element tag names +const SVGTagMap: Record = { + altglyph: 'altGlyph', + altglyphdef: 'altGlyphDef', + altglyphitem: 'altGlyphItem', + animatecolor: 'animateColor', + animatemotion: 'animateMotion', + animatetransform: 'animateTransform', + clippath: 'clipPath', + feblend: 'feBlend', + fecolormatrix: 'feColorMatrix', + fecomponenttransfer: 'feComponentTransfer', + fecomposite: 'feComposite', + feconvolvematrix: 'feConvolveMatrix', + fediffuselighting: 'feDiffuseLighting', + fedisplacementmap: 'feDisplacementMap', + fedistantlight: 'feDistantLight', + fedropshadow: 'feDropShadow', + feflood: 'feFlood', + fefunca: 'feFuncA', + fefuncb: 'feFuncB', + fefuncg: 'feFuncG', + fefuncr: 'feFuncR', + fegaussianblur: 'feGaussianBlur', + feimage: 'feImage', + femerge: 'feMerge', + femergenode: 'feMergeNode', + femorphology: 'feMorphology', + feoffset: 'feOffset', + fepointlight: 'fePointLight', + fespecularlighting: 'feSpecularLighting', + fespotlight: 'feSpotLight', + fetile: 'feTile', + feturbulence: 'feTurbulence', + foreignobject: 'foreignObject', + glyphref: 'glyphRef', + lineargradient: 'linearGradient', + radialgradient: 'radialGradient', +}; + export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { switch (newTree.nodeType) { case NodeType.Element: @@ -53,12 +99,8 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { if (typeof newValue === 'boolean' || typeof newValue === 'number') { // TODO Some special cases for some kinds of elements. e.g. checked, rr_scrollLeft } else { - if ((newTree.__sn as elementNode).isSVG && attribute === 'xlink:href') - oldTree.setAttributeNS( - 'http://www.w3.org/1999/xlink', - attribute, - newValue, - ); + if ((newTree.__sn as elementNode).isSVG && NAMESPACES[attribute]) + oldTree.setAttributeNS(NAMESPACES[attribute], attribute, newValue); else oldTree.setAttribute(attribute, newValue); } } @@ -153,12 +195,14 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { let node = mirror.getNode(rrNode.__sn.id); if (node !== null) return node; if (rrNode instanceof RRElement) { - if ((rrNode.__sn as elementNode).isSVG) + let tagName = rrNode.tagName.toLowerCase(); + tagName = SVGTagMap[tagName] || tagName; + if ((rrNode.__sn as elementNode).isSVG) { node = (document.createElementNS( - 'http://www.w3.org/2000/svg', + NAMESPACES['svg'], rrNode.tagName.toLowerCase(), ) as unknown) as INode; - else node = (document.createElement(rrNode.tagName) as unknown) as INode; + } else node = (document.createElement(rrNode.tagName) as unknown) as INode; } else if (rrNode instanceof RRText) { node = (document.createTextNode(rrNode.textContent) as unknown) as INode; } else if (rrNode instanceof RRComment) { diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 5157530a2a..b8907f4d73 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -88,15 +88,15 @@ export class RRWindow { } export class RRDocument extends RRNode { - public mirror = { + public mirror: Mirror = { map: {}, - getId(n: RRNode) { + getId(n) { return n.__sn.id >= 0 ? n.__sn.id : -1; }, - getNode(id: number): RRNode | null { + getNode(id) { return this.map[id] || null; }, - removeNodeFromMap(n: RRNode) { + removeNodeFromMap(n) { const id = n.__sn.id; delete this.map[id]; if (n.childNodes) { @@ -105,7 +105,7 @@ export class RRDocument extends RRNode { ); } }, - has(id: number) { + has(id) { return this.map.hasOwnProperty(id); }, reset() { @@ -213,13 +213,13 @@ export class RRDocument extends RRNode { element = new RRIframeElement(upperTagName); break; case 'IMG': - element = new RRImageElement('IMG'); + element = new RRImageElement(upperTagName); break; case 'CANVAS': - element = new RRCanvasElement('CANVAS'); + element = new RRCanvasElement(upperTagName); break; case 'STYLE': - element = new RRStyleElement('STYLE'); + element = new RRStyleElement(upperTagName); break; default: element = new RRElement(upperTagName); @@ -265,158 +265,6 @@ export class RRDocument extends RRNode { close() {} - buildFromDom(dom: Document) { - let notSerializedId = -1; - const NodeTypeMap: Record = {}; - NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; - NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; - NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; - NodeTypeMap[document.TEXT_NODE] = NodeType.Text; - NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; - NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; - - function getValidTagName(element: HTMLElement): string { - // https://github.com/rrweb-io/rrweb-snapshot/issues/56 - if (element instanceof HTMLFormElement) { - return 'FORM'; - } - return element.tagName.toUpperCase().trim(); - } - - const walk = function (node: INode) { - let serializedNodeWithId = node.__sn; - let rrNode: RRNode; - if (!serializedNodeWithId) { - serializedNodeWithId = { - type: NodeTypeMap[node.nodeType], - textContent: '', - id: notSerializedId, - }; - notSerializedId -= 1; - node.__sn = serializedNodeWithId; - } - if (!this.mirror.has(serializedNodeWithId.id)) { - switch (node.nodeType) { - case node.DOCUMENT_NODE: - if ( - serializedNodeWithId.rootId && - serializedNodeWithId.rootId !== serializedNodeWithId.id - ) - rrNode = this.createDocument(); - else rrNode = this; - break; - case node.DOCUMENT_TYPE_NODE: - const documentType = (node as unknown) as DocumentType; - rrNode = this.createDocumentType( - documentType.name, - documentType.publicId, - documentType.systemId, - ); - break; - case node.ELEMENT_NODE: - const elementNode = (node as unknown) as HTMLElement; - const tagName = getValidTagName(elementNode); - rrNode = this.createElement(tagName); - const rrElement = rrNode as RRElement; - for (const { name, value } of Array.from(elementNode.attributes)) { - rrElement.attributes[name] = value; - } - // form fields - if ( - tagName === 'INPUT' || - tagName === 'TEXTAREA' || - tagName === 'SELECT' - ) { - const value = (elementNode as - | HTMLInputElement - | HTMLTextAreaElement).value; - if ( - ['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes( - rrElement.attributes.type as string, - ) && - value - ) { - rrElement.attributes.value = value; - } else if ((elementNode as HTMLInputElement).checked) { - rrElement.attributes.checked = (elementNode as HTMLInputElement).checked; - } - } - if (tagName === 'OPTION') { - const selectValue = (elementNode as HTMLOptionElement) - .parentElement; - if ( - rrElement.attributes.value === - (selectValue as HTMLSelectElement).value - ) { - rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected; - } - } - // canvas image data - if (tagName === 'CANVAS') { - rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); - } - // media elements - if (tagName === 'AUDIO' || tagName === 'VIDEO') { - const rrMediaElement = rrElement as RRMediaElement; - rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; - rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; - } - // scroll - if (elementNode.scrollLeft) { - rrElement.scrollLeft = elementNode.scrollLeft; - } - if (elementNode.scrollTop) { - rrElement.scrollTop = elementNode.scrollTop; - } - break; - case node.TEXT_NODE: - rrNode = this.createTextNode( - ((node as unknown) as Text).textContent, - ); - break; - case node.CDATA_SECTION_NODE: - rrNode = this.createCDATASection(); - break; - case node.COMMENT_NODE: - rrNode = this.createComment( - ((node as unknown) as Comment).textContent || '', - ); - break; - default: - return; - } - rrNode.__sn = serializedNodeWithId; - this.mirror.map[serializedNodeWithId.id] = rrNode; - } else { - rrNode = this.mirror.getNode(serializedNodeWithId.id); - rrNode.parentElement = null; - rrNode.parentNode = null; - rrNode.children = []; - } - const parentNode = node.parentElement || node.parentNode; - if (parentNode) { - const parentSN = ((parentNode as unknown) as INode).__sn; - const parentRRNode = this.mirror.getNode(parentSN.id); - parentRRNode.appendChild(rrNode); - rrNode.parentNode = parentRRNode; - rrNode.parentElement = - parentRRNode instanceof RRElement ? parentRRNode : null; - } - - if ( - serializedNodeWithId.type === NodeType.Document || - serializedNodeWithId.type === NodeType.Element - ) { - node.childNodes.forEach((node) => walk((node as unknown) as INode)); - } - }.bind(this); - - if (dom) { - this.destroyTree(); - walk((dom as unknown) as INode); - } - } - destroyTree() { this.children = []; this.mirror.reset(); @@ -427,6 +275,156 @@ export class RRDocument extends RRNode { } } +/** + * Build a rrdom from a real document tree. + * @param dom the real document tree + * @param rrdomToBuild the rrdom object to be constructed + * @returns the build rrdom + */ +export function buildFromDom( + dom: Document, + rrdomToBuild?: RRDocument, + mirror?: Mirror, +) { + let rrdom = rrdomToBuild ?? new RRDocument(); + + let notSerializedId = -1; + const NodeTypeMap: Record = {}; + NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; + NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; + NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; + NodeTypeMap[document.TEXT_NODE] = NodeType.Text; + NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; + NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; + + function getValidTagName(element: HTMLElement): string { + // https://github.com/rrweb-io/rrweb-snapshot/issues/56 + if (element instanceof HTMLFormElement) { + return 'FORM'; + } + return element.tagName.toUpperCase(); + } + + const walk = function (node: INode, parentRRNode: RRNode | null) { + let serializedNodeWithId = node.__sn; + let rrNode: RRNode; + if (!serializedNodeWithId) { + serializedNodeWithId = { + type: NodeTypeMap[node.nodeType], + textContent: '', + id: notSerializedId, + }; + notSerializedId -= 1; + node.__sn = serializedNodeWithId; + } + + switch (node.nodeType) { + case node.DOCUMENT_NODE: + if ( + serializedNodeWithId.rootId && + serializedNodeWithId.rootId !== serializedNodeWithId.id + ) + rrNode = rrdom.createDocument(null, '', null); + else rrNode = rrdom; + break; + case node.DOCUMENT_TYPE_NODE: + const documentType = (node as unknown) as DocumentType; + rrNode = rrdom.createDocumentType( + documentType.name, + documentType.publicId, + documentType.systemId, + ); + break; + case node.ELEMENT_NODE: + const elementNode = (node as unknown) as HTMLElement; + const tagName = getValidTagName(elementNode); + rrNode = rrdom.createElement(tagName); + const rrElement = rrNode as RRElement; + for (const { name, value } of Array.from(elementNode.attributes)) { + rrElement.attributes[name] = value; + } + // form fields + if ( + tagName === 'INPUT' || + tagName === 'TEXTAREA' || + tagName === 'SELECT' + ) { + const value = (elementNode as HTMLInputElement | HTMLTextAreaElement) + .value; + if ( + ['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes( + rrElement.attributes.type as string, + ) && + value + ) { + rrElement.attributes.value = value; + } else if ((elementNode as HTMLInputElement).checked) { + rrElement.attributes.checked = (elementNode as HTMLInputElement).checked; + } + } + if (tagName === 'OPTION') { + const selectValue = (elementNode as HTMLOptionElement).parentElement; + if ( + rrElement.attributes.value === + (selectValue as HTMLSelectElement).value + ) { + rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected; + } + } + // canvas image data + if (tagName === 'CANVAS') { + rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); + } + // media elements + if (tagName === 'AUDIO' || tagName === 'VIDEO') { + const rrMediaElement = rrElement as RRMediaElement; + rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; + rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; + } + // scroll + if (elementNode.scrollLeft) { + rrElement.scrollLeft = elementNode.scrollLeft; + } + if (elementNode.scrollTop) { + rrElement.scrollTop = elementNode.scrollTop; + } + break; + case node.TEXT_NODE: + rrNode = rrdom.createTextNode( + ((node as unknown) as Text).textContent || '', + ); + break; + case node.CDATA_SECTION_NODE: + rrNode = rrdom.createCDATASection(''); + break; + case node.COMMENT_NODE: + rrNode = rrdom.createComment( + ((node as unknown) as Comment).textContent || '', + ); + break; + default: + return; + } + rrNode.__sn = serializedNodeWithId; + mirror && (mirror.map[serializedNodeWithId.id] = rrNode); + + parentRRNode?.appendChild(rrNode); + rrNode.parentNode = parentRRNode; + rrNode.parentElement = parentRRNode as RRElement; + + if ( + serializedNodeWithId.type === NodeType.Document || + serializedNodeWithId.type === NodeType.Element + ) { + node.childNodes.forEach((node) => + walk((node as unknown) as INode, rrNode), + ); + } + }; + walk((dom as unknown) as INode, null); + return rrdom; +} + export class RRDocumentType extends RRNode { readonly name: string; readonly publicId: string; @@ -500,17 +498,11 @@ export class RRElement extends RRNode { } getAttribute(name: string) { - let upperName = name && name.toLowerCase(); - if (upperName in this.attributes) return this.attributes[upperName]; - return null; + return this.attributes[name] ?? null; } setAttribute(name: string, attribute: string) { - this.attributes[name.toLowerCase()] = attribute; - } - - hasAttribute(name: string) { - return (name && name.toLowerCase()) in this.attributes; + this.attributes[name] = attribute; } setAttributeNS( @@ -663,6 +655,17 @@ export class RRCDATASection extends RRNode { } } +type Mirror = { + map: { + [key: number]: RRNode; + }; + getId(n: RRNode): number; + getNode(id: number): RRNode | null; + removeNodeFromMap(n: RRNode): void; + has(id: number): boolean; + reset(): void; +}; + interface RRElementTagNameMap { img: RRImageElement; audio: RRMediaElement; diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 2689a07f17..6a5b031fbf 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -75,7 +75,6 @@ export class RRWindow { } export class RRDocument extends RRNode { - private mirror: Map = new Map(); private _nwsapi: NWSAPI; get nwsapi() { if (!this._nwsapi) { @@ -271,162 +270,6 @@ export class RRDocument extends RRNode { close() {} - buildFromDom(dom: Document) { - let notSerializedId = -1; - const NodeTypeMap: Record = {}; - NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; - NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; - NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; - NodeTypeMap[document.TEXT_NODE] = NodeType.Text; - NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; - NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; - - function getValidTagName(element: HTMLElement): string { - if (element instanceof HTMLFormElement) { - return 'FORM'; - } - return element.tagName.toUpperCase().trim(); - } - - const walk = function (node: INode) { - let serializedNodeWithId = node.__sn; - let rrNode: RRNode; - if (!serializedNodeWithId) { - serializedNodeWithId = { - type: NodeTypeMap[node.nodeType], - textContent: '', - id: notSerializedId, - }; - notSerializedId -= 1; - node.__sn = serializedNodeWithId; - } - if (!this.mirror.has(serializedNodeWithId.id)) { - switch (node.nodeType) { - case node.DOCUMENT_NODE: - if ( - serializedNodeWithId.rootId && - serializedNodeWithId.rootId !== serializedNodeWithId.id - ) - rrNode = this.createDocument(); - else rrNode = this; - break; - case node.DOCUMENT_TYPE_NODE: - const documentType = (node as unknown) as DocumentType; - rrNode = this.createDocumentType( - documentType.name, - documentType.publicId, - documentType.systemId, - ); - break; - case node.ELEMENT_NODE: - const elementNode = (node as unknown) as HTMLElement; - const tagName = getValidTagName(elementNode); - rrNode = this.createElement(tagName); - const rrElement = rrNode as RRElement; - for (const { name, value } of Array.from(elementNode.attributes)) { - rrElement.attributes[name] = value; - } - // form fields - if ( - tagName === 'INPUT' || - tagName === 'TEXTAREA' || - tagName === 'SELECT' - ) { - const value = (elementNode as - | HTMLInputElement - | HTMLTextAreaElement).value; - if ( - ['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes( - rrElement.attributes.type as string, - ) && - value - ) { - rrElement.attributes.value = value; - } else if ((elementNode as HTMLInputElement).checked) { - rrElement.attributes.checked = (elementNode as HTMLInputElement).checked; - } - } - if (tagName === 'OPTION') { - const selectValue = (elementNode as HTMLOptionElement) - .parentElement; - if ( - rrElement.attributes.value === - (selectValue as HTMLSelectElement).value - ) { - rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected; - } - } - // canvas image data - if (tagName === 'CANVAS') { - rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); - } - // media elements - if (tagName === 'AUDIO' || tagName === 'VIDEO') { - const rrMediaElement = rrElement as RRMediaElement; - rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; - rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; - } - // scroll - if (elementNode.scrollLeft) { - rrElement.scrollLeft = elementNode.scrollLeft; - } - if (elementNode.scrollTop) { - rrElement.scrollTop = elementNode.scrollTop; - } - break; - case node.TEXT_NODE: - rrNode = this.createTextNode( - ((node as unknown) as Text).textContent, - ); - break; - case node.CDATA_SECTION_NODE: - rrNode = this.createCDATASection(); - break; - case node.COMMENT_NODE: - rrNode = this.createComment( - ((node as unknown) as Comment).textContent || '', - ); - break; - default: - return; - } - rrNode.__sn = serializedNodeWithId; - this.mirror.set(serializedNodeWithId.id, rrNode); - } else { - rrNode = this.mirror.get(serializedNodeWithId.id); - rrNode.parentElement = null; - rrNode.parentNode = null; - rrNode.children = []; - } - const parentNode = node.parentElement || node.parentNode; - if (parentNode) { - const parentSN = ((parentNode as unknown) as INode).__sn; - const parentRRNode = this.mirror.get(parentSN.id); - parentRRNode.appendChild(rrNode); - rrNode.parentNode = parentRRNode; - rrNode.parentElement = - parentRRNode instanceof RRElement ? parentRRNode : null; - } - - if ( - serializedNodeWithId.type === NodeType.Document || - serializedNodeWithId.type === NodeType.Element - ) { - node.childNodes.forEach((node) => walk((node as unknown) as INode)); - } - }.bind(this); - - if (dom) { - this.destroyTree(); - walk((dom as unknown) as INode); - } - } - - destroyTree() { - this.children = []; - this.mirror.clear(); - } - toString() { return super.toString('RRDocument'); } @@ -580,7 +423,7 @@ export class RRElement extends RRNode { } getElementById(elementId: string): RRElement | null { - if (this instanceof RRElement && this.id === elementId) return this; + if (this.id === elementId) return this; for (const child of this.children) { if (child instanceof RRElement) { const result = child.getElementById(elementId); diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 774b7250dc..5071af3612 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -4,6 +4,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs'; +import { + RRDocument as RRBrowserDocument, + buildFromDom, +} from '../src/document-browser'; import { printRRDom } from './util'; describe('RRDocument for nodejs environment', () => { @@ -14,7 +18,7 @@ describe('RRDocument for nodejs environment', () => { // create RRDocument from document const rrdoc = new RRDocument(); - rrdoc.buildFromDom(document); + buildFromDom(document, (rrdoc as unknown) as RRBrowserDocument); expect(printRRDom(rrdoc)).toMatchSnapshot(); }); }); @@ -25,7 +29,7 @@ describe('RRDocument for nodejs environment', () => { // initialize rrdom document.write(getHtml('main.html')); rrdom = new RRDocument(); - rrdom.buildFromDom(document); + buildFromDom(document, (rrdom as unknown) as RRBrowserDocument); }); it('get className', () => { @@ -199,6 +203,7 @@ describe('RRDocument for nodejs environment', () => { it('querySelectorAll querying id', () => { for (let query of ['#block1', '#block2', '#block3']) { + console.log(rrdom.children[1]); expect(rrdom.querySelectorAll(query).length).toEqual(1); const targetElement = rrdom.querySelectorAll(query)[0] as RRElement; expect(targetElement.id).toEqual(query.substring(1, query.length)); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 846ecfcd5b..2acdb86b59 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -5,6 +5,7 @@ import { NodeType, BuildCache, createCache, + idNodeMap, } from 'rrweb-snapshot'; import { RRNode, @@ -13,6 +14,7 @@ import { RRStyleElement, StyleRuleType, VirtualStyleRules, + buildFromDom, diff, } from 'rrdom/es/document-browser'; import * as mittProxy from 'mitt'; @@ -1224,7 +1226,7 @@ export class Replayer { private applyMutation(d: mutationData, useVirtualParent: boolean) { if (!this.usingRRDom && useVirtualParent) { this.usingRRDom = true; - this.rrdom.buildFromDom(this.iframe.contentDocument!); + buildFromDom(this.iframe.contentDocument!, this.rrdom, this.rrdom.mirror); } const mirror = useVirtualParent ? this.rrdom.mirror : this.mirror; d.removes.forEach((mutation) => { @@ -1334,7 +1336,7 @@ export class Replayer { } const target = buildNodeWithSN(mutation.node, { doc: targetDoc as Document, - map: mirror.map, + map: mirror.map as idNodeMap, skipChild: true, hackCss: true, cache: this.cache, From 38dbb23133b645b53f7b62202cf6f4a042e3bc37 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 19 Jan 2022 20:29:17 +1100 Subject: [PATCH 11/79] feat: replace treeIndex optimization with rrdom --- packages/rrdom/src/diff.ts | 49 ++++-- packages/rrdom/src/document-browser.ts | 3 + packages/rrweb/src/replay/index.ts | 60 +++----- packages/rrweb/src/utils.ts | 201 ------------------------- 4 files changed, 55 insertions(+), 258 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index d48ef7f254..14a69b1a1f 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,5 +1,5 @@ import { elementNode, INode, NodeType } from 'rrweb-snapshot'; -import type { Mirror } from 'rrweb/src/types'; +import type { inputData, Mirror, scrollData } from 'rrweb/src/types'; import { RRCDATASection, RRComment, @@ -57,15 +57,29 @@ const SVGTagMap: Record = { radialgradient: 'radialGradient', }; -export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { +type ReplayerHandler = { + mirror: Mirror; + applyInput: (data: inputData) => void; + applyScroll: (data: scrollData) => void; +}; + +export function diff( + oldTree: INode, + newTree: RRNode, + replayer: ReplayerHandler, +) { switch (newTree.nodeType) { case NodeType.Element: - diffProps((oldTree as unknown) as HTMLElement, newTree as RRElement); - if (newTree instanceof RRStyleElement && newTree.rules.length > 0) + const newElement = newTree as RRElement; + diffProps((oldTree as unknown) as HTMLElement, newElement); + newElement.inputData && replayer.applyInput(newElement.inputData); + newElement.scrollData && replayer.applyScroll(newElement.scrollData); + if (newTree instanceof RRStyleElement && newTree.rules.length > 0) { applyVirtualStyleRulesToNode( (oldTree as Node) as HTMLStyleElement, newTree.rules, ); + } break; // TODO: Diff other kinds of nodes. default: @@ -77,7 +91,7 @@ export function diff(oldTree: INode, newTree: RRNode, mirror: Mirror) { (Array.from(oldChildren) as unknown) as INode[], newChildren, oldTree, - mirror, + replayer, ); } } @@ -110,7 +124,7 @@ function diffChildren( oldChildren: (INode | undefined)[], newChildren: RRNode[], parentNode: INode, - mirror: Mirror, + replayer: ReplayerHandler, ) { let oldStartIndex = 0, oldEndIndex = oldChildren.length - 1, @@ -128,21 +142,21 @@ function diffChildren( } else if (oldEndNode === undefined) { oldEndNode = oldChildren[--oldEndIndex]; } else if (oldStartNode.__sn?.id === newStartNode.__sn.id) { - diff(oldStartNode, newStartNode, mirror); + diff(oldStartNode, newStartNode, replayer); oldStartNode = oldChildren[++oldStartIndex]; newStartNode = newChildren[++newStartIndex]; } else if (oldEndNode.__sn?.id === newEndNode.__sn.id) { - diff(oldEndNode, newEndNode, mirror); + diff(oldEndNode, newEndNode, replayer); oldEndNode = oldChildren[--oldEndIndex]; newEndNode = newChildren[--newEndIndex]; } else if (oldStartNode.__sn?.id === newEndNode.__sn.id) { parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); - diff(oldStartNode, newEndNode, mirror); + diff(oldStartNode, newEndNode, replayer); oldStartNode = oldChildren[++oldStartIndex]; newEndNode = newChildren[--newEndIndex]; } else if (oldEndNode.__sn?.id === newStartNode.__sn.id) { parentNode.insertBefore(oldEndNode, oldStartNode); - diff(oldEndNode, newStartNode, mirror); + diff(oldEndNode, newStartNode, replayer); oldEndNode = oldChildren[--oldEndIndex]; newStartNode = newChildren[++newStartIndex]; } else { @@ -157,12 +171,12 @@ function diffChildren( if (indexInOld) { const nodeToMove = oldChildren[indexInOld]!; parentNode.insertBefore(nodeToMove, oldStartNode); - diff(nodeToMove, newStartNode, mirror); + diff(nodeToMove, newStartNode, replayer); oldChildren[indexInOld] = undefined; } else { - const newNode = createOrGetNode(newStartNode, mirror); + const newNode = createOrGetNode(newStartNode, replayer.mirror); parentNode.insertBefore(newNode, oldStartNode); - diff(newNode, newStartNode, mirror); + diff(newNode, newStartNode, replayer); } newStartNode = newChildren[++newStartIndex]; } @@ -176,16 +190,19 @@ function diffChildren( referenceNode = child; }); for (; newStartIndex <= newEndIndex; ++newStartIndex) { - const newNode = createOrGetNode(newChildren[newStartIndex], mirror); + const newNode = createOrGetNode( + newChildren[newStartIndex], + replayer.mirror, + ); parentNode.insertBefore(newNode, referenceNode); - diff(newNode, newChildren[newStartIndex], mirror); + diff(newNode, newChildren[newStartIndex], replayer); } } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { const node = oldChildren[oldStartIndex]; if (node) { parentNode.removeChild(node); - mirror.removeNodeFromMap(node); + replayer.mirror.removeNodeFromMap(node); } } } diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index b8907f4d73..2b74b773b6 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -1,4 +1,5 @@ import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { inputData, scrollData } from 'rrweb/src/types'; import { parseCSSText, camelize, toCSSText } from './style'; export abstract class RRNode { @@ -448,6 +449,8 @@ export class RRElement extends RRNode { scrollLeft: number = 0; scrollTop: number = 0; shadowRoot: RRElement | null = null; + inputData: inputData | null = null; + scrollData: scrollData | null = null; constructor(tagName: string) { super(); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 2acdb86b59..491a2daa48 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -53,7 +53,6 @@ import { import { createMirror, polyfill, - TreeIndex, queueToResolveTrees, iterateResolveTree, AppendedIframe, @@ -114,7 +113,6 @@ export class Replayer { // tslint:disable-next-line: variable-name private legacy_missingNodeRetryMap: missingNodeMap = {}; - private treeIndex!: TreeIndex; private fragmentParentMap!: Map; // The replayer uses the cache to speed up replay and scrubbing. @@ -165,16 +163,15 @@ export class Replayer { this.setupDom(); - this.treeIndex = new TreeIndex(); this.fragmentParentMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { if (this.usingRRDom) { - diff( - (this.iframe.contentDocument! as unknown) as INode, - this.rrdom, - this.mirror, - ); + diff((this.iframe.contentDocument! as unknown) as INode, this.rrdom, { + mirror: this.mirror, + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), + }); this.rrdom.destroyTree(); this.usingRRDom = false; } @@ -189,15 +186,6 @@ export class Replayer { ); } this.mousePos = null; - - const { scrollMap, inputMap } = this.treeIndex.flush(); - - for (const d of scrollMap.values()) { - this.applyScroll(d); - } - for (const d of inputMap.values()) { - this.applyInput(d); - } }); this.emitter.on(ReplayerEvents.PlayBack, () => { this.firstFullSnapshot = null; @@ -847,12 +835,6 @@ export class Replayer { const { data: d } = e; switch (d.source) { case IncrementalSource.Mutation: { - if (isSync) { - d.adds.forEach((m) => this.treeIndex.add(m)); - d.texts.forEach((m) => this.treeIndex.text(m)); - d.attributes.forEach((m) => this.treeIndex.attribute(m)); - d.removes.forEach((m) => this.treeIndex.remove(m, this.mirror)); - } try { this.applyMutation(d, isSync); } catch (error) { @@ -982,8 +964,12 @@ export class Replayer { if (d.id === -1) { break; } - if (isSync) { - this.treeIndex.scroll(d); + if (this.usingRRDom) { + const target = this.rrdom.mirror.getNode(d.id) as RRElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.scrollData = d; break; } this.applyScroll(d); @@ -1005,8 +991,12 @@ export class Replayer { if (d.id === -1) { break; } - if (isSync) { - this.treeIndex.input(d); + if (this.usingRRDom) { + const target = this.rrdom.mirror.getNode(d.id) as RRElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.inputData = d; break; } this.applyInput(d); @@ -1708,11 +1698,7 @@ export class Replayer { } private warnNodeNotFound(d: incrementalData, id: number) { - if (this.treeIndex.idRemoved(id)) { - this.warn(`Node with id '${id}' was previously removed. `, d); - } else { - this.warn(`Node with id '${id}' not found. `, d); - } + this.warn(`Node with id '${id}' not found. `, d); } private warnCanvasMutationFailed( @@ -1730,15 +1716,7 @@ export class Replayer { * is microtask, so events fired on a removed DOM may emit * snapshots in the reverse order. */ - if (this.treeIndex.idRemoved(id)) { - this.debug( - REPLAY_CONSOLE_PREFIX, - `Node with id '${id}' was previously removed. `, - d, - ); - } else { - this.debug(REPLAY_CONSOLE_PREFIX, `Node with id '${id}' not found. `, d); - } + this.debug(REPLAY_CONSOLE_PREFIX, `Node with id '${id}' not found. `, d); } private warn(...args: Parameters) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 4c26f5288e..819ccfd09c 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -4,14 +4,7 @@ import { listenerHandler, hookResetter, blockClass, - IncrementalSource, addedNodeMutation, - removedNodeMutation, - textMutation, - attributeMutation, - mutationData, - scrollData, - inputData, DocumentDimension, IWindow, } from './types'; @@ -314,200 +307,6 @@ export function polyfill(win = window) { } } -export type TreeNode = { - id: number; - mutation: addedNodeMutation; - parent?: TreeNode; - children: Record; - texts: textMutation[]; - attributes: attributeMutation[]; -}; -export class TreeIndex { - public tree!: Record; - - private removeNodeMutations!: removedNodeMutation[]; - private textMutations!: textMutation[]; - private attributeMutations!: attributeMutation[]; - private indexes!: Map; - private removeIdSet!: Set; - private scrollMap!: Map; - private inputMap!: Map; - - constructor() { - this.reset(); - } - - public add(mutation: addedNodeMutation) { - const parentTreeNode = this.indexes.get(mutation.parentId); - const treeNode: TreeNode = { - id: mutation.node.id, - mutation, - children: [], - texts: [], - attributes: [], - }; - if (!parentTreeNode) { - this.tree[treeNode.id] = treeNode; - } else { - treeNode.parent = parentTreeNode; - parentTreeNode.children[treeNode.id] = treeNode; - } - this.indexes.set(treeNode.id, treeNode); - } - - public remove(mutation: removedNodeMutation, mirror: Mirror) { - const parentTreeNode = this.indexes.get(mutation.parentId); - const treeNode = this.indexes.get(mutation.id); - - const deepRemoveFromMirror = (id: number) => { - this.removeIdSet.add(id); - const node = mirror.getNode(id); - node?.childNodes.forEach((childNode) => { - if ('__sn' in childNode) { - deepRemoveFromMirror(((childNode as unknown) as INode).__sn.id); - } - }); - }; - const deepRemoveFromTreeIndex = (node: TreeNode) => { - this.removeIdSet.add(node.id); - Object.values(node.children).forEach((n) => deepRemoveFromTreeIndex(n)); - const _treeNode = this.indexes.get(node.id); - if (_treeNode) { - const _parentTreeNode = _treeNode.parent; - if (_parentTreeNode) { - delete _treeNode.parent; - delete _parentTreeNode.children[_treeNode.id]; - this.indexes.delete(mutation.id); - } - } - }; - - if (!treeNode) { - this.removeNodeMutations.push(mutation); - deepRemoveFromMirror(mutation.id); - } else if (!parentTreeNode) { - delete this.tree[treeNode.id]; - this.indexes.delete(treeNode.id); - deepRemoveFromTreeIndex(treeNode); - } else { - delete treeNode.parent; - delete parentTreeNode.children[treeNode.id]; - this.indexes.delete(mutation.id); - deepRemoveFromTreeIndex(treeNode); - } - } - - public text(mutation: textMutation) { - const treeNode = this.indexes.get(mutation.id); - if (treeNode) { - treeNode.texts.push(mutation); - } else { - this.textMutations.push(mutation); - } - } - - public attribute(mutation: attributeMutation) { - const treeNode = this.indexes.get(mutation.id); - if (treeNode) { - treeNode.attributes.push(mutation); - } else { - this.attributeMutations.push(mutation); - } - } - - public scroll(d: scrollData) { - this.scrollMap.set(d.id, d); - } - - public input(d: inputData) { - this.inputMap.set(d.id, d); - } - - public flush(): { - mutationData: mutationData; - scrollMap: TreeIndex['scrollMap']; - inputMap: TreeIndex['inputMap']; - } { - const { - tree, - removeNodeMutations, - textMutations, - attributeMutations, - } = this; - - const batchMutationData: mutationData = { - source: IncrementalSource.Mutation, - removes: removeNodeMutations, - texts: textMutations, - attributes: attributeMutations, - adds: [], - }; - - const walk = (treeNode: TreeNode, removed: boolean) => { - if (removed) { - this.removeIdSet.add(treeNode.id); - } - batchMutationData.texts = batchMutationData.texts - .concat(removed ? [] : treeNode.texts) - .filter((m) => !this.removeIdSet.has(m.id)); - batchMutationData.attributes = batchMutationData.attributes - .concat(removed ? [] : treeNode.attributes) - .filter((m) => !this.removeIdSet.has(m.id)); - if ( - !this.removeIdSet.has(treeNode.id) && - !this.removeIdSet.has(treeNode.mutation.parentId) && - !removed - ) { - batchMutationData.adds.push(treeNode.mutation); - if (treeNode.children) { - Object.values(treeNode.children).forEach((n) => walk(n, false)); - } - } else { - Object.values(treeNode.children).forEach((n) => walk(n, true)); - } - }; - - Object.values(tree).forEach((n) => walk(n, false)); - - for (const id of this.scrollMap.keys()) { - if (this.removeIdSet.has(id)) { - this.scrollMap.delete(id); - } - } - for (const id of this.inputMap.keys()) { - if (this.removeIdSet.has(id)) { - this.inputMap.delete(id); - } - } - - const scrollMap = new Map(this.scrollMap); - const inputMap = new Map(this.inputMap); - - this.reset(); - - return { - mutationData: batchMutationData, - scrollMap, - inputMap, - }; - } - - private reset() { - this.tree = []; - this.indexes = new Map(); - this.removeNodeMutations = []; - this.textMutations = []; - this.attributeMutations = []; - this.removeIdSet = new Set(); - this.scrollMap = new Map(); - this.inputMap = new Map(); - } - - public idRemoved(id: number): boolean { - return this.removeIdSet.has(id); - } -} - type ResolveTree = { value: addedNodeMutation; children: ResolveTree[]; From 8cac9c0aea923dd1d30fe6e2d3c1ddba735263f5 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 22 Jan 2022 01:00:15 +1100 Subject: [PATCH 12/79] fix bug of diffProps and disable smooth scrolling animation in fast-forward mode --- packages/rrdom/src/diff.ts | 43 ++++----- packages/rrdom/src/document-browser.ts | 16 ++-- packages/rrdom/test/diff.test.ts | 116 +++++++++++++++---------- packages/rrweb/src/replay/index.ts | 16 +++- 4 files changed, 109 insertions(+), 82 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 14a69b1a1f..160738a522 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -9,6 +9,7 @@ import { RRText, VirtualStyleRules, StyleRuleType, + RRDocument, } from './document-browser'; const NAMESPACES: Record = { @@ -57,10 +58,10 @@ const SVGTagMap: Record = { radialgradient: 'radialGradient', }; -type ReplayerHandler = { +export type ReplayerHandler = { mirror: Mirror; applyInput: (data: inputData) => void; - applyScroll: (data: scrollData) => void; + applyScroll: (data: scrollData, isSync: boolean) => void; }; export function diff( @@ -69,11 +70,17 @@ export function diff( replayer: ReplayerHandler, ) { switch (newTree.nodeType) { + case NodeType.Document: + const newRRDocument = newTree as RRDocument; + newRRDocument.scrollData && + replayer.applyScroll(newRRDocument.scrollData, true); + break; case NodeType.Element: - const newElement = newTree as RRElement; - diffProps((oldTree as unknown) as HTMLElement, newElement); - newElement.inputData && replayer.applyInput(newElement.inputData); - newElement.scrollData && replayer.applyScroll(newElement.scrollData); + const newRRElement = newTree as RRElement; + diffProps((oldTree as unknown) as HTMLElement, newRRElement); + newRRElement.inputData && replayer.applyInput(newRRElement.inputData); + newRRElement.scrollData && + replayer.applyScroll(newRRElement.scrollData, true); if (newTree instanceof RRStyleElement && newTree.rules.length > 0) { applyVirtualStyleRulesToNode( (oldTree as Node) as HTMLStyleElement, @@ -100,24 +107,20 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { const oldAttributes = oldTree.attributes; const newAttributes = newTree.attributes; - for (const { name, value } of Array.from(oldAttributes)) { - if (!(name in newAttributes)) oldTree.removeAttribute(name); + for (const name in newAttributes) { const newValue = newAttributes[name]; - if (value === newValue) continue; - else oldTree.setAttribute(name, newValue as string); - } - - for (let attribute in newAttributes) { - const newValue = newAttributes[attribute]; - if (oldAttributes.hasOwnProperty(attribute)) continue; - if (typeof newValue === 'boolean' || typeof newValue === 'number') { - // TODO Some special cases for some kinds of elements. e.g. checked, rr_scrollLeft + if (typeof newValue === 'boolean') { + // TODO Some special cases for some kinds of elements. e.g. selected, rr_scrollLeft + } else if (typeof newValue === 'number') { } else { - if ((newTree.__sn as elementNode).isSVG && NAMESPACES[attribute]) - oldTree.setAttributeNS(NAMESPACES[attribute], attribute, newValue); - else oldTree.setAttribute(attribute, newValue); + if ((newTree.__sn as elementNode).isSVG && NAMESPACES[name]) + oldTree.setAttributeNS(NAMESPACES[name], name, newValue); + else oldTree.setAttribute(name, newValue); } } + + for (const { name } of Array.from(oldAttributes)) + if (!(name in newAttributes)) oldTree.removeAttribute(name); } function diffChildren( diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 2b74b773b6..583628524b 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -113,6 +113,7 @@ export class RRDocument extends RRNode { this.map = {}; }, }; + scrollData: scrollData | null = null; get documentElement(): RRElement { return this.children.find( @@ -359,9 +360,11 @@ export function buildFromDom( value ) { rrElement.attributes.value = value; - } else if ((elementNode as HTMLInputElement).checked) { - rrElement.attributes.checked = (elementNode as HTMLInputElement).checked; } + /** + * We don't have to record the 'checked' value of input element at the beginning. + * Because if the 'checked' value is changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm executed. + */ } if (tagName === 'OPTION') { const selectValue = (elementNode as HTMLOptionElement).parentElement; @@ -382,13 +385,6 @@ export function buildFromDom( rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; } - // scroll - if (elementNode.scrollLeft) { - rrElement.scrollLeft = elementNode.scrollLeft; - } - if (elementNode.scrollTop) { - rrElement.scrollTop = elementNode.scrollTop; - } break; case node.TEXT_NODE: rrNode = rrdom.createTextNode( @@ -446,8 +442,6 @@ export class RRDocumentType extends RRNode { export class RRElement extends RRNode { tagName: string; attributes: Record = {}; - scrollLeft: number = 0; - scrollTop: number = 0; shadowRoot: RRElement | null = null; inputData: inputData | null = null; scrollData: scrollData | null = null; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index affa7e089f..1bcc85302a 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -12,6 +12,7 @@ import { applyVirtualStyleRulesToNode, createOrGetNode, diff, + ReplayerHandler, } from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; @@ -86,6 +87,11 @@ describe('diff algorithm for rrdom', () => { this.map = {}; }, }; + const replayer: ReplayerHandler = { + mirror, + applyInput: () => {}, + applyScroll: () => {}, + }; describe('diff properties', () => { it('add new properties', () => { @@ -96,61 +102,77 @@ describe('diff algorithm for rrdom', () => { const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('node'); + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).id).toBe('node1'); + expect(((node as Node) as HTMLElement).className).toBe('node'); }); it('update exist properties', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; node.__sn = Object.assign({}, elementSn, { tagName }); - ((node as unknown) as HTMLElement).id = 'element1'; - ((node as unknown) as HTMLElement).className = 'element'; - ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + ((node as Node) as HTMLElement).id = 'element1'; + ((node as Node) as HTMLElement).className = 'element'; + ((node as Node) as HTMLElement).setAttribute('style', 'color: black'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1', class: 'node', style: 'color: white' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('node'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).id).toBe('node1'); + expect(((node as Node) as HTMLElement).className).toBe('node'); + expect(((node as Node) as HTMLElement).getAttribute('style')).toBe( 'color: white', ); rrNode.attributes = { id: 'node2' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node2'); - expect(((node as unknown) as HTMLElement).className).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( - 'undefined', - ); + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).id).toBe('node2'); + expect(((node as Node) as HTMLElement).className).toBe(''); + expect(((node as Node) as HTMLElement).getAttribute('style')).toBe(null); }); it('delete old properties', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; node.__sn = Object.assign({}, elementSn, { tagName }); - ((node as unknown) as HTMLElement).id = 'element1'; - ((node as unknown) as HTMLElement).className = 'element'; - ((node as unknown) as HTMLElement).setAttribute('style', 'color: black'); + ((node as Node) as HTMLElement).id = 'element1'; + ((node as Node) as HTMLElement).className = 'element'; + ((node as Node) as HTMLElement).setAttribute('style', 'color: black'); const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attributes = { id: 'node1' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('node1'); - expect(((node as unknown) as HTMLElement).className).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('style')).toBe( - 'undefined', - ); + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).id).toBe('node1'); + expect(((node as Node) as HTMLElement).className).toBe(''); + expect(((node as Node) as HTMLElement).getAttribute('style')).toBe(null); rrNode.attributes = { src: 'link' }; - diff(node, rrNode, mirror); - expect(((node as unknown) as HTMLElement).id).toBe('undefined'); - expect(((node as unknown) as HTMLElement).getAttribute('src')).toBe( - 'link', + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).id).toBe(''); + expect(((node as Node) as HTMLElement).className).toBe(''); + expect(((node as Node) as HTMLElement).getAttribute('src')).toBe('link'); + }); + + it('omit "checked" property for input elements', () => { + const tagName = 'INPUT'; + const node = (document.createElement(tagName) as unknown) as INode; + node.__sn = Object.assign({}, elementSn, { tagName }); + ((node as Node) as HTMLElement).setAttribute('type', 'checkbox'); + ((node as Node) as HTMLElement).setAttribute('checked', ''); // default checked + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.attributes = { + checked: true, + }; + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).getAttribute('checked')).toBe(''); + + ((node as Node) as HTMLElement).removeAttribute('checked'); // default unchecked + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).getAttribute('checked')).toBe( + null, ); }); }); @@ -172,7 +194,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -198,7 +220,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -224,7 +246,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -250,7 +272,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -275,7 +297,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -300,7 +322,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(0); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(0); expect(rrNode.childNodes.length).toEqual(0); }); @@ -326,7 +348,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -357,7 +379,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -388,7 +410,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -419,7 +441,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -450,7 +472,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(3); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(3); expect(rrNode.childNodes.length).toEqual(3); expect( @@ -476,7 +498,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -502,7 +524,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(4); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(4); expect(rrNode.childNodes.length).toEqual(4); expect( @@ -528,7 +550,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(5); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(5); expect(rrNode.childNodes.length).toEqual(5); expect( @@ -554,7 +576,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(2); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(2); expect(rrNode.childNodes.length).toEqual(2); expect( @@ -586,7 +608,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(8); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(8); expect(rrNode.childNodes.length).toEqual(8); expect( @@ -629,7 +651,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(newElementsNum); expect(rrNode.childNodes.length).toEqual(newElementsNum); expect( @@ -672,7 +694,7 @@ describe('diff algorithm for rrdom', () => { new RRDocument(), ) as RRNode; expect(rrNode.childNodes.length).toEqual(newElementsNum); - diff(node, rrNode, mirror); + diff(node, rrNode, replayer); expect(node.childNodes.length).toEqual(newElementsNum); expect(rrNode.childNodes.length).toEqual(newElementsNum); expect( diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 491a2daa48..efea033d43 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -972,7 +972,8 @@ export class Replayer { target.scrollData = d; break; } - this.applyScroll(d); + // Use isSync rather than this.usingRRDom because not every fast-forward process uses virtual dom optimization. + this.applyScroll(d, isSync); break; } case IncrementalSource.ViewportResize: @@ -1214,6 +1215,7 @@ export class Replayer { } private applyMutation(d: mutationData, useVirtualParent: boolean) { + // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a rrdom tree and executing the diff algorithm is usually higher than directly applying other kind of events. if (!this.usingRRDom && useVirtualParent) { this.usingRRDom = true; buildFromDom(this.iframe.contentDocument!, this.rrdom, this.rrdom.mirror); @@ -1503,7 +1505,13 @@ export class Replayer { }); } - private applyScroll(d: scrollData) { + /** + * Apply the scroll data on real elements. + * If the replayer is in sync mode, smooth scroll behavior should be disabled. + * @param d the scroll data + * @param isSync whether the replayer is in sync mode(fast-forward) + */ + private applyScroll(d: scrollData, isSync: boolean) { const target = this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); @@ -1512,14 +1520,14 @@ export class Replayer { this.iframe.contentWindow!.scrollTo({ top: d.y, left: d.x, - behavior: 'smooth', + behavior: isSync ? 'auto' : 'smooth', }); } else if (target.__sn.type === NodeType.Document) { // nest iframe content document ((target as unknown) as Document).defaultView!.scrollTo({ top: d.y, left: d.x, - behavior: 'smooth', + behavior: isSync ? 'auto' : 'smooth', }); } else { try { From 623e7c50bbaa3d319b5832c88fc3428acf9169be Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 28 Jan 2022 17:38:47 +1100 Subject: [PATCH 13/79] feat: add iframe support --- packages/rrdom/package.json | 2 + packages/rrdom/src/diff.ts | 16 ++ packages/rrdom/src/document-browser.ts | 149 ++++++++---------- packages/rrdom/src/document-nodejs.ts | 4 +- .../document-browser.test.ts.snap | 103 ++++++++++++ .../document-nodejs.test.ts.snap | 48 ------ packages/rrdom/test/diff.test.ts | 46 +++++- packages/rrdom/test/document-browser.test.ts | 90 +++++++++++ packages/rrdom/test/document-nodejs.test.ts | 18 +-- packages/rrdom/test/html/iframe.html | 30 ++++ packages/rrdom/test/util.ts | 19 --- packages/rrweb/package.json | 2 +- packages/rrweb/src/replay/index.ts | 110 +++++++------ packages/rrweb/src/types.ts | 3 +- packages/rrweb/src/utils.ts | 21 ++- 15 files changed, 432 insertions(+), 229 deletions(-) create mode 100644 packages/rrdom/test/__snapshots__/document-browser.test.ts.snap delete mode 100644 packages/rrdom/test/__snapshots__/document-nodejs.test.ts.snap create mode 100644 packages/rrdom/test/document-browser.test.ts create mode 100644 packages/rrdom/test/html/iframe.html delete mode 100644 packages/rrdom/test/util.ts diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index f5dc85a26e..f24c958e11 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -29,8 +29,10 @@ "@types/cssom": "^0.4.1", "@types/jest": "^27.0.1", "@types/nwsapi": "^2.2.2", + "@types/puppeteer": "^5.4.3", "compare-versions": "^4.1.3", "jest": "^27.1.1", + "puppeteer": "^9.1.1", "rollup": "^2.56.3", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.30.0", diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 160738a522..120a3baf67 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -10,6 +10,8 @@ import { VirtualStyleRules, StyleRuleType, RRDocument, + RRIFrameElement, + RRDocumentType, } from './document-browser'; const NAMESPACES: Record = { @@ -101,6 +103,14 @@ export function diff( replayer, ); } + // IFrame element doesn't have child nodes. + if (newTree instanceof RRIFrameElement) + diff( + (((oldTree as Node) as HTMLIFrameElement) + .contentDocument! as unknown) as INode, + newTree.contentDocument, + replayer, + ); } function diffProps(oldTree: HTMLElement, newTree: RRElement) { @@ -223,6 +233,12 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { rrNode.tagName.toLowerCase(), ) as unknown) as INode; } else node = (document.createElement(rrNode.tagName) as unknown) as INode; + } else if (rrNode instanceof RRDocumentType) { + node = (document.implementation.createDocumentType( + rrNode.name, + rrNode.publicId, + rrNode.systemId, + ) as unknown) as INode; } else if (rrNode instanceof RRText) { node = (document.createTextNode(rrNode.textContent) as unknown) as INode; } else if (rrNode instanceof RRComment) { diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 583628524b..deaa00feae 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -7,7 +7,6 @@ export abstract class RRNode { children: Array = []; parentElement: RRElement | null = null; parentNode: RRNode | null = null; - ownerDocument: RRDocument | null = null; ELEMENT_NODE = 1; TEXT_NODE = 3; @@ -78,17 +77,14 @@ export abstract class RRNode { } } -export class RRWindow { - scrollLeft = 0; - scrollTop = 0; - scrollTo(options?: ScrollToOptions) { - if (!options) return; - if (typeof options.left === 'number') this.scrollLeft = options.left; - if (typeof options.top === 'number') this.scrollTop = options.top; - } -} - export class RRDocument extends RRNode { + _notSerializedId = -1; // used as an id to identify not serialized node + /** + * Every time the id is used, it will minus 1 automatically to avoid collisions. + */ + get notSerializedId(): number { + return this._notSerializedId--; + } public mirror: Mirror = { map: {}, getId(n) { @@ -113,6 +109,7 @@ export class RRDocument extends RRNode { this.map = {}; }, }; + scrollData: scrollData | null = null; get documentElement(): RRElement { @@ -158,7 +155,6 @@ export class RRDocument extends RRNode { } childNode.parentElement = null; childNode.parentNode = this; - childNode.ownerDocument = this; this.children.push(childNode); return childNode; } @@ -173,7 +169,6 @@ export class RRDocument extends RRNode { this.children.splice(childIndex, 0, newChild); newChild.parentElement = null; newChild.parentNode = this; - newChild.ownerDocument = this; return newChild; } @@ -195,7 +190,6 @@ export class RRDocument extends RRNode { publicId, systemId, ); - documentTypeNode.ownerDocument = this; return documentTypeNode; } @@ -212,7 +206,7 @@ export class RRDocument extends RRNode { element = new RRMediaElement(upperTagName); break; case 'IFRAME': - element = new RRIframeElement(upperTagName); + element = new RRIFrameElement(upperTagName); break; case 'IMG': element = new RRImageElement(upperTagName); @@ -227,7 +221,6 @@ export class RRDocument extends RRNode { element = new RRElement(upperTagName); break; } - element.ownerDocument = this; return element; } @@ -240,19 +233,16 @@ export class RRDocument extends RRNode { createComment(data: string) { const commentNode = new RRComment(data); - commentNode.ownerDocument = this; return commentNode; } createCDATASection(data: string) { const sectionNode = new RRCDATASection(data); - sectionNode.ownerDocument = this; return sectionNode; } createTextNode(data: string) { const textNode = new RRText(data); - textNode.ownerDocument = this; return textNode; } @@ -267,6 +257,38 @@ export class RRDocument extends RRNode { close() {} + /** + * Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot). + * There are two lines used this function: + * 1. doc.write('') + * 2. doc.write('') + */ + write(content: string) { + let publicId; + if ( + content === + '' + ) + publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; + else if ( + content === + '' + ) + publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; + if (publicId) { + const doctype = new RRDocumentType('html', publicId, ''); + doctype.__sn = { + type: NodeType.DocumentType, + name: 'html', + publicId: publicId, + systemId: '', + id: this.notSerializedId, + }; + this.open(); + this.appendChild(doctype); + } + } + destroyTree() { this.children = []; this.mirror.reset(); @@ -290,7 +312,6 @@ export function buildFromDom( ) { let rrdom = rrdomToBuild ?? new RRDocument(); - let notSerializedId = -1; const NodeTypeMap: Record = {}; NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; @@ -310,23 +331,19 @@ export function buildFromDom( const walk = function (node: INode, parentRRNode: RRNode | null) { let serializedNodeWithId = node.__sn; let rrNode: RRNode; - if (!serializedNodeWithId) { + if (!serializedNodeWithId || serializedNodeWithId.id < 0) { serializedNodeWithId = { type: NodeTypeMap[node.nodeType], textContent: '', - id: notSerializedId, + id: rrdom.notSerializedId, }; - notSerializedId -= 1; node.__sn = serializedNodeWithId; } switch (node.nodeType) { case node.DOCUMENT_NODE: - if ( - serializedNodeWithId.rootId && - serializedNodeWithId.rootId !== serializedNodeWithId.id - ) - rrNode = rrdom.createDocument(null, '', null); + if (parentRRNode && parentRRNode instanceof RRIFrameElement) + rrNode = parentRRNode.contentDocument; else rrNode = rrdom; break; case node.DOCUMENT_TYPE_NODE: @@ -345,36 +362,10 @@ export function buildFromDom( for (const { name, value } of Array.from(elementNode.attributes)) { rrElement.attributes[name] = value; } - // form fields - if ( - tagName === 'INPUT' || - tagName === 'TEXTAREA' || - tagName === 'SELECT' - ) { - const value = (elementNode as HTMLInputElement | HTMLTextAreaElement) - .value; - if ( - ['RADIO', 'CHECKBOX', 'SUBMIT', 'BUTTON'].includes( - rrElement.attributes.type as string, - ) && - value - ) { - rrElement.attributes.value = value; - } - /** - * We don't have to record the 'checked' value of input element at the beginning. - * Because if the 'checked' value is changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm executed. - */ - } - if (tagName === 'OPTION') { - const selectValue = (elementNode as HTMLOptionElement).parentElement; - if ( - rrElement.attributes.value === - (selectValue as HTMLSelectElement).value - ) { - rrElement.attributes.selected = (elementNode as HTMLOptionElement).selected; - } - } + /** + * We don't have to record special values of input elements at the beginning. + * Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed. + */ // canvas image data if (tagName === 'CANVAS') { rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); @@ -405,18 +396,29 @@ export function buildFromDom( rrNode.__sn = serializedNodeWithId; mirror && (mirror.map[serializedNodeWithId.id] = rrNode); - parentRRNode?.appendChild(rrNode); - rrNode.parentNode = parentRRNode; - rrNode.parentElement = parentRRNode as RRElement; - + if (parentRRNode instanceof RRIFrameElement) { + parentRRNode.contentDocument = rrNode as RRDocument; + } else { + parentRRNode?.appendChild(rrNode); + rrNode.parentNode = parentRRNode; + rrNode.parentElement = parentRRNode as RRElement; + } if ( - serializedNodeWithId.type === NodeType.Document || - serializedNodeWithId.type === NodeType.Element - ) { + node.nodeType === node.ELEMENT_NODE && + ((node as Node) as HTMLElement).tagName === 'IFRAME' + ) + walk( + (((node as Node) as HTMLIFrameElement) + .contentDocument as unknown) as INode, + rrNode, + ); + else if ( + node.nodeType === node.DOCUMENT_NODE || + node.nodeType === node.ELEMENT_NODE + ) node.childNodes.forEach((node) => walk((node as unknown) as INode, rrNode), ); - } }; walk((dom as unknown) as INode, null); return rrdom; @@ -518,7 +520,6 @@ export class RRElement extends RRNode { this.children.push(newChild); newChild.parentNode = this; newChild.parentElement = this; - newChild.ownerDocument = this.ownerDocument; return newChild; } @@ -532,7 +533,6 @@ export class RRElement extends RRNode { this.children.splice(childIndex, 0, newChild); newChild.parentElement = this; newChild.parentNode = this; - newChild.ownerDocument = this.ownerDocument; return newChild; } @@ -588,20 +588,11 @@ export class RRStyleElement extends RRElement { public rules: VirtualStyleRules = []; } -export class RRIframeElement extends RRElement { +export class RRIFrameElement extends RRElement { width: string = ''; height: string = ''; src: string = ''; contentDocument: RRDocument = new RRDocument(); - contentWindow: RRWindow = new RRWindow(); - - constructor(tagName: string) { - super(tagName); - const htmlElement = this.contentDocument.createElement('HTML'); - this.contentDocument.appendChild(htmlElement); - htmlElement.appendChild(this.contentDocument.createElement('HEAD')); - htmlElement.appendChild(this.contentDocument.createElement('BODY')); - } } export class RRText extends RRNode { @@ -755,7 +746,7 @@ function walk(node: RRNode, blankSpace: string) { let printText = `${blankSpace}${node.toString()}\n`; for (const child of node.childNodes) printText += walk(child, blankSpace + ' '); - if (node instanceof RRIframeElement) + if (node instanceof RRIFrameElement) printText += walk(node.contentDocument, blankSpace + ' '); return printText; } diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 6a5b031fbf..8da8c835f2 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -215,7 +215,7 @@ export class RRDocument extends RRNode { element = new RRMediaElement(upperTagName); break; case 'IFRAME': - element = new RRIframeElement(upperTagName); + element = new RRIFrameElement(upperTagName); break; case 'IMG': element = new RRImageElement('IMG'); @@ -526,7 +526,7 @@ export class RRStyleElement extends RRElement { } } -export class RRIframeElement extends RRElement { +export class RRIFrameElement extends RRElement { width: string = ''; height: string = ''; src: string = ''; diff --git a/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap b/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap new file mode 100644 index 0000000000..74dda13789 --- /dev/null +++ b/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RRDocument for browser environment create a RRDocument from a html document can build from a common html 1`] = ` +"-1 RRDocument + -2 RRDocumentType + -3 HTML lang=\\"en\\" + -4 HEAD + -5 RRText text=\\"\\\\n \\" + -6 META charset=\\"UTF-8\\" + -7 RRText text=\\"\\\\n \\" + -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -9 RRText text=\\"\\\\n \\" + -10 TITLE + -11 RRText text=\\"Main\\" + -12 RRText text=\\"\\\\n \\" + -13 LINK rel=\\"stylesheet\\" href=\\"somelink\\" + -14 RRText text=\\"\\\\n \\" + -15 STYLE + -16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url(\\\\\\"main.css\\\\\\");\\\\n \\" + -17 RRText text=\\"\\\\n \\" + -18 RRText text=\\"\\\\n \\" + -19 BODY + -20 RRText text=\\"\\\\n \\" + -21 H1 + -22 RRText text=\\"This is a h1 heading\\" + -23 RRText text=\\"\\\\n \\" + -24 H1 style=\\"font-size: 16px\\" + -25 RRText text=\\"This is a h1 heading with styles\\" + -26 RRText text=\\"\\\\n \\" + -27 DIV id=\\"block1\\" class=\\"blocks blocks1\\" + -28 RRText text=\\"\\\\n \\" + -29 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\" + -30 RRText text=\\"\\\\n Text 1\\\\n \\" + -31 DIV id=\\"block3\\" + -32 RRText text=\\"\\\\n \\" + -33 P + -34 RRText text=\\"This is a paragraph\\" + -35 RRText text=\\"\\\\n \\" + -36 BUTTON + -37 RRText text=\\"button1\\" + -38 RRText text=\\"\\\\n \\" + -39 RRText text=\\"\\\\n Text 2\\\\n \\" + -40 RRText text=\\"\\\\n \\" + -41 IMG src=\\"somelink\\" alt=\\"This is an image\\" + -42 RRText text=\\"\\\\n \\" + -43 RRText text=\\"\\\\n \\\\n\\\\n\\" +" +`; + +exports[`RRDocument for browser environment create a RRDocument from a html document can build from an iframe html 1`] = ` +"-1 RRDocument + -2 RRDocumentType + -3 HTML lang=\\"en\\" + -4 HEAD + -5 RRText text=\\"\\\\n \\" + -6 META charset=\\"UTF-8\\" + -7 RRText text=\\"\\\\n \\" + -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -9 RRText text=\\"\\\\n \\" + -10 TITLE + -11 RRText text=\\"Iframe\\" + -12 RRText text=\\"\\\\n \\" + -13 RRText text=\\"\\\\n \\" + -14 BODY + -15 RRText text=\\"\\\\n \\" + -16 IFRAME id=\\"iframe1\\" srcdoc=\\" + + + + + + +
This is a block inside the iframe1.
+ + \\" + -17 RRDocument + -18 HTML + -19 HEAD + -20 RRText text=\\"\\\\n \\" + -21 META charset=\\"UTF-8\\" + -22 RRText text=\\"\\\\n \\" + -23 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -24 RRText text=\\"\\\\n \\" + -25 RRText text=\\"\\\\n \\" + -26 BODY + -27 RRText text=\\"\\\\n \\" + -28 DIV + -29 RRText text=\\"This is a block inside the iframe1.\\" + -30 RRText text=\\"\\\\n \\\\n \\" + -31 RRText text=\\"\\\\n \\" + -32 IFRAME id=\\"iframe2\\" srcdoc=\\"
This is a block inside the iframe2.
\\" + -33 RRDocument + -34 HTML + -35 HEAD + -36 BODY + -37 DIV + -38 RRText text=\\"This is a block inside the iframe2.\\" + -39 RRText text=\\"\\\\n \\\\n\\\\n\\" +" +`; diff --git a/packages/rrdom/test/__snapshots__/document-nodejs.test.ts.snap b/packages/rrdom/test/__snapshots__/document-nodejs.test.ts.snap deleted file mode 100644 index 04748dd7a8..0000000000 --- a/packages/rrdom/test/__snapshots__/document-nodejs.test.ts.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RRDocument for nodejs environment buildFromDom should create an RRDocument from a html document 1`] = ` -"-1 RRDocument - -2 RRDocumentType - -3 HTML lang=\\"en\\" - -4 HEAD - -5 RRText text=\\"\\\\n \\" - -6 META charset=\\"UTF-8\\" - -7 RRText text=\\"\\\\n \\" - -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" - -9 RRText text=\\"\\\\n \\" - -10 TITLE - -11 RRText text=\\"Main\\" - -12 RRText text=\\"\\\\n \\" - -13 LINK rel=\\"stylesheet\\" href=\\"somelink\\" - -14 RRText text=\\"\\\\n \\" - -15 STYLE - -16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url(\\\\\\"main.css\\\\\\");\\\\n \\" - -17 RRText text=\\"\\\\n \\" - -18 RRText text=\\"\\\\n \\" - -19 BODY - -20 RRText text=\\"\\\\n \\" - -21 H1 - -22 RRText text=\\"This is a h1 heading\\" - -23 RRText text=\\"\\\\n \\" - -24 H1 style=\\"font-size: 16px\\" - -25 RRText text=\\"This is a h1 heading with styles\\" - -26 RRText text=\\"\\\\n \\" - -27 DIV id=\\"block1\\" class=\\"blocks blocks1\\" - -28 RRText text=\\"\\\\n \\" - -29 DIV id=\\"block2\\" class=\\"blocks blocks1 :hover\\" - -30 RRText text=\\"\\\\n Text 1\\\\n \\" - -31 DIV id=\\"block3\\" - -32 RRText text=\\"\\\\n \\" - -33 P - -34 RRText text=\\"This is a paragraph\\" - -35 RRText text=\\"\\\\n \\" - -36 BUTTON - -37 RRText text=\\"button1\\" - -38 RRText text=\\"\\\\n \\" - -39 RRText text=\\"\\\\n Text 2\\\\n \\" - -40 RRText text=\\"\\\\n \\" - -41 IMG src=\\"somelink\\" alt=\\"This is an image\\" - -42 RRText text=\\"\\\\n \\" - -43 RRText text=\\"\\\\n \\\\n\\\\n\\" -" -`; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 1bcc85302a..219f36e515 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -93,6 +93,27 @@ describe('diff algorithm for rrdom', () => { applyScroll: () => {}, }; + describe('diff single node', () => { + it('should a diff document node', () => { + document.close(); + document.open(); + expect(document.childNodes.length).toEqual(0); + const rrNode = new RRDocument(); + rrNode; + const htmlContent = + ''; + rrNode.write(htmlContent); + diff((document as unknown) as INode, rrNode, replayer); + expect(document.childNodes.length).toEqual(1); + expect(document.childNodes[0]).toBeInstanceOf(DocumentType); + expect(document.doctype?.name).toEqual('html'); + expect(document.doctype?.publicId).toEqual( + '-//W3C//DTD XHTML 1.0 Transitional//EN', + ); + expect(document.doctype?.systemId).toEqual(''); + }); + }); + describe('diff properties', () => { it('add new properties', () => { const tagName = 'DIV'; @@ -713,7 +734,7 @@ describe('diff algorithm for rrdom', () => { let result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(HTMLElement); expect(result.__sn.id).toBe(0); - expect(((result as unknown) as HTMLElement).tagName).toBe('DIV'); + expect(((result as Node) as HTMLElement).tagName).toBe('DIV'); }); it('create a node from RRNode', () => { @@ -724,14 +745,33 @@ describe('diff algorithm for rrdom', () => { let result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Text); expect(result.__sn.id).toBe(0); - expect(((result as unknown) as Text).textContent).toBe(textContent); + expect(((result as Node) as Text).textContent).toBe(textContent); rrNode = rrDocument.createComment(textContent); rrNode.__sn = { id: 0, type: NodeType.Comment, textContent }; result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Comment); expect(result.__sn.id).toBe(0); - expect(((result as unknown) as Comment).textContent).toBe(textContent); + expect(((result as Node) as Comment).textContent).toBe(textContent); + }); + + it('create a DocumentType from RRDocumentType', () => { + const rrDocument = new RRDocument(); + const publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; + let rrNode: RRNode = rrDocument.createDocumentType('html', publicId, ''); + rrNode.__sn = { + id: 0, + type: NodeType.DocumentType, + name: 'html', + publicId, + systemId: '', + }; + let result = createOrGetNode(rrNode, mirror); + expect(result).toBeInstanceOf(DocumentType); + expect(result.__sn.id).toBe(0); + expect(((result as Node) as DocumentType).name).toEqual('html'); + expect(((result as Node) as DocumentType).publicId).toEqual(publicId); + expect(((result as Node) as DocumentType).systemId).toEqual(''); }); }); diff --git a/packages/rrdom/test/document-browser.test.ts b/packages/rrdom/test/document-browser.test.ts new file mode 100644 index 0000000000..5e31f4a8f0 --- /dev/null +++ b/packages/rrdom/test/document-browser.test.ts @@ -0,0 +1,90 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as puppeteer from 'puppeteer'; +import * as rollup from 'rollup'; +import resolve from '@rollup/plugin-node-resolve'; +import * as typescript from 'rollup-plugin-typescript2'; + +const _typescript = (typescript as unknown) as typeof typescript.default; +const printRRDomCode = ` +/** + * Print the RRDom as a string. + * @param rootNode the root node of the RRDom tree + * @returns printed string + */ +function printRRDom(rootNode) { + return walk(rootNode, ''); +} +function walk(node, blankSpace) { + let printText = \`\${blankSpace}\${node.toString()}\n\`; + for (const child of node.childNodes) + printText += walk(child, blankSpace + ' '); + if (node instanceof rrdom.RRIFrameElement) + printText += walk(node.contentDocument, blankSpace + ' '); + return printText; +} +`; + +describe('RRDocument for browser environment', () => { + let browser: puppeteer.Browser; + let code: string; + beforeAll(async () => { + browser = await puppeteer.launch(); + const bundle = await rollup.rollup({ + input: path.resolve(__dirname, '../src/document-browser.ts'), + plugins: [ + resolve(), + _typescript({ + tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, + }), + ], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrdom', + format: 'iife', + }); + code = _code; + }); + afterAll(async () => { + await browser.close(); + }); + + describe('create a RRDocument from a html document', () => { + let page: puppeteer.Page; + beforeEach(async () => { + page = await browser.newPage(); + await page.goto('about:blank'); + await page.evaluate(code + printRRDomCode); + }); + + afterEach(async () => { + await page.close(); + }); + it('can build from a common html', async () => { + await page.setContent(getHtml('main.html')); + const result = await page.evaluate(` + const doc = new rrdom.RRDocument(); + rrdom.buildFromDom(document,doc); + printRRDom(doc); + `); + expect(result).toMatchSnapshot(); + }); + + it('can build from an iframe html ', async () => { + await page.setContent(getHtml('iframe.html')); + const result = await page.evaluate(` + const doc = new rrdom.RRDocument(); + rrdom.buildFromDom(document,doc); + console.log(doc); + printRRDom(doc); + `); + expect(result).toMatchSnapshot(); + }); + }); +}); +function getHtml(fileName: string) { + const filePath = path.resolve(__dirname, `./html/${fileName}`); + return fs.readFileSync(filePath, 'utf8'); +} diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 5071af3612..7f3be46689 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -5,31 +5,18 @@ import * as fs from 'fs'; import * as path from 'path'; import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs'; import { - RRDocument as RRBrowserDocument, + RRDocument as RRDocumentBrowser, buildFromDom, } from '../src/document-browser'; -import { printRRDom } from './util'; describe('RRDocument for nodejs environment', () => { - describe('buildFromDom', () => { - it('should create an RRDocument from a html document', () => { - // setup document - document.write(getHtml('main.html')); - - // create RRDocument from document - const rrdoc = new RRDocument(); - buildFromDom(document, (rrdoc as unknown) as RRBrowserDocument); - expect(printRRDom(rrdoc)).toMatchSnapshot(); - }); - }); - describe('RRDocument API', () => { let rrdom: RRDocument; beforeAll(() => { // initialize rrdom document.write(getHtml('main.html')); rrdom = new RRDocument(); - buildFromDom(document, (rrdom as unknown) as RRBrowserDocument); + buildFromDom(document, (rrdom as unknown) as RRDocumentBrowser); }); it('get className', () => { @@ -203,7 +190,6 @@ describe('RRDocument for nodejs environment', () => { it('querySelectorAll querying id', () => { for (let query of ['#block1', '#block2', '#block3']) { - console.log(rrdom.children[1]); expect(rrdom.querySelectorAll(query).length).toEqual(1); const targetElement = rrdom.querySelectorAll(query)[0] as RRElement; expect(targetElement.id).toEqual(query.substring(1, query.length)); diff --git a/packages/rrdom/test/html/iframe.html b/packages/rrdom/test/html/iframe.html new file mode 100644 index 0000000000..eb5a8779db --- /dev/null +++ b/packages/rrdom/test/html/iframe.html @@ -0,0 +1,30 @@ + + + + + + Iframe + + + + + + diff --git a/packages/rrdom/test/util.ts b/packages/rrdom/test/util.ts deleted file mode 100644 index 09b860fee0..0000000000 --- a/packages/rrdom/test/util.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { RRIframeElement, RRNode } from '../src/document-nodejs'; - -/** - * Print the RRDom as a string. - * @param rootNode the root node of the RRDom tree - * @returns printed string - */ -export function printRRDom(rootNode: RRNode) { - return walk(rootNode, ''); -} - -function walk(node: RRNode, blankSpace: string) { - let printText = `${blankSpace}${node.toString()}\n`; - for (const child of node.childNodes) - printText += walk(child, blankSpace + ' '); - if (node instanceof RRIframeElement) - printText += walk(node.contentDocument, blankSpace + ' '); - return printText; -} diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index ce63ff2222..5ebf5933e3 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -72,7 +72,7 @@ "@xstate/fsm": "^1.4.0", "fflate": "^0.4.4", "mitt": "^1.1.3", - "rrdom": "^0.0.0", + "rrdom": "^0.1.0", "rrweb-snapshot": "^1.1.12" } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index efea033d43..53f3734651 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -12,6 +12,7 @@ import { RRDocument, RRElement, RRStyleElement, + RRIFrameElement, StyleRuleType, VirtualStyleRules, buildFromDom, @@ -57,6 +58,7 @@ import { iterateResolveTree, AppendedIframe, isIframeINode, + isRRIFrameElement, getBaseDimension, hasShadowRoot, getNestedRule, @@ -113,8 +115,6 @@ export class Replayer { // tslint:disable-next-line: variable-name private legacy_missingNodeRetryMap: missingNodeMap = {}; - private fragmentParentMap!: Map; - // The replayer uses the cache to speed up replay and scrubbing. private cache: BuildCache = createCache(); @@ -163,8 +163,6 @@ export class Replayer { this.setupDom(); - this.fragmentParentMap = new Map(); - this.emitter.on(ReplayerEvents.Flush, () => { if (this.usingRRDom) { diff((this.iframe.contentDocument! as unknown) as INode, this.rrdom, { @@ -650,7 +648,10 @@ export class Replayer { ); if (builtNode.contentDocument) { const { documentElement, head } = builtNode.contentDocument; - this.insertStyleRules(documentElement, head); + this.insertStyleRules( + documentElement, + head! as HTMLHeadElement | RRElement, + ); } } const { documentElement, head } = this.iframe.contentDocument; @@ -670,11 +671,10 @@ export class Replayer { } private insertStyleRules( - documentElement: HTMLElement, - head: HTMLHeadElement, + documentElement: HTMLElement | RRElement, + head: HTMLHeadElement | RRElement, ) { - const styleEl = document.createElement('style'); - documentElement!.insertBefore(styleEl, head); + // TODO add unit tests const injectStylesRules = getInjectStyleRules( this.config.blockClass, ).concat(this.config.insertStyleRules); @@ -683,34 +683,42 @@ export class Replayer { 'html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }', ); } - for (let idx = 0; idx < injectStylesRules.length; idx++) { - (styleEl.sheet! as CSSStyleSheet).insertRule(injectStylesRules[idx], idx); + if (this.usingRRDom) { + const styleEl = this.rrdom.createElement('style') as RRStyleElement; + (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); + for (let idx = 0; idx < injectStylesRules.length; idx++) { + // push virtual styles + styleEl.rules.push({ + cssText: injectStylesRules[idx], + type: StyleRuleType.Insert, + index: idx, + }); + } + } else { + const styleEl = document.createElement('style'); + (documentElement as HTMLElement)!.insertBefore( + styleEl, + head as HTMLHeadElement, + ); + for (let idx = 0; idx < injectStylesRules.length; idx++) { + (styleEl.sheet! as CSSStyleSheet).insertRule( + injectStylesRules[idx], + idx, + ); + } } } private attachDocumentToIframe( mutation: addedNodeMutation, - iframeEl: HTMLIFrameElement, + iframeEl: HTMLIFrameElement | RRIFrameElement, ) { - // TODO adopt rrdom here const collected: AppendedIframe[] = []; - // If iframeEl is detached from dom, iframeEl.contentDocument is null. - if (!iframeEl.contentDocument) { - let parent = iframeEl.parentNode; - while (parent) { - // The parent of iframeEl is virtual parent and we need to mount it on the dom. - if (this.fragmentParentMap.has((parent as unknown) as INode)) { - const frag = (parent as unknown) as INode; - const realParent = this.fragmentParentMap.get(frag)!; - this.restoreRealParent(frag, realParent); - break; - } - parent = parent.parentNode; - } - } buildNodeWithSN(mutation.node, { - doc: iframeEl.contentDocument!, - map: this.mirror.map, + doc: (iframeEl.contentDocument! as unknown) as Document, + map: this.usingRRDom + ? ((this.rrdom.mirror.map as unknown) as idNodeMap) + : this.mirror.map, hackCss: true, skipChild: false, afterAppend: (builtNode) => { @@ -725,7 +733,10 @@ export class Replayer { ); if (builtNode.contentDocument) { const { documentElement, head } = builtNode.contentDocument; - this.insertStyleRules(documentElement, head); + this.insertStyleRules( + documentElement, + head! as HTMLHeadElement | RRElement, + ); } } } @@ -734,7 +745,7 @@ export class Replayer { collected: AppendedIframe[], builtNode: INode, ) { - if (isIframeINode(builtNode)) { + if (isIframeINode(builtNode) || isRRIFrameElement(builtNode)) { const mutationInQueue = this.newDocumentQueue.find( (m) => m.parentId === builtNode.__sn.id, ); @@ -804,6 +815,7 @@ export class Replayer { * pause when there are some canvas drawImage args need to be loaded */ private preloadAllImages() { + // TODO check useful status let beforeLoadState = this.service.state; const stateHandler = () => { beforeLoadState = this.service.state; @@ -1322,7 +1334,7 @@ export class Replayer { : useVirtualParent ? this.rrdom : this.iframe.contentDocument; - if (isIframeINode(parent)) { + if (isIframeINode(parent) || isRRIFrameElement(parent)) { this.attachDocumentToIframe(mutation, parent); return; } @@ -1332,7 +1344,7 @@ export class Replayer { skipChild: true, hackCss: true, cache: this.cache, - }) as INode; + }) as INode | RRNode; // legacy data, we should not have -1 siblings any more if (mutation.previousId === -1 || mutation.nextId === -1) { @@ -1385,7 +1397,10 @@ export class Replayer { parent.appendChild(target as Node & RRNode); } - if (isIframeINode(target)) { + /** + * isRRIFrameElement won't actually be executed but it's used to make type inference of variable 'target' right. + */ + if (isIframeINode(target) || isRRIFrameElement(target)) { const mutationInQueue = this.newDocumentQueue.find( (m) => m.parentId === target.__sn.id, ); @@ -1397,7 +1412,10 @@ export class Replayer { } if (target.contentDocument) { const { documentElement, head } = target.contentDocument; - this.insertStyleRules(documentElement, head); + this.insertStyleRules( + documentElement, + head! as HTMLHeadElement | RRElement, + ); } } @@ -1683,28 +1701,6 @@ export class Replayer { }); } - /** - * Replace the virtual parent with the real parent. - * @param frag fragment document, the virtual parent - * @param parent real parent element - */ - private restoreRealParent(frag: INode, parent: INode) { - this.mirror.map[parent.__sn.id] = parent; - /** - * If we have already set value attribute on textarea, - * then we could not apply text content as default value any more. - */ - if ( - parent.__sn.type === NodeType.Element && - parent.__sn.tagName === 'textarea' && - frag.textContent - ) { - ((parent as unknown) as HTMLTextAreaElement).value = frag.textContent; - } - parent.appendChild(frag); - // restore state of elements after they are mounted - } - private warnNodeNotFound(d: incrementalData, id: number) { this.warn(`Node with id '${id}' not found. `, d); } diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index e87bf57419..045ebc33d7 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -11,6 +11,7 @@ import { PackFn, UnpackFn } from './packer/base'; import { IframeManager } from './record/iframe-manager'; import { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; +import { RRNode } from 'rrdom/es/document-browser'; export enum EventType { DomContentLoaded, @@ -556,7 +557,7 @@ export type playerMetaData = { }; export type missingNode = { - node: Node; + node: Node | RRNode; mutation: addedNodeMutation; }; export type missingNodeMap = { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 819ccfd09c..d5533ade4a 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -15,7 +15,7 @@ import { NodeType, isShadowRoot, } from 'rrweb-snapshot'; -import { RRNode } from 'rrdom/es/document-browser'; +import { RRNode, RRIFrameElement } from 'rrdom/es/document-browser'; export function on( type: string, @@ -377,7 +377,7 @@ type HTMLIFrameINode = HTMLIFrameElement & { }; export type AppendedIframe = { mutationInQueue: addedNodeMutation; - builtNode: HTMLIFrameINode; + builtNode: HTMLIFrameINode | RRIFrameElement; }; export function isIframeINode( @@ -388,7 +388,22 @@ export function isIframeINode( node.__sn.type === NodeType.Element && node.__sn.tagName === 'iframe' ); } - // node can be document fragment when using the virtual parent feature + return false; +} + +/** + * This function is quite similar to isIframeINode. It is used to make the type inference of node accurate but won't affect isIframeINode's original functionality. + */ +export function isRRIFrameElement( + node: INode | ShadowRoot | RRNode, +): node is RRIFrameElement { + if ('__sn' in node) { + return ( + node.__sn.type === NodeType.Element && + node.__sn.tagName === 'iframe' && + node instanceof RRIFrameElement + ); + } return false; } From d3a7009f4431170a6b5628bb28fea0795273c7a1 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 28 Jan 2022 17:47:15 +1100 Subject: [PATCH 14/79] fix: @rollup/plugin-typescript build errors in rrweb-player Error: @rollup/plugin-typescript TS1371: This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValues' is set to 'error' --- packages/rrweb/src/packer/base.ts | 2 +- packages/rrweb/src/record/iframe-manager.ts | 4 +- packages/rrweb/src/record/mutation.ts | 6 +-- packages/rrweb/src/record/observer.ts | 12 +++--- .../rrweb/src/record/shadow-dom-manager.ts | 6 +-- packages/rrweb/src/types.ts | 10 ++--- packages/rrweb/src/utils.ts | 2 +- packages/rrweb/typings/packer/base.d.ts | 2 +- .../rrweb/typings/record/iframe-manager.d.ts | 4 +- packages/rrweb/typings/record/mutation.d.ts | 6 +-- packages/rrweb/typings/record/observer.d.ts | 4 +- .../typings/record/shadow-dom-manager.d.ts | 6 +-- packages/rrweb/typings/replay/index.d.ts | 3 -- packages/rrweb/typings/types.d.ts | 12 +++--- packages/rrweb/typings/utils.d.ts | 39 ++----------------- 15 files changed, 43 insertions(+), 75 deletions(-) diff --git a/packages/rrweb/src/packer/base.ts b/packages/rrweb/src/packer/base.ts index ef15efb6cf..00cf3748e6 100644 --- a/packages/rrweb/src/packer/base.ts +++ b/packages/rrweb/src/packer/base.ts @@ -1,4 +1,4 @@ -import { eventWithTime } from '../types'; +import type { eventWithTime } from '../types'; export type PackFn = (event: eventWithTime) => string; export type UnpackFn = (raw: string) => eventWithTime; diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 1825c1786f..b0606a6e81 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,5 +1,5 @@ -import { serializedNodeWithId, INode } from 'rrweb-snapshot'; -import { mutationCallBack } from '../types'; +import type { serializedNodeWithId, INode } from 'rrweb-snapshot'; +import type { mutationCallBack } from '../types'; export class IframeManager { private iframes: WeakMap = new WeakMap(); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 275843fbb7..a2cf384176 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -11,7 +11,7 @@ import { MaskTextFn, MaskInputFn, } from 'rrweb-snapshot'; -import { +import type { mutationRecord, blockClass, maskTextClass, @@ -30,8 +30,8 @@ import { isIframeINode, hasShadowRoot, } from '../utils'; -import { IframeManager } from './iframe-manager'; -import { ShadowDomManager } from './shadow-dom-manager'; +import type { IframeManager } from './iframe-manager'; +import type { ShadowDomManager } from './shadow-dom-manager'; type DoubleLinkedListNode = { previous: DoubleLinkedListNode | null; diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index f69e81e918..3fa11259a9 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -6,7 +6,7 @@ import { MaskInputFn, MaskTextFn, } from 'rrweb-snapshot'; -import { FontFaceSet } from 'css-font-loading-module'; +import type { FontFaceSet } from 'css-font-loading-module'; import { throttle, on, @@ -47,8 +47,8 @@ import { IWindow, } from '../types'; import MutationBuffer from './mutation'; -import { IframeManager } from './iframe-manager'; -import { ShadowDomManager } from './shadow-dom-manager'; +import type { IframeManager } from './iframe-manager'; +import type { ShadowDomManager } from './shadow-dom-manager'; type WindowWithStoredMutationObserver = IWindow & { __rrMutationObserver?: MutationObserver; @@ -150,9 +150,9 @@ export function initMutationObserver( typeof MutationObserver >)[angularZoneSymbol]; } - const observer = new mutationObserverCtor( - mutationBuffer.processMutations.bind(mutationBuffer), - ); + const observer = new (mutationObserverCtor as new ( + callback: MutationCallback, + ) => MutationObserver)(mutationBuffer.processMutations.bind(mutationBuffer)); observer.observe(rootEl, { attributes: true, attributeOldValue: true, diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 6f2ed26a66..838fe87760 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -1,4 +1,4 @@ -import { +import type { mutationCallBack, blockClass, maskTextClass, @@ -6,13 +6,13 @@ import { scrollCallback, SamplingStrategy, } from '../types'; -import { +import type { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn, } from 'rrweb-snapshot'; -import { IframeManager } from './iframe-manager'; +import type { IframeManager } from './iframe-manager'; import { initMutationObserver, initScrollObserver } from './observer'; type BypassOptions = { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 045ebc33d7..608b13ddfa 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -1,4 +1,4 @@ -import { +import type { serializedNodeWithId, idNodeMap, INode, @@ -7,11 +7,11 @@ import { MaskInputFn, MaskTextFn, } from 'rrweb-snapshot'; -import { PackFn, UnpackFn } from './packer/base'; -import { IframeManager } from './record/iframe-manager'; -import { ShadowDomManager } from './record/shadow-dom-manager'; +import type { PackFn, UnpackFn } from './packer/base'; +import type { IframeManager } from './record/iframe-manager'; +import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; -import { RRNode } from 'rrdom/es/document-browser'; +import type { RRNode } from 'rrdom/es/document-browser'; export enum EventType { DomContentLoaded, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index d5533ade4a..0d3655ba54 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -1,4 +1,4 @@ -import { +import type { Mirror, throttleOptions, listenerHandler, diff --git a/packages/rrweb/typings/packer/base.d.ts b/packages/rrweb/typings/packer/base.d.ts index 08a8485da5..77d6837045 100644 --- a/packages/rrweb/typings/packer/base.d.ts +++ b/packages/rrweb/typings/packer/base.d.ts @@ -1,4 +1,4 @@ -import { eventWithTime } from '../types'; +import type { eventWithTime } from '../types'; export declare type PackFn = (event: eventWithTime) => string; export declare type UnpackFn = (raw: string) => eventWithTime; export declare type eventWithTimeAndPacker = eventWithTime & { diff --git a/packages/rrweb/typings/record/iframe-manager.d.ts b/packages/rrweb/typings/record/iframe-manager.d.ts index 4300a7abce..8b4b2464b4 100644 --- a/packages/rrweb/typings/record/iframe-manager.d.ts +++ b/packages/rrweb/typings/record/iframe-manager.d.ts @@ -1,5 +1,5 @@ -import { serializedNodeWithId, INode } from 'rrweb-snapshot'; -import { mutationCallBack } from '../types'; +import type { serializedNodeWithId, INode } from 'rrweb-snapshot'; +import type { mutationCallBack } from '../types'; export declare class IframeManager { private iframes; private mutationCb; diff --git a/packages/rrweb/typings/record/mutation.d.ts b/packages/rrweb/typings/record/mutation.d.ts index e47b119721..bc44702ec3 100644 --- a/packages/rrweb/typings/record/mutation.d.ts +++ b/packages/rrweb/typings/record/mutation.d.ts @@ -1,7 +1,7 @@ import { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot'; -import { mutationRecord, blockClass, maskTextClass, mutationCallBack, Mirror } from '../types'; -import { IframeManager } from './iframe-manager'; -import { ShadowDomManager } from './shadow-dom-manager'; +import type { mutationRecord, blockClass, maskTextClass, mutationCallBack, Mirror } from '../types'; +import type { IframeManager } from './iframe-manager'; +import type { ShadowDomManager } from './shadow-dom-manager'; export default class MutationBuffer { private frozen; private locked; diff --git a/packages/rrweb/typings/record/observer.d.ts b/packages/rrweb/typings/record/observer.d.ts index 58965d552d..e00a2284f0 100644 --- a/packages/rrweb/typings/record/observer.d.ts +++ b/packages/rrweb/typings/record/observer.d.ts @@ -1,8 +1,8 @@ import { MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot'; import { mutationCallBack, observerParam, listenerHandler, scrollCallback, blockClass, maskTextClass, hooksParam, SamplingStrategy, Mirror } from '../types'; import MutationBuffer from './mutation'; -import { IframeManager } from './iframe-manager'; -import { ShadowDomManager } from './shadow-dom-manager'; +import type { IframeManager } from './iframe-manager'; +import type { ShadowDomManager } from './shadow-dom-manager'; export declare const mutationBuffers: MutationBuffer[]; export declare function initMutationObserver(cb: mutationCallBack, doc: Document, blockClass: blockClass, blockSelector: string | null, maskTextClass: maskTextClass, maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, maskTextFn: MaskTextFn | undefined, maskInputFn: MaskInputFn | undefined, recordCanvas: boolean, inlineImages: boolean, slimDOMOptions: SlimDOMOptions, mirror: Mirror, iframeManager: IframeManager, shadowDomManager: ShadowDomManager, rootEl: Node): MutationObserver; export declare function initScrollObserver(cb: scrollCallback, doc: Document, mirror: Mirror, blockClass: blockClass, sampling: SamplingStrategy): listenerHandler; diff --git a/packages/rrweb/typings/record/shadow-dom-manager.d.ts b/packages/rrweb/typings/record/shadow-dom-manager.d.ts index d31d572759..906729d330 100644 --- a/packages/rrweb/typings/record/shadow-dom-manager.d.ts +++ b/packages/rrweb/typings/record/shadow-dom-manager.d.ts @@ -1,6 +1,6 @@ -import { mutationCallBack, blockClass, maskTextClass, Mirror, scrollCallback, SamplingStrategy } from '../types'; -import { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot'; -import { IframeManager } from './iframe-manager'; +import type { mutationCallBack, blockClass, maskTextClass, Mirror, scrollCallback, SamplingStrategy } from '../types'; +import type { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot'; +import type { IframeManager } from './iframe-manager'; declare type BypassOptions = { blockClass: blockClass; blockSelector: string | null; diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index b9c6e12ff8..81f200bf94 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -15,8 +15,6 @@ export declare class Replayer { private emitter; private nextUserInteractionEvent; private legacy_missingNodeRetryMap; - private treeIndex; - private fragmentParentMap; private cache; private imageMap; private mirror; @@ -62,7 +60,6 @@ export declare class Replayer { private hoverElements; private isUserInteraction; private backToNormal; - private restoreRealParent; private warnNodeNotFound; private warnCanvasMutationFailed; private debugNodeNotFound; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index be1eb836dd..70c7e4cbc3 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -1,8 +1,10 @@ -import { serializedNodeWithId, idNodeMap, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot'; -import { PackFn, UnpackFn } from './packer/base'; -import { IframeManager } from './record/iframe-manager'; -import { ShadowDomManager } from './record/shadow-dom-manager'; +/// +import type { serializedNodeWithId, idNodeMap, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot'; +import type { PackFn, UnpackFn } from './packer/base'; +import type { IframeManager } from './record/iframe-manager'; +import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; +import type { RRNode } from 'rrdom/es/document-browser'; export declare enum EventType { DomContentLoaded = 0, Load = 1, @@ -416,7 +418,7 @@ export declare type playerMetaData = { totalTime: number; }; export declare type missingNode = { - node: Node; + node: Node | RRNode; mutation: addedNodeMutation; }; export declare type missingNodeMap = { diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index 0ad030b147..d93a0b992a 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,6 +1,6 @@ -import { Mirror, throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, removedNodeMutation, textMutation, attributeMutation, mutationData, scrollData, inputData, DocumentDimension, IWindow } from './types'; +import type { Mirror, throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow } from './types'; import { INode, serializedNodeWithId } from 'rrweb-snapshot'; -import { RRNode } from 'rrdom/es/document-browser'; +import { RRNode, RRIFrameElement } from 'rrdom/es/document-browser'; export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler; export declare function createMirror(): Mirror; export declare let _mirror: Mirror; @@ -16,38 +16,6 @@ export declare function isIgnored(n: Node | INode): boolean; export declare function isAncestorRemoved(target: INode, mirror: Mirror): boolean; export declare function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent; export declare function polyfill(win?: Window & typeof globalThis): void; -export declare type TreeNode = { - id: number; - mutation: addedNodeMutation; - parent?: TreeNode; - children: Record; - texts: textMutation[]; - attributes: attributeMutation[]; -}; -export declare class TreeIndex { - tree: Record; - private removeNodeMutations; - private textMutations; - private attributeMutations; - private indexes; - private removeIdSet; - private scrollMap; - private inputMap; - constructor(); - add(mutation: addedNodeMutation): void; - remove(mutation: removedNodeMutation, mirror: Mirror): void; - text(mutation: textMutation): void; - attribute(mutation: attributeMutation): void; - scroll(d: scrollData): void; - input(d: inputData): void; - flush(): { - mutationData: mutationData; - scrollMap: TreeIndex['scrollMap']; - inputMap: TreeIndex['inputMap']; - }; - private reset; - idRemoved(id: number): boolean; -} declare type ResolveTree = { value: addedNodeMutation; children: ResolveTree[]; @@ -60,9 +28,10 @@ declare type HTMLIFrameINode = HTMLIFrameElement & { }; export declare type AppendedIframe = { mutationInQueue: addedNodeMutation; - builtNode: HTMLIFrameINode; + builtNode: HTMLIFrameINode | RRIFrameElement; }; export declare function isIframeINode(node: INode | ShadowRoot | RRNode): node is HTMLIFrameINode; +export declare function isRRIFrameElement(node: INode | ShadowRoot | RRNode): node is RRIFrameElement; export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension; export declare function hasShadowRoot(n: T): n is T & { shadowRoot: ShadowRoot; From 16ec25d706f8593ad26d1f459c83634c566db123 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 29 Jan 2022 03:12:31 +1100 Subject: [PATCH 15/79] fix: bug when fast-forward input events and add test for it --- packages/rrdom/src/diff.ts | 17 ++- packages/rrweb/test/events/input.ts | 216 +++++++++++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 78 ++++++++++ 3 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 packages/rrweb/test/events/input.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 120a3baf67..344d26b948 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -71,18 +71,18 @@ export function diff( newTree: RRNode, replayer: ReplayerHandler, ) { + let inputDataToApply = null, + scrollDataToApply = null; switch (newTree.nodeType) { case NodeType.Document: const newRRDocument = newTree as RRDocument; - newRRDocument.scrollData && - replayer.applyScroll(newRRDocument.scrollData, true); + scrollDataToApply = newRRDocument.scrollData; break; case NodeType.Element: const newRRElement = newTree as RRElement; diffProps((oldTree as unknown) as HTMLElement, newRRElement); - newRRElement.inputData && replayer.applyInput(newRRElement.inputData); - newRRElement.scrollData && - replayer.applyScroll(newRRElement.scrollData, true); + scrollDataToApply = newRRElement.scrollData; + inputDataToApply = newRRElement.inputData; if (newTree instanceof RRStyleElement && newTree.rules.length > 0) { applyVirtualStyleRulesToNode( (oldTree as Node) as HTMLStyleElement, @@ -111,6 +111,13 @@ export function diff( newTree.contentDocument, replayer, ); + + scrollDataToApply && replayer.applyScroll(scrollDataToApply, true); + /** + * Input data need to get applied after all children of this node are updated. + * Otherwise when we set a value for a select element whose options are empty, the value won't actually update. + */ + inputDataToApply && replayer.applyInput(inputDataToApply); } function diffProps(oldTree: HTMLElement, newTree: RRElement) { diff --git a/packages/rrweb/test/events/input.ts b/packages/rrweb/test/events/input.ts new file mode 100644 index 0000000000..072eea6f7e --- /dev/null +++ b/packages/rrweb/test/events/input.ts @@ -0,0 +1,216 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds select elements + { + type: 3, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'select', + attributes: { value: 'valueB' }, + childNodes: [], + id: 26, + }, + }, + { + parentId: 26, + nextId: null, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueC' }, + childNodes: [], + id: 27, + }, + }, + { + parentId: 27, + nextId: null, + node: { type: 3, textContent: 'C', id: 28 }, + }, + { + parentId: 26, + nextId: 27, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueB', selected: true }, + childNodes: [], + id: 29, + }, + }, + { + parentId: 26, + nextId: 29, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueA' }, + childNodes: [], + id: 30, + }, + }, + { + parentId: 30, + nextId: null, + node: { type: 3, textContent: 'A', id: 31 }, + }, + { + parentId: 29, + nextId: null, + node: { type: 3, textContent: 'B', id: 32 }, + }, + ], + }, + timestamp: now + 1000, + }, + // input event + { + type: 3, + data: { + source: IncrementalSource.Input, + text: 'valueA', + isChecked: false, + id: 26, + }, + timestamp: now + 1500, + }, + // input event + { + type: 3, + data: { + source: IncrementalSource.Input, + text: 'valueC', + isChecked: false, + id: 26, + }, + timestamp: now + 2000, + }, + // mutation that adds an input element + { + type: 3, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'input', + attributes: {}, + childNodes: [], + id: 33, + }, + }, + ], + }, + timestamp: now + 2500, + }, + // an input event + { + type: 3, + data: { + source: IncrementalSource.Input, + text: 'test input', + isChecked: false, + id: 33, + }, + timestamp: now + 3000, + }, + // remove the select element + { + type: 3, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 26 }], + adds: [], + }, + timestamp: now + 3500, + }, + // remove the input element + { + type: 3, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 33 }], + adds: [], + }, + timestamp: now + 4000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index ef5b949bb4..26c4ee3ec8 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -11,6 +11,7 @@ import { } from './utils'; import styleSheetRuleEvents from './events/style-sheet-rule-events'; import orderingEvents from './events/ordering'; +import inputEvents from './events/input'; interface ISuite { code: string; @@ -222,6 +223,83 @@ describe('replayer', function () { expect(result).toEqual(false); }); + it('can fast forward input events', async () => { + await page.evaluate(` + events = ${JSON.stringify(inputEvents)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events); + replayer.pause(1050); + `); + let iframe = await page.$('iframe'); + let contentDocument = await iframe!.contentFrame()!; + expect(await contentDocument!.$('select')).not.toBeNull(); + expect( + await contentDocument!.$eval( + 'select', + (element: Element) => (element as HTMLSelectElement).value, + ), + ).toEqual('valueB'); // the default value + + const delay = 50; + // restart the replayer + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + + await page.evaluate('replayer.pause(1550);'); + // the value get changed to 'valueA' at 1500 + expect( + await contentDocument!.$eval( + 'select', + (element: Element) => (element as HTMLSelectElement).value, + ), + ).toEqual('valueA'); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(2050);'); + // the value get changed to 'valueC' at 2000 + expect( + await contentDocument!.$eval( + 'select', + (element: Element) => (element as HTMLSelectElement).value, + ), + ).toEqual('valueC'); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(2550);'); + // add a new input element at 2500 + expect( + await contentDocument!.$eval( + 'input', + (element: Element) => (element as HTMLSelectElement).value, + ), + ).toEqual(''); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(3050);'); + // set the value 'test input' for the input element at 3000 + expect( + await contentDocument!.$eval( + 'input', + (element: Element) => (element as HTMLSelectElement).value, + ), + ).toEqual('test input'); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(3550);'); + // remove the select element at 3500 + expect(await contentDocument!.$('select')).toBeNull(); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(4050);'); + // remove the input element at 4000 + expect(await contentDocument!.$('input')).toBeNull(); + }); + it('can stream events in live mode', async () => { const status = await page.evaluate(` const { Replayer } = rrweb; From b2a0cbeccbb260d6750c057321810f6f72de270d Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 29 Jan 2022 23:39:06 +1100 Subject: [PATCH 16/79] add test for fast-forward scroll events --- packages/rrweb/test/events/input.ts | 14 +-- packages/rrweb/test/events/scroll.ts | 128 +++++++++++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 67 +++++++++++++- 3 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 packages/rrweb/test/events/scroll.ts diff --git a/packages/rrweb/test/events/input.ts b/packages/rrweb/test/events/input.ts index 072eea6f7e..a6dc13b0fb 100644 --- a/packages/rrweb/test/events/input.ts +++ b/packages/rrweb/test/events/input.ts @@ -60,7 +60,7 @@ const events: eventWithTime[] = [ }, // mutation that adds select elements { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Mutation, texts: [], @@ -132,7 +132,7 @@ const events: eventWithTime[] = [ }, // input event { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Input, text: 'valueA', @@ -143,7 +143,7 @@ const events: eventWithTime[] = [ }, // input event { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Input, text: 'valueC', @@ -154,7 +154,7 @@ const events: eventWithTime[] = [ }, // mutation that adds an input element { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Mutation, texts: [], @@ -178,7 +178,7 @@ const events: eventWithTime[] = [ }, // an input event { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Input, text: 'test input', @@ -189,7 +189,7 @@ const events: eventWithTime[] = [ }, // remove the select element { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Mutation, texts: [], @@ -201,7 +201,7 @@ const events: eventWithTime[] = [ }, // remove the input element { - type: 3, + type: EventType.IncrementalSnapshot, data: { source: IncrementalSource.Mutation, texts: [], diff --git a/packages/rrweb/test/events/scroll.ts b/packages/rrweb/test/events/scroll.ts new file mode 100644 index 0000000000..5069b5fe11 --- /dev/null +++ b/packages/rrweb/test/events/scroll.ts @@ -0,0 +1,128 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds two div elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: { + id: 'container', + style: 'height: 1000px; overflow: scroll;', + }, + childNodes: [], + id: 6, + }, + }, + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: { + id: 'block', + style: 'height: 10000px; background-color: yellow;', + }, + childNodes: [], + id: 7, + }, + }, + ], + }, + timestamp: now + 500, + }, + // scroll event on the "#container" div + { + type: EventType.IncrementalSnapshot, + data: { source: IncrementalSource.Scroll, id: 6, x: 0, y: 2500 }, + timestamp: now + 1000, + }, + // scroll event on document + { + type: EventType.IncrementalSnapshot, + data: { source: IncrementalSource.Scroll, id: 1, x: 0, y: 250 }, + timestamp: now + 1500, + }, + // remove the "#container" div + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 6 }], + adds: [], + }, + timestamp: now + 2000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 26c4ee3ec8..d8e41f68f5 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -11,6 +11,7 @@ import { } from './utils'; import styleSheetRuleEvents from './events/style-sheet-rule-events'; import orderingEvents from './events/ordering'; +import scrollEvents from './events/scroll'; import inputEvents from './events/input'; interface ISuite { @@ -223,15 +224,75 @@ describe('replayer', function () { expect(result).toEqual(false); }); + it('can fast forward scroll events', async () => { + await page.evaluate(` + events = ${JSON.stringify(scrollEvents)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events,{showDebug:true}); + replayer.pause(550); + `); + // add the "#container" element at 500 + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; + expect(await contentDocument!.$('#container')).not.toBeNull(); + expect(await contentDocument!.$('#block')).not.toBeNull(); + expect( + await contentDocument!.$eval( + '#container', + (element: Element) => element.scrollTop, + ), + ).toEqual(0); + + const delay = 50; + // restart the replayer + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + + await page.evaluate('replayer.pause(1050);'); + // scroll the "#container" div' at 1000 + expect( + await contentDocument!.$eval( + '#container', + (element: Element) => element.scrollTop, + ), + ).toEqual(2500); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(1550);'); + // scroll the document at 1500 + expect( + await page.$eval( + 'iframe', + (element: Element) => + (element as HTMLIFrameElement)!.contentWindow!.scrollY, + ), + ).toEqual(250); + + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(2050);'); + // remove the "#container" element at 2000 + expect(await contentDocument!.$('#container')).toBeNull(); + expect(await contentDocument!.$('#block')).toBeNull(); + expect( + await page.$eval( + 'iframe', + (element: Element) => + (element as HTMLIFrameElement)!.contentWindow!.scrollY, + ), + ).toEqual(0); + }); + it('can fast forward input events', async () => { await page.evaluate(` events = ${JSON.stringify(inputEvents)}; const { Replayer } = rrweb; - var replayer = new Replayer(events); + var replayer = new Replayer(events,{showDebug:true}); replayer.pause(1050); `); - let iframe = await page.$('iframe'); - let contentDocument = await iframe!.contentFrame()!; + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; expect(await contentDocument!.$('select')).not.toBeNull(); expect( await contentDocument!.$eval( From e3a3da73a7c7ffe0c57d64a620f7a7ba3ee03f55 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 3 Feb 2022 18:45:13 +1100 Subject: [PATCH 17/79] fix: custom style rules don't get inserted into some iframe elements --- packages/rrdom/src/document-browser.ts | 8 +- packages/rrweb/src/replay/index.ts | 29 +- packages/rrweb/test/events/iframe.ts | 591 +++++++++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 110 +++++ 4 files changed, 712 insertions(+), 26 deletions(-) create mode 100644 packages/rrweb/test/events/iframe.ts diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index deaa00feae..ca6b4749e1 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -120,17 +120,17 @@ export class RRDocument extends RRNode { get body() { return ( - this.documentElement?.children.find( + (this.documentElement?.children.find( (node) => node instanceof RRElement && node.tagName === 'BODY', - ) || null + ) as RRElement) || null ); } get head() { return ( - this.documentElement?.children.find( + (this.documentElement?.children.find( (node) => node instanceof RRElement && node.tagName === 'HEAD', - ) || null + ) as RRElement) || null ); } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 53f3734651..8b39ca47e1 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -646,13 +646,6 @@ export class Replayer { this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, ); - if (builtNode.contentDocument) { - const { documentElement, head } = builtNode.contentDocument; - this.insertStyleRules( - documentElement, - head! as HTMLHeadElement | RRElement, - ); - } } const { documentElement, head } = this.iframe.contentDocument; this.insertStyleRules(documentElement, head); @@ -674,7 +667,6 @@ export class Replayer { documentElement: HTMLElement | RRElement, head: HTMLHeadElement | RRElement, ) { - // TODO add unit tests const injectStylesRules = getInjectStyleRules( this.config.blockClass, ).concat(this.config.insertStyleRules); @@ -723,6 +715,13 @@ export class Replayer { skipChild: false, afterAppend: (builtNode) => { this.collectIframeAndAttachDocument(collected, builtNode); + if ( + builtNode.nodeType === builtNode.ELEMENT_NODE && + ((builtNode as Node) as HTMLElement).tagName.toUpperCase() === 'HTML' + ) { + const { documentElement, head } = iframeEl.contentDocument!; + this.insertStyleRules(documentElement, head); + } }, cache: this.cache, }); @@ -731,13 +730,6 @@ export class Replayer { this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, ); - if (builtNode.contentDocument) { - const { documentElement, head } = builtNode.contentDocument; - this.insertStyleRules( - documentElement, - head! as HTMLHeadElement | RRElement, - ); - } } } @@ -1410,13 +1402,6 @@ export class Replayer { (m) => m !== mutationInQueue, ); } - if (target.contentDocument) { - const { documentElement, head } = target.contentDocument; - this.insertStyleRules( - documentElement, - head! as HTMLHeadElement | RRElement, - ); - } } if (mutation.previousId || mutation.nextId) { diff --git a/packages/rrweb/test/events/iframe.ts b/packages/rrweb/test/events/iframe.ts new file mode 100644 index 0000000000..d4110d2070 --- /dev/null +++ b/packages/rrweb/test/events/iframe.ts @@ -0,0 +1,591 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + timestamp: now + 200, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'one' }, + childNodes: [], + id: 6, + }, + }, + ], + }, + timestamp: now + 500, + }, + // add iframe one + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 7, + id: 8, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 7, + id: 10, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n\t\tiframe 1\n\t', + rootId: 7, + id: 13, + }, + ], + rootId: 7, + id: 12, + }, + { type: 3, textContent: '\n\t', rootId: 7, id: 14 }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 7, + id: 16, + }, + ], + rootId: 7, + id: 15, + }, + { type: 3, textContent: '\t\n', rootId: 7, id: 17 }, + ], + rootId: 7, + id: 11, + }, + ], + rootId: 7, + id: 9, + }, + ], + id: 7, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'two' }, + childNodes: [], + id: 38, + }, + }, + ], + }, + timestamp: now + 1000, + }, + // add iframe two + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 38, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 39, + id: 40, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', rootId: 39, id: 43 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + rootId: 39, + id: 44, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 45 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + rootId: 39, + id: 46, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 47 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'iframe 2', + rootId: 39, + id: 49, + }, + ], + rootId: 39, + id: 48, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 50 }, + ], + rootId: 39, + id: 42, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 51 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n iframe 2\n ', + rootId: 39, + id: 53, + }, + { + type: 2, + tagName: 'iframe', + attributes: { id: 'three', frameborder: '0' }, + childNodes: [], + rootId: 39, + id: 54, + }, + { type: 3, textContent: '\n ', rootId: 39, id: 55 }, + { + type: 2, + tagName: 'iframe', + attributes: { id: 'four', frameborder: '0' }, + childNodes: [], + rootId: 39, + id: 56, + }, + { type: 3, textContent: '\n \n\n', rootId: 39, id: 57 }, + ], + rootId: 39, + id: 52, + }, + ], + rootId: 39, + id: 41, + }, + ], + id: 39, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1000, + }, + // add iframe three + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 54, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 58, + id: 60, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + rootId: 58, + id: 61, + }, + ], + rootId: 58, + id: 59, + }, + ], + id: 58, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1000, + }, + // add iframe four + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 56, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 1, + name: 'html', + publicId: '', + systemId: '', + rootId: 62, + id: 63, + }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 3, textContent: '\n ', rootId: 62, id: 66 }, + { + type: 2, + tagName: 'meta', + attributes: { charset: 'UTF-8' }, + childNodes: [], + rootId: 62, + id: 67, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 68 }, + { + type: 2, + tagName: 'meta', + attributes: { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + childNodes: [], + rootId: 62, + id: 69, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 70 }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'iframe 4', + rootId: 62, + id: 72, + }, + ], + rootId: 62, + id: 71, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 73 }, + ], + rootId: 62, + id: 65, + }, + { type: 3, textContent: '\n ', rootId: 62, id: 74 }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: '\n iframe 4\n \n ', + rootId: 62, + id: 76, + }, + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 62, + id: 78, + }, + ], + rootId: 62, + id: 77, + }, + { type: 3, textContent: '\n\n', rootId: 62, id: 79 }, + ], + rootId: 62, + id: 75, + }, + ], + rootId: 62, + id: 64, + }, + ], + id: 62, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 1500, + }, + // add iframe five + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: 80, + nextId: null, + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + rootId: 81, + id: 83, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'script', + attributes: {}, + childNodes: [ + { + type: 3, + textContent: 'SCRIPT_PLACEHOLDER', + rootId: 81, + id: 86, + }, + ], + rootId: 81, + id: 85, + }, + ], + rootId: 81, + id: 84, + }, + ], + rootId: 81, + id: 82, + }, + ], + id: 81, + }, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + timestamp: now + 2000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 75, + nextId: null, + node: { + type: 2, + tagName: 'iframe', + attributes: { id: 'five' }, + childNodes: [], + rootId: 62, + id: 80, + }, + }, + ], + }, + timestamp: now + 2000, + }, + // remove the html element of iframe four + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 62, id: 64 }], + adds: [], + }, + timestamp: now + 2500, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index d8e41f68f5..8eee742389 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -13,6 +13,7 @@ import styleSheetRuleEvents from './events/style-sheet-rule-events'; import orderingEvents from './events/ordering'; import scrollEvents from './events/scroll'; import inputEvents from './events/input'; +import iframeEvents from './events/iframe'; interface ISuite { code: string; @@ -361,6 +362,115 @@ describe('replayer', function () { expect(await contentDocument!.$('input')).toBeNull(); }); + it('can fast-forward mutation events containing nested iframe elements', async () => { + await page.evaluate(` + events = ${JSON.stringify(iframeEvents)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events,{showDebug:true}); + replayer.pause(250); + `); + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; + expect(await contentDocument!.$('iframe')).toBeNull(); + + const delay = 50; + // restart the replayer + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500 + expect(await contentDocument!.$('iframe')).not.toBeNull(); + const iframeOneDocument = await (await contentDocument!.$( + 'iframe', + ))!.contentFrame(); + expect(iframeOneDocument).not.toBeNull(); + expect(await iframeOneDocument!.$('noscript')).not.toBeNull(); + // make sure custom style rules are inserted rules + expect((await iframeOneDocument!.$$('style')).length).toBe(1); + expect( + await iframeOneDocument!.$eval( + 'noscript', + (element) => window.getComputedStyle(element).display, + ), + ).toEqual('none'); + + // add 'iframe two' and 'iframe three' at 1000 + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(1050);'); + expect((await contentDocument!.$$('iframe')).length).toEqual(2); + let iframeTwoDocument = await ( + await contentDocument!.$$('iframe') + )[1]!.contentFrame(); + expect(iframeTwoDocument).not.toBeNull(); + expect((await iframeTwoDocument!.$$('iframe')).length).toEqual(2); + let iframeThreeDocument = await ( + await iframeTwoDocument!.$$('iframe') + )[0]!.contentFrame(); + let iframeFourDocument = await ( + await iframeTwoDocument!.$$('iframe') + )[1]!.contentFrame(); + expect(iframeThreeDocument).not.toBeNull(); + expect(iframeFourDocument).not.toBeNull(); + + // add 'iframe four' at 1500 + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(1550);'); + iframeTwoDocument = await ( + await contentDocument!.$$('iframe') + )[1]!.contentFrame(); + iframeFourDocument = await ( + await iframeTwoDocument!.$$('iframe') + )[1]!.contentFrame(); + expect(await iframeFourDocument!.$('iframe')).toBeNull(); + expect(await iframeFourDocument!.$('style')).not.toBeNull(); + expect(await iframeFourDocument!.title()).toEqual('iframe 4'); + + // add 'iframe five' at 2000 + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(2050);'); + iframeTwoDocument = await ( + await contentDocument!.$$('iframe') + )[1]!.contentFrame(); + iframeFourDocument = await ( + await iframeTwoDocument!.$$('iframe') + )[1]!.contentFrame(); + expect(await iframeFourDocument!.$('iframe')).not.toBeNull(); + const iframeFiveDocument = await (await iframeFourDocument!.$( + 'iframe', + ))!.contentFrame(); + expect(iframeFiveDocument).not.toBeNull(); + expect((await iframeFiveDocument!.$$('style')).length).toBe(1); + expect(await iframeFiveDocument!.$('noscript')).not.toBeNull(); + expect( + await iframeFiveDocument!.$eval( + 'noscript', + (element) => window.getComputedStyle(element).display, + ), + ).toEqual('none'); + + // remove the html element of 'iframe four' at 2500 + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(delay); + await page.evaluate('replayer.pause(2550);'); + iframeTwoDocument = await ( + await contentDocument!.$$('iframe') + )[1]!.contentFrame(); + iframeFourDocument = await ( + await iframeTwoDocument!.$$('iframe') + )[1]!.contentFrame(); + // the html element should be removed + expect(await iframeFourDocument!.$('html')).toBeNull(); + // the doctype should still exist + expect( + await iframeTwoDocument!.evaluate( + (iframe) => (iframe as HTMLIFrameElement)!.contentDocument!.doctype, + (await iframeTwoDocument!.$$('iframe'))[1], + ), + ).not.toBeNull(); + }); + it('can stream events in live mode', async () => { const status = await page.evaluate(` const { Replayer } = rrweb; From 150a40fe36f20ffb9dc3a2f16ec9bebbcc63481f Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 3 Feb 2022 18:56:23 +1100 Subject: [PATCH 18/79] code style tweak --- packages/rrweb/src/replay/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 8b39ca47e1..1a40b8a324 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -716,8 +716,8 @@ export class Replayer { afterAppend: (builtNode) => { this.collectIframeAndAttachDocument(collected, builtNode); if ( - builtNode.nodeType === builtNode.ELEMENT_NODE && - ((builtNode as Node) as HTMLElement).tagName.toUpperCase() === 'HTML' + builtNode.__sn.type === NodeType.Element && + builtNode.__sn.tagName.toUpperCase() === 'HTML' ) { const { documentElement, head } = iframeEl.contentDocument!; this.insertStyleRules(documentElement, head); From 32e06c714c264f9f6f4ff8ecba1c514ed3a6e105 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Feb 2022 14:53:47 +1100 Subject: [PATCH 19/79] fix: enable to diff iframe elements --- packages/rrdom/src/diff.ts | 34 +++++++++++++++++++++++------- packages/rrweb/src/replay/index.ts | 7 ++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 344d26b948..44e92922c0 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -104,13 +104,20 @@ export function diff( ); } // IFrame element doesn't have child nodes. - if (newTree instanceof RRIFrameElement) - diff( - (((oldTree as Node) as HTMLIFrameElement) - .contentDocument! as unknown) as INode, - newTree.contentDocument, - replayer, - ); + if (newTree instanceof RRIFrameElement) { + const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) + .contentDocument as unknown) as INode; + // If the iframe is cross-origin, the contentDocument will be null. + if (oldContentDocument) { + if (newTree.contentDocument.__sn) { + oldContentDocument.__sn = newTree.contentDocument.__sn; + replayer.mirror.map[ + newTree.contentDocument.__sn.id + ] = oldContentDocument; + } + diff(oldContentDocument, newTree.contentDocument, replayer); + } + } scrollDataToApply && replayer.applyScroll(scrollDataToApply, true); /** @@ -195,7 +202,18 @@ function diffChildren( oldChildren[indexInOld] = undefined; } else { const newNode = createOrGetNode(newStartNode, replayer.mirror); - parentNode.insertBefore(newNode, oldStartNode); + if ( + parentNode.__sn.type === NodeType.Document && + newNode.__sn.type === NodeType.Element && + newNode.__sn.tagName.toUpperCase() === 'HTML' + ) { + parentNode.removeChild( + ((parentNode as Node) as Document).documentElement, + ); + oldChildren[oldStartIndex] = undefined; + oldStartNode = undefined; + } + parentNode.insertBefore(newNode, oldStartNode || null); diff(newNode, newStartNode, replayer); } newStartNode = newChildren[++newStartIndex]; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 1a40b8a324..e76c42d3bd 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -677,6 +677,13 @@ export class Replayer { } if (this.usingRRDom) { const styleEl = this.rrdom.createElement('style') as RRStyleElement; + styleEl.__sn = { + type: NodeType.Element, + tagName: 'style', + childNodes: [], + attributes: {}, + id: this.rrdom._notSerializedId, + }; (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { // push virtual styles From 0ba43493631bc43053a4c02f7a2628cce2250c0d Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Feb 2022 17:28:31 +1100 Subject: [PATCH 20/79] fix the jest error "Unexpected token 'export'" --- packages/rrdom/src/document-browser.ts | 17 ----------------- packages/rrweb/jest.config.js | 10 ++++++++++ .../test/replay/preload-all-images.test.ts | 1 - packages/rrweb/test/replay/webgl.test.ts | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index ca6b4749e1..37c36316cf 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -733,20 +733,3 @@ export type VirtualStyleRules = Array< >; export { diff } from './diff'; -/** - * Print the RRDom as a string. - * @param rootNode the root node of the RRDom tree - * @returns printed string - */ -export function printRRDom(rootNode: RRNode) { - return walk(rootNode, ''); -} - -function walk(node: RRNode, blankSpace: string) { - let printText = `${blankSpace}${node.toString()}\n`; - for (const child of node.childNodes) - printText += walk(child, blankSpace + ' '); - if (node instanceof RRIFrameElement) - printText += walk(node.contentDocument, blankSpace + ' '); - return printText; -} diff --git a/packages/rrweb/jest.config.js b/packages/rrweb/jest.config.js index 29db4e7fa0..7403e73daa 100644 --- a/packages/rrweb/jest.config.js +++ b/packages/rrweb/jest.config.js @@ -6,4 +6,14 @@ module.exports = { moduleNameMapper: { '\\.css$': 'identity-obj-proxy', }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + globals: { + 'ts-jest': { + tsconfig: { + allowJs: true, + }, + }, + }, }; diff --git a/packages/rrweb/test/replay/preload-all-images.test.ts b/packages/rrweb/test/replay/preload-all-images.test.ts index 4b4e0a4c71..c021996474 100644 --- a/packages/rrweb/test/replay/preload-all-images.test.ts +++ b/packages/rrweb/test/replay/preload-all-images.test.ts @@ -5,7 +5,6 @@ import { polyfillWebGLGlobals } from '../utils'; polyfillWebGLGlobals(); import { Replayer } from '../../src/replay'; -import {} from '../../src/types'; import { CanvasContext, SerializedWebGlArg, diff --git a/packages/rrweb/test/replay/webgl.test.ts b/packages/rrweb/test/replay/webgl.test.ts index f7d0498f17..4fe538219e 100644 --- a/packages/rrweb/test/replay/webgl.test.ts +++ b/packages/rrweb/test/replay/webgl.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { assertDomSnapshot, launchPuppeteer } from '../utils'; +import { launchPuppeteer } from '../utils'; import { toMatchImageSnapshot } from 'jest-image-snapshot'; import * as puppeteer from 'puppeteer'; import events from '../events/webgl'; From 5bc3bbda559aac85fa9c0616d3f14d64975f3e41 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Feb 2022 18:36:34 +1100 Subject: [PATCH 21/79] try to fix build error of rrweb-player --- packages/rrweb/src/record/mutation.ts | 2 +- packages/rrweb/src/record/observer.ts | 2 +- packages/rrweb/src/record/observers/canvas/2d.ts | 2 +- .../rrweb/src/record/observers/canvas/canvas-manager.ts | 4 ++-- packages/rrweb/src/record/observers/canvas/canvas.ts | 4 ++-- .../rrweb/src/record/observers/canvas/serialize-args.ts | 2 +- packages/rrweb/src/record/observers/canvas/webgl.ts | 2 +- packages/rrweb/src/record/shadow-dom-manager.ts | 2 +- packages/rrweb/src/replay/canvas/2d.ts | 4 ++-- packages/rrweb/src/replay/canvas/index.ts | 2 +- packages/rrweb/src/replay/canvas/webgl.ts | 7 +++---- packages/rrweb/src/types.ts | 2 +- packages/rrweb/typings/record/mutation.d.ts | 2 +- packages/rrweb/typings/record/observer.d.ts | 2 +- .../typings/record/observers/canvas/canvas-manager.d.ts | 2 +- packages/rrweb/typings/record/observers/canvas/canvas.d.ts | 2 +- .../typings/record/observers/canvas/serialize-args.d.ts | 2 +- packages/rrweb/typings/record/shadow-dom-manager.d.ts | 2 +- packages/rrweb/typings/replay/canvas/2d.d.ts | 4 ++-- packages/rrweb/typings/replay/canvas/index.d.ts | 2 +- packages/rrweb/typings/replay/canvas/webgl.d.ts | 2 +- packages/rrweb/typings/types.d.ts | 2 +- 22 files changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index e2d1783db3..8385439197 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -32,7 +32,7 @@ import { } from '../utils'; import type { IframeManager } from './iframe-manager'; import type { ShadowDomManager } from './shadow-dom-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; type DoubleLinkedListNode = { previous: DoubleLinkedListNode | null; diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 078f61e129..ca6fd1928b 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -49,7 +49,7 @@ import { import MutationBuffer from './mutation'; import type { IframeManager } from './iframe-manager'; import type { ShadowDomManager } from './shadow-dom-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; type WindowWithStoredMutationObserver = IWindow & { __rrMutationObserver?: MutationObserver; diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts index dd63469b1e..72cbc7dc7b 100644 --- a/packages/rrweb/src/record/observers/canvas/2d.ts +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -1,4 +1,4 @@ -import { INode } from 'rrweb-snapshot'; +import type { INode } from 'rrweb-snapshot'; import { blockClass, CanvasContext, diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index b7fac7c67b..d625f321f3 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -1,5 +1,5 @@ -import { INode } from 'rrweb-snapshot'; -import { +import type { INode } from 'rrweb-snapshot'; +import type { blockClass, canvasManagerMutationCallback, canvasMutationCallback, diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts index 437af7d5f3..74f70b5468 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas.ts @@ -1,5 +1,5 @@ -import { INode, ICanvas } from 'rrweb-snapshot'; -import { blockClass, IWindow, listenerHandler } from '../../../types'; +import type { INode, ICanvas } from 'rrweb-snapshot'; +import type { blockClass, IWindow, listenerHandler } from '../../../types'; import { isBlocked, patch } from '../../../utils'; export default function initCanvasContextObserver( diff --git a/packages/rrweb/src/record/observers/canvas/serialize-args.ts b/packages/rrweb/src/record/observers/canvas/serialize-args.ts index e245ee4123..75e9276762 100644 --- a/packages/rrweb/src/record/observers/canvas/serialize-args.ts +++ b/packages/rrweb/src/record/observers/canvas/serialize-args.ts @@ -1,5 +1,5 @@ import { encode } from 'base64-arraybuffer'; -import { IWindow, SerializedWebGlArg } from '../../../types'; +import type { IWindow, SerializedWebGlArg } from '../../../types'; // TODO: unify with `replay/webgl.ts` type GLVarMap = Map; diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts index 45b03928f3..d4d4373a10 100644 --- a/packages/rrweb/src/record/observers/canvas/webgl.ts +++ b/packages/rrweb/src/record/observers/canvas/webgl.ts @@ -1,4 +1,4 @@ -import { INode } from 'rrweb-snapshot'; +import type { INode } from 'rrweb-snapshot'; import { blockClass, CanvasContext, diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 631511a8e9..1f895cf4a3 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -14,7 +14,7 @@ import type { } from 'rrweb-snapshot'; import type { IframeManager } from './iframe-manager'; import { initMutationObserver, initScrollObserver } from './observer'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; type BypassOptions = { blockClass: blockClass; diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts index b9fde639b3..23980ccf2c 100644 --- a/packages/rrweb/src/replay/canvas/2d.ts +++ b/packages/rrweb/src/replay/canvas/2d.ts @@ -1,5 +1,5 @@ -import { Replayer } from '../'; -import { canvasMutationCommand } from '../../types'; +import type { Replayer } from '../'; +import type { canvasMutationCommand } from '../../types'; export default function canvasMutation({ event, diff --git a/packages/rrweb/src/replay/canvas/index.ts b/packages/rrweb/src/replay/canvas/index.ts index 73411a2b10..6246af353f 100644 --- a/packages/rrweb/src/replay/canvas/index.ts +++ b/packages/rrweb/src/replay/canvas/index.ts @@ -1,4 +1,4 @@ -import { Replayer } from '..'; +import type { Replayer } from '..'; import { CanvasContext, canvasMutationCommand, diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts index 58d323dc3d..bdb9ce29b2 100644 --- a/packages/rrweb/src/replay/canvas/webgl.ts +++ b/packages/rrweb/src/replay/canvas/webgl.ts @@ -1,5 +1,5 @@ import { decode } from 'base64-arraybuffer'; -import { Replayer } from '../'; +import type { Replayer } from '../'; import { CanvasContext, canvasMutationCommand, @@ -36,9 +36,8 @@ function getContext( // you might have to do `ctx.flush()` before every webgl canvas event try { if (type === CanvasContext.WebGL) { - return ( - target.getContext('webgl')! || target.getContext('experimental-webgl') - ); + return (target.getContext('webgl')! || + target.getContext('experimental-webgl')) as WebGLRenderingContext; } return target.getContext('webgl2')!; } catch (e) { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 81c40c5932..19943a97bd 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -12,7 +12,7 @@ import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; import type { RRNode } from 'rrdom/es/document-browser'; -import { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export enum EventType { DomContentLoaded, diff --git a/packages/rrweb/typings/record/mutation.d.ts b/packages/rrweb/typings/record/mutation.d.ts index a6c327f68f..4cc44a0641 100644 --- a/packages/rrweb/typings/record/mutation.d.ts +++ b/packages/rrweb/typings/record/mutation.d.ts @@ -2,7 +2,7 @@ import { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb import type { mutationRecord, blockClass, maskTextClass, mutationCallBack, Mirror } from '../types'; import type { IframeManager } from './iframe-manager'; import type { ShadowDomManager } from './shadow-dom-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; export default class MutationBuffer { private frozen; private locked; diff --git a/packages/rrweb/typings/record/observer.d.ts b/packages/rrweb/typings/record/observer.d.ts index 7f1bffeb39..4304a2b7c5 100644 --- a/packages/rrweb/typings/record/observer.d.ts +++ b/packages/rrweb/typings/record/observer.d.ts @@ -3,7 +3,7 @@ import { mutationCallBack, observerParam, listenerHandler, scrollCallback, block import MutationBuffer from './mutation'; import type { IframeManager } from './iframe-manager'; import type { ShadowDomManager } from './shadow-dom-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; export declare const mutationBuffers: MutationBuffer[]; export declare function initMutationObserver(cb: mutationCallBack, doc: Document, blockClass: blockClass, blockSelector: string | null, maskTextClass: maskTextClass, maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, maskTextFn: MaskTextFn | undefined, maskInputFn: MaskInputFn | undefined, recordCanvas: boolean, inlineImages: boolean, slimDOMOptions: SlimDOMOptions, mirror: Mirror, iframeManager: IframeManager, shadowDomManager: ShadowDomManager, canvasManager: CanvasManager, rootEl: Node): MutationObserver; export declare function initScrollObserver(cb: scrollCallback, doc: Document, mirror: Mirror, blockClass: blockClass, sampling: SamplingStrategy): listenerHandler; diff --git a/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts b/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts index 56c136c55c..c9ed77c52c 100644 --- a/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts @@ -1,4 +1,4 @@ -import { blockClass, canvasMutationCallback, IWindow, Mirror } from '../../../types'; +import type { blockClass, canvasMutationCallback, IWindow, Mirror } from '../../../types'; export declare type RafStamps = { latestId: number; invokeId: number | null; diff --git a/packages/rrweb/typings/record/observers/canvas/canvas.d.ts b/packages/rrweb/typings/record/observers/canvas/canvas.d.ts index 359d95928d..c35e97aa55 100644 --- a/packages/rrweb/typings/record/observers/canvas/canvas.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/canvas.d.ts @@ -1,2 +1,2 @@ -import { blockClass, IWindow, listenerHandler } from '../../../types'; +import type { blockClass, IWindow, listenerHandler } from '../../../types'; export default function initCanvasContextObserver(win: IWindow, blockClass: blockClass): listenerHandler; diff --git a/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts index 5a1c90ce77..7692c34f97 100644 --- a/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts @@ -1,4 +1,4 @@ -import { IWindow, SerializedWebGlArg } from '../../../types'; +import type { IWindow, SerializedWebGlArg } from '../../../types'; export declare function variableListFor(ctx: WebGLRenderingContext | WebGL2RenderingContext, ctor: string): any[]; export declare const saveWebGLVar: (value: any, win: IWindow, ctx: WebGL2RenderingContext | WebGLRenderingContext) => number | void; export declare function serializeArg(value: any, win: IWindow, ctx: WebGL2RenderingContext | WebGLRenderingContext): SerializedWebGlArg; diff --git a/packages/rrweb/typings/record/shadow-dom-manager.d.ts b/packages/rrweb/typings/record/shadow-dom-manager.d.ts index f70ca6ff2a..d74d39a644 100644 --- a/packages/rrweb/typings/record/shadow-dom-manager.d.ts +++ b/packages/rrweb/typings/record/shadow-dom-manager.d.ts @@ -1,7 +1,7 @@ import type { mutationCallBack, blockClass, maskTextClass, Mirror, scrollCallback, SamplingStrategy } from '../types'; import type { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot'; import type { IframeManager } from './iframe-manager'; -import { CanvasManager } from './observers/canvas/canvas-manager'; +import type { CanvasManager } from './observers/canvas/canvas-manager'; declare type BypassOptions = { blockClass: blockClass; blockSelector: string | null; diff --git a/packages/rrweb/typings/replay/canvas/2d.d.ts b/packages/rrweb/typings/replay/canvas/2d.d.ts index 338cbf28f9..6a6cb10a5b 100644 --- a/packages/rrweb/typings/replay/canvas/2d.d.ts +++ b/packages/rrweb/typings/replay/canvas/2d.d.ts @@ -1,5 +1,5 @@ -import { Replayer } from '../'; -import { canvasMutationCommand } from '../../types'; +import type { Replayer } from '../'; +import type { canvasMutationCommand } from '../../types'; export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: { event: Parameters[0]; mutation: canvasMutationCommand; diff --git a/packages/rrweb/typings/replay/canvas/index.d.ts b/packages/rrweb/typings/replay/canvas/index.d.ts index 72c6bdfce3..085fdb9a33 100644 --- a/packages/rrweb/typings/replay/canvas/index.d.ts +++ b/packages/rrweb/typings/replay/canvas/index.d.ts @@ -1,4 +1,4 @@ -import { Replayer } from '..'; +import type { Replayer } from '..'; import { canvasMutationData } from '../../types'; export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: { event: Parameters[0]; diff --git a/packages/rrweb/typings/replay/canvas/webgl.d.ts b/packages/rrweb/typings/replay/canvas/webgl.d.ts index 6e51941047..28dfe8e6b3 100644 --- a/packages/rrweb/typings/replay/canvas/webgl.d.ts +++ b/packages/rrweb/typings/replay/canvas/webgl.d.ts @@ -1,4 +1,4 @@ -import { Replayer } from '../'; +import type { Replayer } from '../'; import { CanvasContext, canvasMutationCommand, SerializedWebGlArg } from '../../types'; export declare function variableListFor(ctx: WebGLRenderingContext | WebGL2RenderingContext, ctor: string): any[]; export declare function deserializeArg(imageMap: Replayer['imageMap'], ctx: WebGLRenderingContext | WebGL2RenderingContext): (arg: SerializedWebGlArg) => any; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 2db4503c9d..dc57d457e7 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -5,7 +5,7 @@ import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; import type { RRNode } from 'rrdom/es/document-browser'; -import { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export declare enum EventType { DomContentLoaded = 0, Load = 1, From 7ca376ded7bf7d9451b7cc96bb8dacb5f52a08a4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Feb 2022 19:13:15 +1100 Subject: [PATCH 22/79] correct the attributes definition in rrdom --- packages/rrdom/src/diff.ts | 11 +++-------- packages/rrdom/src/document-browser.ts | 4 ++-- packages/rrdom/src/document-nodejs.ts | 2 +- packages/rrdom/test/diff.test.ts | 21 --------------------- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 44e92922c0..a88fe04802 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -133,14 +133,9 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { for (const name in newAttributes) { const newValue = newAttributes[name]; - if (typeof newValue === 'boolean') { - // TODO Some special cases for some kinds of elements. e.g. selected, rr_scrollLeft - } else if (typeof newValue === 'number') { - } else { - if ((newTree.__sn as elementNode).isSVG && NAMESPACES[name]) - oldTree.setAttributeNS(NAMESPACES[name], name, newValue); - else oldTree.setAttribute(name, newValue); - } + if ((newTree.__sn as elementNode).isSVG && NAMESPACES[name]) + oldTree.setAttributeNS(NAMESPACES[name], name, newValue); + else oldTree.setAttribute(name, newValue); } for (const { name } of Array.from(oldAttributes)) diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 37c36316cf..a68b679e0f 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -78,7 +78,7 @@ export abstract class RRNode { } export class RRDocument extends RRNode { - _notSerializedId = -1; // used as an id to identify not serialized node + private _notSerializedId = -1; // used as an id to identify not serialized node /** * Every time the id is used, it will minus 1 automatically to avoid collisions. */ @@ -443,7 +443,7 @@ export class RRDocumentType extends RRNode { export class RRElement extends RRNode { tagName: string; - attributes: Record = {}; + attributes: Record = {}; shadowRoot: RRElement | null = null; inputData: inputData | null = null; scrollData: scrollData | null = null; diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 8da8c835f2..4498814655 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -294,7 +294,7 @@ export class RRDocumentType extends RRNode { export class RRElement extends RRNode { tagName: string; - attributes: Record = {}; + attributes: Record = {}; scrollLeft: number = 0; scrollTop: number = 0; shadowRoot: RRElement | null = null; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 219f36e515..f3b8321338 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -175,27 +175,6 @@ describe('diff algorithm for rrdom', () => { expect(((node as Node) as HTMLElement).className).toBe(''); expect(((node as Node) as HTMLElement).getAttribute('src')).toBe('link'); }); - - it('omit "checked" property for input elements', () => { - const tagName = 'INPUT'; - const node = (document.createElement(tagName) as unknown) as INode; - node.__sn = Object.assign({}, elementSn, { tagName }); - ((node as Node) as HTMLElement).setAttribute('type', 'checkbox'); - ((node as Node) as HTMLElement).setAttribute('checked', ''); // default checked - const rrDocument = new RRDocument(); - const rrNode = rrDocument.createElement(tagName); - rrNode.attributes = { - checked: true, - }; - diff(node, rrNode, replayer); - expect(((node as Node) as HTMLElement).getAttribute('checked')).toBe(''); - - ((node as Node) as HTMLElement).removeAttribute('checked'); // default unchecked - diff(node, rrNode, replayer); - expect(((node as Node) as HTMLElement).getAttribute('checked')).toBe( - null, - ); - }); }); describe('diff children', () => { From 9c5ec6afb35931c188a58cc922d3ff5218c6a1c3 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Feb 2022 19:30:32 +1100 Subject: [PATCH 23/79] fix: custom style rules are not inserted in some iframes --- packages/rrdom/src/diff.ts | 5 +++++ packages/rrweb/src/replay/index.ts | 2 +- packages/rrweb/test/replayer.test.ts | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index a88fe04802..725c08e9bb 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -197,6 +197,11 @@ function diffChildren( oldChildren[indexInOld] = undefined; } else { const newNode = createOrGetNode(newStartNode, replayer.mirror); + + /** + * A mounted iframe element has an automatically created HTML element. + * We should delete it before insert a serialized one. Otherwise, an error 'Only one element on document allowed' will be thrown. + */ if ( parentNode.__sn.type === NodeType.Document && newNode.__sn.type === NodeType.Element && diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 344667f324..1e382d3944 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -684,7 +684,7 @@ export class Replayer { tagName: 'style', childNodes: [], attributes: {}, - id: this.rrdom._notSerializedId, + id: this.rrdom.notSerializedId, }; (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 8eee742389..a20e5ffd5d 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -379,7 +379,7 @@ describe('replayer', function () { await page.waitForTimeout(delay); await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500 expect(await contentDocument!.$('iframe')).not.toBeNull(); - const iframeOneDocument = await (await contentDocument!.$( + let iframeOneDocument = await (await contentDocument!.$( 'iframe', ))!.contentFrame(); expect(iframeOneDocument).not.toBeNull(); @@ -397,12 +397,19 @@ describe('replayer', function () { await page.evaluate('replayer.play(0);'); await page.waitForTimeout(delay); await page.evaluate('replayer.pause(1050);'); + // check the inserted style of iframe 'one' again + iframeOneDocument = await (await contentDocument!.$( + 'iframe', + ))!.contentFrame(); + expect((await iframeOneDocument!.$$('style')).length).toBe(1); + expect((await contentDocument!.$$('iframe')).length).toEqual(2); let iframeTwoDocument = await ( await contentDocument!.$$('iframe') )[1]!.contentFrame(); expect(iframeTwoDocument).not.toBeNull(); expect((await iframeTwoDocument!.$$('iframe')).length).toEqual(2); + expect((await iframeTwoDocument!.$$('style')).length).toBe(1); let iframeThreeDocument = await ( await iframeTwoDocument!.$$('iframe') )[0]!.contentFrame(); @@ -410,6 +417,7 @@ describe('replayer', function () { await iframeTwoDocument!.$$('iframe') )[1]!.contentFrame(); expect(iframeThreeDocument).not.toBeNull(); + expect((await iframeThreeDocument!.$$('style')).length).toBe(1); expect(iframeFourDocument).not.toBeNull(); // add 'iframe four' at 1500 @@ -419,11 +427,12 @@ describe('replayer', function () { iframeTwoDocument = await ( await contentDocument!.$$('iframe') )[1]!.contentFrame(); + expect((await iframeTwoDocument!.$$('style')).length).toBe(1); iframeFourDocument = await ( await iframeTwoDocument!.$$('iframe') )[1]!.contentFrame(); expect(await iframeFourDocument!.$('iframe')).toBeNull(); - expect(await iframeFourDocument!.$('style')).not.toBeNull(); + expect((await iframeFourDocument!.$$('style')).length).toBe(1); expect(await iframeFourDocument!.title()).toEqual('iframe 4'); // add 'iframe five' at 2000 @@ -436,6 +445,7 @@ describe('replayer', function () { iframeFourDocument = await ( await iframeTwoDocument!.$$('iframe') )[1]!.contentFrame(); + expect((await iframeFourDocument!.$$('style')).length).toBe(1); expect(await iframeFourDocument!.$('iframe')).not.toBeNull(); const iframeFiveDocument = await (await iframeFourDocument!.$( 'iframe', From 9bd5fd8f7041f64e6e0b43702d7a3f27002c4009 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 11 Feb 2022 01:38:22 +1100 Subject: [PATCH 24/79] add support for shadow dom --- packages/rrdom/src/diff.ts | 17 +- packages/rrdom/src/document-browser.ts | 29 ++- .../document-browser.test.ts.snap | 63 ++++++- packages/rrdom/test/diff.test.ts | 17 ++ packages/rrdom/test/document-browser.test.ts | 13 +- packages/rrdom/test/html/iframe.html | 1 + packages/rrdom/test/html/shadow-dom.html | 20 ++ packages/rrweb/test/events/shadow-dom.ts | 172 ++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 46 +++++ 9 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 packages/rrdom/test/html/shadow-dom.html create mode 100644 packages/rrweb/test/events/shadow-dom.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 725c08e9bb..3e2a54491b 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -79,16 +79,29 @@ export function diff( scrollDataToApply = newRRDocument.scrollData; break; case NodeType.Element: + const oldElement = (oldTree as Node) as HTMLElement; const newRRElement = newTree as RRElement; - diffProps((oldTree as unknown) as HTMLElement, newRRElement); + diffProps(oldElement, newRRElement); scrollDataToApply = newRRElement.scrollData; inputDataToApply = newRRElement.inputData; if (newTree instanceof RRStyleElement && newTree.rules.length > 0) { applyVirtualStyleRulesToNode( - (oldTree as Node) as HTMLStyleElement, + oldElement as HTMLStyleElement, newTree.rules, ); } + if (newRRElement.shadowRoot) { + if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' }); + const oldChildren = oldElement.shadowRoot!.childNodes; + const newChildren = newRRElement.shadowRoot.childNodes; + if (oldChildren.length > 0 || newChildren.length > 0) + diffChildren( + (Array.from(oldChildren) as unknown) as INode[], + newChildren, + (oldElement.shadowRoot! as unknown) as INode, + replayer, + ); + } break; // TODO: Diff other kinds of nodes. default: diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index a68b679e0f..203ec8ef9d 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -390,6 +390,10 @@ export function buildFromDom( ((node as unknown) as Comment).textContent || '', ); break; + // if node is a shadow root + case node.DOCUMENT_FRAGMENT_NODE: + rrNode = (parentRRNode as RRElement).attachShadow({ mode: 'open' }); + break; default: return; } @@ -398,11 +402,14 @@ export function buildFromDom( if (parentRRNode instanceof RRIFrameElement) { parentRRNode.contentDocument = rrNode as RRDocument; - } else { + } + // if node isn't a shadow root + else if (node.nodeType !== node.DOCUMENT_FRAGMENT_NODE) { parentRRNode?.appendChild(rrNode); rrNode.parentNode = parentRRNode; rrNode.parentElement = parentRRNode as RRElement; } + if ( node.nodeType === node.ELEMENT_NODE && ((node as Node) as HTMLElement).tagName === 'IFRAME' @@ -414,11 +421,21 @@ export function buildFromDom( ); else if ( node.nodeType === node.DOCUMENT_NODE || - node.nodeType === node.ELEMENT_NODE - ) + node.nodeType === node.ELEMENT_NODE || + node.nodeType === node.DOCUMENT_FRAGMENT_NODE + ) { + if ( + node.nodeType === Node.ELEMENT_NODE && + ((node as Node) as HTMLElement).shadowRoot + ) + walk( + (((node as Node) as HTMLElement).shadowRoot! as unknown) as INode, + rrNode, + ); node.childNodes.forEach((node) => walk((node as unknown) as INode, rrNode), ); + } }; walk((dom as unknown) as INode, null); return rrdom; @@ -543,9 +560,9 @@ export class RRElement extends RRNode { /** * Creates a shadow root for element and returns it. */ - attachShadow(init: ShadowRootInit): RRElement { - this.shadowRoot = init.mode === 'open' ? this : null; - return this; + attachShadow(_init: ShadowRootInit): RRElement { + this.shadowRoot = new RRElement('SHADOWROOT'); + return this.shadowRoot; } toString() { diff --git a/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap b/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap index 74dda13789..8bd66b9035 100644 --- a/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap +++ b/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap @@ -47,6 +47,41 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu " `; +exports[`RRDocument for browser environment create a RRDocument from a html document can build from a html containing nested shadow doms 1`] = ` +"-1 RRDocument + -2 RRDocumentType + -3 HTML lang=\\"en\\" + -4 HEAD + -5 RRText text=\\"\\\\n \\" + -6 META charset=\\"UTF-8\\" + -7 RRText text=\\"\\\\n \\" + -8 META name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" + -9 RRText text=\\"\\\\n \\" + -10 TITLE + -11 RRText text=\\"shadow dom\\" + -12 RRText text=\\"\\\\n \\" + -13 RRText text=\\"\\\\n \\" + -14 BODY + -15 RRText text=\\"\\\\n \\" + -16 DIV + -17 SHADOWROOT + -18 RRText text=\\"\\\\n \\" + -19 SPAN + -20 RRText text=\\" shadow dom one \\" + -21 RRText text=\\"\\\\n \\" + -22 DIV + -23 SHADOWROOT + -24 RRText text=\\"\\\\n \\" + -25 SPAN + -26 RRText text=\\" shadow dom two \\" + -27 RRText text=\\"\\\\n \\" + -28 RRText text=\\"\\\\n \\\\n \\" + -29 RRText text=\\"\\\\n \\" + -30 RRText text=\\"\\\\n \\\\n \\" + -31 RRText text=\\"\\\\n \\\\n\\\\n\\" +" +`; + exports[`RRDocument for browser environment create a RRDocument from a html document can build from an iframe html 1`] = ` "-1 RRDocument -2 RRDocumentType @@ -74,6 +109,7 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu
This is a block inside the iframe1.
+ diff --git a/packages/rrdom/test/html/shadow-dom.html b/packages/rrdom/test/html/shadow-dom.html new file mode 100644 index 0000000000..0704dc0430 --- /dev/null +++ b/packages/rrdom/test/html/shadow-dom.html @@ -0,0 +1,20 @@ + + + + + + shadow dom + + +
+ +
+ + diff --git a/packages/rrweb/test/events/shadow-dom.ts b/packages/rrweb/test/events/shadow-dom.ts new file mode 100644 index 0000000000..88956a833c --- /dev/null +++ b/packages/rrweb/test/events/shadow-dom.ts @@ -0,0 +1,172 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); + +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1200, + height: 500, + }, + timestamp: now + 100, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + timestamp: now + 200, + }, + // add shadow dom elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 6, + isShadowHost: true, + }, + }, + ], + }, + timestamp: now + 500, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [], + id: 7, + isShadow: true, + }, + }, + { + parentId: 7, + nextId: null, + node: { type: 3, textContent: 'shadow dom one', id: 8 }, + }, + ], + }, + timestamp: now + 500, + }, + // add nested shadow dom elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 6, + nextId: null, + node: { + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 9, + isShadow: true, + isShadowHost: true, + }, + }, + ], + }, + timestamp: now + 1000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 9, + nextId: null, + node: { + type: 2, + tagName: 'span', + attributes: {}, + childNodes: [], + id: 10, + isShadow: true, + }, + }, + { + parentId: 10, + nextId: null, + node: { type: 3, textContent: 'shadow dom two', id: 11 }, + }, + ], + }, + timestamp: now + 1000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index a20e5ffd5d..f5255bb435 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -14,6 +14,7 @@ import orderingEvents from './events/ordering'; import scrollEvents from './events/scroll'; import inputEvents from './events/input'; import iframeEvents from './events/iframe'; +import shadowDomEvents from './events/shadow-dom'; interface ISuite { code: string; @@ -481,6 +482,51 @@ describe('replayer', function () { ).not.toBeNull(); }); + it('can fast-forward mutation events containing nested shadow doms', async () => { + await page.evaluate(` + events = ${JSON.stringify(shadowDomEvents)}; + const { Replayer } = rrweb; + var replayer = new Replayer(events,{showDebug:true}); + replayer.pause(550); + `); + // add shadow dom 'one' at 500 + const iframe = await page.$('iframe'); + const contentDocument = await iframe!.contentFrame()!; + expect( + await contentDocument!.$eval('div', (element) => element.shadowRoot), + ).not.toBeNull(); + expect( + await contentDocument!.evaluate( + () => + document + .querySelector('body > div')! + .shadowRoot!.querySelector('span')!.textContent, + ), + ).toEqual('shadow dom one'); + + // add shadow dom 'two' at 1000 + await page.evaluate('replayer.play(0);'); + await page.waitForTimeout(50); + await page.evaluate('replayer.pause(1050);'); + expect( + await contentDocument!.evaluate( + () => + document + .querySelector('body > div')! + .shadowRoot!.querySelector('div')!.shadowRoot, + ), + ).not.toBeNull(); + expect( + await contentDocument!.evaluate( + () => + document + .querySelector('body > div')! + .shadowRoot!.querySelector('div')! + .shadowRoot!.querySelector('span')!.textContent, + ), + ).toEqual('shadow dom two'); + }); + it('can stream events in live mode', async () => { const status = await page.evaluate(` const { Replayer } = rrweb; From 039c5bde475adb08f43951e103838a14510a61aa Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 13 Feb 2022 01:34:41 +1100 Subject: [PATCH 25/79] add support for MediaInteraction --- packages/rrdom/src/diff.ts | 13 ++++++++++- packages/rrdom/src/document-browser.ts | 32 ++++++++------------------ packages/rrweb/src/replay/index.ts | 16 ++++--------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 3e2a54491b..f32db7ec2c 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,5 +1,5 @@ import { elementNode, INode, NodeType } from 'rrweb-snapshot'; -import type { inputData, Mirror, scrollData } from 'rrweb/src/types'; +import { inputData, Mirror, scrollData } from 'rrweb/src/types'; import { RRCDATASection, RRComment, @@ -12,6 +12,7 @@ import { RRDocument, RRIFrameElement, RRDocumentType, + RRMediaElement, } from './document-browser'; const NAMESPACES: Record = { @@ -89,6 +90,16 @@ export function diff( oldElement as HTMLStyleElement, newTree.rules, ); + } else if (newTree instanceof RRMediaElement) { + const oldMediaElement = (oldTree as Node) as HTMLMediaElement; + if (newTree.paused !== undefined) + newTree.paused ? oldMediaElement.pause() : oldMediaElement.play(); + if (newTree.muted !== undefined) oldMediaElement.muted = newTree.muted; + if (newTree.volume !== undefined) + oldMediaElement.volume = newTree.volume; + // MediaInteraction events have been applied. + if (newTree.currentTime !== undefined) + oldMediaElement.currentTime = newTree.currentTime; } if (newRRElement.shadowRoot) { if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' }); diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 203ec8ef9d..0b4f4db32c 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -1,5 +1,5 @@ import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; -import { inputData, scrollData } from 'rrweb/src/types'; +import type { inputData, scrollData } from 'rrweb/src/types'; import { parseCSSText, camelize, toCSSText } from './style'; export abstract class RRNode { @@ -208,9 +208,6 @@ export class RRDocument extends RRNode { case 'IFRAME': element = new RRIFrameElement(upperTagName); break; - case 'IMG': - element = new RRImageElement(upperTagName); - break; case 'CANVAS': element = new RRCanvasElement(upperTagName); break; @@ -370,12 +367,6 @@ export function buildFromDom( if (tagName === 'CANVAS') { rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); } - // media elements - if (tagName === 'AUDIO' || tagName === 'VIDEO') { - const rrMediaElement = rrElement as RRMediaElement; - rrMediaElement.paused = (elementNode as HTMLMediaElement).paused; - rrMediaElement.currentTime = (elementNode as HTMLMediaElement).currentTime; - } break; case node.TEXT_NODE: rrNode = rrdom.createTextNode( @@ -574,20 +565,15 @@ export class RRElement extends RRNode { } } -export class RRImageElement extends RRElement { - src: string; - width: number; - height: number; - onload: ((this: GlobalEventHandlers, ev: Event) => any) | null; -} - export class RRMediaElement extends RRElement { - currentTime: number = 0; - paused: boolean = true; - async play() { + currentTime?: number; + volume?: number; + paused?: boolean; + muted?: boolean; + play() { this.paused = false; } - async pause() { + pause() { this.paused = true; } } @@ -672,8 +658,10 @@ type Mirror = { }; interface RRElementTagNameMap { - img: RRImageElement; audio: RRMediaElement; + canvas: RRCanvasElement; + iframe: RRIFrameElement; + style: RRStyleElement; video: RRMediaElement; } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 1e382d3944..2323eee069 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -13,6 +13,7 @@ import { RRElement, RRStyleElement, RRIFrameElement, + RRMediaElement, StyleRuleType, VirtualStyleRules, buildFromDom, @@ -473,14 +474,7 @@ export class Replayer { case EventType.FullSnapshot: case EventType.Meta: case EventType.Plugin: - break; case EventType.IncrementalSnapshot: - switch (event.data.source) { - case IncrementalSource.MediaInteraction: - continue; - default: - break; - } break; default: break; @@ -847,7 +841,6 @@ export class Replayer { * pause when there are some canvas drawImage args need to be loaded */ private preloadAllImages() { - // TODO check useful status let beforeLoadState = this.service.state; const stateHandler = () => { beforeLoadState = this.service.state; @@ -1064,12 +1057,13 @@ export class Replayer { break; } case IncrementalSource.MediaInteraction: { - // TODO what if the media element doesn't exist - const target = this.mirror.getNode(d.id); + const target = this.usingRRDom + ? this.rrdom.mirror.getNode(d.id) + : this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); } - const mediaEl = (target as Node) as HTMLMediaElement; + const mediaEl = target as HTMLMediaElement | RRMediaElement; try { if (d.currentTime) { mediaEl.currentTime = d.currentTime; From 1dd18c59cb78a8bdaf84aa2287ea7b43c35dcbfd Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 15 Feb 2022 15:13:33 +1100 Subject: [PATCH 26/79] add canvas support --- packages/rrdom/src/diff.ts | 36 +++++++++++++++++-- packages/rrdom/src/document-browser.ts | 16 +++++++-- packages/rrweb-snapshot/src/rebuild.ts | 25 ++++++++------ packages/rrweb/src/replay/index.ts | 48 +++++++++++++++++++------- 4 files changed, 98 insertions(+), 27 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index f32db7ec2c..c7eb948637 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,5 +1,11 @@ import { elementNode, INode, NodeType } from 'rrweb-snapshot'; -import { inputData, Mirror, scrollData } from 'rrweb/src/types'; +import { + canvasMutationData, + incrementalSnapshotEvent, + inputData, + Mirror, + scrollData, +} from 'rrweb/src/types'; import { RRCDATASection, RRComment, @@ -13,6 +19,7 @@ import { RRIFrameElement, RRDocumentType, RRMediaElement, + RRCanvasElement, } from './document-browser'; const NAMESPACES: Record = { @@ -63,6 +70,14 @@ const SVGTagMap: Record = { export type ReplayerHandler = { mirror: Mirror; + applyCanvas: ( + canvasEvent: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => void; applyInput: (data: inputData) => void; applyScroll: (data: scrollData, isSync: boolean) => void; }; @@ -100,6 +115,14 @@ export function diff( // MediaInteraction events have been applied. if (newTree.currentTime !== undefined) oldMediaElement.currentTime = newTree.currentTime; + } else if (newTree instanceof RRCanvasElement) { + newTree.canvasMutation.forEach((canvasMutation) => + replayer.applyCanvas( + canvasMutation.event, + canvasMutation.mutation, + (oldTree as Node) as HTMLCanvasElement, + ), + ); } if (newRRElement.shadowRoot) { if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' }); @@ -159,7 +182,16 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { const newValue = newAttributes[name]; if ((newTree.__sn as elementNode).isSVG && NAMESPACES[name]) oldTree.setAttributeNS(NAMESPACES[name], name, newValue); - else oldTree.setAttribute(name, newValue); + else if (newTree instanceof RRCanvasElement && name === 'rr_dataURL') { + const image = document.createElement('img'); + image.src = newValue; + image.onload = () => { + const ctx = (oldTree as HTMLCanvasElement).getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } + }; + } else oldTree.setAttribute(name, newValue); } for (const { name } of Array.from(oldAttributes)) diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 0b4f4db32c..46be021ace 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -1,5 +1,10 @@ import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; -import type { inputData, scrollData } from 'rrweb/src/types'; +import type { + canvasMutationData, + incrementalSnapshotEvent, + inputData, + scrollData, +} from 'rrweb/src/types'; import { parseCSSText, camelize, toCSSText } from './style'; export abstract class RRNode { @@ -579,8 +584,15 @@ export class RRMediaElement extends RRElement { } export class RRCanvasElement extends RRElement { + public canvasMutation: { + event: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }; + mutation: canvasMutationData; + }[] = []; /** - * This is just a dummy implementation to prevent rrweb replayer from drawing mouse tail. If further analysis of canvas is needed, we may implement it with node-canvas. + * This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement. */ getContext(): CanvasRenderingContext2D | null { return null; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 847cdd0698..f0e430df8b 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -215,7 +215,10 @@ function buildNode( n.attributes.rr_dataURL ) { // backup original img srcset - node.setAttribute('rrweb-original-srcset', n.attributes.srcset as string); + node.setAttribute( + 'rrweb-original-srcset', + n.attributes.srcset as string, + ); } else { node.setAttribute(name, value); } @@ -225,23 +228,25 @@ function buildNode( } else { // handle internal attributes if (tagName === 'canvas' && name === 'rr_dataURL') { - const image = document.createElement('img'); - image.src = value; - image.onload = () => { - const ctx = (node as HTMLCanvasElement).getContext('2d'); - if (ctx) { + const ctx = (node as HTMLCanvasElement).getContext('2d'); + // If node is a RRCanvasElement, keep 'rr_dataURL' until the diff stage. + if (!ctx) { + node.setAttribute(name, value); + } else { + const image = document.createElement('img'); + image.src = value; + image.onload = () => ctx.drawImage(image, 0, 0, image.width, image.height); - } - }; + } } else if (tagName === 'img' && name === 'rr_dataURL') { const image = node as HTMLImageElement; - if (!image.currentSrc.startsWith('data:')) { + if (!image.getAttribute('src')?.startsWith('data:')) { // Backup original img src. It may not have been set yet. image.setAttribute( 'rrweb-original-src', n.attributes.src as string, ); - image.src = value; + image.setAttribute('src', value); } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 2323eee069..296393ac24 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -18,6 +18,7 @@ import { VirtualStyleRules, buildFromDom, diff, + RRCanvasElement, } from 'rrdom/es/document-browser'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; @@ -170,6 +171,22 @@ export class Replayer { if (this.usingRRDom) { diff((this.iframe.contentDocument! as unknown) as INode, this.rrdom, { mirror: this.mirror, + applyCanvas: ( + canvasEvent: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => { + canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + }, applyInput: this.applyInput.bind(this), applyScroll: this.applyScroll.bind(this), }); @@ -1211,23 +1228,28 @@ export class Replayer { break; } case IncrementalSource.CanvasMutation: { - // TODO adopt rrdom here if (!this.config.UNSAFE_replayCanvas) { return; } - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); + if (this.usingRRDom) { + const target = this.rrdom.mirror.getNode(d.id) as RRCanvasElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.canvasMutation.push({ event: e, mutation: d }); + } else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + canvasMutation({ + event: e, + mutation: d, + target: (target as unknown) as HTMLCanvasElement, + imageMap: this.imageMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); } - - canvasMutation({ - event: e, - mutation: d, - target: (target as unknown) as HTMLCanvasElement, - imageMap: this.imageMap, - errorHandler: this.warnCanvasMutationFailed.bind(this), - }); - break; } case IncrementalSource.Font: { From 8937ea13e95edb97bc4169e4ca3e9379801e8709 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 15 Feb 2022 15:58:28 +1100 Subject: [PATCH 27/79] fix unit test error in rrdom --- packages/rrdom/test/diff.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 54b6095fda..b9fe83741f 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -89,6 +89,7 @@ describe('diff algorithm for rrdom', () => { }; const replayer: ReplayerHandler = { mirror, + applyCanvas: () => {}, applyInput: () => {}, applyScroll: () => {}, }; From 62398a96fcda05e8f6b4029cd77fee24e1511489 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 17 Feb 2022 02:19:49 +1100 Subject: [PATCH 28/79] add support for Text, Comment --- packages/rrdom/src/diff.ts | 15 ++++++++-- packages/rrdom/src/document-browser.ts | 38 +++++++++++++++++--------- packages/rrdom/test/diff.test.ts | 13 ++++++++- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index c7eb948637..02179b6670 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -137,7 +137,18 @@ export function diff( ); } break; - // TODO: Diff other kinds of nodes. + case NodeType.Text: + case NodeType.Comment: + case NodeType.CDATA: + if ( + oldTree.textContent !== + (newTree as RRText | RRComment | RRCDATASection).data + ) + oldTree.textContent = (newTree as + | RRText + | RRComment + | RRCDATASection).data; + break; default: } const oldChildren = oldTree.childNodes; @@ -321,7 +332,7 @@ export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { rrNode.systemId, ) as unknown) as INode; } else if (rrNode instanceof RRText) { - node = (document.createTextNode(rrNode.textContent) as unknown) as INode; + node = (document.createTextNode(rrNode.data) as unknown) as INode; } else if (rrNode instanceof RRComment) { node = (document.createComment(rrNode.data) as unknown) as INode; } else if (rrNode instanceof RRCDATASection) { diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index 46be021ace..d9f2e5583a 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -40,8 +40,28 @@ export abstract class RRNode { return siblings[index + 1] ?? null; } - set textContent(textContent: string) { - if (this instanceof RRText) this.textContent = textContent; + get textContent(): string | null { + if ( + this instanceof RRText || + this instanceof RRComment || + this instanceof RRCDATASection + ) + return this.data; + else if (this instanceof RRElement) { + let result = ''; + this.childNodes.forEach((node) => result + node.textContent); + return result; + } else return null; + } + + set textContent(textContent: string | null) { + textContent = textContent || ''; + if ( + this instanceof RRText || + this instanceof RRComment || + this instanceof RRCDATASection + ) + this.data = textContent; else if (this instanceof RRElement) { if (this.childNodes[0] instanceof RRText) this.childNodes[0].textContent = textContent; @@ -611,23 +631,15 @@ export class RRIFrameElement extends RRElement { } export class RRText extends RRNode { - private _textContent: string; - public get textContent(): string { - return this._textContent; - } - public set textContent(value: string) { - this._textContent = value; - } + data: string; constructor(data: string) { super(); - this._textContent = data; + this.data = data; } toString() { - return `${super.toString('RRText')} text=${JSON.stringify( - this.textContent, - )}`; + return `${super.toString('RRText')} text=${JSON.stringify(this.data)}`; } } diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index b9fe83741f..aba3e8db65 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -95,7 +95,7 @@ describe('diff algorithm for rrdom', () => { }; describe('diff single node', () => { - it('should a diff document node', () => { + it('should diff a document node', () => { document.close(); document.open(); expect(document.childNodes.length).toEqual(0); @@ -113,6 +113,17 @@ describe('diff algorithm for rrdom', () => { ); expect(document.doctype?.systemId).toEqual(''); }); + + it('should diff a Text node', () => { + const node = (document.createTextNode('old text') as unknown) as INode; + expect(node.textContent).toEqual('old text'); + node.__sn = Object.assign({}, elementSn); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createTextNode('new text'); + rrNode.__sn = Object.assign({}, elementSn); + diff(node, rrNode, replayer); + expect(node.textContent).toEqual('new text'); + }); }); describe('diff properties', () => { From fc883615ac8aba52d7cfa2376cfce944809ff986 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 18 Feb 2022 20:07:07 +1100 Subject: [PATCH 29/79] try to refactor RRDom --- packages/rrdom/src/diff.ts | 214 ++++++--- packages/rrdom/src/document-browser.ts | 641 ++++--------------------- packages/rrdom/src/document.ts | 549 +++++++++++++++++++++ packages/rrweb/src/replay/index.ts | 11 +- 4 files changed, 801 insertions(+), 614 deletions(-) create mode 100644 packages/rrdom/src/document.ts diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 02179b6670..c92e159adf 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -7,19 +7,21 @@ import { scrollData, } from 'rrweb/src/types'; import { - RRCDATASection, - RRComment, - RRElement, - RRStyleElement, - RRNode, - RRText, - VirtualStyleRules, - StyleRuleType, + IRRCDATASection, + IRRComment, + IRRDocument, + IRRDocumentType, + IRRElement, + IRRNode, + IRRText, +} from './document'; +import type { + RRCanvasElement, RRDocument, + RRElement, RRIFrameElement, - RRDocumentType, RRMediaElement, - RRCanvasElement, + RRStyleElement, } from './document-browser'; const NAMESPACES: Record = { @@ -84,45 +86,54 @@ export type ReplayerHandler = { export function diff( oldTree: INode, - newTree: RRNode, + newTree: IRRNode, replayer: ReplayerHandler, ) { let inputDataToApply = null, scrollDataToApply = null; - switch (newTree.nodeType) { + switch (newTree.RRNodeType) { case NodeType.Document: - const newRRDocument = newTree as RRDocument; - scrollDataToApply = newRRDocument.scrollData; + const newRRDocument = newTree as IRRDocument; + scrollDataToApply = (newRRDocument as RRDocument).scrollData; break; case NodeType.Element: const oldElement = (oldTree as Node) as HTMLElement; - const newRRElement = newTree as RRElement; + const newRRElement = newTree as IRRElement; diffProps(oldElement, newRRElement); - scrollDataToApply = newRRElement.scrollData; - inputDataToApply = newRRElement.inputData; - if (newTree instanceof RRStyleElement && newTree.rules.length > 0) { - applyVirtualStyleRulesToNode( - oldElement as HTMLStyleElement, - newTree.rules, - ); - } else if (newTree instanceof RRMediaElement) { - const oldMediaElement = (oldTree as Node) as HTMLMediaElement; - if (newTree.paused !== undefined) - newTree.paused ? oldMediaElement.pause() : oldMediaElement.play(); - if (newTree.muted !== undefined) oldMediaElement.muted = newTree.muted; - if (newTree.volume !== undefined) - oldMediaElement.volume = newTree.volume; - // MediaInteraction events have been applied. - if (newTree.currentTime !== undefined) - oldMediaElement.currentTime = newTree.currentTime; - } else if (newTree instanceof RRCanvasElement) { - newTree.canvasMutation.forEach((canvasMutation) => - replayer.applyCanvas( - canvasMutation.event, - canvasMutation.mutation, - (oldTree as Node) as HTMLCanvasElement, - ), - ); + scrollDataToApply = (newRRElement as RRElement).scrollData; + inputDataToApply = (newRRElement as RRElement).inputData; + switch (newRRElement.tagName) { + case 'AUDIO': + case 'VIDEO': + const oldMediaElement = (oldTree as Node) as HTMLMediaElement; + const newMediaRRElement = (newRRElement as unknown) as RRMediaElement; + if (newMediaRRElement.paused !== undefined) + newMediaRRElement.paused + ? oldMediaElement.pause() + : oldMediaElement.play(); + if (newMediaRRElement.muted !== undefined) + oldMediaElement.muted = newMediaRRElement.muted; + if (newMediaRRElement.volume !== undefined) + oldMediaElement.volume = newMediaRRElement.volume; + if (newMediaRRElement.currentTime !== undefined) + oldMediaElement.currentTime = newMediaRRElement.currentTime; + break; + case 'CANVAS': + (newTree as RRCanvasElement).canvasMutation.forEach( + (canvasMutation) => + replayer.applyCanvas( + canvasMutation.event, + canvasMutation.mutation, + (oldTree as Node) as HTMLCanvasElement, + ), + ); + break; + case 'STYLE': + applyVirtualStyleRulesToNode( + oldElement as HTMLStyleElement, + (newTree as RRStyleElement).rules, + ); + break; } if (newRRElement.shadowRoot) { if (!oldElement.shadowRoot) oldElement.attachShadow({ mode: 'open' }); @@ -142,12 +153,12 @@ export function diff( case NodeType.CDATA: if ( oldTree.textContent !== - (newTree as RRText | RRComment | RRCDATASection).data + (newTree as IRRText | IRRComment | IRRCDATASection).data ) oldTree.textContent = (newTree as - | RRText - | RRComment - | RRCDATASection).data; + | IRRText + | IRRComment + | IRRCDATASection).data; break; default: } @@ -162,18 +173,22 @@ export function diff( ); } // IFrame element doesn't have child nodes. - if (newTree instanceof RRIFrameElement) { + if ( + newTree.RRNodeType === NodeType.Element && + (newTree as IRRElement).tagName === 'IFRAME' + ) { const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) .contentDocument as unknown) as INode; + const newIFrameElement = newTree as RRIFrameElement; // If the iframe is cross-origin, the contentDocument will be null. if (oldContentDocument) { - if (newTree.contentDocument.__sn) { - oldContentDocument.__sn = newTree.contentDocument.__sn; + if (newIFrameElement.contentDocument.__sn) { + oldContentDocument.__sn = newIFrameElement.contentDocument.__sn; replayer.mirror.map[ - newTree.contentDocument.__sn.id + newIFrameElement.contentDocument.__sn.id ] = oldContentDocument; } - diff(oldContentDocument, newTree.contentDocument, replayer); + diff(oldContentDocument, newIFrameElement.contentDocument, replayer); } } @@ -185,7 +200,7 @@ export function diff( inputDataToApply && replayer.applyInput(inputDataToApply); } -function diffProps(oldTree: HTMLElement, newTree: RRElement) { +function diffProps(oldTree: HTMLElement, newTree: IRRElement) { const oldAttributes = oldTree.attributes; const newAttributes = newTree.attributes; @@ -193,7 +208,7 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { const newValue = newAttributes[name]; if ((newTree.__sn as elementNode).isSVG && NAMESPACES[name]) oldTree.setAttributeNS(NAMESPACES[name], name, newValue); - else if (newTree instanceof RRCanvasElement && name === 'rr_dataURL') { + else if (newTree.tagName === 'CANVAS' && name === 'rr_dataURL') { const image = document.createElement('img'); image.src = newValue; image.onload = () => { @@ -211,7 +226,7 @@ function diffProps(oldTree: HTMLElement, newTree: RRElement) { function diffChildren( oldChildren: (INode | undefined)[], - newChildren: RRNode[], + newChildren: IRRNode[], parentNode: INode, replayer: ReplayerHandler, ) { @@ -313,31 +328,49 @@ function diffChildren( } } -export function createOrGetNode(rrNode: RRNode, mirror: Mirror): INode { +export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { let node = mirror.getNode(rrNode.__sn.id); if (node !== null) return node; - if (rrNode instanceof RRElement) { - let tagName = rrNode.tagName.toLowerCase(); - tagName = SVGTagMap[tagName] || tagName; - if ((rrNode.__sn as elementNode).isSVG) { - node = (document.createElementNS( - NAMESPACES['svg'], - rrNode.tagName.toLowerCase(), + switch (rrNode.RRNodeType) { + case NodeType.DocumentType: + node = (document.implementation.createDocumentType( + (rrNode as IRRDocumentType).name, + (rrNode as IRRDocumentType).publicId, + (rrNode as IRRDocumentType).systemId, ) as unknown) as INode; - } else node = (document.createElement(rrNode.tagName) as unknown) as INode; - } else if (rrNode instanceof RRDocumentType) { - node = (document.implementation.createDocumentType( - rrNode.name, - rrNode.publicId, - rrNode.systemId, - ) as unknown) as INode; - } else if (rrNode instanceof RRText) { - node = (document.createTextNode(rrNode.data) as unknown) as INode; - } else if (rrNode instanceof RRComment) { - node = (document.createComment(rrNode.data) as unknown) as INode; - } else if (rrNode instanceof RRCDATASection) { - node = (document.createCDATASection(rrNode.data) as unknown) as INode; - } else throw new Error('Unknown rrNode type ' + rrNode.toString()); + break; + case NodeType.Element: + let tagName = (rrNode as IRRElement).tagName.toLowerCase(); + tagName = SVGTagMap[tagName] || tagName; + if ((rrNode.__sn as elementNode).isSVG) { + node = (document.createElementNS( + NAMESPACES['svg'], + (rrNode as IRRElement).tagName.toLowerCase(), + ) as unknown) as INode; + } else + node = (document.createElement( + (rrNode as IRRElement).tagName, + ) as unknown) as INode; + break; + case NodeType.Text: + node = (document.createTextNode( + (rrNode as IRRText).data, + ) as unknown) as INode; + break; + case NodeType.Comment: + node = (document.createComment( + (rrNode as IRRComment).data, + ) as unknown) as INode; + break; + case NodeType.CDATA: + node = (document.createCDATASection( + (rrNode as IRRCDATASection).data, + ) as unknown) as INode; + break; + default: + // RRDocument of RRIFrameElement won't be created here because it's automatically generated when RRIFrameElement is created. + throw new Error('Unknown rrNode type ' + rrNode.toString()); + } node.__sn = { ...rrNode.__sn }; mirror.map[rrNode.__sn.id] = node; return node; @@ -359,6 +392,39 @@ export function getNestedRule( } } +export enum StyleRuleType { + Insert, + Remove, + Snapshot, + SetProperty, + RemoveProperty, +} +type InsertRule = { + cssText: string; + type: StyleRuleType.Insert; + index?: number | number[]; +}; +type RemoveRule = { + type: StyleRuleType.Remove; + index: number | number[]; +}; +type SetPropertyRule = { + type: StyleRuleType.SetProperty; + index: number[]; + property: string; + value: string | null; + priority: string | undefined; +}; +type RemovePropertyRule = { + type: StyleRuleType.RemoveProperty; + index: number[]; + property: string; +}; + +export type VirtualStyleRules = Array< + InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule +>; + export function getPositionsAndIndex(nestedIndex: number[]) { const positions = [...nestedIndex]; const index = positions.pop(); diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index d9f2e5583a..acc52925e9 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -1,115 +1,27 @@ -import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { INode, NodeType } from 'rrweb-snapshot'; import type { canvasMutationData, incrementalSnapshotEvent, inputData, scrollData, } from 'rrweb/src/types'; -import { parseCSSText, camelize, toCSSText } from './style'; - -export abstract class RRNode { - __sn: serializedNodeWithId; - children: Array = []; - parentElement: RRElement | null = null; - parentNode: RRNode | null = null; - ELEMENT_NODE = 1; - TEXT_NODE = 3; - - get nodeType() { - if (this instanceof RRDocument) return NodeType.Document; - if (this instanceof RRDocumentType) return NodeType.DocumentType; - if (this instanceof RRElement) return NodeType.Element; - if (this instanceof RRText) return NodeType.Text; - if (this instanceof RRCDATASection) return NodeType.CDATA; - if (this instanceof RRComment) return NodeType.Comment; - } - - get childNodes() { - return this.children; - } - - get firstChild(): RRNode | null { - return this.childNodes[0] ?? null; - } - - get nextSibling(): RRNode | null { - let parentNode = this.parentNode; - if (!parentNode) return null; - const siblings = parentNode.children; - let index = siblings.indexOf(this); - return siblings[index + 1] ?? null; - } - - get textContent(): string | null { - if ( - this instanceof RRText || - this instanceof RRComment || - this instanceof RRCDATASection - ) - return this.data; - else if (this instanceof RRElement) { - let result = ''; - this.childNodes.forEach((node) => result + node.textContent); - return result; - } else return null; - } - - set textContent(textContent: string | null) { - textContent = textContent || ''; - if ( - this instanceof RRText || - this instanceof RRComment || - this instanceof RRCDATASection - ) - this.data = textContent; - else if (this instanceof RRElement) { - if (this.childNodes[0] instanceof RRText) - this.childNodes[0].textContent = textContent; - } - } - - contains(node: RRNode) { - if (node === this) return true; - for (const child of this.children) { - if (child.contains(node)) return true; - } - return false; - } - - appendChild(newChild: RRNode): RRNode { - throw new Error( - `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, - ); - } - - insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { - throw new Error( - `RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`, - ); - } - - removeChild(node: RRNode) { - const indexOfChild = this.children.indexOf(node); - if (indexOfChild !== -1) { - this.children.splice(indexOfChild, 1); - node.parentElement = null; - node.parentNode = null; - } - } - - toString(nodeName?: string) { - return `${JSON.stringify(this.__sn?.id) || ''} ${nodeName}`; - } -} - -export class RRDocument extends RRNode { - private _notSerializedId = -1; // used as an id to identify not serialized node - /** - * Every time the id is used, it will minus 1 automatically to avoid collisions. - */ - get notSerializedId(): number { - return this._notSerializedId--; - } +import { + BaseRRCDATASectionImpl, + BaseRRCommentImpl, + BaseRRDocumentImpl, + BaseRRDocumentTypeImpl, + BaseRRElementImpl, + BaseRRMediaElementImpl, + BaseRRTextImpl, + IRRDocument, + IRRElement, + IRRNode, +} from './document'; +import { VirtualStyleRules } from './diff'; + +export class RRDocument + extends BaseRRDocumentImpl(IRRNode) + implements IRRDocument { public mirror: Mirror = { map: {}, getId(n) { @@ -123,7 +35,7 @@ export class RRDocument extends RRNode { delete this.map[id]; if (n.childNodes) { n.childNodes.forEach((child) => - this.removeNodeFromMap(child as RRNode), + this.removeNodeFromMap(child as IRRNode), ); } }, @@ -137,72 +49,12 @@ export class RRDocument extends RRNode { scrollData: scrollData | null = null; - get documentElement(): RRElement { - return this.children.find( - (node) => node instanceof RRElement && node.tagName === 'HTML', - ) as RRElement; - } - - get body() { - return ( - (this.documentElement?.children.find( - (node) => node instanceof RRElement && node.tagName === 'BODY', - ) as RRElement) || null - ); - } - - get head() { - return ( - (this.documentElement?.children.find( - (node) => node instanceof RRElement && node.tagName === 'HEAD', - ) as RRElement) || null - ); - } - - get implementation() { - return this; - } - - get firstElementChild() { - return this.documentElement; - } - - appendChild(childNode: RRNode) { - const nodeType = childNode.nodeType; - if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { - if (this.children.some((s) => s.nodeType === nodeType)) { - throw new Error( - `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ - nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' - } on RRDocument allowed.`, - ); - } - } - childNode.parentElement = null; - childNode.parentNode = this; - this.children.push(childNode); - return childNode; - } - - insertBefore(newChild: RRNode, refChild: RRNode | null) { - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.children.splice(childIndex, 0, newChild); - newChild.parentElement = null; - newChild.parentNode = this; - return newChild; - } - createDocument( _namespace: string | null, _qualifiedName: string | null, _doctype?: DocumentType | null, ) { - return new RRDocument(); + return (new RRDocument() as unknown) as IRRDocument; } createDocumentType( @@ -210,12 +62,7 @@ export class RRDocument extends RRNode { publicId: string, systemId: string, ) { - const documentTypeNode = new RRDocumentType( - qualifiedName, - publicId, - systemId, - ); - return documentTypeNode; + return new RRDocumentType(qualifiedName, publicId, systemId); } createElement( @@ -250,77 +97,101 @@ export class RRDocument extends RRNode { _namespaceURI: 'http://www.w3.org/2000/svg', qualifiedName: string, ) { - return this.createElement(qualifiedName as keyof HTMLElementTagNameMap); + return this.createElement(qualifiedName); } createComment(data: string) { - const commentNode = new RRComment(data); - return commentNode; + return new RRComment(data); } createCDATASection(data: string) { - const sectionNode = new RRCDATASection(data); - return sectionNode; + return new RRCDATASection(data); } createTextNode(data: string) { - const textNode = new RRText(data); - return textNode; + return new RRText(data); } - /** - * This does come with some side effects. For example: - * 1. All event listeners currently registered on the document, nodes inside the document, or the document's window are removed. - * 2. All existing nodes are removed from the document. - */ - open() { + destroyTree() { this.children = []; + this.mirror.reset(); } +} - close() {} +export const RRDocumentType = BaseRRDocumentTypeImpl(IRRNode); + +export class RRElement extends BaseRRElementImpl(IRRNode) { + inputData: inputData | null = null; + scrollData: scrollData | null = null; /** - * Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot). - * There are two lines used this function: - * 1. doc.write('') - * 2. doc.write('') + * Creates a shadow root for element and returns it. */ - write(content: string) { - let publicId; - if ( - content === - '' - ) - publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; - else if ( - content === - '' - ) - publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; - if (publicId) { - const doctype = new RRDocumentType('html', publicId, ''); - doctype.__sn = { - type: NodeType.DocumentType, - name: 'html', - publicId: publicId, - systemId: '', - id: this.notSerializedId, - }; - this.open(); - this.appendChild(doctype); - } + attachShadow(_init: ShadowRootInit): RRElement { + const shadowRoot = new RRElement('SHADOWROOT'); + this.shadowRoot = shadowRoot; + return shadowRoot; } +} - destroyTree() { - this.children = []; - this.mirror.reset(); - } +export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} - toString() { - return super.toString('RRDocument'); +export class RRCanvasElement extends RRElement implements IRRElement { + public canvasMutation: { + event: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }; + mutation: canvasMutationData; + }[] = []; + /** + * This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement. + */ + getContext(): CanvasRenderingContext2D | null { + return null; } } +export class RRStyleElement extends RRElement { + public rules: VirtualStyleRules = []; +} + +export class RRIFrameElement extends RRElement { + contentDocument: RRDocument = new RRDocument(); +} + +export const RRText = BaseRRTextImpl(IRRNode); +export type RRText = typeof RRText; + +export const RRComment = BaseRRCommentImpl(IRRNode); +export type RRComment = typeof RRComment; + +export const RRCDATASection = BaseRRCDATASectionImpl(IRRNode); +export type RRCDATASection = typeof RRCDATASection; + +type Mirror = { + map: { + [key: number]: IRRNode; + }; + getId(n: IRRNode): number; + getNode(id: number): IRRNode | null; + removeNodeFromMap(n: IRRNode): void; + has(id: number): boolean; + reset(): void; +}; + +interface RRElementTagNameMap { + audio: RRMediaElement; + canvas: RRCanvasElement; + iframe: RRIFrameElement; + style: RRStyleElement; + video: RRMediaElement; +} + +type RRElementType< + K extends keyof HTMLElementTagNameMap +> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; + /** * Build a rrdom from a real document tree. * @param dom the real document tree @@ -329,7 +200,7 @@ export class RRDocument extends RRNode { */ export function buildFromDom( dom: Document, - rrdomToBuild?: RRDocument, + rrdomToBuild?: IRRDocument, mirror?: Mirror, ) { let rrdom = rrdomToBuild ?? new RRDocument(); @@ -350,9 +221,9 @@ export function buildFromDom( return element.tagName.toUpperCase(); } - const walk = function (node: INode, parentRRNode: RRNode | null) { + const walk = function (node: INode, parentRRNode: IRRNode | null) { let serializedNodeWithId = node.__sn; - let rrNode: RRNode; + let rrNode: IRRNode; if (!serializedNodeWithId || serializedNodeWithId.id < 0) { serializedNodeWithId = { type: NodeTypeMap[node.nodeType], @@ -364,8 +235,12 @@ export function buildFromDom( switch (node.nodeType) { case node.DOCUMENT_NODE: - if (parentRRNode && parentRRNode instanceof RRIFrameElement) - rrNode = parentRRNode.contentDocument; + if ( + parentRRNode && + parentRRNode.RRNodeType === NodeType.Element && + (parentRRNode as IRRElement).tagName === 'IFRAME' + ) + rrNode = (parentRRNode as RRIFrameElement).contentDocument; else rrNode = rrdom; break; case node.DOCUMENT_TYPE_NODE: @@ -388,10 +263,6 @@ export function buildFromDom( * We don't have to record special values of input elements at the beginning. * Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed. */ - // canvas image data - if (tagName === 'CANVAS') { - rrElement.attributes.rr_dataURL = (elementNode as HTMLCanvasElement).toDataURL(); - } break; case node.TEXT_NODE: rrNode = rrdom.createTextNode( @@ -456,309 +327,5 @@ export function buildFromDom( walk((dom as unknown) as INode, null); return rrdom; } - -export class RRDocumentType extends RRNode { - readonly name: string; - readonly publicId: string; - readonly systemId: string; - - constructor(qualifiedName: string, publicId: string, systemId: string) { - super(); - this.name = qualifiedName; - this.publicId = publicId; - this.systemId = systemId; - } - - toString() { - return super.toString('RRDocumentType'); - } -} - -export class RRElement extends RRNode { - tagName: string; - attributes: Record = {}; - shadowRoot: RRElement | null = null; - inputData: inputData | null = null; - scrollData: scrollData | null = null; - - constructor(tagName: string) { - super(); - this.tagName = tagName; - } - - get classList() { - return new ClassList( - this.attributes.class as string | undefined, - (newClassName) => { - this.attributes.class = newClassName; - }, - ); - } - - get id() { - return this.attributes.id; - } - - get className() { - return this.attributes.class || ''; - } - - get style() { - const style = (this.attributes.style - ? parseCSSText(this.attributes.style as string) - : {}) as Record & { - setProperty: ( - name: string, - value: string | null, - priority?: string | null, - ) => void; - removeProperty: (name: string) => string; - }; - style.setProperty = (name: string, value: string | null) => { - const normalizedName = camelize(name); - if (!value) delete style[normalizedName]; - else style[normalizedName] = value; - this.attributes.style = toCSSText(style); - }; - style.removeProperty = (name: string) => { - const normalizedName = camelize(name); - const value = style[normalizedName] ?? ''; - delete style[normalizedName]; - return value; - }; - return style; - } - - getAttribute(name: string) { - return this.attributes[name] ?? null; - } - - setAttribute(name: string, attribute: string) { - this.attributes[name] = attribute; - } - - setAttributeNS( - _namespace: string | null, - qualifiedName: string, - value: string, - ): void { - this.setAttribute(qualifiedName, value); - } - - removeAttribute(name: string) { - delete this.attributes[name]; - } - - appendChild(newChild: RRNode): RRNode { - this.children.push(newChild); - newChild.parentNode = this; - newChild.parentElement = this; - return newChild; - } - - insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.children.splice(childIndex, 0, newChild); - newChild.parentElement = this; - newChild.parentNode = this; - return newChild; - } - - dispatchEvent(_event: Event) { - return true; - } - - /** - * Creates a shadow root for element and returns it. - */ - attachShadow(_init: ShadowRootInit): RRElement { - this.shadowRoot = new RRElement('SHADOWROOT'); - return this.shadowRoot; - } - - toString() { - let attributeString = ''; - for (let attribute in this.attributes) { - attributeString += `${attribute}="${this.attributes[attribute]}" `; - } - return `${super.toString(this.tagName)} ${attributeString}`; - } -} - -export class RRMediaElement extends RRElement { - currentTime?: number; - volume?: number; - paused?: boolean; - muted?: boolean; - play() { - this.paused = false; - } - pause() { - this.paused = true; - } -} - -export class RRCanvasElement extends RRElement { - public canvasMutation: { - event: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; - }; - mutation: canvasMutationData; - }[] = []; - /** - * This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement. - */ - getContext(): CanvasRenderingContext2D | null { - return null; - } -} - -export class RRStyleElement extends RRElement { - public rules: VirtualStyleRules = []; -} - -export class RRIFrameElement extends RRElement { - width: string = ''; - height: string = ''; - src: string = ''; - contentDocument: RRDocument = new RRDocument(); -} - -export class RRText extends RRNode { - data: string; - - constructor(data: string) { - super(); - this.data = data; - } - - toString() { - return `${super.toString('RRText')} text=${JSON.stringify(this.data)}`; - } -} - -export class RRComment extends RRNode { - data: string; - - constructor(data: string) { - super(); - this.data = data; - } - - toString() { - return `${super.toString('RRComment')} data=${JSON.stringify(this.data)}`; - } -} -export class RRCDATASection extends RRNode { - data: string; - - constructor(data: string) { - super(); - this.data = data; - } - - toString() { - return `${super.toString('RRCDATASection')} data=${JSON.stringify( - this.data, - )}`; - } -} - -type Mirror = { - map: { - [key: number]: RRNode; - }; - getId(n: RRNode): number; - getNode(id: number): RRNode | null; - removeNodeFromMap(n: RRNode): void; - has(id: number): boolean; - reset(): void; -}; - -interface RRElementTagNameMap { - audio: RRMediaElement; - canvas: RRCanvasElement; - iframe: RRIFrameElement; - style: RRStyleElement; - video: RRMediaElement; -} - -type RRElementType< - K extends keyof HTMLElementTagNameMap -> = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; - -class ClassList extends Array { - private onChange: ((newClassText: string) => void) | undefined; - - constructor( - classText?: string, - onChange?: ((newClassText: string) => void) | undefined, - ) { - super(); - if (classText) { - const classes = classText.trim().split(/\s+/); - super.push(...classes); - } - this.onChange = onChange; - } - - add = (...classNames: string[]) => { - for (const item of classNames) { - const className = String(item); - if (super.indexOf(className) >= 0) continue; - super.push(className); - } - this.onChange && this.onChange(super.join(' ')); - }; - - remove = (...classNames: string[]) => { - for (const item of classNames) { - const className = String(item); - const index = super.indexOf(className); - if (index < 0) continue; - super.splice(index, 1); - } - this.onChange && this.onChange(super.join(' ')); - }; -} - -export enum StyleRuleType { - Insert, - Remove, - Snapshot, - SetProperty, - RemoveProperty, -} -type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; - -export type VirtualStyleRules = Array< - InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule ->; - -export { diff } from './diff'; +export { IRRNode as RRNode } from './document'; +export { diff, StyleRuleType, VirtualStyleRules } from './diff'; diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts new file mode 100644 index 0000000000..cd7c6dac88 --- /dev/null +++ b/packages/rrdom/src/document.ts @@ -0,0 +1,549 @@ +import { NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { parseCSSText, camelize, toCSSText } from './style'; + +export type IRRDocument = IRRNode & { + notSerializedId: number; + + createDocument( + _namespace: string | null, + _qualifiedName: string | null, + _doctype?: DocumentType | null, + ): IRRDocument; + + createDocumentType( + qualifiedName: string, + publicId: string, + systemId: string, + ): IRRDocumentType; + + createElement(tagName: string): IRRElement; + + createElementNS( + _namespaceURI: 'http://www.w3.org/2000/svg', + qualifiedName: string, + ): IRRElement; + + createTextNode(data: string): IRRText; + + createComment(data: string): IRRComment; + + createCDATASection(data: string): IRRCDATASection; +}; +export type IRRElement = IRRNode & { + tagName: string; + attributes: Record; + shadowRoot: IRRElement | null; +}; +export type IRRDocumentType = IRRNode & { + readonly name: string; + readonly publicId: string; + readonly systemId: string; +}; +export type IRRText = IRRNode & { + data: string; +}; +export type IRRComment = IRRNode & { + data: string; +}; +export type IRRCDATASection = IRRNode & { + data: string; +}; + +export abstract class IRRNode { + __sn: serializedNodeWithId; + children: IRRNode[] = []; + parentElement: IRRNode | null = null; + parentNode: IRRNode | null = null; + ELEMENT_NODE = 1; + TEXT_NODE = 3; + // corresponding nodeType value of standard HTML Node + readonly nodeType: number; + readonly RRNodeType: NodeType; + + get childNodes(): IRRNode[] { + return this.children; + } + + get firstChild(): IRRNode | null { + return this.childNodes[0] ?? null; + } + + get nextSibling(): IRRNode | null { + let parentNode = this.parentNode; + if (!parentNode) return null; + const siblings = parentNode.children; + let index = siblings.indexOf(this); + return siblings[index + 1] ?? null; + } + + get textContent(): string | null { + if ( + this.RRNodeType === NodeType.Text || + this.RRNodeType === NodeType.Comment || + this.RRNodeType === NodeType.CDATA + ) + return ((this as unknown) as IRRText | IRRComment | IRRCDATASection).data; + else if (this.RRNodeType === NodeType.Element) { + let result = ''; + this.childNodes.forEach((node) => result + node.textContent); + return result; + } else return null; + } + + set textContent(textContent: string | null) { + textContent = textContent || ''; + + if ( + this.RRNodeType === NodeType.Text || + this.RRNodeType === NodeType.Comment || + this.RRNodeType === NodeType.CDATA + ) + ((this as unknown) as + | IRRText + | IRRComment + | IRRCDATASection).data = textContent; + else if (this.RRNodeType === NodeType.Element) { + if (this.childNodes[0].RRNodeType === NodeType.Text) + this.childNodes[0].textContent = textContent; + } + } + + contains(node: IRRNode) { + if (node === this) return true; + for (const child of this.children) { + if (child.contains(node)) return true; + } + return false; + } + + appendChild(_newChild: IRRNode): IRRNode { + throw new Error( + `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, + ); + } + + insertBefore(_newChild: IRRNode, _refChild: IRRNode | null): IRRNode { + throw new Error( + `RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`, + ); + } + + removeChild(node: IRRNode) { + const indexOfChild = this.children.indexOf(node); + if (indexOfChild !== -1) { + this.children.splice(indexOfChild, 1); + node.parentElement = null; + node.parentNode = null; + } + } + + toString(nodeName?: string) { + return `${this.__sn?.id || ''} ${nodeName}`; + } +} + +export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { + return class extends RRNodeClass implements IRRDocument { + readonly nodeType = 9; + readonly RRNodeType = NodeType.Document; + _notSerializedId = -1; // used as an id to identify not serialized node + + /** + * Every time the id is used, it will minus 1 automatically to avoid collisions. + */ + get notSerializedId(): number { + return this._notSerializedId--; + } + + get documentElement(): IRRElement | null { + return ( + (this.children.find( + (node) => + node.RRNodeType === NodeType.Element && + (node as IRRElement).tagName === 'HTML', + ) as IRRElement) || null + ); + } + + get body(): IRRElement | null { + return ( + (this.documentElement?.children.find( + (node) => + node.RRNodeType === NodeType.Element && + (node as IRRElement).tagName === 'BODY', + ) as IRRElement) || null + ); + } + + get head(): IRRElement | null { + return ( + (this.documentElement?.children.find( + (node) => + node.RRNodeType === NodeType.Element && + (node as IRRElement).tagName === 'HEAD', + ) as IRRElement) || null + ); + } + + get implementation(): IRRDocument { + return (this as unknown) as IRRDocument; + } + + get firstElementChild(): IRRElement | null { + return this.documentElement; + } + + appendChild(childNode: IRRNode): IRRNode { + const nodeType = childNode.RRNodeType; + if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { + if (this.children.some((s) => s.RRNodeType === nodeType)) { + throw new Error( + `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ + nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' + } on RRDocument allowed.`, + ); + } + } + childNode.parentElement = null; + childNode.parentNode = this; + this.children.push(childNode); + return childNode; + } + + insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + if (refChild === null) return this.appendChild(newChild); + const childIndex = this.children.indexOf(refChild); + if (childIndex == -1) + throw new Error( + "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", + ); + this.children.splice(childIndex, 0, newChild); + newChild.parentElement = null; + newChild.parentNode = this; + return newChild; + } + + open() { + this.children = []; + } + + close() {} + + /** + * Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot). + * There are two lines used this function: + * 1. doc.write('') + * 2. doc.write('') + */ + write(content: string) { + let publicId; + if ( + content === + '' + ) + publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; + else if ( + content === + '' + ) + publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; + if (publicId) { + const doctype = this.createDocumentType('html', publicId, ''); + doctype.__sn = { + type: NodeType.DocumentType, + name: 'html', + publicId: publicId, + systemId: '', + id: this.notSerializedId, + }; + this.open(); + this.appendChild(doctype); + } + } + + createDocument( + _namespace: string | null, + _qualifiedName: string | null, + _doctype?: DocumentType | null, + ): IRRDocument { + throw new Error('Method not implemented.'); + } + + createDocumentType( + _qualifiedName: string, + _publicId: string, + _systemId: string, + ): IRRDocumentType { + throw new Error('Method not implemented.'); + } + + createElement(_tagName: string): IRRElement { + throw new Error('Method not implemented.'); + } + + createElementNS( + _namespaceURI: 'http://www.w3.org/2000/svg', + _qualifiedName: string, + ): IRRElement { + throw new Error('Method not implemented.'); + } + + createTextNode(_data: string): IRRText { + throw new Error('Method not implemented.'); + } + + createComment(_data: string): IRRComment { + throw new Error('Method not implemented.'); + } + + createCDATASection(_data: string): IRRCDATASection { + throw new Error('Method not implemented.'); + } + + toString() { + return super.toString('RRDocument'); + } + }; +} + +export function BaseRRDocumentTypeImpl(RRNodeClass: typeof IRRNode) { + return class extends RRNodeClass { + readonly nodeType = 10; + readonly RRNodeType = NodeType.DocumentType; + readonly name: string; + readonly publicId: string; + readonly systemId: string; + + constructor(qualifiedName: string, publicId: string, systemId: string) { + super(); + this.name = qualifiedName; + this.publicId = publicId; + this.systemId = systemId; + } + + toString() { + return super.toString('RRDocumentType'); + } + }; +} + +export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { + return class BaseRRElementImpl extends RRNodeClass implements IRRElement { + readonly nodeType = 1; + readonly RRNodeType = NodeType.Element; + tagName: string; + attributes: Record = {}; + shadowRoot: IRRElement | null = null; + + constructor(tagName: string) { + super(); + this.tagName = tagName; + } + + get classList() { + return new ClassList( + this.attributes.class as string | undefined, + (newClassName) => { + this.attributes.class = newClassName; + }, + ); + } + + get id() { + return this.attributes.id; + } + + get className() { + return this.attributes.class || ''; + } + + get style() { + const style = (this.attributes.style + ? parseCSSText(this.attributes.style as string) + : {}) as Record & { + setProperty: ( + name: string, + value: string | null, + priority?: string | null, + ) => void; + removeProperty: (name: string) => string; + }; + style.setProperty = (name: string, value: string | null) => { + const normalizedName = camelize(name); + if (!value) delete style[normalizedName]; + else style[normalizedName] = value; + this.attributes.style = toCSSText(style); + }; + style.removeProperty = (name: string) => { + const normalizedName = camelize(name); + const value = style[normalizedName] ?? ''; + delete style[normalizedName]; + return value; + }; + return style; + } + + getAttribute(name: string) { + return this.attributes[name] ?? null; + } + + setAttribute(name: string, attribute: string) { + this.attributes[name] = attribute; + } + + setAttributeNS( + _namespace: string | null, + qualifiedName: string, + value: string, + ): void { + this.setAttribute(qualifiedName, value); + } + + removeAttribute(name: string) { + delete this.attributes[name]; + } + + appendChild(newChild: IRRNode): IRRNode { + this.children.push(newChild); + newChild.parentNode = this; + newChild.parentElement = this; + return newChild; + } + + insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + if (refChild === null) return this.appendChild(newChild); + const childIndex = this.children.indexOf(refChild); + if (childIndex == -1) + throw new Error( + "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", + ); + this.children.splice(childIndex, 0, newChild); + newChild.parentElement = this; + newChild.parentNode = this; + return newChild; + } + + dispatchEvent(_event: Event) { + return true; + } + + toString() { + let attributeString = ''; + for (let attribute in this.attributes) { + attributeString += `${attribute}="${this.attributes[attribute]}" `; + } + return `${super.toString(this.tagName)} ${attributeString}`; + } + }; +} + +export function BaseRRMediaElementImpl( + RRElementClass: ReturnType, +) { + return class extends RRElementClass { + public currentTime?: number; + public volume?: number; + public paused?: boolean; + public muted?: boolean; + attachShadow(_init: ShadowRootInit): IRRElement { + throw new Error( + `Uncaught DOMException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`, + ); + } + public play() { + this.paused = false; + } + public pause() { + this.paused = true; + } + }; +} + +export function BaseRRTextImpl(RRNodeClass: typeof IRRNode) { + return class extends RRNodeClass implements IRRText { + readonly nodeType = 3; + readonly RRNodeType = NodeType.Text; + data: string; + + constructor(data: string) { + super(); + this.data = data; + } + + toString() { + return `${super.toString('RRText')} text=${JSON.stringify(this.data)}`; + } + }; +} + +export function BaseRRCommentImpl(RRNodeClass: typeof IRRNode) { + return class extends RRNodeClass implements IRRComment { + readonly nodeType = 8; + readonly RRNodeType = NodeType.Comment; + data: string; + + constructor(data: string) { + super(); + this.data = data; + } + + toString() { + return `${super.toString('RRComment')} text=${JSON.stringify(this.data)}`; + } + }; +} + +export function BaseRRCDATASectionImpl(RRNodeClass: typeof IRRNode) { + // @ts-ignore + return class extends RRNodeClass implements IRRCDATASection { + readonly nodeType = 4; + readonly RRNodeType = NodeType.CDATA; + data: string; + + constructor(data: string) { + super(); + this.data = data; + } + + toString() { + return `${super.toString('RRCDATASection')} data=${JSON.stringify( + this.data, + )}`; + } + }; +} + +export class ClassList extends Array { + private onChange: ((newClassText: string) => void) | undefined; + + constructor( + classText?: string, + onChange?: ((newClassText: string) => void) | undefined, + ) { + super(); + if (classText) { + const classes = classText.trim().split(/\s+/); + super.push(...classes); + } + this.onChange = onChange; + } + + add = (...classNames: string[]) => { + for (const item of classNames) { + const className = String(item); + if (super.indexOf(className) >= 0) continue; + super.push(className); + } + this.onChange && this.onChange(super.join(' ')); + }; + + remove = (...classNames: string[]) => { + for (const item of classNames) { + const className = String(item); + const index = super.indexOf(className); + if (index < 0) continue; + super.splice(index, 1); + } + this.onChange && this.onChange(super.join(' ')); + }; +} diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 296393ac24..2e39895876 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -740,7 +740,10 @@ export class Replayer { builtNode.__sn.tagName.toUpperCase() === 'HTML' ) { const { documentElement, head } = iframeEl.contentDocument!; - this.insertStyleRules(documentElement, head); + this.insertStyleRules( + documentElement as HTMLElement | RRElement, + head as HTMLElement | RRElement, + ); } }, cache: this.cache, @@ -1408,9 +1411,11 @@ export class Replayer { ) { // https://github.com/rrweb-io/rrweb/issues/745 // parent is textarea, will only keep one child node as the value - for (const c of Array.from(parent.childNodes as Iterable)) { + for (const c of Array.from( + parent.childNodes as Iterable, + )) { if (c.nodeType === parent.TEXT_NODE) { - parent.removeChild(c as Node & RRNode); + parent.removeChild(c as INode & RRNode); } } } From b98f0a26d4ed078e9fa46d3e6910570dcb377091 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 19 Feb 2022 19:56:19 +1100 Subject: [PATCH 30/79] refactor RRDom to reduce duplicate code --- packages/rrdom/src/diff.ts | 2 +- packages/rrdom/src/document-browser.ts | 46 ++- packages/rrdom/src/document-nodejs.ts | 397 +++----------------- packages/rrdom/src/document.ts | 361 ++++++++++-------- packages/rrdom/src/polyfill.ts | 4 +- packages/rrdom/test/diff.test.ts | 4 +- packages/rrdom/test/document-nodejs.test.ts | 12 +- packages/rrdom/tsconfig.json | 2 +- 8 files changed, 307 insertions(+), 521 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index c92e159adf..e5f2d69500 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -106,7 +106,7 @@ export function diff( case 'AUDIO': case 'VIDEO': const oldMediaElement = (oldTree as Node) as HTMLMediaElement; - const newMediaRRElement = (newRRElement as unknown) as RRMediaElement; + const newMediaRRElement = newRRElement as RRMediaElement; if (newMediaRRElement.paused !== undefined) newMediaRRElement.paused ? oldMediaElement.pause() diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/document-browser.ts index acc52925e9..d61c49ef83 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/document-browser.ts @@ -6,6 +6,7 @@ import type { scrollData, } from 'rrweb/src/types'; import { + BaseRRNode, BaseRRCDATASectionImpl, BaseRRCommentImpl, BaseRRDocumentImpl, @@ -19,9 +20,9 @@ import { } from './document'; import { VirtualStyleRules } from './diff'; -export class RRDocument - extends BaseRRDocumentImpl(IRRNode) - implements IRRDocument { +export const RRNode = BaseRRNode; + +export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = { map: {}, getId(n) { @@ -34,9 +35,7 @@ export class RRDocument const id = n.__sn.id; delete this.map[id]; if (n.childNodes) { - n.childNodes.forEach((child) => - this.removeNodeFromMap(child as IRRNode), - ); + n.childNodes.forEach((child) => this.removeNodeFromMap(child)); } }, has(id) { @@ -54,7 +53,7 @@ export class RRDocument _qualifiedName: string | null, _doctype?: DocumentType | null, ) { - return (new RRDocument() as unknown) as IRRDocument; + return new RRDocument(); } createDocumentType( @@ -113,20 +112,17 @@ export class RRDocument } destroyTree() { - this.children = []; + this.childNodes = []; this.mirror.reset(); } } -export const RRDocumentType = BaseRRDocumentTypeImpl(IRRNode); +export const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); -export class RRElement extends BaseRRElementImpl(IRRNode) { +export class RRElement extends BaseRRElementImpl(RRNode) { inputData: inputData | null = null; scrollData: scrollData | null = null; - /** - * Creates a shadow root for element and returns it. - */ attachShadow(_init: ShadowRootInit): RRElement { const shadowRoot = new RRElement('SHADOWROOT'); this.shadowRoot = shadowRoot; @@ -160,13 +156,13 @@ export class RRIFrameElement extends RRElement { contentDocument: RRDocument = new RRDocument(); } -export const RRText = BaseRRTextImpl(IRRNode); +export const RRText = BaseRRTextImpl(RRNode); export type RRText = typeof RRText; -export const RRComment = BaseRRCommentImpl(IRRNode); +export const RRComment = BaseRRCommentImpl(RRNode); export type RRComment = typeof RRComment; -export const RRCDATASection = BaseRRCDATASectionImpl(IRRNode); +export const RRCDATASection = BaseRRCDATASectionImpl(RRNode); export type RRCDATASection = typeof RRCDATASection; type Mirror = { @@ -203,7 +199,7 @@ export function buildFromDom( rrdomToBuild?: IRRDocument, mirror?: Mirror, ) { - let rrdom = rrdomToBuild ?? new RRDocument(); + let rrdom = rrdomToBuild || new RRDocument(); const NodeTypeMap: Record = {}; NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; @@ -244,7 +240,7 @@ export function buildFromDom( else rrNode = rrdom; break; case node.DOCUMENT_TYPE_NODE: - const documentType = (node as unknown) as DocumentType; + const documentType = (node as Node) as DocumentType; rrNode = rrdom.createDocumentType( documentType.name, documentType.publicId, @@ -252,7 +248,7 @@ export function buildFromDom( ); break; case node.ELEMENT_NODE: - const elementNode = (node as unknown) as HTMLElement; + const elementNode = (node as Node) as HTMLElement; const tagName = getValidTagName(elementNode); rrNode = rrdom.createElement(tagName); const rrElement = rrNode as RRElement; @@ -266,7 +262,7 @@ export function buildFromDom( break; case node.TEXT_NODE: rrNode = rrdom.createTextNode( - ((node as unknown) as Text).textContent || '', + ((node as Node) as Text).textContent || '', ); break; case node.CDATA_SECTION_NODE: @@ -274,7 +270,7 @@ export function buildFromDom( break; case node.COMMENT_NODE: rrNode = rrdom.createComment( - ((node as unknown) as Comment).textContent || '', + ((node as Node) as Comment).textContent || '', ); break; // if node is a shadow root @@ -287,8 +283,11 @@ export function buildFromDom( rrNode.__sn = serializedNodeWithId; mirror && (mirror.map[serializedNodeWithId.id] = rrNode); - if (parentRRNode instanceof RRIFrameElement) { - parentRRNode.contentDocument = rrNode as RRDocument; + if ( + parentRRNode?.RRNodeType === NodeType.Element && + (parentRRNode as IRRElement).tagName === 'IFRAME' + ) { + (parentRRNode as RRIFrameElement).contentDocument = rrNode as RRDocument; } // if node isn't a shadow root else if (node.nodeType !== node.DOCUMENT_FRAGMENT_NODE) { @@ -327,5 +326,4 @@ export function buildFromDom( walk((dom as unknown) as INode, null); return rrdom; } -export { IRRNode as RRNode } from './document'; export { diff, StyleRuleType, VirtualStyleRules } from './diff'; diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 4498814655..a349a1d279 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -1,67 +1,22 @@ -import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { NodeType } from 'rrweb-snapshot'; import { NWSAPI } from 'nwsapi'; -import { parseCSSText, camelize, toCSSText } from './style'; +import { + BaseRRCDATASectionImpl, + BaseRRCommentImpl, + BaseRRDocumentImpl, + BaseRRDocumentTypeImpl, + BaseRRElementImpl, + BaseRRMediaElementImpl, + BaseRRNode, + BaseRRTextImpl, + ClassList, + IRRDocument, +} from './document'; const nwsapi = require('nwsapi'); const cssom = require('cssom'); -export abstract class RRNode { - __sn: serializedNodeWithId | undefined; - children: Array = []; - parentElement: RRElement | null = null; - parentNode: RRNode | null = null; +export class RRNode extends BaseRRNode { ownerDocument: RRDocument | null = null; - ELEMENT_NODE = 1; - TEXT_NODE = 3; - - get firstChild() { - return this.children[0]; - } - - get nodeType() { - if (this instanceof RRDocument) return NodeType.Document; - if (this instanceof RRDocumentType) return NodeType.DocumentType; - if (this instanceof RRElement) return NodeType.Element; - if (this instanceof RRText) return NodeType.Text; - if (this instanceof RRCDATASection) return NodeType.CDATA; - if (this instanceof RRComment) return NodeType.Comment; - } - - get childNodes() { - return this.children; - } - - appendChild(newChild: RRNode): RRNode { - throw new Error( - `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, - ); - } - - insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { - throw new Error( - `RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`, - ); - } - - contains(node: RRNode) { - if (node === this) return true; - for (const child of this.children) { - if (child.contains(node)) return true; - } - return false; - } - - removeChild(node: RRNode) { - const indexOfChild = this.children.indexOf(node); - if (indexOfChild !== -1) { - this.children.splice(indexOfChild, 1); - node.parentElement = null; - node.parentNode = null; - } - } - - toString(nodeName?: string) { - return `${JSON.stringify(this.__sn?.id) || ''} ${nodeName}`; - } } export class RRWindow { @@ -74,7 +29,9 @@ export class RRWindow { } } -export class RRDocument extends RRNode { +export class RRDocument + extends BaseRRDocumentImpl(RRNode) + implements IRRDocument { private _nwsapi: NWSAPI; get nwsapi() { if (!this._nwsapi) { @@ -94,66 +51,32 @@ export class RRDocument extends RRNode { return this._nwsapi; } - get documentElement(): RRElement { - return this.children.find( - (node) => node instanceof RRElement && node.tagName === 'HTML', - ) as RRElement; + get documentElement(): RRElement | null { + return super.documentElement as RRElement | null; } - get body() { - return ( - this.documentElement?.children.find( - (node) => node instanceof RRElement && node.tagName === 'BODY', - ) || null - ); + get body(): RRElement | null { + return super.body as RRElement | null; } get head() { - return ( - this.documentElement?.children.find( - (node) => node instanceof RRElement && node.tagName === 'HEAD', - ) || null - ); + return super.head as RRElement | null; } - get implementation() { + get implementation(): RRDocument { return this; } - get firstElementChild() { - return this.documentElement; + get firstElementChild(): RRElement | null { + return this.documentElement as RRElement | null; } appendChild(childNode: RRNode) { - const nodeType = childNode.nodeType; - if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { - if (this.children.some((s) => s.nodeType === nodeType)) { - throw new Error( - `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ - nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' - } on RRDocument allowed.`, - ); - } - } - childNode.parentElement = null; - childNode.parentNode = this; - childNode.ownerDocument = this; - this.children.push(childNode); - return childNode; + return super.appendChild(childNode); } insertBefore(newChild: RRNode, refChild: RRNode | null) { - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.children.splice(childIndex, 0, newChild); - newChild.parentElement = null; - newChild.parentNode = this; - newChild.ownerDocument = this; - return newChild; + return super.insertBefore(newChild, refChild); } querySelectorAll(selectors: string): RRNode[] { @@ -258,160 +181,59 @@ export class RRDocument extends RRNode { textNode.ownerDocument = this; return textNode; } - - /** - * This does come with some side effects. For example: - * 1. All event listeners currently registered on the document, nodes inside the document, or the document's window are removed. - * 2. All existing nodes are removed from the document. - */ - open() { - this.children = []; - } - - close() {} - - toString() { - return super.toString('RRDocument'); - } } -export class RRDocumentType extends RRNode { - readonly name: string; - readonly publicId: string; - readonly systemId: string; - - constructor(qualifiedName: string, publicId: string, systemId: string) { - super(); - this.name = qualifiedName; - this.publicId = publicId; - this.systemId = systemId; - } +export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} - toString() { - return super.toString('RRDocumentType'); +export class RRElement extends BaseRRElementImpl(RRNode) { + attachShadow(_init: ShadowRootInit): RRElement { + const shadowRoot = new RRElement('SHADOWROOT'); + this.shadowRoot = shadowRoot; + return shadowRoot; } -} -export class RRElement extends RRNode { - tagName: string; - attributes: Record = {}; - scrollLeft: number = 0; - scrollTop: number = 0; - shadowRoot: RRElement | null = null; - - constructor(tagName: string) { - super(); - this.tagName = tagName; + appendChild(newChild: RRNode): RRNode { + return super.appendChild(newChild) as RRNode; } - get classList() { - return new ClassList( - this.attributes.class as string | undefined, - (newClassName) => { - this.attributes.class = newClassName; - }, - ); + insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { + return super.insertBefore(newChild, refChild) as RRNode; } - get id() { - return this.attributes.id; + getAttribute(name: string) { + let upperName = name && name.toLowerCase(); + if (upperName in this.attributes) return this.attributes[upperName]; + return null; } - get className() { - return this.attributes.class || ''; + setAttribute(name: string, attribute: string) { + this.attributes[name.toLowerCase()] = attribute; } - get textContent() { - return ''; + hasAttribute(name: string) { + return name.toLowerCase() in this.attributes; } - set textContent(newText: string) {} - - get style() { - const style = (this.attributes.style - ? parseCSSText(this.attributes.style as string) - : {}) as Record & { - setProperty: ( - name: string, - value: string | null, - priority?: string | null, - ) => void; - }; - style.setProperty = (name: string, value: string | null) => { - const normalizedName = camelize(name); - if (!value) delete style[normalizedName]; - else style[normalizedName] = value; - this.attributes.style = toCSSText(style); - }; - // This is used to bypass the smoothscroll polyfill in rrweb player. - style.scrollBehavior = ''; - return style; + removeAttribute(name: string) { + delete this.attributes[name.toLowerCase()]; } get firstElementChild(): RRElement | null { - for (let child of this.children) - if (child instanceof RRElement) return child; + for (let child of this.childNodes) + if (child.RRNodeType === NodeType.Element) return child as RRElement; return null; } get nextElementSibling(): RRElement | null { let parentNode = this.parentNode; if (!parentNode) return null; - const siblings = parentNode.children; + const siblings = parentNode.childNodes; let index = siblings.indexOf(this); for (let i = index + 1; i < siblings.length; i++) if (siblings[i] instanceof RRElement) return siblings[i] as RRElement; return null; } - getAttribute(name: string) { - let upperName = name && name.toLowerCase(); - if (upperName in this.attributes) return this.attributes[upperName]; - return null; - } - - setAttribute(name: string, attribute: string) { - this.attributes[name.toLowerCase()] = attribute; - } - - hasAttribute(name: string) { - return (name && name.toLowerCase()) in this.attributes; - } - - setAttributeNS( - _namespace: string | null, - qualifiedName: string, - value: string, - ): void { - this.setAttribute(qualifiedName, value); - } - - removeAttribute(name: string) { - delete this.attributes[name]; - } - - appendChild(newChild: RRNode): RRNode { - this.children.push(newChild); - newChild.parentNode = this; - newChild.parentElement = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; - } - - insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.children.splice(childIndex, 0, newChild); - newChild.parentElement = this; - newChild.parentNode = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; - } - querySelectorAll(selectors: string): RRNode[] { if (this.ownerDocument !== null) { return (this.ownerDocument.nwsapi.select( @@ -424,7 +246,7 @@ export class RRElement extends RRNode { getElementById(elementId: string): RRElement | null { if (this.id === elementId) return this; - for (const child of this.children) { + for (const child of this.childNodes) { if (child instanceof RRElement) { const result = child.getElementById(elementId); if (result !== null) return result; @@ -444,7 +266,7 @@ export class RRElement extends RRNode { ).length == queryClassList.length ) elements.push(this); - for (const child of this.children) { + for (const child of this.childNodes) { if (child instanceof RRElement) elements = elements.concat(child.getElementsByClassName(className)); } @@ -456,32 +278,12 @@ export class RRElement extends RRNode { const normalizedTagName = tagName.toUpperCase(); if (this instanceof RRElement && this.tagName === normalizedTagName) elements.push(this); - for (const child of this.children) { + for (const child of this.childNodes) { if (child instanceof RRElement) elements = elements.concat(child.getElementsByTagName(tagName)); } return elements; } - - dispatchEvent(_event: Event) { - return true; - } - - /** - * Creates a shadow root for element and returns it. - */ - attachShadow(init: ShadowRootInit): RRElement { - this.shadowRoot = init.mode === 'open' ? this : null; - return this; - } - - toString() { - let attributeString = ''; - for (let attribute in this.attributes) { - attributeString += `${attribute}="${this.attributes[attribute]}" `; - } - return `${super.toString(this.tagName)} ${attributeString}`; - } } export class RRImageElement extends RRElement { @@ -491,16 +293,7 @@ export class RRImageElement extends RRElement { onload: ((this: GlobalEventHandlers, ev: Event) => any) | null; } -export class RRMediaElement extends RRElement { - currentTime: number = 0; - paused: boolean = true; - async play() { - this.paused = false; - } - async pause() { - this.paused = true; - } -} +export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} export class RRCanvasElement extends RRElement { /** @@ -542,89 +335,21 @@ export class RRIFrameElement extends RRElement { } } -export class RRText extends RRNode { - textContent: string; +export class RRText extends BaseRRTextImpl(RRNode) {} - constructor(data: string) { - super(); - this.textContent = data; - } - - toString() { - return `${super.toString('RRText')} text=${JSON.stringify( - this.textContent, - )}`; - } -} +export class RRComment extends BaseRRCommentImpl(RRNode) {} -export class RRComment extends RRNode { - data: string; - - constructor(data: string) { - super(); - this.data = data; - } - - toString() { - return `${super.toString('RRComment')} data=${JSON.stringify(this.data)}`; - } -} -export class RRCDATASection extends RRNode { - data: string; - - constructor(data: string) { - super(); - this.data = data; - } - - toString() { - return `${super.toString('RRCDATASection')} data=${JSON.stringify( - this.data, - )}`; - } -} +export class RRCDATASection extends BaseRRCDATASectionImpl(RRNode) {} interface RRElementTagNameMap { - img: RRImageElement; audio: RRMediaElement; + canvas: RRCanvasElement; + iframe: RRIFrameElement; + img: RRImageElement; + style: RRStyleElement; video: RRMediaElement; } type RRElementType< K extends keyof HTMLElementTagNameMap > = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; - -class ClassList extends Array { - private onChange: ((newClassText: string) => void) | undefined; - - constructor( - classText?: string, - onChange?: ((newClassText: string) => void) | undefined, - ) { - super(); - if (classText) { - const classes = classText.trim().split(/\s+/); - super.push(...classes); - } - this.onChange = onChange; - } - - add = (...classNames: string[]) => { - for (const item of classNames) { - const className = String(item); - if (super.indexOf(className) >= 0) continue; - super.push(className); - } - this.onChange && this.onChange(super.join(' ')); - }; - - remove = (...classNames: string[]) => { - for (const item of classNames) { - const className = String(item); - const index = super.indexOf(className); - if (index < 0) continue; - super.splice(index, 1); - } - this.onChange && this.onChange(super.join(' ')); - }; -} diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index cd7c6dac88..a178373734 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -1,9 +1,46 @@ import { NodeType, serializedNodeWithId } from 'rrweb-snapshot'; import { parseCSSText, camelize, toCSSText } from './style'; -export type IRRDocument = IRRNode & { +export interface IRRNode { + __sn: serializedNodeWithId; + parentElement: IRRNode | null; + parentNode: IRRNode | null; + childNodes: IRRNode[]; + ELEMENT_NODE: 1; + TEXT_NODE: 3; + // corresponding nodeType value of standard HTML Node + readonly nodeType: number; + readonly RRNodeType: NodeType; + + firstChild: IRRNode | null; + + nextSibling: IRRNode | null; + + textContent: string | null; + + contains(node: IRRNode): boolean; + + appendChild(newChild: IRRNode): IRRNode; + + insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode; + + removeChild(node: IRRNode): void; + + toString(nodeName?: string): string; +} +export interface IRRDocument extends IRRNode { notSerializedId: number; + documentElement: IRRElement | null; + + body: IRRElement | null; + + head: IRRElement | null; + + implementation: IRRDocument; + + firstElementChild: IRRElement | null; + createDocument( _namespace: string | null, _qualifiedName: string | null, @@ -18,146 +55,117 @@ export type IRRDocument = IRRNode & { createElement(tagName: string): IRRElement; - createElementNS( - _namespaceURI: 'http://www.w3.org/2000/svg', - qualifiedName: string, - ): IRRElement; + createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement; createTextNode(data: string): IRRText; createComment(data: string): IRRComment; createCDATASection(data: string): IRRCDATASection; -}; -export type IRRElement = IRRNode & { +} +export interface IRRElement extends IRRNode { tagName: string; attributes: Record; shadowRoot: IRRElement | null; -}; -export type IRRDocumentType = IRRNode & { + scrollLeft?: number; + scrollTop?: number; +} +export interface IRRDocumentType extends IRRNode { readonly name: string; readonly publicId: string; readonly systemId: string; -}; -export type IRRText = IRRNode & { +} +export interface IRRText extends IRRNode { data: string; -}; -export type IRRComment = IRRNode & { +} +export interface IRRComment extends IRRNode { data: string; -}; -export type IRRCDATASection = IRRNode & { +} +export interface IRRCDATASection extends IRRNode { data: string; -}; +} -export abstract class IRRNode { - __sn: serializedNodeWithId; - children: IRRNode[] = []; - parentElement: IRRNode | null = null; - parentNode: IRRNode | null = null; - ELEMENT_NODE = 1; - TEXT_NODE = 3; +type ConstrainedConstructor = new (...args: any[]) => T; + +export class BaseRRNode implements IRRNode { + public __sn: serializedNodeWithId; + public childNodes: IRRNode[] = []; + public parentElement: IRRNode | null = null; + public parentNode: IRRNode | null = null; + public textContent: string | null; + public ELEMENT_NODE: 1 = 1; + public TEXT_NODE: 3 = 3; // corresponding nodeType value of standard HTML Node - readonly nodeType: number; - readonly RRNodeType: NodeType; + public readonly nodeType: number; + public readonly RRNodeType: NodeType; - get childNodes(): IRRNode[] { - return this.children; - } + constructor(...args: any[]) {} - get firstChild(): IRRNode | null { - return this.childNodes[0] ?? null; + public get firstChild(): IRRNode | null { + return this.childNodes[0] || null; } - get nextSibling(): IRRNode | null { + public get nextSibling(): IRRNode | null { let parentNode = this.parentNode; if (!parentNode) return null; - const siblings = parentNode.children; + const siblings = parentNode.childNodes; let index = siblings.indexOf(this); - return siblings[index + 1] ?? null; - } - - get textContent(): string | null { - if ( - this.RRNodeType === NodeType.Text || - this.RRNodeType === NodeType.Comment || - this.RRNodeType === NodeType.CDATA - ) - return ((this as unknown) as IRRText | IRRComment | IRRCDATASection).data; - else if (this.RRNodeType === NodeType.Element) { - let result = ''; - this.childNodes.forEach((node) => result + node.textContent); - return result; - } else return null; + return siblings[index + 1] || null; } - set textContent(textContent: string | null) { - textContent = textContent || ''; - - if ( - this.RRNodeType === NodeType.Text || - this.RRNodeType === NodeType.Comment || - this.RRNodeType === NodeType.CDATA - ) - ((this as unknown) as - | IRRText - | IRRComment - | IRRCDATASection).data = textContent; - else if (this.RRNodeType === NodeType.Element) { - if (this.childNodes[0].RRNodeType === NodeType.Text) - this.childNodes[0].textContent = textContent; - } - } - - contains(node: IRRNode) { + public contains(node: IRRNode) { if (node === this) return true; - for (const child of this.children) { + for (const child of this.childNodes) { if (child.contains(node)) return true; } return false; } - appendChild(_newChild: IRRNode): IRRNode { + public appendChild(_newChild: IRRNode): IRRNode { throw new Error( `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, ); } - insertBefore(_newChild: IRRNode, _refChild: IRRNode | null): IRRNode { + public insertBefore(_newChild: IRRNode, _refChild: IRRNode | null): IRRNode { throw new Error( `RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`, ); } - removeChild(node: IRRNode) { - const indexOfChild = this.children.indexOf(node); + public removeChild(node: IRRNode) { + const indexOfChild = this.childNodes.indexOf(node); if (indexOfChild !== -1) { - this.children.splice(indexOfChild, 1); + this.childNodes.splice(indexOfChild, 1); node.parentElement = null; node.parentNode = null; } } - toString(nodeName?: string) { + public toString(nodeName?: string) { return `${this.__sn?.id || ''} ${nodeName}`; } } -export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { - return class extends RRNodeClass implements IRRDocument { - readonly nodeType = 9; - readonly RRNodeType = NodeType.Document; +export function BaseRRDocumentImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + return class BaseRRDocument extends RRNodeClass implements IRRDocument { + public readonly nodeType = 9; + public readonly RRNodeType = NodeType.Document; + public textContent: string | null = null; _notSerializedId = -1; // used as an id to identify not serialized node /** * Every time the id is used, it will minus 1 automatically to avoid collisions. */ - get notSerializedId(): number { + public get notSerializedId(): number { return this._notSerializedId--; } - get documentElement(): IRRElement | null { + public get documentElement(): IRRElement | null { return ( - (this.children.find( + (this.childNodes.find( (node) => node.RRNodeType === NodeType.Element && (node as IRRElement).tagName === 'HTML', @@ -165,9 +173,9 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { ); } - get body(): IRRElement | null { + public get body(): IRRElement | null { return ( - (this.documentElement?.children.find( + (this.documentElement?.childNodes.find( (node) => node.RRNodeType === NodeType.Element && (node as IRRElement).tagName === 'BODY', @@ -175,9 +183,9 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { ); } - get head(): IRRElement | null { + public get head(): IRRElement | null { return ( - (this.documentElement?.children.find( + (this.documentElement?.childNodes.find( (node) => node.RRNodeType === NodeType.Element && (node as IRRElement).tagName === 'HEAD', @@ -185,18 +193,18 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { ); } - get implementation(): IRRDocument { - return (this as unknown) as IRRDocument; + public get implementation(): IRRDocument { + return this; } - get firstElementChild(): IRRElement | null { + public get firstElementChild(): IRRElement | null { return this.documentElement; } - appendChild(childNode: IRRNode): IRRNode { + public appendChild(childNode: IRRNode): IRRNode { const nodeType = childNode.RRNodeType; if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { - if (this.children.some((s) => s.RRNodeType === nodeType)) { + if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { throw new Error( `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' @@ -206,28 +214,28 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { } childNode.parentElement = null; childNode.parentNode = this; - this.children.push(childNode); + this.childNodes.push(childNode); return childNode; } - insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); + const childIndex = this.childNodes.indexOf(refChild); if (childIndex == -1) throw new Error( "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", ); - this.children.splice(childIndex, 0, newChild); + this.childNodes.splice(childIndex, 0, newChild); newChild.parentElement = null; newChild.parentNode = this; return newChild; } - open() { - this.children = []; + public open() { + this.childNodes = []; } - close() {} + public close() {} /** * Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot). @@ -235,7 +243,7 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { * 1. doc.write('') * 2. doc.write('') */ - write(content: string) { + public write(content: string) { let publicId; if ( content === @@ -281,10 +289,7 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { throw new Error('Method not implemented.'); } - createElementNS( - _namespaceURI: 'http://www.w3.org/2000/svg', - _qualifiedName: string, - ): IRRElement { + createElementNS(_namespaceURI: string, _qualifiedName: string): IRRElement { throw new Error('Method not implemented.'); } @@ -306,13 +311,19 @@ export function BaseRRDocumentImpl(RRNodeClass: typeof IRRNode) { }; } -export function BaseRRDocumentTypeImpl(RRNodeClass: typeof IRRNode) { - return class extends RRNodeClass { - readonly nodeType = 10; - readonly RRNodeType = NodeType.DocumentType; - readonly name: string; - readonly publicId: string; - readonly systemId: string; +export function BaseRRDocumentTypeImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRDocumentType + extends RRNodeClass + implements IRRDocumentType { + public readonly nodeType = 10; + public readonly RRNodeType = NodeType.DocumentType; + public readonly name: string; + public readonly publicId: string; + public readonly systemId: string; + public textContent: string | null = null; constructor(qualifiedName: string, publicId: string, systemId: string) { super(); @@ -327,20 +338,36 @@ export function BaseRRDocumentTypeImpl(RRNodeClass: typeof IRRNode) { }; } -export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { - return class BaseRRElementImpl extends RRNodeClass implements IRRElement { - readonly nodeType = 1; - readonly RRNodeType = NodeType.Element; - tagName: string; - attributes: Record = {}; - shadowRoot: IRRElement | null = null; +export function BaseRRElementImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRElement extends RRNodeClass implements IRRElement { + public readonly nodeType = 1; + public readonly RRNodeType = NodeType.Element; + public tagName: string; + public attributes: Record = {}; + public shadowRoot: IRRElement | null = null; + public scrollLeft?: number; + public scrollTop?: number; constructor(tagName: string) { super(); this.tagName = tagName; } - get classList() { + public get textContent(): string { + let result = ''; + this.childNodes.forEach((node) => result + node.textContent); + return result; + } + + public set textContent(textContent: string) { + if (this.childNodes[0].RRNodeType === NodeType.Text) + this.childNodes[0].textContent = textContent; + } + + public get classList() { return new ClassList( this.attributes.class as string | undefined, (newClassName) => { @@ -349,15 +376,15 @@ export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { ); } - get id() { + public get id() { return this.attributes.id; } - get className() { + public get className() { return this.attributes.class || ''; } - get style() { + public get style() { const style = (this.attributes.style ? parseCSSText(this.attributes.style as string) : {}) as Record & { @@ -376,22 +403,22 @@ export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { }; style.removeProperty = (name: string) => { const normalizedName = camelize(name); - const value = style[normalizedName] ?? ''; + const value = style[normalizedName] || ''; delete style[normalizedName]; return value; }; return style; } - getAttribute(name: string) { - return this.attributes[name] ?? null; + public getAttribute(name: string) { + return this.attributes[name] || null; } - setAttribute(name: string, attribute: string) { + public setAttribute(name: string, attribute: string) { this.attributes[name] = attribute; } - setAttributeNS( + public setAttributeNS( _namespace: string | null, qualifiedName: string, value: string, @@ -399,31 +426,31 @@ export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { this.setAttribute(qualifiedName, value); } - removeAttribute(name: string) { + public removeAttribute(name: string) { delete this.attributes[name]; } - appendChild(newChild: IRRNode): IRRNode { - this.children.push(newChild); + public appendChild(newChild: IRRNode): IRRNode { + this.childNodes.push(newChild); newChild.parentNode = this; newChild.parentElement = this; return newChild; } - insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { if (refChild === null) return this.appendChild(newChild); - const childIndex = this.children.indexOf(refChild); + const childIndex = this.childNodes.indexOf(refChild); if (childIndex == -1) throw new Error( "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", ); - this.children.splice(childIndex, 0, newChild); + this.childNodes.splice(childIndex, 0, newChild); newChild.parentElement = this; newChild.parentNode = this; return newChild; } - dispatchEvent(_event: Event) { + public dispatchEvent(_event: Event) { return true; } @@ -437,10 +464,10 @@ export function BaseRRElementImpl(RRNodeClass: typeof IRRNode) { }; } -export function BaseRRMediaElementImpl( - RRElementClass: ReturnType, -) { - return class extends RRElementClass { +export function BaseRRMediaElementImpl< + RRElement extends ConstrainedConstructor +>(RRElementClass: RRElement) { + return class BaseRRMediaElement extends RRElementClass { public currentTime?: number; public volume?: number; public paused?: boolean; @@ -459,52 +486,86 @@ export function BaseRRMediaElementImpl( }; } -export function BaseRRTextImpl(RRNodeClass: typeof IRRNode) { - return class extends RRNodeClass implements IRRText { - readonly nodeType = 3; - readonly RRNodeType = NodeType.Text; - data: string; +export function BaseRRTextImpl>( + RRNodeClass: RRNode, +) { + // @ts-ignore + return class BaseRRText extends RRNodeClass implements IRRText { + public readonly nodeType = 3; + public readonly RRNodeType = NodeType.Text; + public data: string; constructor(data: string) { super(); this.data = data; } + public get textContent(): string { + return this.data; + } + + public set textContent(textContent: string) { + this.data = textContent; + } + toString() { return `${super.toString('RRText')} text=${JSON.stringify(this.data)}`; } }; } -export function BaseRRCommentImpl(RRNodeClass: typeof IRRNode) { - return class extends RRNodeClass implements IRRComment { - readonly nodeType = 8; - readonly RRNodeType = NodeType.Comment; - data: string; +export function BaseRRCommentImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRComment extends RRNodeClass implements IRRComment { + public readonly nodeType = 8; + public readonly RRNodeType = NodeType.Comment; + public data: string; constructor(data: string) { super(); this.data = data; } + public get textContent(): string { + return this.data; + } + + public set textContent(textContent: string) { + this.data = textContent; + } + toString() { return `${super.toString('RRComment')} text=${JSON.stringify(this.data)}`; } }; } -export function BaseRRCDATASectionImpl(RRNodeClass: typeof IRRNode) { +export function BaseRRCDATASectionImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { // @ts-ignore - return class extends RRNodeClass implements IRRCDATASection { - readonly nodeType = 4; - readonly RRNodeType = NodeType.CDATA; - data: string; + return class BaseRRCDATASection + extends RRNodeClass + implements IRRCDATASection { + public readonly nodeType = 4; + public readonly RRNodeType = NodeType.CDATA; + public data: string; constructor(data: string) { super(); this.data = data; } + public get textContent(): string { + return this.data; + } + + public set textContent(textContent: string) { + this.data = textContent; + } + toString() { return `${super.toString('RRCDATASection')} data=${JSON.stringify( this.data, diff --git a/packages/rrdom/src/polyfill.ts b/packages/rrdom/src/polyfill.ts index 4114e19c1a..b93c556542 100644 --- a/packages/rrdom/src/polyfill.ts +++ b/packages/rrdom/src/polyfill.ts @@ -82,8 +82,8 @@ export function polyfillDocument() { const rrdom = new RRDocument(); (() => { rrdom.appendChild(rrdom.createElement('html')); - rrdom.documentElement.appendChild(rrdom.createElement('head')); - rrdom.documentElement.appendChild(rrdom.createElement('body')); + rrdom.documentElement!.appendChild(rrdom.createElement('head')); + rrdom.documentElement!.appendChild(rrdom.createElement('body')); })(); global.document = (rrdom as unknown) as Document; } diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index aba3e8db65..095efeb7cb 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -4,7 +4,6 @@ import { RRDocument, RRElement, - RRNode, StyleRuleType, VirtualStyleRules, } from '../src/document-browser'; @@ -15,6 +14,7 @@ import { ReplayerHandler, } from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; +import { IRRNode } from '../src/document'; const elementSn = { type: NodeType.Element, @@ -30,6 +30,8 @@ type ElementType = { children?: ElementType[]; }; +type RRNode = IRRNode; + /** * Create a document tree or a RRDom tree according to the given ElementType data. * diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 7f3be46689..3c3974193b 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -54,7 +54,7 @@ describe('RRDocument for nodejs environment', () => { it('get firstElementChild', () => { expect(rrdom.firstElementChild).toBeDefined(); - expect(rrdom.firstElementChild.tagName).toEqual('HTML'); + expect(rrdom.firstElementChild!.tagName).toEqual('HTML'); const div1 = rrdom.getElementById('block1'); expect(div1).toBeDefined(); @@ -65,16 +65,16 @@ describe('RRDocument for nodejs environment', () => { }); it('get nextElementSibling', () => { - expect(rrdom.documentElement.firstElementChild).not.toBeNull(); - expect(rrdom.documentElement.firstElementChild!.tagName).toEqual('HEAD'); + expect(rrdom.documentElement!.firstElementChild).not.toBeNull(); + expect(rrdom.documentElement!.firstElementChild!.tagName).toEqual('HEAD'); expect( - rrdom.documentElement.firstElementChild!.nextElementSibling, + rrdom.documentElement!.firstElementChild!.nextElementSibling, ).not.toBeNull(); expect( - rrdom.documentElement.firstElementChild!.nextElementSibling!.tagName, + rrdom.documentElement!.firstElementChild!.nextElementSibling!.tagName, ).toEqual('BODY'); expect( - rrdom.documentElement.firstElementChild!.nextElementSibling! + rrdom.documentElement!.firstElementChild!.nextElementSibling! .nextElementSibling, ).toBeNull(); diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index 15e1aac33c..1a9974856a 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "module": "commonjs", "noImplicitAny": true, "strictNullChecks": true, From f96477c09146d1bd692fbd0e3b84595505f8d66e Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 19 Feb 2022 20:29:00 +1100 Subject: [PATCH 31/79] rename document-browser to virtual-dom --- packages/rrdom/rollup.config.js | 4 ++-- packages/rrdom/src/diff.ts | 2 +- packages/rrdom/src/{document-browser.ts => virtual-dom.ts} | 2 +- ...ument-browser.test.ts.snap => virtual-dom.test.ts.snap} | 0 packages/rrdom/test/diff.test.ts | 2 +- packages/rrdom/test/document-nodejs.test.ts | 7 ++----- .../test/{document-browser.test.ts => virtual-dom.test.ts} | 2 +- packages/rrweb/src/replay/index.ts | 4 ++-- packages/rrweb/src/types.ts | 2 +- packages/rrweb/src/utils.ts | 2 +- 10 files changed, 12 insertions(+), 15 deletions(-) rename packages/rrdom/src/{document-browser.ts => virtual-dom.ts} (99%) rename packages/rrdom/test/__snapshots__/{document-browser.test.ts.snap => virtual-dom.test.ts.snap} (100%) rename packages/rrdom/test/{document-browser.test.ts => virtual-dom.test.ts} (97%) diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index ee8e668c5e..ecb174a91c 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -28,9 +28,9 @@ const baseConfigs = [ path: 'document-nodejs', }, { - input: './src/document-browser.ts', + input: './src/virtual-dom.ts', name: 'RRDocument', - path: 'document-browser', + path: 'virtual-dom', }, ]; diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index e5f2d69500..4078835f81 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -22,7 +22,7 @@ import type { RRIFrameElement, RRMediaElement, RRStyleElement, -} from './document-browser'; +} from './virtual-dom'; const NAMESPACES: Record = { svg: 'http://www.w3.org/2000/svg', diff --git a/packages/rrdom/src/document-browser.ts b/packages/rrdom/src/virtual-dom.ts similarity index 99% rename from packages/rrdom/src/document-browser.ts rename to packages/rrdom/src/virtual-dom.ts index d61c49ef83..a4af83c402 100644 --- a/packages/rrdom/src/document-browser.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -20,7 +20,7 @@ import { } from './document'; import { VirtualStyleRules } from './diff'; -export const RRNode = BaseRRNode; +export class RRNode extends BaseRRNode {} export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = { diff --git a/packages/rrdom/test/__snapshots__/document-browser.test.ts.snap b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap similarity index 100% rename from packages/rrdom/test/__snapshots__/document-browser.test.ts.snap rename to packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 095efeb7cb..6e5d5bf19f 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -6,7 +6,7 @@ import { RRElement, StyleRuleType, VirtualStyleRules, -} from '../src/document-browser'; +} from '../src/virtual-dom'; import { applyVirtualStyleRulesToNode, createOrGetNode, diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 3c3974193b..2b28ae85be 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -4,10 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs'; -import { - RRDocument as RRDocumentBrowser, - buildFromDom, -} from '../src/document-browser'; +import { buildFromDom } from '../src/virtual-dom'; describe('RRDocument for nodejs environment', () => { describe('RRDocument API', () => { @@ -16,7 +13,7 @@ describe('RRDocument for nodejs environment', () => { // initialize rrdom document.write(getHtml('main.html')); rrdom = new RRDocument(); - buildFromDom(document, (rrdom as unknown) as RRDocumentBrowser); + buildFromDom(document, rrdom); }); it('get className', () => { diff --git a/packages/rrdom/test/document-browser.test.ts b/packages/rrdom/test/virtual-dom.test.ts similarity index 97% rename from packages/rrdom/test/document-browser.test.ts rename to packages/rrdom/test/virtual-dom.test.ts index 2e5eac81e9..f631b0d080 100644 --- a/packages/rrdom/test/document-browser.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -33,7 +33,7 @@ describe('RRDocument for browser environment', () => { beforeAll(async () => { browser = await puppeteer.launch(); const bundle = await rollup.rollup({ - input: path.resolve(__dirname, '../src/document-browser.ts'), + input: path.resolve(__dirname, '../src/virtual-dom.ts'), plugins: [ resolve(), _typescript({ diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 2e39895876..b419a73138 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -14,12 +14,12 @@ import { RRStyleElement, RRIFrameElement, RRMediaElement, + RRCanvasElement, StyleRuleType, VirtualStyleRules, buildFromDom, diff, - RRCanvasElement, -} from 'rrdom/es/document-browser'; +} from 'rrdom/es/virtual-dom'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 7120296017..8246f6f87d 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -11,7 +11,7 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; -import type { RRNode } from 'rrdom/es/document-browser'; +import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export enum EventType { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 0d3655ba54..0ab5531209 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -15,7 +15,7 @@ import { NodeType, isShadowRoot, } from 'rrweb-snapshot'; -import { RRNode, RRIFrameElement } from 'rrdom/es/document-browser'; +import { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; export function on( type: string, From d98087836d2000625772c3e246c175f5b0f6f815 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 2 Mar 2022 19:29:16 +1100 Subject: [PATCH 32/79] increase the test coverage for document.ts and add ownerDocument for it --- packages/rrdom/src/document-nodejs.ts | 16 +- packages/rrdom/src/document.ts | 188 ++-- packages/rrdom/src/style.ts | 79 +- packages/rrdom/src/virtual-dom.ts | 39 +- packages/rrdom/test/document-nodejs.test.ts | 2 +- packages/rrdom/test/document.test.ts | 932 ++++++++++++++++++++ 6 files changed, 1135 insertions(+), 121 deletions(-) create mode 100644 packages/rrdom/test/document.test.ts diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index a349a1d279..ac3b71c8a4 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -15,9 +15,7 @@ import { const nwsapi = require('nwsapi'); const cssom = require('cssom'); -export class RRNode extends BaseRRNode { - ownerDocument: RRDocument | null = null; -} +export class RRNode extends BaseRRNode {} export class RRWindow { scrollLeft = 0; @@ -187,9 +185,7 @@ export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} export class RRElement extends BaseRRElementImpl(RRNode) { attachShadow(_init: ShadowRootInit): RRElement { - const shadowRoot = new RRElement('SHADOWROOT'); - this.shadowRoot = shadowRoot; - return shadowRoot; + return super.attachShadow(_init) as RRElement; } appendChild(newChild: RRNode): RRNode { @@ -236,7 +232,7 @@ export class RRElement extends BaseRRElementImpl(RRNode) { querySelectorAll(selectors: string): RRNode[] { if (this.ownerDocument !== null) { - return (this.ownerDocument.nwsapi.select( + return ((this.ownerDocument as RRDocument).nwsapi.select( selectors, (this as unknown) as Element, ) as unknown) as RRNode[]; @@ -261,9 +257,9 @@ export class RRElement extends BaseRRElementImpl(RRNode) { // Make sure this element has all queried class names. if ( this instanceof RRElement && - queryClassList.filter((queriedClassName) => - this.classList.some((name) => name === queriedClassName), - ).length == queryClassList.length + queryClassList.classes.filter((queriedClassName) => + this.classList.classes.some((name) => name === queriedClassName), + ).length == queryClassList.classes.length ) elements.push(this); for (const child of this.childNodes) { diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index a178373734..5562d78292 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -6,6 +6,7 @@ export interface IRRNode { parentElement: IRRNode | null; parentNode: IRRNode | null; childNodes: IRRNode[]; + ownerDocument: IRRDocument; ELEMENT_NODE: 1; TEXT_NODE: 3; // corresponding nodeType value of standard HTML Node @@ -24,7 +25,7 @@ export interface IRRNode { insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode; - removeChild(node: IRRNode): void; + removeChild(node: IRRNode): IRRNode; toString(nodeName?: string): string; } @@ -62,6 +63,12 @@ export interface IRRDocument extends IRRNode { createComment(data: string): IRRComment; createCDATASection(data: string): IRRCDATASection; + + open(): void; + + close(): void; + + write(content: string): void; } export interface IRRElement extends IRRNode { tagName: string; @@ -69,6 +76,26 @@ export interface IRRElement extends IRRNode { shadowRoot: IRRElement | null; scrollLeft?: number; scrollTop?: number; + id: string; + className: string; + classList: ClassList; + style: CSSStyleDeclaration; + + attachShadow(init: ShadowRootInit): IRRElement; + + getAttribute(name: string): string | null; + + setAttribute(name: string, attribute: string): void; + + setAttributeNS( + namespace: string | null, + qualifiedName: string, + value: string, + ): void; + + removeAttribute(name: string): void; + + dispatchEvent(event: Event): boolean; } export interface IRRDocumentType extends IRRNode { readonly name: string; @@ -93,6 +120,7 @@ export class BaseRRNode implements IRRNode { public parentElement: IRRNode | null = null; public parentNode: IRRNode | null = null; public textContent: string | null; + public ownerDocument: IRRDocument; public ELEMENT_NODE: 1 = 1; public TEXT_NODE: 3 = 3; // corresponding nodeType value of standard HTML Node @@ -133,13 +161,10 @@ export class BaseRRNode implements IRRNode { ); } - public removeChild(node: IRRNode) { - const indexOfChild = this.childNodes.indexOf(node); - if (indexOfChild !== -1) { - this.childNodes.splice(indexOfChild, 1); - node.parentElement = null; - node.parentNode = null; - } + public removeChild(node: IRRNode): IRRNode { + throw new Error( + `RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method.`, + ); } public toString(nodeName?: string) { @@ -219,6 +244,16 @@ export function BaseRRDocumentImpl< } public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + const nodeType = newChild.RRNodeType; + if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { + if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { + throw new Error( + `RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one ${ + nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' + } on RRDocument allowed.`, + ); + } + } if (refChild === null) return this.appendChild(newChild); const childIndex = this.childNodes.indexOf(refChild); if (childIndex == -1) @@ -231,8 +266,21 @@ export function BaseRRDocumentImpl< return newChild; } + public removeChild(node: IRRNode) { + const indexOfChild = this.childNodes.indexOf(node); + if (indexOfChild === -1) + throw new Error( + "Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode.", + ); + this.childNodes.splice(indexOfChild, 1); + node.parentElement = null; + node.parentNode = null; + return node; + } + public open() { this.childNodes = []; + this._notSerializedId = -1; } public close() {} @@ -274,35 +322,49 @@ export function BaseRRDocumentImpl< _qualifiedName: string | null, _doctype?: DocumentType | null, ): IRRDocument { - throw new Error('Method not implemented.'); + return new BaseRRDocument(); } createDocumentType( - _qualifiedName: string, - _publicId: string, - _systemId: string, + qualifiedName: string, + publicId: string, + systemId: string, ): IRRDocumentType { - throw new Error('Method not implemented.'); + const doctype = new (BaseRRDocumentTypeImpl(BaseRRNode))( + qualifiedName, + publicId, + systemId, + ); + doctype.ownerDocument = this; + return doctype; } - createElement(_tagName: string): IRRElement { - throw new Error('Method not implemented.'); + createElement(tagName: string): IRRElement { + const element = new (BaseRRElementImpl(BaseRRNode))(tagName); + element.ownerDocument = this; + return element; } - createElementNS(_namespaceURI: string, _qualifiedName: string): IRRElement { - throw new Error('Method not implemented.'); + createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement { + return this.createElement(qualifiedName); } - createTextNode(_data: string): IRRText { - throw new Error('Method not implemented.'); + createTextNode(data: string): IRRText { + const text = new (BaseRRTextImpl(BaseRRNode))(data); + text.ownerDocument = this; + return text; } - createComment(_data: string): IRRComment { - throw new Error('Method not implemented.'); + createComment(data: string): IRRComment { + const comment = new (BaseRRCommentImpl(BaseRRNode))(data); + comment.ownerDocument = this; + return comment; } - createCDATASection(_data: string): IRRCDATASection { - throw new Error('Method not implemented.'); + createCDATASection(data: string): IRRCDATASection { + const CDATASection = new (BaseRRCDATASectionImpl(BaseRRNode))(data); + CDATASection.ownerDocument = this; + return CDATASection; } toString() { @@ -353,21 +415,20 @@ export function BaseRRElementImpl< constructor(tagName: string) { super(); - this.tagName = tagName; + this.tagName = tagName.toUpperCase(); } public get textContent(): string { let result = ''; - this.childNodes.forEach((node) => result + node.textContent); + this.childNodes.forEach((node) => (result += node.textContent)); return result; } public set textContent(textContent: string) { - if (this.childNodes[0].RRNodeType === NodeType.Text) - this.childNodes[0].textContent = textContent; + this.childNodes = [this.ownerDocument.createTextNode(textContent)]; } - public get classList() { + public get classList(): ClassList { return new ClassList( this.attributes.class as string | undefined, (newClassName) => { @@ -377,7 +438,7 @@ export function BaseRRElementImpl< } public get id() { - return this.attributes.id; + return this.attributes.id || ''; } public get className() { @@ -387,24 +448,23 @@ export function BaseRRElementImpl< public get style() { const style = (this.attributes.style ? parseCSSText(this.attributes.style as string) - : {}) as Record & { - setProperty: ( - name: string, - value: string | null, - priority?: string | null, - ) => void; - removeProperty: (name: string) => string; - }; - style.setProperty = (name: string, value: string | null) => { + : {}) as CSSStyleDeclaration; + style.setProperty = ( + name: string, + value: string | null, + priority?: string, + ) => { const normalizedName = camelize(name); if (!value) delete style[normalizedName]; else style[normalizedName] = value; + if (priority === 'important') style[normalizedName] += ' !important'; this.attributes.style = toCSSText(style); }; style.removeProperty = (name: string) => { const normalizedName = camelize(name); const value = style[normalizedName] || ''; delete style[normalizedName]; + this.attributes.style = toCSSText(style); return value; }; return style; @@ -450,6 +510,24 @@ export function BaseRRElementImpl< return newChild; } + public removeChild(node: IRRNode): IRRNode { + const indexOfChild = this.childNodes.indexOf(node); + if (indexOfChild === -1) + throw new Error( + "Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode.", + ); + this.childNodes.splice(indexOfChild, 1); + node.parentElement = null; + node.parentNode = null; + return node; + } + + public attachShadow(_init: ShadowRootInit): IRRElement { + const shadowRoot = this.ownerDocument.createElement('SHADOWROOT'); + this.shadowRoot = shadowRoot; + return shadowRoot; + } + public dispatchEvent(_event: Event) { return true; } @@ -474,7 +552,7 @@ export function BaseRRMediaElementImpl< public muted?: boolean; attachShadow(_init: ShadowRootInit): IRRElement { throw new Error( - `Uncaught DOMException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`, + `RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`, ); } public play() { @@ -574,17 +652,17 @@ export function BaseRRCDATASectionImpl< }; } -export class ClassList extends Array { +export class ClassList { private onChange: ((newClassText: string) => void) | undefined; + classes: string[] = []; constructor( classText?: string, onChange?: ((newClassText: string) => void) | undefined, ) { - super(); if (classText) { const classes = classText.trim().split(/\s+/); - super.push(...classes); + this.classes.push(...classes); } this.onChange = onChange; } @@ -592,19 +670,25 @@ export class ClassList extends Array { add = (...classNames: string[]) => { for (const item of classNames) { const className = String(item); - if (super.indexOf(className) >= 0) continue; - super.push(className); + if (this.classes.indexOf(className) >= 0) continue; + this.classes.push(className); } - this.onChange && this.onChange(super.join(' ')); + this.onChange && this.onChange(this.classes.join(' ')); }; remove = (...classNames: string[]) => { - for (const item of classNames) { - const className = String(item); - const index = super.indexOf(className); - if (index < 0) continue; - super.splice(index, 1); - } - this.onChange && this.onChange(super.join(' ')); + this.classes = this.classes.filter( + (item) => classNames.indexOf(item) === -1, + ); + this.onChange && this.onChange(this.classes.join(' ')); }; } + +export type CSSStyleDeclaration = Record & { + setProperty: ( + name: string, + value: string | null, + priority?: string | null, + ) => void; + removeProperty: (name: string) => string; +}; diff --git a/packages/rrdom/src/style.ts b/packages/rrdom/src/style.ts index 1e49b83619..ffcda389c6 100644 --- a/packages/rrdom/src/style.ts +++ b/packages/rrdom/src/style.ts @@ -1,40 +1,43 @@ export function parseCSSText(cssText: string): Record { - const res: Record = {}; - const listDelimiter = /;(?![^(]*\))/g; - const propertyDelimiter = /:(.+)/; - cssText.split(listDelimiter).forEach(function (item) { - if (item) { - const tmp = item.split(propertyDelimiter); - tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim()); - } - }); - return res; + const res: Record = {}; + const listDelimiter = /;(?![^(]*\))/g; + const propertyDelimiter = /:(.+)/; + const comment = /\/\*.*\*\//; + cssText + .replace(comment, '') + .split(listDelimiter) + .forEach(function (item) { + if (item) { + const tmp = item.split(propertyDelimiter); + tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim()); + } + }); + return res; +} + +export function toCSSText(style: Record): string { + const properties = []; + for (let name in style) { + const value = style[name]; + if (typeof value !== 'string') continue; + const normalizedName = hyphenate(name); + properties.push(`${normalizedName}:${value};`); } - - export function toCSSText(style: Record): string { - const properties = []; - for (let name in style) { - const value = style[name]; - if (typeof value !== 'string') continue; - const normalizedName = hyphenate(name); - properties.push(`${normalizedName}:${value};`); - } - return properties.join(' '); - } - - /** - * Camelize a hyphen-delimited string. - */ - const camelizeRE = /-(\w)/g; - export const camelize = (str: string): string => { - return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); - }; - - /** - * Hyphenate a camelCase string. - */ - const hyphenateRE = /\B([A-Z])/g; - export const hyphenate = (str: string): string => { - return str.replace(hyphenateRE, '-$1').toLowerCase(); - }; - \ No newline at end of file + return properties.join(' '); +} + +/** + * Camelize a hyphen-delimited string. + */ +const camelizeRE = /-(\w)/g; +export const camelize = (str: string): string => { + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); +}; + +/** + * Hyphenate a camelCase string. + */ +const hyphenateRE = /\B([A-Z])/g; +export const hyphenate = (str: string): string => { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +}; diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index a4af83c402..bc952534a2 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -6,7 +6,7 @@ import type { scrollData, } from 'rrweb/src/types'; import { - BaseRRNode, + BaseRRNode as RRNode, BaseRRCDATASectionImpl, BaseRRCommentImpl, BaseRRDocumentImpl, @@ -20,8 +20,6 @@ import { } from './document'; import { VirtualStyleRules } from './diff'; -export class RRNode extends BaseRRNode {} - export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = { map: {}, @@ -61,7 +59,13 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { publicId: string, systemId: string, ) { - return new RRDocumentType(qualifiedName, publicId, systemId); + const documentTypeNode = new RRDocumentType( + qualifiedName, + publicId, + systemId, + ); + documentTypeNode.ownerDocument = this; + return documentTypeNode; } createElement( @@ -89,26 +93,26 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { element = new RRElement(upperTagName); break; } + element.ownerDocument = this; return element; } - createElementNS( - _namespaceURI: 'http://www.w3.org/2000/svg', - qualifiedName: string, - ) { - return this.createElement(qualifiedName); - } - createComment(data: string) { - return new RRComment(data); + const commentNode = new RRComment(data); + commentNode.ownerDocument = this; + return commentNode; } createCDATASection(data: string) { - return new RRCDATASection(data); + const sectionNode = new RRCDATASection(data); + sectionNode.ownerDocument = this; + return sectionNode; } createTextNode(data: string) { - return new RRText(data); + const textNode = new RRText(data); + textNode.ownerDocument = this; + return textNode; } destroyTree() { @@ -122,12 +126,6 @@ export const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); export class RRElement extends BaseRRElementImpl(RRNode) { inputData: inputData | null = null; scrollData: scrollData | null = null; - - attachShadow(_init: ShadowRootInit): RRElement { - const shadowRoot = new RRElement('SHADOWROOT'); - this.shadowRoot = shadowRoot; - return shadowRoot; - } } export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} @@ -326,4 +324,5 @@ export function buildFromDom( walk((dom as unknown) as INode, null); return rrdom; } +export { RRNode }; export { diff, StyleRuleType, VirtualStyleRules } from './diff'; diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 2b28ae85be..c71e44c310 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -181,7 +181,7 @@ describe('RRDocument for nodejs environment', () => { } for (let element of rrdom.querySelectorAll('.\\:hover')) { expect(element).toBeInstanceOf(RRElement); - expect((element as RRElement).classList).toContain(':hover'); + expect((element as RRElement).classList.classes).toContain(':hover'); } }); diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts new file mode 100644 index 0000000000..619ffc0d49 --- /dev/null +++ b/packages/rrdom/test/document.test.ts @@ -0,0 +1,932 @@ +/** + * @jest-environment jsdom + */ +import { NodeType } from 'rrweb-snapshot'; +import { + BaseRRCDATASectionImpl, + BaseRRCommentImpl, + BaseRRDocumentImpl, + BaseRRDocumentTypeImpl, + BaseRRElementImpl, + BaseRRMediaElementImpl, + BaseRRNode, + BaseRRTextImpl, + IRRDocumentType, +} from '../src/document'; + +describe('Basic RRDocument implementation', () => { + const RRNode = BaseRRNode; + const RRDocument = BaseRRDocumentImpl(RRNode); + const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); + const RRElement = BaseRRElementImpl(RRNode); + class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} + + describe('Basic RRNode implementation', () => { + it('should have basic properties', () => { + const node = new RRNode(); + node.__sn = { + type: NodeType.Element, + tagName: 'DIV', + attributes: {}, + childNodes: [], + id: 1, + }; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBeUndefined(); + expect(node.textContent).toBeUndefined(); + expect(node.RRNodeType).toBeUndefined(); + expect(node.nodeType).toBeUndefined(); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.toString('RRNode')).toEqual('1 RRNode'); + }); + + it('can get first child node', () => { + const parentNode = new RRNode(); + const childNode1 = new RRNode(); + const childNode2 = new RRNode(); + expect(parentNode.firstChild).toBeNull(); + parentNode.childNodes = [childNode1]; + expect(parentNode.firstChild).toBe(childNode1); + parentNode.childNodes = [childNode1, childNode2]; + expect(parentNode.firstChild).toBe(childNode1); + parentNode.childNodes = [childNode2, childNode1]; + expect(parentNode.firstChild).toBe(childNode2); + }); + + it('can get nextSibling', () => { + const parentNode = new RRNode(); + const childNode1 = new RRNode(); + const childNode2 = new RRNode(); + expect(parentNode.nextSibling).toBeNull(); + expect(childNode1.nextSibling).toBeNull(); + childNode1.parentNode = parentNode; + parentNode.childNodes = [childNode1]; + expect(childNode1.nextSibling).toBeNull(); + childNode2.parentNode = parentNode; + parentNode.childNodes = [childNode1, childNode2]; + expect(childNode1.nextSibling).toBe(childNode2); + expect(childNode2.nextSibling).toBeNull(); + }); + + it('should return whether the node contains another node', () => { + const parentNode = new RRNode(); + const childNode1 = new RRNode(); + const childNode2 = new RRNode(); + parentNode.childNodes = [childNode1]; + expect(parentNode.contains(childNode1)).toBeTruthy(); + expect(parentNode.contains(childNode2)).toBeFalsy(); + childNode1.childNodes = [childNode2]; + expect(parentNode.contains(childNode2)).toBeTruthy(); + }); + + it('should not implement appendChild', () => { + const parentNode = new RRNode(); + const childNode = new RRNode(); + expect(() => + parentNode.appendChild(childNode), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method."`, + ); + }); + + it('should not implement insertBefore', () => { + const parentNode = new RRNode(); + const childNode = new RRNode(); + expect(() => + parentNode.insertBefore(childNode, null), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method."`, + ); + }); + + it('should not implement removeChild', () => { + const parentNode = new RRNode(); + const childNode = new RRNode(); + expect(() => + parentNode.removeChild(childNode), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method."`, + ); + }); + }); + + describe('Basic RRDocument implementation', () => { + it('should have basic properties', () => { + const node = new RRDocument(); + node.__sn = { + type: NodeType.Document, + id: 1, + childNodes: [], + }; + + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBeUndefined(); + expect(node.textContent).toBeNull(); + expect(node.RRNodeType).toBe(NodeType.Document); + expect(node.nodeType).toBe(document.nodeType); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.notSerializedId).toBe(-1); + expect(node.documentElement).toBeNull(); + expect(node.body).toBeNull(); + expect(node.head).toBeNull(); + expect(node.implementation).toBe(node); + expect(node.firstElementChild).toBeNull(); + expect(node.createDocument).toBeDefined(); + expect(node.createDocumentType).toBeDefined(); + expect(node.createElement).toBeDefined(); + expect(node.createElementNS).toBeDefined(); + expect(node.createTextNode).toBeDefined(); + expect(node.createComment).toBeDefined(); + expect(node.createCDATASection).toBeDefined(); + expect(node.open).toBeDefined(); + expect(node.close).toBeDefined(); + expect(node.write).toBeDefined(); + expect(node.toString()).toEqual('1 RRDocument'); + }); + + it('can access the a unique notSerializedId every time', () => { + const node = new RRDocument(); + for (let i = 1; i <= 100; i++) expect(node.notSerializedId).toBe(-i); + }); + + it('can get documentElement', () => { + const node = new RRDocument(); + expect(node.documentElement).toBeNull(); + const element = node.createElement('html'); + node.appendChild(element); + expect(node.documentElement).toBe(element); + }); + + it('can get head', () => { + const node = new RRDocument(); + expect(node.head).toBeNull(); + const element = node.createElement('html'); + node.appendChild(element); + expect(node.head).toBeNull(); + const head = node.createElement('head'); + element.appendChild(head); + expect(node.head).toBe(head); + }); + + it('can get body', () => { + const node = new RRDocument(); + expect(node.body).toBeNull(); + const element = node.createElement('html'); + node.appendChild(element); + expect(node.body).toBeNull(); + const body = node.createElement('body'); + element.appendChild(body); + expect(node.body).toBe(body); + const head = node.createElement('head'); + element.appendChild(head); + expect(node.body).toBe(body); + }); + + it('can get firstElementChild', () => { + const node = new RRDocument(); + expect(node.firstElementChild).toBeNull(); + const element = node.createElement('html'); + node.appendChild(element); + expect(node.firstElementChild).toBe(element); + }); + + it('can append child', () => { + const node = new RRDocument(); + expect(node.firstElementChild).toBeNull(); + + const documentType = node.createDocumentType('html', '', ''); + expect(node.appendChild(documentType)).toBe(documentType); + expect(node.childNodes[0]).toEqual(documentType); + expect(documentType.parentElement).toBeNull(); + expect(documentType.parentNode).toBe(node); + expect(() => + node.appendChild(documentType), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, + ); + + const element = node.createElement('html'); + expect(node.appendChild(element)).toBe(element); + expect(node.childNodes[1]).toEqual(element); + expect(element.parentElement).toBeNull(); + expect(element.parentNode).toBe(node); + const div = node.createElement('div'); + expect(() => node.appendChild(div)).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one RRElement on RRDocument allowed."`, + ); + }); + + it('can insert new child before an existing child', () => { + const node = new RRDocument(); + const docType = node.createDocumentType('', '', ''); + expect(() => + node.insertBefore(node, docType), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, + ); + expect(node.insertBefore(docType, null)).toBe(docType); + expect(() => + node.insertBefore(docType, null), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, + ); + node.removeChild(docType); + + const documentElement = node.createElement('html'); + expect(() => + node.insertBefore(documentElement, docType), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, + ); + expect(node.insertBefore(documentElement, null)).toBe(documentElement); + expect(() => + node.insertBefore(documentElement, null), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`, + ); + expect(node.insertBefore(docType, documentElement)).toBe(docType); + expect(node.childNodes[0]).toBe(docType); + expect(node.childNodes[1]).toBe(documentElement); + expect(docType.parentElement).toBeNull(); + expect(documentElement.parentElement).toBeNull(); + expect(docType.parentNode).toBe(node); + expect(documentElement.parentNode).toBe(node); + }); + + it('can remove an existing child', () => { + const node = new RRDocument(); + const documentType = node.createDocumentType('html', '', ''); + const documentElement = node.createElement('html'); + node.appendChild(documentType); + node.appendChild(documentElement); + expect(documentType.parentNode).toBe(node); + expect(documentElement.parentNode).toBe(node); + + expect(() => + node.removeChild(node.createElement('div')), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode."`, + ); + expect(node.removeChild(documentType)).toBe(documentType); + expect(documentType.parentNode).toBeNull(); + expect(node.removeChild(documentElement)).toBe(documentElement); + expect(documentElement.parentNode).toBeNull(); + }); + + it('should implement create node functions', () => { + const node = new RRDocument(); + expect(node.createDocument(null, '', null).RRNodeType).toEqual( + NodeType.Document, + ); + expect(node.createDocumentType('', '', '').RRNodeType).toEqual( + NodeType.DocumentType, + ); + expect(node.createElement('html').RRNodeType).toEqual(NodeType.Element); + expect(node.createElementNS('', 'html').RRNodeType).toEqual( + NodeType.Element, + ); + expect(node.createTextNode('text').RRNodeType).toEqual(NodeType.Text); + expect(node.createComment('comment').RRNodeType).toEqual( + NodeType.Comment, + ); + expect(node.createCDATASection('data').RRNodeType).toEqual( + NodeType.CDATA, + ); + }); + + it('can close and open a RRDocument', () => { + const node = new RRDocument(); + const documentType = node.createDocumentType('html', '', ''); + node.appendChild(documentType); + expect(node.childNodes[0]).toBe(documentType); + expect(node.notSerializedId).toBe(-1); + expect(node.close()); + expect(node.open()); + expect(node.childNodes.length).toEqual(0); + expect(node.notSerializedId).toBe(-1); + }); + + it('can cover the usage of write() in rrweb-snapshot', () => { + const node = new RRDocument(); + node.write( + '', + ); + expect(node.childNodes.length).toBe(1); + let doctype = node.childNodes[0] as IRRDocumentType; + expect(doctype.RRNodeType).toEqual(NodeType.DocumentType); + expect(doctype.parentNode).toEqual(node); + expect(doctype.name).toEqual('html'); + expect(doctype.publicId).toEqual( + '-//W3C//DTD XHTML 1.0 Transitional//EN', + ); + expect(doctype.systemId).toEqual(''); + + node.write( + '', + ); + expect(node.childNodes.length).toBe(1); + doctype = node.childNodes[0] as IRRDocumentType; + expect(doctype.RRNodeType).toEqual(NodeType.DocumentType); + expect(doctype.parentNode).toEqual(node); + expect(doctype.name).toEqual('html'); + expect(doctype.publicId).toEqual('-//W3C//DTD HTML 4.0 Transitional//EN'); + expect(doctype.systemId).toEqual(''); + }); + }); + + describe('Basic RRDocumentType implementation', () => { + it('should have basic properties', () => { + const name = 'name', + publicId = 'publicId', + systemId = 'systemId'; + const node = new RRDocumentType(name, publicId, systemId); + node.__sn = { + type: NodeType.DocumentType, + name, + publicId, + systemId, + id: 1, + }; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBeUndefined(); + expect(node.textContent).toBeNull(); + expect(node.RRNodeType).toBe(NodeType.DocumentType); + expect(node.nodeType).toBe(document.DOCUMENT_TYPE_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.name).toBe(name); + expect(node.publicId).toBe(publicId); + expect(node.systemId).toBe(systemId); + expect(node.toString()).toEqual('1 RRDocumentType'); + }); + }); + + describe('Basic RRElement implementation', () => { + const document = new RRDocument(); + + it('should have basic properties', () => { + const node = document.createElement('div'); + node.__sn = { + type: NodeType.Element, + tagName: 'div', + attributes: {}, + childNodes: [], + id: 1, + }; + node.scrollLeft = 100; + node.scrollTop = 200; + node.attributes.id = 'id'; + node.attributes.class = 'className'; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBe(document); + expect(node.textContent).toEqual(''); + expect(node.RRNodeType).toBe(NodeType.Element); + expect(node.nodeType).toBe(document.ELEMENT_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.tagName).toEqual('DIV'); + expect(node.attributes).toEqual({ id: 'id', class: 'className' }); + expect(node.shadowRoot).toBeNull(); + expect(node.scrollLeft).toEqual(100); + expect(node.scrollTop).toEqual(200); + expect(node.id).toEqual('id'); + expect(node.className).toEqual('className'); + expect(node.classList).toBeDefined(); + expect(node.style).toBeDefined(); + expect(node.getAttribute).toBeDefined(); + expect(node.setAttribute).toBeDefined(); + expect(node.setAttributeNS).toBeDefined(); + expect(node.removeAttribute).toBeDefined(); + expect(node.attachShadow).toBeDefined(); + expect(node.dispatchEvent).toBeDefined(); + expect(node.dispatchEvent((null as unknown) as Event)).toBeTruthy(); + expect(node.toString()).toEqual('1 DIV id="id" class="className" '); + }); + + it('can get textContent', () => { + const node = document.createElement('div'); + node.appendChild(document.createTextNode('text1 ')); + node.appendChild(document.createTextNode('text2')); + expect(node.textContent).toEqual('text1 text2'); + }); + + it('can set textContent', () => { + const node = document.createElement('div'); + node.appendChild(document.createTextNode('text1 ')); + node.appendChild(document.createTextNode('text2')); + expect(node.textContent).toEqual('text1 text2'); + node.textContent = 'new text'; + expect(node.textContent).toEqual('new text'); + }); + + it('can get id', () => { + const node = document.createElement('div'); + expect(node.id).toEqual(''); + node.attributes.id = 'idName'; + expect(node.id).toEqual('idName'); + }); + + it('can get className', () => { + const node = document.createElement('div'); + expect(node.className).toEqual(''); + node.attributes.class = 'className'; + expect(node.className).toEqual('className'); + }); + + it('can get classList', () => { + const node = document.createElement('div'); + const classList = node.classList; + expect(classList.add).toBeDefined(); + expect(classList.remove).toBeDefined(); + }); + + it('classList can add class name', () => { + const node = document.createElement('div'); + expect(node.className).toEqual(''); + const classList = node.classList; + classList.add('c1'); + expect(node.className).toEqual('c1'); + classList.add('c2'); + expect(node.className).toEqual('c1 c2'); + }); + + it('classList can remove class name', () => { + const node = document.createElement('div'); + expect(node.className).toEqual(''); + const classList = node.classList; + classList.add('c1', 'c2', 'c3'); + expect(node.className).toEqual('c1 c2 c3'); + classList.remove('c2'); + expect(node.className).toEqual('c1 c3'); + classList.remove('c3'); + expect(node.className).toEqual('c1'); + classList.remove('c1'); + expect(node.className).toEqual(''); + classList.remove('c1'); + expect(node.className).toEqual(''); + }); + + it('classList can remove duplicate class names', () => { + const node = document.createElement('div'); + expect(node.className).toEqual(''); + node.setAttribute('class', 'c1 c1 c1'); + expect(node.className).toEqual('c1 c1 c1'); + const classList = node.classList; + classList.remove('c1'); + expect(node.className).toEqual(''); + }); + + it('can get CSS style declaration', () => { + const node = document.createElement('div'); + const style = node.style; + expect(style).toBeDefined(); + expect(style.setProperty).toBeDefined(); + expect(style.removeProperty).toBeDefined(); + + node.attributes.style = + 'color: blue; background-color: red; width: 78%; height: 50vh !important;'; + expect(node.style.color).toBe('blue'); + expect(node.style.backgroundColor).toBe('red'); + expect(node.style.width).toBe('78%'); + expect(node.style.height).toBe('50vh !important'); + }); + + it('can set CSS property', () => { + const node = document.createElement('div'); + const style = node.style; + style.setProperty('color', 'red'); + expect(node.attributes.style).toEqual('color:red;'); + // camelCase style + style.setProperty('backgroundColor', 'blue'); + expect(node.attributes.style).toEqual( + 'color:red; background-color:blue;', + ); + style.setProperty('height', '50vh', 'important'); + expect(node.attributes.style).toEqual( + 'color:red; background-color:blue; height:50vh !important;', + ); + + // kebab-case + style.setProperty('background-color', 'red'); + expect(node.attributes.style).toEqual( + 'color:red; background-color:red; height:50vh !important;', + ); + + // remove the property + style.setProperty('background-color', null); + expect(node.attributes.style).toEqual( + 'color:red; height:50vh !important;', + ); + }); + + it('can remove CSS property', () => { + const node = document.createElement('div'); + node.attributes.style = + 'color:blue; background-color:red; width:78%; height:50vh !important;'; + const style = node.style; + expect(style.removeProperty('color')).toEqual('blue'); + expect(node.attributes.style).toEqual( + 'background-color:red; width:78%; height:50vh !important;', + ); + expect(style.removeProperty('height')).toEqual('50vh !important'); + expect(node.attributes.style).toEqual('background-color:red; width:78%;'); + // kebab-case + expect(style.removeProperty('background-color')).toEqual('red'); + expect(node.attributes.style).toEqual('width:78%;'); + style.setProperty('background-color', 'red'); + expect(node.attributes.style).toEqual('width:78%; background-color:red;'); + expect(style.removeProperty('backgroundColor')).toEqual('red'); + expect(node.attributes.style).toEqual('width:78%;'); + }); + + it('can parse more inline styles correctly', () => { + const node = document.createElement('div'); + // general + node.attributes.style = + 'display: inline-block; margin: 0 auto; border: 5px solid #BADA55; font-size: .75em; position:absolute;width: 33.3%; z-index:1337; font-family: "Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif;'; + + let style = node.style; + expect(style.display).toEqual('inline-block'); + expect(style.margin).toEqual('0 auto'); + expect(style.border).toEqual('5px solid #BADA55'); + expect(style.fontSize).toEqual('.75em'); + expect(style.position).toEqual('absolute'); + expect(style.width).toEqual('33.3%'); + expect(style.zIndex).toEqual('1337'); + expect(style.fontFamily).toEqual( + '"Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif', + ); + + // multiple of same property + node.attributes.style = 'color:rgba(0,0,0,1);color:white'; + style = node.style; + expect(style.color).toEqual('white'); + + // url + node.attributes.style = + 'background-image: url("http://example.com/img.png")'; + expect(node.style.backgroundImage).toEqual( + 'url("http://example.com/img.png")', + ); + + // vendor prefixes + node.attributes.style = ` + -moz-border-radius: 10px 5px; + -webkit-border-top-left-radius: 10px; + -webkit-border-bottom-left-radius: 5px; + border-radius: 10px 5px; + `; + style = node.style; + expect(style.MozBorderRadius).toEqual('10px 5px'); + expect(style.WebkitBorderTopLeftRadius).toEqual('10px'); + expect(style.WebkitBorderBottomLeftRadius).toEqual('5px'); + expect(style.borderRadius).toEqual('10px 5px'); + + // comment + node.attributes.style = 'top: 0; /* comment */ bottom: 42rem;'; + expect(node.style.top).toEqual('0'); + expect(node.style.bottom).toEqual('42rem'); + + // incomplete + node.attributes.style = 'overflow:'; + expect(node.style.overflow).toBeUndefined(); + }); + + it('can get attribute', () => { + const node = document.createElement('div'); + node.attributes.class = 'className'; + expect(node.getAttribute('class')).toEqual('className'); + expect(node.getAttribute('id')).toEqual(null); + node.attributes.id = 'id'; + expect(node.getAttribute('id')).toEqual('id'); + }); + + it('can set attribute', () => { + const node = document.createElement('div'); + expect(node.getAttribute('class')).toEqual(null); + node.setAttribute('class', 'className'); + expect(node.getAttribute('class')).toEqual('className'); + expect(node.getAttribute('id')).toEqual(null); + node.setAttribute('id', 'id'); + expect(node.getAttribute('id')).toEqual('id'); + }); + + it('can setAttributeNS', () => { + const node = document.createElement('div'); + expect(node.getAttribute('class')).toEqual(null); + node.setAttributeNS('namespace', 'class', 'className'); + expect(node.getAttribute('class')).toEqual('className'); + expect(node.getAttribute('id')).toEqual(null); + node.setAttributeNS('namespace', 'id', 'id'); + expect(node.getAttribute('id')).toEqual('id'); + }); + + it('can remove attribute', () => { + const node = document.createElement('div'); + node.setAttribute('class', 'className'); + expect(node.getAttribute('class')).toEqual('className'); + node.removeAttribute('class'); + expect(node.getAttribute('class')).toEqual(null); + node.removeAttribute('id'); + expect(node.getAttribute('id')).toEqual(null); + }); + + it('can attach shadow dom', () => { + const node = document.createElement('div'); + expect(node.shadowRoot).toBeNull(); + node.attachShadow({ mode: 'open' }); + expect(node.shadowRoot).not.toBeNull(); + expect(node.shadowRoot!.RRNodeType).toBe(NodeType.Element); + expect(node.shadowRoot!.tagName).toBe('SHADOWROOT'); + expect(node.parentNode).toBeNull(); + }); + + it('can append child', () => { + const node = document.createElement('div'); + expect(node.childNodes.length).toBe(0); + + const child1 = document.createComment('span'); + expect(node.appendChild(child1)).toBe(child1); + expect(node.childNodes[0]).toEqual(child1); + expect(child1.parentElement).toBe(node); + expect(child1.parentNode).toBe(node); + + const child2 = document.createElement('p'); + expect(node.appendChild(child2)).toBe(child2); + expect(node.childNodes[1]).toEqual(child2); + expect(child2.parentElement).toBe(node); + expect(child2.parentNode).toBe(node); + }); + + it('can insert new child before an existing child', () => { + const node = document.createElement('div'); + const child1 = document.createElement('h1'); + const child2 = document.createElement('h2'); + expect(() => + node.insertBefore(node, child1), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, + ); + expect(node.insertBefore(child1, null)).toBe(child1); + expect(node.childNodes[0]).toBe(child1); + expect(child1.parentNode).toBe(node); + expect(child1.parentElement).toBe(node); + + expect(node.insertBefore(child2, child1)).toBe(child2); + expect(node.childNodes.length).toBe(2); + expect(node.childNodes[0]).toBe(child2); + expect(node.childNodes[1]).toBe(child1); + expect(child2.parentNode).toBe(node); + expect(child2.parentElement).toBe(node); + }); + + it('can remove an existing child', () => { + const node = document.createElement('div'); + const child1 = document.createElement('h1'); + const child2 = document.createElement('h2'); + node.appendChild(child1); + node.appendChild(child2); + expect(node.childNodes.length).toBe(2); + expect(child1.parentNode).toBe(node); + expect(child2.parentNode).toBe(node); + expect(child1.parentElement).toBe(node); + expect(child2.parentElement).toBe(node); + + expect(() => + node.removeChild(document.createElement('div')), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode."`, + ); + expect(node.removeChild(child1)).toBe(child1); + expect(child1.parentNode).toBeNull(); + expect(child1.parentElement).toBeNull(); + expect(node.childNodes.length).toBe(1); + expect(node.removeChild(child2)).toBe(child2); + expect(node.childNodes.length).toBe(0); + expect(child2.parentNode).toBeNull(); + expect(child2.parentElement).toBeNull(); + }); + }); + + describe('Basic RRText implementation', () => { + const dom = new RRDocument(); + + it('should have basic properties', () => { + const node = dom.createTextNode('text'); + node.__sn = { + type: NodeType.Text, + textContent: 'text', + id: 1, + }; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBe(dom); + expect(node.textContent).toEqual('text'); + expect(node.RRNodeType).toBe(NodeType.Text); + expect(node.nodeType).toBe(document.TEXT_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.toString()).toEqual('1 RRText text="text"'); + }); + + it('can set textContent', () => { + const node = dom.createTextNode('text'); + expect(node.textContent).toEqual('text'); + node.textContent = 'new text'; + expect(node.textContent).toEqual('new text'); + }); + }); + + describe('Basic RRComment implementation', () => { + const dom = new RRDocument(); + + it('should have basic properties', () => { + const node = dom.createComment('comment'); + node.__sn = { + type: NodeType.Comment, + textContent: 'comment', + id: 1, + }; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBe(dom); + expect(node.textContent).toEqual('comment'); + expect(node.RRNodeType).toBe(NodeType.Comment); + expect(node.nodeType).toBe(document.COMMENT_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.toString()).toEqual('1 RRComment text="comment"'); + }); + + it('can set textContent', () => { + const node = dom.createComment('comment'); + expect(node.textContent).toEqual('comment'); + node.textContent = 'new comment'; + expect(node.textContent).toEqual('new comment'); + }); + }); + + describe('Basic RRCDATASection implementation', () => { + const dom = new RRDocument(); + + it('should have basic properties', () => { + const node = dom.createCDATASection('data'); + node.__sn = { + type: NodeType.CDATA, + textContent: '', + id: 1, + }; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBe(dom); + expect(node.textContent).toEqual('data'); + expect(node.RRNodeType).toBe(NodeType.CDATA); + expect(node.nodeType).toBe(document.CDATA_SECTION_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.toString()).toEqual('1 RRCDATASection data="data"'); + }); + + it('can set textContent', () => { + const node = dom.createCDATASection('data'); + expect(node.textContent).toEqual('data'); + node.textContent = 'new data'; + expect(node.textContent).toEqual('new data'); + }); + }); + + describe('Basic RRMediaElement implementation', () => { + it('should have basic properties', () => { + const node = new RRMediaElement('video'); + node.__sn = { + type: NodeType.Element, + tagName: 'video', + attributes: {}, + childNodes: [], + id: 1, + }; + node.scrollLeft = 100; + node.scrollTop = 200; + expect(node.parentNode).toEqual(null); + expect(node.parentElement).toEqual(null); + expect(node.childNodes).toBeInstanceOf(Array); + expect(node.childNodes.length).toBe(0); + expect(node.ownerDocument).toBeUndefined(); + expect(node.textContent).toEqual(''); + expect(node.RRNodeType).toBe(NodeType.Element); + expect(node.nodeType).toBe(document.ELEMENT_NODE); + expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); + expect(node.TEXT_NODE).toBe(document.TEXT_NODE); + expect(node.firstChild).toBeNull(); + expect(node.nextSibling).toBeNull(); + expect(node.contains).toBeDefined(); + expect(node.appendChild).toBeDefined(); + expect(node.insertBefore).toBeDefined(); + expect(node.removeChild).toBeDefined(); + expect(node.tagName).toEqual('VIDEO'); + expect(node.attributes).toEqual({}); + expect(node.shadowRoot).toBeNull(); + expect(node.scrollLeft).toEqual(100); + expect(node.scrollTop).toEqual(200); + expect(node.id).toEqual(''); + expect(node.className).toEqual(''); + expect(node.classList).toBeDefined(); + expect(node.style).toBeDefined(); + expect(node.getAttribute).toBeDefined(); + expect(node.setAttribute).toBeDefined(); + expect(node.setAttributeNS).toBeDefined(); + expect(node.removeAttribute).toBeDefined(); + expect(node.attachShadow).toBeDefined(); + expect(node.dispatchEvent).toBeDefined(); + expect(node.currentTime).toBeUndefined(); + expect(node.volume).toBeUndefined(); + expect(node.paused).toBeUndefined(); + expect(node.muted).toBeUndefined(); + expect(node.play).toBeDefined(); + expect(node.pause).toBeDefined(); + expect(node.toString()).toEqual('1 VIDEO '); + }); + + it('can play and pause the media', () => { + const node = new RRMediaElement('video'); + expect(node.paused).toBeUndefined(); + node.play(); + expect(node.paused).toBeFalsy(); + node.pause(); + expect(node.paused).toBeTruthy(); + node.play(); + expect(node.paused).toBeFalsy(); + }); + + it('should not support attachShadow function', () => { + const node = new RRMediaElement('video'); + expect(() => + node.attachShadow({ mode: 'open' }), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow"`, + ); + }); + }); +}); From a8633bf0ea76fef790ddd1ddae9aff44a9a9062a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 2 Mar 2022 19:33:33 +1100 Subject: [PATCH 33/79] Merge branch 'master' into virtual-dom --- guide.md | 2 +- guide.zh_CN.md | 2 +- packages/rrdom/package.json | 4 +- packages/rrweb-player/package.json | 4 +- packages/rrweb-snapshot/package.json | 2 +- packages/rrweb-snapshot/src/snapshot.ts | 21 ++- packages/rrweb-snapshot/src/types.ts | 5 + packages/rrweb-snapshot/src/utils.ts | 3 +- .../rrweb-snapshot/test/integration.test.ts | 9 +- packages/rrweb-snapshot/typings/snapshot.d.ts | 4 +- packages/rrweb-snapshot/typings/types.d.ts | 4 + packages/rrweb/package.json | 8 +- .../src/plugins/console/record/stringify.ts | 4 +- packages/rrweb/src/utils.ts | 6 +- .../__snapshots__/integration.test.ts.snap | 159 ++++++++++++------ packages/rrweb/test/html/frame1.html | 8 + .../observers/canvas/canvas-manager.d.ts | 2 +- 17 files changed, 168 insertions(+), 79 deletions(-) diff --git a/guide.md b/guide.md index 1ad42a23bb..b95b4c2da3 100644 --- a/guide.md +++ b/guide.md @@ -398,7 +398,7 @@ And there are three rrweb-replayer event will be emitted in the same way: You can also play with rrweb by using the REPL testing tool which does not need installation. -Run `npm run repl` to launch a browser and ask for a URL you want to test on the CLI: +Run `yarn repl` to launch a browser and ask for a URL you want to test on the CLI: ``` Enter the url you want to record, e.g https://react-redux.realworld.io: diff --git a/guide.zh_CN.md b/guide.zh_CN.md index 43edece2ed..bdb0a93305 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -396,7 +396,7 @@ replayer.on(EVENT_NAME, (payload) => { 在不安装 rrweb 的情况下,也可以通过使用 REPL 工具试用 rrweb 录制 web 应用。 -运行 `npm run repl`,将会启动浏览器并在命令行要求输入一个测试的 url: +运行 `yarn repl`,将会启动浏览器并在命令行要求输入一个测试的 url: ``` Enter the url you want to record, e.g https://react-redux.realworld.io: diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 28aa368d38..6ee332c04e 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -1,6 +1,6 @@ { "name": "rrdom", - "version": "0.1.0", + "version": "0.1.1", "scripts": { "dev": "rollup -c -w", "bundle": "rollup --config", @@ -36,7 +36,7 @@ "rollup": "^2.56.3", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.30.0", - "rrweb-snapshot": "^1.1.12", + "rrweb-snapshot": "^1.1.13", "ts-jest": "^27.0.5", "typescript": "^3.9.5" }, diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 73cb810b4f..bd0309f459 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -1,6 +1,6 @@ { "name": "rrweb-player", - "version": "0.7.12", + "version": "0.7.13", "devDependencies": { "@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-node-resolve": "^7.0.0", @@ -25,7 +25,7 @@ }, "dependencies": { "@tsconfig/svelte": "^1.0.0", - "rrweb": "^1.1.1" + "rrweb": "^1.1.2" }, "scripts": { "build": "rollup -c", diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index ef2182e3c1..1a4cb19cd4 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -1,6 +1,6 @@ { "name": "rrweb-snapshot", - "version": "1.1.12", + "version": "1.1.13", "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer", "scripts": { "prepare": "npm run prepack", diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 0f8de04a84..0854176010 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -7,6 +7,7 @@ import { idNodeMap, MaskInputOptions, SlimDOMOptions, + DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn, @@ -219,7 +220,7 @@ export function absoluteToDoc(doc: Document, attributeValue: string): string { } function isSVGElement(el: Element): boolean { - return el.tagName === 'svg' || el instanceof SVGElement; + return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); } function getHref() { @@ -378,6 +379,7 @@ function serializeNode( maskInputOptions: MaskInputOptions; maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; + dataURLOptions?: DataURLOptions, inlineImages: boolean; recordCanvas: boolean; keepIframeSrcFn: KeepIframeSrcFn; @@ -393,6 +395,7 @@ function serializeNode( maskInputOptions = {}, maskTextFn, maskInputFn, + dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, @@ -513,17 +516,17 @@ function serializeNode( if ((n as ICanvas).__context === '2d') { // only record this on 2d canvas if (!is2DCanvasBlank(n as HTMLCanvasElement)) { - attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL(); + attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL(dataURLOptions.type, dataURLOptions.quality); } } else if (!('__context' in n)) { // context is unknown, better not call getContext to trigger it - const canvasDataURL = (n as HTMLCanvasElement).toDataURL(); + const canvasDataURL = (n as HTMLCanvasElement).toDataURL(dataURLOptions.type, dataURLOptions.quality); // create blank canvas of same dimensions const blankCanvas = document.createElement('canvas'); blankCanvas.width = (n as HTMLCanvasElement).width; blankCanvas.height = (n as HTMLCanvasElement).height; - const blankCanvasDataURL = blankCanvas.toDataURL(); + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); // no need to save dataURL if it's the same as blank canvas if (canvasDataURL !== blankCanvasDataURL) { @@ -545,7 +548,7 @@ function serializeNode( canvasService!.width = image.naturalWidth; canvasService!.height = image.naturalHeight; canvasCtx!.drawImage(image, 0, 0); - attributes.rr_dataURL = canvasService!.toDataURL(); + attributes.rr_dataURL = canvasService!.toDataURL(dataURLOptions.type, dataURLOptions.quality); } catch (err) { console.warn( `Cannot inline img src=${image.currentSrc}! Error: ${err}`, @@ -774,6 +777,7 @@ export function serializeNodeWithId( maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; slimDOMOptions: SlimDOMOptions; + dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; inlineImages?: boolean; recordCanvas?: boolean; @@ -796,6 +800,7 @@ export function serializeNodeWithId( maskTextFn, maskInputFn, slimDOMOptions, + dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, @@ -814,6 +819,7 @@ export function serializeNodeWithId( maskInputOptions, maskTextFn, maskInputFn, + dataURLOptions, inlineImages, recordCanvas, keepIframeSrcFn, @@ -880,6 +886,7 @@ export function serializeNodeWithId( maskTextFn, maskInputFn, slimDOMOptions, + dataURLOptions, inlineImages, recordCanvas, preserveWhiteSpace, @@ -933,6 +940,7 @@ export function serializeNodeWithId( maskTextFn, maskInputFn, slimDOMOptions, + dataURLOptions, inlineImages, recordCanvas, preserveWhiteSpace, @@ -966,6 +974,7 @@ function snapshot( maskTextFn?: MaskTextFn; maskInputFn?: MaskTextFn; slimDOM?: boolean | SlimDOMOptions; + dataURLOptions?: DataURLOptions, inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; @@ -987,6 +996,7 @@ function snapshot( maskTextFn, maskInputFn, slimDOM = false, + dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, @@ -1051,6 +1061,7 @@ function snapshot( maskTextFn, maskInputFn, slimDOMOptions, + dataURLOptions, inlineImages, recordCanvas, preserveWhiteSpace, diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index f239213d32..eedd49252f 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -112,6 +112,11 @@ export type SlimDOMOptions = Partial<{ headMetaVerification: boolean; }>; +export type DataURLOptions = Partial<{ + type: string; + quality: number; +}>; + export type MaskTextFn = (text: string) => string; export type MaskInputFn = (text: string) => string; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 2b059765d1..7f84809929 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -60,7 +60,8 @@ export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean { // even if we can already tell from the first chunk(s) that // the canvas isn't blank const pixelBuffer = new Uint32Array( - originalGetImageData( + originalGetImageData.call( + ctx, x, y, Math.min(chunkSize, canvas.width - x), diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index c9aa91b9d8..0d5af440b6 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -199,14 +199,17 @@ iframe.contentDocument.querySelector('center').clientHeight waitUntil: 'load', }); await page.waitForSelector('img', { timeout: 1000 }); - await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, {inlineImages: true, inlineStylesheet: false}); - `); + await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, { + dataURLOptions: { type: "image/webp", quality: 0.8 }, + inlineImages: true, + inlineStylesheet: false + })`); await page.waitFor(100); const snapshot = await page.evaluate( 'JSON.stringify(snapshot[0], null, 2);', ); assert(snapshot.includes('"rr_dataURL"')); - assert(snapshot.includes('data:image/png;base64,')); + assert(snapshot.includes('data:image/webp;base64,')); }); }); diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts index af06efc2b2..b970f751eb 100644 --- a/packages/rrweb-snapshot/typings/snapshot.d.ts +++ b/packages/rrweb-snapshot/typings/snapshot.d.ts @@ -1,4 +1,4 @@ -import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn } from './types'; +import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOptions, DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn } from './types'; export declare const IGNORED_NODE = -2; export declare function absoluteToStylesheet(cssText: string | null, href: string): string; export declare function absoluteToDoc(doc: Document, attributeValue: string): string; @@ -18,6 +18,7 @@ export declare function serializeNodeWithId(n: Node | INode, options: { maskTextFn: MaskTextFn | undefined; maskInputFn: MaskInputFn | undefined; slimDOMOptions: SlimDOMOptions; + dataURLOptions?: DataURLOptions; keepIframeSrcFn?: KeepIframeSrcFn; inlineImages?: boolean; recordCanvas?: boolean; @@ -36,6 +37,7 @@ declare function snapshot(n: Document, options?: { maskTextFn?: MaskTextFn; maskInputFn?: MaskTextFn; slimDOM?: boolean | SlimDOMOptions; + dataURLOptions?: DataURLOptions; inlineImages?: boolean; recordCanvas?: boolean; preserveWhiteSpace?: boolean; diff --git a/packages/rrweb-snapshot/typings/types.d.ts b/packages/rrweb-snapshot/typings/types.d.ts index 262d3f9aef..a8ccc0d305 100644 --- a/packages/rrweb-snapshot/typings/types.d.ts +++ b/packages/rrweb-snapshot/typings/types.d.ts @@ -91,6 +91,10 @@ export declare type SlimDOMOptions = Partial<{ headMetaAuthorship: boolean; headMetaVerification: boolean; }>; +export declare type DataURLOptions = Partial<{ + type: string; + quality: number; +}>; export declare type MaskTextFn = (text: string) => string; export declare type MaskInputFn = (text: string) => string; export declare type KeepIframeSrcFn = (src: string) => boolean; diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 0e05279e09..203823cbe0 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -1,6 +1,6 @@ { "name": "rrweb", - "version": "1.1.1", + "version": "1.1.2", "description": "record and replay the web", "scripts": { "prepare": "npm run prepack", @@ -46,7 +46,7 @@ "@types/chai": "^4.1.6", "@types/inquirer": "0.0.43", "@types/jest": "^27.0.2", - "@types/jest-image-snapshot": "^4.3.1", + "@types/jest-image-snapshot": "^4.3.1", "@types/node": "^12.20.16", "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.4", @@ -76,7 +76,7 @@ "base64-arraybuffer": "^1.0.1", "fflate": "^0.4.4", "mitt": "^1.1.3", - "rrdom": "^0.1.0", - "rrweb-snapshot": "^1.1.12" + "rrdom": "^0.1.1", + "rrweb-snapshot": "^1.1.13" } } diff --git a/packages/rrweb/src/plugins/console/record/stringify.ts b/packages/rrweb/src/plugins/console/record/stringify.ts index 1538fb3470..3804d48e88 100644 --- a/packages/rrweb/src/plugins/console/record/stringify.ts +++ b/packages/rrweb/src/plugins/console/record/stringify.ts @@ -137,7 +137,9 @@ export function stringify( } return value.nodeName; } else if (value instanceof Error) { - return value.name + ': ' + value.message; + return value.stack + ? value.stack + '\nEnd of stack for Error object' + : value.name + ': ' + value.message; } return value; }); diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 0ab5531209..261578daca 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -224,7 +224,11 @@ export function isBlocked(node: Node | null, blockClass: blockClass): boolean { if (node.nodeType === node.ELEMENT_NODE) { let needBlock = false; if (typeof blockClass === 'string') { - needBlock = (node as HTMLElement).classList.contains(blockClass); + if ((node as HTMLElement).closest !== undefined) { + return (node as HTMLElement).closest('.' + blockClass) !== null; + } else { + needBlock = (node as HTMLElement).classList.contains(blockClass); + } } else { (node as HTMLElement).classList.forEach((className) => { if (blockClass.test(className)) { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 9de563069a..6d853e216d 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4874,9 +4874,58 @@ exports[`record integration tests should nest record iframe 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"textContent\\": \\"\\\\n \\", \\"rootId\\": 32, \\"id\\": 50 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"svg\\", + \\"attributes\\": { + \\"xmlns\\": \\"http://www.w3.org/2000/svg\\", + \\"xmlns:xlink\\": \\"http://www.w3.org/1999/xlink\\", + \\"width\\": \\"300\\", + \\"height\\": \\"300\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 52 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"rect\\", + \\"attributes\\": { + \\"id\\": \\"el\\", + \\"width\\": \\"100\\", + \\"height\\": \\"50\\", + \\"x\\": \\"40\\", + \\"y\\": \\"20\\", + \\"fill\\": \\"red\\" + }, + \\"childNodes\\": [], + \\"isSVG\\": true, + \\"rootId\\": 32, + \\"id\\": 53 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 32, + \\"id\\": 54 + } + ], + \\"isSVG\\": true, + \\"rootId\\": 32, + \\"id\\": 51 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 32, + \\"id\\": 55 } ], \\"rootId\\": 32, @@ -4918,24 +4967,24 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"tagName\\": \\"head\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"rootId\\": 51, - \\"id\\": 53 + \\"rootId\\": 56, + \\"id\\": 58 }, { \\"type\\": 2, \\"tagName\\": \\"body\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"rootId\\": 51, - \\"id\\": 54 + \\"rootId\\": 56, + \\"id\\": 59 } ], - \\"rootId\\": 51, - \\"id\\": 52 + \\"rootId\\": 56, + \\"id\\": 57 } ], \\"compatMode\\": \\"BackCompat\\", - \\"id\\": 51 + \\"id\\": 56 } } ], @@ -4961,8 +5010,8 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"name\\": \\"html\\", \\"publicId\\": \\"\\", \\"systemId\\": \\"\\", - \\"rootId\\": 55, - \\"id\\": 56 + \\"rootId\\": 60, + \\"id\\": 61 }, { \\"type\\": 2, @@ -4979,8 +5028,8 @@ exports[`record integration tests should nest record iframe 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 55, - \\"id\\": 59 + \\"rootId\\": 60, + \\"id\\": 64 }, { \\"type\\": 2, @@ -4989,14 +5038,14 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"charset\\": \\"UTF-8\\" }, \\"childNodes\\": [], - \\"rootId\\": 55, - \\"id\\": 60 + \\"rootId\\": 60, + \\"id\\": 65 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 55, - \\"id\\": 61 + \\"rootId\\": 60, + \\"id\\": 66 }, { \\"type\\": 2, @@ -5006,14 +5055,14 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"content\\": \\"width=device-width, initial-scale=1.0\\" }, \\"childNodes\\": [], - \\"rootId\\": 55, - \\"id\\": 62 + \\"rootId\\": 60, + \\"id\\": 67 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 55, - \\"id\\": 63 + \\"rootId\\": 60, + \\"id\\": 68 }, { \\"type\\": 2, @@ -5023,28 +5072,28 @@ exports[`record integration tests should nest record iframe 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"Frame 2\\", - \\"rootId\\": 55, - \\"id\\": 65 + \\"rootId\\": 60, + \\"id\\": 70 } ], - \\"rootId\\": 55, - \\"id\\": 64 + \\"rootId\\": 60, + \\"id\\": 69 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 55, - \\"id\\": 66 + \\"rootId\\": 60, + \\"id\\": 71 } ], - \\"rootId\\": 55, - \\"id\\": 58 + \\"rootId\\": 60, + \\"id\\": 63 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 55, - \\"id\\": 67 + \\"rootId\\": 60, + \\"id\\": 72 }, { \\"type\\": 2, @@ -5054,8 +5103,8 @@ exports[`record integration tests should nest record iframe 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"\\\\n frame 2\\\\n \\\\n \\", - \\"rootId\\": 55, - \\"id\\": 69 + \\"rootId\\": 60, + \\"id\\": 74 }, { \\"type\\": 2, @@ -5065,29 +5114,29 @@ exports[`record integration tests should nest record iframe 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"rootId\\": 55, - \\"id\\": 71 + \\"rootId\\": 60, + \\"id\\": 76 } ], - \\"rootId\\": 55, - \\"id\\": 70 + \\"rootId\\": 60, + \\"id\\": 75 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n\\\\n\\", - \\"rootId\\": 55, - \\"id\\": 72 + \\"rootId\\": 60, + \\"id\\": 77 } ], - \\"rootId\\": 55, - \\"id\\": 68 + \\"rootId\\": 60, + \\"id\\": 73 } ], - \\"rootId\\": 55, - \\"id\\": 57 + \\"rootId\\": 60, + \\"id\\": 62 } ], - \\"id\\": 55 + \\"id\\": 60 } } ], @@ -5106,7 +5155,7 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"removes\\": [], \\"adds\\": [ { - \\"parentId\\": 68, + \\"parentId\\": 73, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, @@ -5115,8 +5164,8 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"id\\": \\"five\\" }, \\"childNodes\\": [], - \\"rootId\\": 55, - \\"id\\": 73 + \\"rootId\\": 60, + \\"id\\": 78 } } ] @@ -5128,7 +5177,7 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"source\\": 0, \\"adds\\": [ { - \\"parentId\\": 73, + \\"parentId\\": 78, \\"nextId\\": null, \\"node\\": { \\"type\\": 0, @@ -5143,24 +5192,24 @@ exports[`record integration tests should nest record iframe 1`] = ` \\"tagName\\": \\"head\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"rootId\\": 74, - \\"id\\": 76 + \\"rootId\\": 79, + \\"id\\": 81 }, { \\"type\\": 2, \\"tagName\\": \\"body\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"rootId\\": 74, - \\"id\\": 77 + \\"rootId\\": 79, + \\"id\\": 82 } ], - \\"rootId\\": 74, - \\"id\\": 75 + \\"rootId\\": 79, + \\"id\\": 80 } ], \\"compatMode\\": \\"BackCompat\\", - \\"id\\": 74 + \\"id\\": 79 } } ], @@ -7816,7 +7865,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"__puppeteer_evaluation_script__:19:37\\" ], \\"payload\\": [ - \\"\\\\\\"TypeError: a message\\\\\\"\\" + \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:41\\\\\\\\nEnd of stack for Error object\\\\\\"\\" ] } } diff --git a/packages/rrweb/test/html/frame1.html b/packages/rrweb/test/html/frame1.html index 8810af4608..36a4d335be 100644 --- a/packages/rrweb/test/html/frame1.html +++ b/packages/rrweb/test/html/frame1.html @@ -9,5 +9,13 @@ frame 1 + + + diff --git a/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts b/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts index c9ed77c52c..9e1112af07 100644 --- a/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts @@ -8,7 +8,7 @@ export declare class CanvasManager { private rafStamps; private mirror; private mutationCb; - private resetObservers; + private resetObservers?; private frozen; private locked; reset(): void; From 80cc629e3ac8938c9b724e4f8f6b9c33a8aa7207 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 3 Mar 2022 16:32:09 +1100 Subject: [PATCH 34/79] add more test for virtual-dom.ts --- packages/rrdom/src/diff.ts | 2 +- packages/rrdom/src/virtual-dom.ts | 2 +- .../__snapshots__/virtual-dom.test.ts.snap | 13 +- packages/rrdom/test/diff.test.ts | 235 +++++++++++++++++- packages/rrdom/test/document.test.ts | 9 +- packages/rrdom/test/html/main.html | 8 +- packages/rrdom/test/virtual-dom.test.ts | 200 +++++++++++++-- 7 files changed, 428 insertions(+), 41 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 4078835f81..dab9e07b0b 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -287,7 +287,7 @@ function diffChildren( if ( parentNode.__sn.type === NodeType.Document && newNode.__sn.type === NodeType.Element && - newNode.__sn.tagName.toUpperCase() === 'HTML' + ((parentNode as Node) as Document).documentElement ) { parentNode.removeChild( ((parentNode as Node) as Document).documentElement, diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index bc952534a2..475443ccfb 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -24,7 +24,7 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = { map: {}, getId(n) { - return n.__sn.id >= 0 ? n.__sn.id : -1; + return n?.__sn?.id >= 0 ? n.__sn.id : -1; }, getNode(id) { return this.map[id] || null; diff --git a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap index 8bd66b9035..71f639094a 100644 --- a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap +++ b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap @@ -16,7 +16,7 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu -13 LINK rel=\\"stylesheet\\" href=\\"somelink\\" -14 RRText text=\\"\\\\n \\" -15 STYLE - -16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url(\\\\\\"main.css\\\\\\");\\\\n \\" + -16 RRText text=\\"\\\\n h1 {\\\\n color: 'black';\\\\n }\\\\n .blocks {\\\\n padding: 0;\\\\n }\\\\n .blocks1 {\\\\n margin: 0;\\\\n }\\\\n #block1 {\\\\n width: 100px;\\\\n height: 200px;\\\\n }\\\\n @import url('main.css');\\\\n \\" -17 RRText text=\\"\\\\n \\" -18 RRText text=\\"\\\\n \\" -19 BODY @@ -42,8 +42,15 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu -39 RRText text=\\"\\\\n Text 2\\\\n \\" -40 RRText text=\\"\\\\n \\" -41 IMG src=\\"somelink\\" alt=\\"This is an image\\" - -42 RRText text=\\"\\\\n \\" - -43 RRText text=\\"\\\\n \\\\n\\\\n\\" + -42 RRText text=\\"\\\\n \\" + -43 RRComment text=\\" This is a line of comment \\" + -44 RRText text=\\"\\\\n \\" + -45 FORM + -46 RRText text=\\"\\\\n \\" + -47 INPUT type=\\"text\\" id=\\"input1\\" + -48 RRText text=\\"\\\\n \\" + -49 RRText text=\\"\\\\n \\" + -50 RRText text=\\"\\\\n \\\\n\\\\n\\" " `; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 6e5d5bf19f..a0786eefab 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -4,6 +4,7 @@ import { RRDocument, RRElement, + RRMediaElement, StyleRuleType, VirtualStyleRules, } from '../src/virtual-dom'; @@ -15,6 +16,12 @@ import { } from '../src/diff'; import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; import { IRRNode } from '../src/document'; +import { + canvasMutationData, + EventType, + IncrementalSource, + Mirror, +} from 'rrweb/src/types'; const elementSn = { type: NodeType.Element, @@ -73,7 +80,7 @@ function shuffle(list: number[]) { describe('diff algorithm for rrdom', () => { // An adhoc mirror just for unit tests. - const mirror = { + const mirror: Mirror = { map: {}, getId() { return 0; @@ -102,10 +109,17 @@ describe('diff algorithm for rrdom', () => { document.open(); expect(document.childNodes.length).toEqual(0); const rrNode = new RRDocument(); - rrNode; const htmlContent = ''; rrNode.write(htmlContent); + // add scroll data for the document + rrNode.scrollData = { + source: IncrementalSource.Scroll, + id: 0, + x: 0, + y: 0, + }; + replayer.applyScroll = jest.fn(); diff((document as unknown) as INode, rrNode, replayer); expect(document.childNodes.length).toEqual(1); expect(document.childNodes[0]).toBeInstanceOf(DocumentType); @@ -114,18 +128,113 @@ describe('diff algorithm for rrdom', () => { '-//W3C//DTD XHTML 1.0 Transitional//EN', ); expect(document.doctype?.systemId).toEqual(''); + expect(replayer.applyScroll).toBeCalledTimes(1); + }); + + it('should apply input data on an input element', () => { + const element = (document.createElement('input') as unknown) as INode; + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement('input'); + rrNode.inputData = { + source: IncrementalSource.Input, + text: '', + id: 0, + isChecked: false, + }; + replayer.applyInput = jest.fn(); + diff(element, rrNode, replayer); + expect(replayer.applyInput).toHaveBeenCalledTimes(1); }); it('should diff a Text node', () => { const node = (document.createTextNode('old text') as unknown) as INode; expect(node.textContent).toEqual('old text'); - node.__sn = Object.assign({}, elementSn); const rrDocument = new RRDocument(); const rrNode = rrDocument.createTextNode('new text'); - rrNode.__sn = Object.assign({}, elementSn); diff(node, rrNode, replayer); expect(node.textContent).toEqual('new text'); }); + + it('should diff a style element', () => { + document.write(''); + const element = document.createElement('style'); + document.documentElement.appendChild(element); + const rrDocument = new RRDocument(); + const rrStyle = rrDocument.createElement('style'); + rrStyle.rules = [ + { cssText: 'div{color: black;}', type: StyleRuleType.Insert, index: 0 }, + ]; + diff((element as unknown) as INode, rrStyle, replayer); + expect(element.sheet!.cssRules.length).toEqual(1); + expect(element.sheet!.cssRules[0].cssText).toEqual('div {color: black;}'); + }); + + it('should diff a canvas element', () => { + const element = document.createElement('canvas'); + const rrDocument = new RRDocument(); + const rrCanvas = rrDocument.createElement('canvas'); + const canvasMutation = { + source: IncrementalSource.CanvasMutation, + id: 0, + type: 0, + commands: [{ property: 'fillStyle', args: [{}], setter: true }], + } as canvasMutationData; + const MutationNumber = 100; + Array.from(Array(MutationNumber)).forEach(() => + rrCanvas.canvasMutation.push({ + event: { + timestamp: Date.now(), + type: EventType.IncrementalSnapshot, + data: canvasMutation, + }, + mutation: canvasMutation, + }), + ); + replayer.applyCanvas = jest.fn(); + diff((element as unknown) as INode, rrCanvas, replayer); + expect(replayer.applyCanvas).toHaveBeenCalledTimes(MutationNumber); + }); + + it('should diff a media element', async () => { + // mock the HTMLMediaElement of jsdom + let paused = true; + window.HTMLMediaElement.prototype.play = async () => { + paused = false; + }; + window.HTMLMediaElement.prototype.pause = async () => { + paused = true; + }; + Object.defineProperty(HTMLMediaElement.prototype, 'paused', { + get() { + return paused; + }, + }); + + for (const tagName of ['AUDIO', 'VIDEO']) { + const element = document.createElement(tagName) as HTMLMediaElement; + expect(element.volume).toEqual(1); + expect(element.currentTime).toEqual(0); + expect(element.muted).toEqual(false); + expect(element.paused).toEqual(true); + + const rrDocument = new RRDocument(); + const rrMedia = rrDocument.createElement(tagName) as RRMediaElement; + rrMedia.volume = 0.5; + rrMedia.currentTime = 100; + rrMedia.muted = true; + rrMedia.paused = false; + + diff((element as unknown) as INode, rrMedia, replayer); + expect(element.volume).toEqual(0.5); + expect(element.currentTime).toEqual(100); + expect(element.muted).toEqual(true); + expect(element.paused).toEqual(false); + + rrMedia.paused = true; + diff((element as unknown) as INode, rrMedia, replayer); + expect(element.paused).toEqual(true); + } + }); }); describe('diff properties', () => { @@ -189,6 +298,51 @@ describe('diff algorithm for rrdom', () => { expect(((node as Node) as HTMLElement).className).toBe(''); expect(((node as Node) as HTMLElement).getAttribute('src')).toBe('link'); }); + + it('can diff properties for SVG elements', () => { + const element = (document.createElement('svg') as unknown) as INode; + const rrDocument = new RRDocument(); + const node = rrDocument.createElement('svg'); + node.__sn = Object.assign({}, elementSn, { tagName: 'svg', isSVG: true }); + const value = 'http://www.w3.org/2000/svg'; + node.attributes.xmlns = value; + + const setAttributeNS = jest.fn(); + const originalFun = SVGElement.prototype.setAttributeNS; + HTMLElement.prototype.setAttributeNS = function () { + setAttributeNS(...arguments); + return originalFun.apply(this, arguments); + }; + + diff(element, node, replayer); + expect(((element as Node) as SVGElement).getAttribute('xmlns')).toBe( + value, + ); + expect(setAttributeNS).toHaveBeenCalledWith( + 'http://www.w3.org/2000/xmlns/', + 'xmlns', + value, + ); + }); + + it('can diff properties for canvas', async () => { + const element = (document.createElement('canvas') as unknown) as INode; + const rrDocument = new RRDocument(); + const rrCanvas = rrDocument.createElement('canvas'); + rrCanvas.__sn = Object.assign({}, elementSn, { tagName: 'canvas' }); + rrCanvas.attributes['rr_dataURL'] = 'data:image/png;base64,'; + + // Patch the function to detect whether an img element is created. + const originalFn = document.createElement; + const createImageElement = jest.fn(); + document.createElement = function () { + createImageElement(...arguments); + return originalFn.apply(this, arguments); + }; + + diff(element, rrCanvas, replayer); + expect(createImageElement).toHaveBeenCalledWith('img'); + }); }); describe('diff children', () => { @@ -730,9 +884,48 @@ describe('diff algorithm for rrdom', () => { const rrNode = rrDocument.createElement(tagName); rrNode.__sn = Object.assign({}, elementSn, { tagName }); rrNode.attachShadow({ mode: 'open' }); + const child = rrDocument.createElement('div'); + child.__sn = Object.assign({}, elementSn, { tagName, id: 1 }); + rrNode.shadowRoot!.appendChild(child); + expect(rrNode.shadowRoot!.childNodes.length).toBe(1); diff(node, rrNode, replayer); expect(((node as Node) as HTMLElement).shadowRoot).not.toBeNull(); + expect( + ((node as Node) as HTMLElement).shadowRoot!.childNodes.length, + ).toBe(1); + const childElement = ((node as Node) as HTMLElement).shadowRoot! + .childNodes[0] as HTMLElement; + expect(childElement.tagName).toEqual('DIV'); + }); + }); + + describe('diff iframe elements', () => { + it('should add an element to the contentDocument of an iframe element', () => { + document.write(''); + const node = document.createElement('iframe'); + document.documentElement.appendChild(node); + expect(node.contentDocument).toBeDefined(); + + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement('iframe'); + rrNode.contentDocument.__sn = { + type: NodeType.Document, + childNodes: [], + id: 1, + }; + const childElement = rrNode.contentDocument.createElement('div'); + childElement.__sn = Object.assign({}, elementSn, { + tagName: 'div', + id: 2, + }); + rrNode.contentDocument.appendChild(childElement); + + diff((node as unknown) as INode, rrNode, replayer); + expect(node.contentDocument!.childNodes.length).toBe(1); + const element = node.contentDocument!.childNodes[0] as HTMLElement; + expect(element.tagName).toBe('DIV'); + expect(((element as unknown) as INode).__sn.id).toEqual(2); }); }); @@ -763,6 +956,14 @@ describe('diff algorithm for rrdom', () => { expect(result).toBeInstanceOf(Comment); expect(result.__sn.id).toBe(0); expect(((result as Node) as Comment).textContent).toBe(textContent); + + rrNode = rrDocument.createCDATASection(''); + rrNode.__sn = { id: 0, type: NodeType.CDATA, textContent: '' }; + expect(() => + createOrGetNode(rrNode, mirror), + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot create CDATA sections in HTML documents"`, + ); }); it('create a DocumentType from RRDocumentType', () => { @@ -783,6 +984,32 @@ describe('diff algorithm for rrdom', () => { expect(((result as Node) as DocumentType).publicId).toEqual(publicId); expect(((result as Node) as DocumentType).systemId).toEqual(''); }); + + it('can get a node if it already exists', () => { + mirror.getNode = function (id) { + return this.map[id] || null; + }; + const rrDocument = new RRDocument(); + const textContent = 'Text Content'; + const text = document.createTextNode(textContent); + ((text as unknown) as INode).__sn = { + id: 0, + type: NodeType.Text, + textContent: 'text of the existed node', + }; + // Add the text node to the mirror to make it look like already existing. + mirror.map[0] = (text as unknown) as INode; + const rrNode: RRNode = rrDocument.createTextNode(textContent); + rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; + let result = createOrGetNode(rrNode, mirror); + + expect(result).toBeInstanceOf(Text); + expect(result.__sn.id).toBe(0); + expect(((result as Node) as Text).textContent).toBe(textContent); + expect(result).toEqual(text); + // To make sure the existed text node is used. + expect(result.__sn).toEqual(((text as unknown) as INode).__sn); + }); }); describe('apply virtual style rules to node', () => { diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 619ffc0d49..59dc97b9bd 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -3,14 +3,11 @@ */ import { NodeType } from 'rrweb-snapshot'; import { - BaseRRCDATASectionImpl, - BaseRRCommentImpl, BaseRRDocumentImpl, BaseRRDocumentTypeImpl, BaseRRElementImpl, BaseRRMediaElementImpl, BaseRRNode, - BaseRRTextImpl, IRRDocumentType, } from '../src/document'; @@ -123,6 +120,8 @@ describe('Basic RRDocument implementation', () => { describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = new RRDocument(); + expect(node.toString()).toEqual(' RRDocument'); + node.__sn = { type: NodeType.Document, id: 1, @@ -486,6 +485,8 @@ describe('Basic RRDocument implementation', () => { expect(node.className).toEqual('c1'); classList.add('c2'); expect(node.className).toEqual('c1 c2'); + classList.add('c2'); + expect(node.className).toEqual('c1 c2'); }); it('classList can remove class name', () => { @@ -575,6 +576,8 @@ describe('Basic RRDocument implementation', () => { expect(node.attributes.style).toEqual('width:78%; background-color:red;'); expect(style.removeProperty('backgroundColor')).toEqual('red'); expect(node.attributes.style).toEqual('width:78%;'); + // remove a non-exist property + expect(style.removeProperty('margin')).toEqual(''); }); it('can parse more inline styles correctly', () => { diff --git a/packages/rrdom/test/html/main.html b/packages/rrdom/test/html/main.html index c9603b3066..40e3c169e2 100644 --- a/packages/rrdom/test/html/main.html +++ b/packages/rrdom/test/html/main.html @@ -4,7 +4,7 @@ Main - + @@ -35,6 +35,10 @@

This is a h1 heading with styles

Text 2 This is an image + +
+ +
diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index f631b0d080..8f64b0dac7 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -1,9 +1,19 @@ +/** + * @jest-environment jsdom + */ import * as fs from 'fs'; import * as path from 'path'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import * as typescript from 'rollup-plugin-typescript2'; +import { NodeType } from 'rrweb-snapshot'; +import { + buildFromDom, + RRCanvasElement, + RRDocument, + RRNode, +} from '../src/virtual-dom'; const _typescript = (typescript as unknown) as typeof typescript.default; const printRRDomCode = ` @@ -28,33 +38,34 @@ function walk(node, blankSpace) { `; describe('RRDocument for browser environment', () => { - let browser: puppeteer.Browser; - let code: string; - beforeAll(async () => { - browser = await puppeteer.launch(); - const bundle = await rollup.rollup({ - input: path.resolve(__dirname, '../src/virtual-dom.ts'), - plugins: [ - resolve(), - _typescript({ - tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, - }), - ], + describe('create a RRDocument from a html document', () => { + let browser: puppeteer.Browser; + let code: string; + let page: puppeteer.Page; + + beforeAll(async () => { + browser = await puppeteer.launch(); + const bundle = await rollup.rollup({ + input: path.resolve(__dirname, '../src/virtual-dom.ts'), + plugins: [ + resolve(), + _typescript({ + tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, + }), + ], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrdom', + format: 'iife', + }); + code = _code; }); - const { - output: [{ code: _code }], - } = await bundle.generate({ - name: 'rrdom', - format: 'iife', + afterAll(async () => { + await browser.close(); }); - code = _code; - }); - afterAll(async () => { - await browser.close(); - }); - describe('create a RRDocument from a html document', () => { - let page: puppeteer.Page; beforeEach(async () => { page = await browser.newPage(); await page.goto('about:blank'); @@ -68,7 +79,7 @@ describe('RRDocument for browser environment', () => { await page.setContent(getHtml('main.html')); const result = await page.evaluate(` const doc = new rrdom.RRDocument(); - rrdom.buildFromDom(document,doc); + rrdom.buildFromDom(document, doc, doc.mirror); printRRDom(doc); `); expect(result).toMatchSnapshot(); @@ -78,7 +89,7 @@ describe('RRDocument for browser environment', () => { await page.setContent(getHtml('iframe.html')); const result = await page.evaluate(` const doc = new rrdom.RRDocument(); - rrdom.buildFromDom(document,doc); + rrdom.buildFromDom(document, doc, doc.mirror); printRRDom(doc); `); expect(result).toMatchSnapshot(); @@ -88,12 +99,147 @@ describe('RRDocument for browser environment', () => { await page.setContent(getHtml('shadow-dom.html')); const result = await page.evaluate(` const doc = new rrdom.RRDocument(); - rrdom.buildFromDom(document,doc); + rrdom.buildFromDom(document, doc, doc.mirror); printRRDom(doc); `); expect(result).toMatchSnapshot(); }); }); + + describe('RRDocument build for virtual dom', () => { + it('can create a new RRDocument', () => { + const dom = new RRDocument(); + const newDom = dom.createDocument('', ''); + expect(newDom).toBeInstanceOf(RRDocument); + }); + + it('can build a RRDocument from a real Dom', () => { + const result = buildFromDom(document); + expect(result.childNodes.length).toBe(2); + expect(result.documentElement).toBeDefined(); + expect(result.head).toBeDefined(); + expect(result.head!.tagName).toBe('HEAD'); + expect(result.body).toBeDefined(); + expect(result.body!.tagName).toBe('BODY'); + }); + + it('can destroy a RRDocument tree', () => { + const dom = new RRDocument(); + const node1 = dom.createDocumentType('', '', ''); + dom.appendChild(node1); + dom.mirror.map[0] = node1; + const node2 = dom.createElement('html'); + dom.appendChild(node2); + dom.mirror.map[1] = node2; + expect(dom.childNodes.length).toEqual(2); + expect(dom.mirror.has(0)).toBeTruthy(); + expect(dom.mirror.has(1)).toBeTruthy(); + + dom.destroyTree(); + expect(dom.childNodes.length).toEqual(0); + expect(dom.mirror.has(0)).toBeFalsy(); + expect(dom.mirror.has(1)).toBeFalsy(); + }); + + it('can execute a dummy getContext function in RRCanvasElement', () => { + const canvas = new RRCanvasElement('CANVAS'); + expect(canvas.getContext).toBeDefined(); + expect(canvas.getContext()).toBeNull(); + }); + + describe('Mirror in the RRDocument', () => { + it('should have a map to store id and node', () => { + const dom = new RRDocument(); + expect(dom.mirror.map).toBeDefined(); + const node1 = dom.createElement('div'); + dom.mirror.map[0] = node1; + const node2 = dom.createTextNode('text'); + dom.mirror.map[1] = node2; + expect(dom.mirror.map[0]).toBe(node1); + expect(dom.mirror.map[1]).toBe(node2); + expect(dom.mirror.getNode(0)).toBe(node1); + expect(dom.mirror.getNode(1)).toBe(node2); + expect(dom.mirror.getNode(2)).toBeNull(); + expect(dom.mirror.getNode(-1)).toBeNull(); + }); + + it('can get node id', () => { + const dom = new RRDocument(); + const node1 = dom.createElement('div'); + node1.__sn = { + type: NodeType.Element, + tagName: 'div', + childNodes: [], + attributes: {}, + id: 0, + }; + dom.mirror.map[0] = node1; + expect(dom.mirror.getId(node1)).toEqual(0); + const node2 = dom.createTextNode('text'); + expect(dom.mirror.getId(node2)).toEqual(-1); + expect(dom.mirror.getId((null as unknown) as RRNode)).toEqual(-1); + }); + + it('has() should return whether the mirror has a node', () => { + const dom = new RRDocument(); + const node1 = dom.createElement('div'); + dom.mirror.map[0] = node1; + const node2 = dom.createTextNode('text'); + dom.mirror.map[1] = node2; + expect(dom.mirror.has(0)).toBeTruthy(); + expect(dom.mirror.has(1)).toBeTruthy(); + expect(dom.mirror.has(2)).toBeFalsy(); + expect(dom.mirror.has(-1)).toBeFalsy(); + }); + + it('can remove node from the mirror', () => { + const dom = new RRDocument(); + const node1 = dom.createElement('div'); + dom.mirror.map[0] = node1; + node1.__sn = { + type: NodeType.Element, + tagName: 'div', + childNodes: [], + attributes: {}, + id: 0, + }; + const node2 = dom.createTextNode('text'); + node2.__sn = { + type: NodeType.Text, + textContent: 'text', + id: 1, + }; + node1.appendChild(node2); + dom.mirror.map[1] = node2; + expect(dom.mirror.has(0)).toBeTruthy(); + expect(dom.mirror.has(1)).toBeTruthy(); + dom.mirror.removeNodeFromMap(node2); + expect(dom.mirror.has(0)).toBeTruthy(); + expect(dom.mirror.has(1)).toBeFalsy(); + + dom.mirror.map[1] = node2; + expect(dom.mirror.has(1)).toBeTruthy(); + // To remove node1 and its child node2 from the mirror. + dom.mirror.removeNodeFromMap(node1); + expect(dom.mirror.has(0)).toBeFalsy(); + expect(dom.mirror.has(1)).toBeFalsy(); + }); + + it('can reset the mirror', () => { + const dom = new RRDocument(); + const node1 = dom.createElement('div'); + dom.mirror.map[0] = node1; + const node2 = dom.createTextNode('text'); + dom.mirror.map[1] = node2; + expect(dom.mirror.has(0)).toBeTruthy(); + expect(dom.mirror.has(1)).toBeTruthy(); + + dom.mirror.reset(); + expect(dom.mirror.has(0)).toBeFalsy(); + expect(dom.mirror.has(1)).toBeFalsy(); + }); + }); + }); }); function getHtml(fileName: string) { const filePath = path.resolve(__dirname, `./html/${fileName}`); From b1487bd3e5ccf184edc1d26327134cd0d3621a5a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 3 Mar 2022 19:20:33 +1100 Subject: [PATCH 35/79] use cssstyle in document-nodejs --- packages/rrdom/package.json | 2 + packages/rrdom/src/document-nodejs.ts | 21 ++++ packages/rrdom/src/document.ts | 3 + packages/rrdom/src/style.ts | 2 +- packages/rrdom/test/document-nodejs.test.ts | 108 ++++++++++++++++++++ packages/rrdom/test/document.test.ts | 38 ++++--- packages/rrdom/test/polyfill.test.ts | 43 ++++++++ yarn.lock | 5 + 8 files changed, 204 insertions(+), 18 deletions(-) diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 6ee332c04e..645f6ed77e 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -27,6 +27,7 @@ "@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-node-resolve": "^13.0.4", "@types/cssom": "^0.4.1", + "@types/cssstyle": "^2.2.1", "@types/jest": "^27.0.1", "@types/nwsapi": "^2.2.2", "@types/puppeteer": "^5.4.4", @@ -42,6 +43,7 @@ }, "dependencies": { "cssom": "^0.5.0", + "cssstyle": "^2.3.0", "nwsapi": "^2.2.0" } } diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index ac3b71c8a4..b1a780369f 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -1,4 +1,5 @@ import { NodeType } from 'rrweb-snapshot'; +import { CSSStyleDeclaration as cssstyle } from 'cssstyle'; import { NWSAPI } from 'nwsapi'; import { BaseRRCDATASectionImpl, @@ -11,6 +12,7 @@ import { BaseRRTextImpl, ClassList, IRRDocument, + CSSStyleDeclaration, } from './document'; const nwsapi = require('nwsapi'); const cssom = require('cssom'); @@ -184,6 +186,25 @@ export class RRDocument export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} export class RRElement extends BaseRRElementImpl(RRNode) { + private _style: cssstyle; + constructor(tagName: string) { + super(tagName); + this._style = new cssstyle(); + const style = this._style; + Object.defineProperty(this.attributes, 'style', { + get() { + return style.cssText; + }, + set(cssText: string) { + style.cssText = cssText; + }, + }); + } + + get style() { + return (this._style as unknown) as CSSStyleDeclaration; + } + attachShadow(_init: ShadowRootInit): RRElement { return super.attachShadow(_init) as RRElement; } diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 5562d78292..641331a447 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -449,11 +449,13 @@ export function BaseRRElementImpl< const style = (this.attributes.style ? parseCSSText(this.attributes.style as string) : {}) as CSSStyleDeclaration; + const hyphenateRE = /\B([A-Z])/g; style.setProperty = ( name: string, value: string | null, priority?: string, ) => { + if (hyphenateRE.test(name)) return; const normalizedName = camelize(name); if (!value) delete style[normalizedName]; else style[normalizedName] = value; @@ -461,6 +463,7 @@ export function BaseRRElementImpl< this.attributes.style = toCSSText(style); }; style.removeProperty = (name: string) => { + if (hyphenateRE.test(name)) return ''; const normalizedName = camelize(name); const value = style[normalizedName] || ''; delete style[normalizedName]; diff --git a/packages/rrdom/src/style.ts b/packages/rrdom/src/style.ts index ffcda389c6..e4dd6bbf64 100644 --- a/packages/rrdom/src/style.ts +++ b/packages/rrdom/src/style.ts @@ -21,7 +21,7 @@ export function toCSSText(style: Record): string { const value = style[name]; if (typeof value !== 'string') continue; const normalizedName = hyphenate(name); - properties.push(`${normalizedName}:${value};`); + properties.push(`${normalizedName}: ${value};`); } return properties.join(' '); } diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index c71e44c310..e4074248d4 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -209,6 +209,114 @@ describe('RRDocument for nodejs environment', () => { expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0); }); + it('can get CSS style declaration', () => { + const node = rrdom.createElement('div'); + const style = node.style; + expect(style).toBeDefined(); + expect(style.setProperty).toBeDefined(); + expect(style.removeProperty).toBeDefined(); + + node.attributes.style = + 'color: blue; background-color: red; width: 78%; height: 50vh !important;'; + expect(node.style.color).toBe('blue'); + expect(node.style.backgroundColor).toBe('red'); + expect(node.style.width).toBe('78%'); + expect(node.style.height).toBe('50vh'); + }); + + it('can set CSS property', () => { + const node = rrdom.createElement('div'); + const style = node.style; + style.setProperty('color', 'red'); + expect(node.attributes.style).toEqual('color: red;'); + // camelCase style is unacceptable + style.setProperty('backgroundColor', 'blue'); + expect(node.attributes.style).toEqual('color: red;'); + style.setProperty('height', '50vh', 'important'); + expect(node.attributes.style).toEqual( + 'color: red; height: 50vh !important;', + ); + + // kebab-case + style.setProperty('background-color', 'red'); + expect(node.attributes.style).toEqual( + 'color: red; height: 50vh !important; background-color: red;', + ); + + // remove the property + style.setProperty('background-color', null); + expect(node.attributes.style).toEqual( + 'color: red; height: 50vh !important;', + ); + }); + + it('can remove CSS property', () => { + const node = rrdom.createElement('div'); + node.attributes.style = + 'color: blue; background-color: red; width: 78%; height: 50vh;'; + const style = node.style; + expect(style.removeProperty('color')).toEqual('blue'); + expect(node.attributes.style).toEqual( + 'background-color: red; width: 78%; height: 50vh;', + ); + expect(style.removeProperty('height')).toEqual('50vh'); + expect(node.attributes.style).toEqual( + 'background-color: red; width: 78%;', + ); + // kebab-case + expect(style.removeProperty('background-color')).toEqual('red'); + expect(node.attributes.style).toEqual('width: 78%;'); + style.setProperty('background-color', 'red'); + expect(node.attributes.style).toEqual( + 'width: 78%; background-color: red;', + ); + expect(style.removeProperty('backgroundColor')).toEqual(''); + expect(node.attributes.style).toEqual( + 'width: 78%; background-color: red;', + ); + // remove a non-exist property + expect(style.removeProperty('margin')).toEqual(''); + }); + + it('can parse more inline styles correctly', () => { + const node = rrdom.createElement('div'); + // general + node.attributes.style = + 'display: inline-block; margin: 0 auto; border: 5px solid #BADA55; font-size: .75em; position:absolute;width: 33.3%; z-index:1337; font-family: "Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif;'; + + const style = node.style; + expect(style.display).toEqual('inline-block'); + expect(style.margin).toEqual('0px auto'); + expect(style.border).toEqual('5px solid #bada55'); + expect(style.fontSize).toEqual('.75em'); + expect(style.position).toEqual('absolute'); + expect(style.width).toEqual('33.3%'); + expect(style.zIndex).toEqual('1337'); + expect(style.fontFamily).toEqual( + '"Goudy Bookletter 1911", Gill Sans Extrabold, sans-serif', + ); + + // multiple of same property + node.attributes.style = 'color:rgba(0,0,0,1);color:white'; + expect(style.color).toEqual('white'); + + // url + node.attributes.style = + 'background-image: url("http://example.com/img.png")'; + expect(node.style.backgroundImage).toEqual( + 'url(http://example.com/img.png)', + ); + + // comment + node.attributes.style = 'top: 0; /* comment */ bottom: 42rem;'; + expect(node.style.top).toEqual('0px'); + expect(node.style.bottom).toEqual('42rem'); + + // incomplete + node.attributes.style = 'overflow:'; + expect(node.style.overflow).toEqual(''); + }); + it('style element', () => { expect(rrdom.getElementsByTagName('style').length).not.toEqual(0); expect(rrdom.getElementsByTagName('style')[0].tagName).toEqual('STYLE'); diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 59dc97b9bd..10fefba905 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -121,7 +121,7 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = new RRDocument(); expect(node.toString()).toEqual(' RRDocument'); - + node.__sn = { type: NodeType.Document, id: 1, @@ -534,48 +534,52 @@ describe('Basic RRDocument implementation', () => { const node = document.createElement('div'); const style = node.style; style.setProperty('color', 'red'); - expect(node.attributes.style).toEqual('color:red;'); - // camelCase style + expect(node.attributes.style).toEqual('color: red;'); + // camelCase style is unacceptable style.setProperty('backgroundColor', 'blue'); - expect(node.attributes.style).toEqual( - 'color:red; background-color:blue;', - ); + expect(node.attributes.style).toEqual('color: red;'); style.setProperty('height', '50vh', 'important'); expect(node.attributes.style).toEqual( - 'color:red; background-color:blue; height:50vh !important;', + 'color: red; height: 50vh !important;', ); // kebab-case style.setProperty('background-color', 'red'); expect(node.attributes.style).toEqual( - 'color:red; background-color:red; height:50vh !important;', + 'color: red; height: 50vh !important; background-color: red;', ); // remove the property style.setProperty('background-color', null); expect(node.attributes.style).toEqual( - 'color:red; height:50vh !important;', + 'color: red; height: 50vh !important;', ); }); it('can remove CSS property', () => { const node = document.createElement('div'); node.attributes.style = - 'color:blue; background-color:red; width:78%; height:50vh !important;'; + 'color: blue; background-color: red; width: 78%; height: 50vh !important;'; const style = node.style; expect(style.removeProperty('color')).toEqual('blue'); expect(node.attributes.style).toEqual( - 'background-color:red; width:78%; height:50vh !important;', + 'background-color: red; width: 78%; height: 50vh !important;', ); expect(style.removeProperty('height')).toEqual('50vh !important'); - expect(node.attributes.style).toEqual('background-color:red; width:78%;'); + expect(node.attributes.style).toEqual( + 'background-color: red; width: 78%;', + ); // kebab-case expect(style.removeProperty('background-color')).toEqual('red'); - expect(node.attributes.style).toEqual('width:78%;'); + expect(node.attributes.style).toEqual('width: 78%;'); style.setProperty('background-color', 'red'); - expect(node.attributes.style).toEqual('width:78%; background-color:red;'); - expect(style.removeProperty('backgroundColor')).toEqual('red'); - expect(node.attributes.style).toEqual('width:78%;'); + expect(node.attributes.style).toEqual( + 'width: 78%; background-color: red;', + ); + expect(style.removeProperty('backgroundColor')).toEqual(''); + expect(node.attributes.style).toEqual( + 'width: 78%; background-color: red;', + ); // remove a non-exist property expect(style.removeProperty('margin')).toEqual(''); }); @@ -599,7 +603,7 @@ describe('Basic RRDocument implementation', () => { ); // multiple of same property - node.attributes.style = 'color:rgba(0,0,0,1);color:white'; + node.attributes.style = 'color: rgba(0,0,0,1);color:white'; style = node.style; expect(style.color).toEqual('white'); diff --git a/packages/rrdom/test/polyfill.test.ts b/packages/rrdom/test/polyfill.test.ts index 429cdb484e..a9d5f381f1 100644 --- a/packages/rrdom/test/polyfill.test.ts +++ b/packages/rrdom/test/polyfill.test.ts @@ -22,6 +22,18 @@ describe('polyfill for nodejs', () => { ); }); + it('should not polyfill performance if it already exists', () => { + if (compare(process.version, 'v16.0.0', '>=')) { + const originalPerformance = global.performance; + polyfillPerformance(); + expect(global.performance).toBe(originalPerformance); + } + const fakePerformance = (jest.fn() as unknown) as Performance; + global.performance = fakePerformance; + polyfillPerformance(); + expect(global.performance).toEqual(fakePerformance); + }); + it('should polyfill requestAnimationFrame', () => { expect(global.requestAnimationFrame).toBeUndefined(); expect(global.cancelAnimationFrame).toBeUndefined(); @@ -61,6 +73,16 @@ describe('polyfill for nodejs', () => { jest.useRealTimers(); }); + it('should not polyfill requestAnimationFrame if it already exists', () => { + const fakeRequestAnimationFrame = (jest.fn() as unknown) as typeof global.requestAnimationFrame; + global.requestAnimationFrame = fakeRequestAnimationFrame; + const fakeCancelAnimationFrame = (jest.fn() as unknown) as typeof global.cancelAnimationFrame; + global.cancelAnimationFrame = fakeCancelAnimationFrame; + polyfillRAF(); + expect(global.requestAnimationFrame).toBe(fakeRequestAnimationFrame); + expect(global.cancelAnimationFrame).toBe(fakeCancelAnimationFrame); + }); + it('should polyfill Event type', () => { // if the second version is greater if (compare(process.version, 'v15.0.0', '<')) @@ -70,6 +92,13 @@ describe('polyfill for nodejs', () => { expect(Event).toBeDefined(); }); + it('should not polyfill Event type if it already exists', () => { + const fakeEvent = (jest.fn() as unknown) as typeof global.Event; + global.Event = fakeEvent; + polyfillEvent(); + expect(global.Event).toBe(fakeEvent); + }); + it('should polyfill Node type', () => { expect(global.Node).toBeUndefined(); polyfillNode(); @@ -78,6 +107,13 @@ describe('polyfill for nodejs', () => { expect(Node).toEqual(RRNode); }); + it('should not polyfill Node type if it already exists', () => { + const fakeNode = (jest.fn() as unknown) as typeof global.Node; + global.Node = fakeNode; + polyfillNode(); + expect(global.Node).toBe(fakeNode); + }); + it('should polyfill document object', () => { expect(global.document).toBeUndefined(); polyfillDocument(); @@ -85,4 +121,11 @@ describe('polyfill for nodejs', () => { expect(document).toBeDefined(); expect(document).toBeInstanceOf(RRDocument); }); + + it('should not polyfill document object if it already exists', () => { + const fakeDocument = (jest.fn() as unknown) as typeof global.document; + global.document = fakeDocument; + polyfillDocument(); + expect(global.document).toBe(fakeDocument); + }); }); diff --git a/yarn.lock b/yarn.lock index bb3776d788..da037d1e58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1899,6 +1899,11 @@ resolved "https://registry.yarnpkg.com/@types/cssom/-/cssom-0.4.1.tgz#fb64e145b425bd6c1b0ed78ebd66ba43b6e088ab" integrity sha512-hHGVfUuGZe5FpgCxpTJccH0gD1bui5gWceW0We0TyAzUr6wBaqDnSLG9Yr3xqS4AkGhnclNOwRSXH/LIfki3fQ== +"@types/cssstyle@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/cssstyle/-/cssstyle-2.2.1.tgz#fa010824006ff47af94a6b9baf9759e031815347" + integrity sha512-CSQFKdZc3dmWoZXLAM0pPL6XiYLG8hMGzImM2MwQ9kavB5LnbeMGan94CCj4oxY65xMl5mRMwrFUfKPOWO4WpQ== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" From e40e19d1874841c9e8fb5f3a3bdd3ad6319f0fe2 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 3 Mar 2022 20:12:18 +1100 Subject: [PATCH 36/79] fix: bundle error --- packages/rrdom/src/document-nodejs.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index b1a780369f..6adab462ee 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -1,6 +1,6 @@ import { NodeType } from 'rrweb-snapshot'; -import { CSSStyleDeclaration as cssstyle } from 'cssstyle'; import { NWSAPI } from 'nwsapi'; +import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle'; import { BaseRRCDATASectionImpl, BaseRRCommentImpl, @@ -16,6 +16,7 @@ import { } from './document'; const nwsapi = require('nwsapi'); const cssom = require('cssom'); +const cssstyle = require('cssstyle'); export class RRNode extends BaseRRNode {} @@ -186,10 +187,10 @@ export class RRDocument export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} export class RRElement extends BaseRRElementImpl(RRNode) { - private _style: cssstyle; + private _style: CSSStyleDeclarationType; constructor(tagName: string) { super(tagName); - this._style = new cssstyle(); + this._style = new cssstyle.CSSStyleDeclaration(); const style = this._style; Object.defineProperty(this.attributes, 'style', { get() { From d8c29741f0446ee3fe0c37e04f0bbf36de1b91bc Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Mar 2022 02:06:57 +1100 Subject: [PATCH 37/79] improve document-nodejs --- packages/rrdom/src/document-nodejs.ts | 24 +- packages/rrdom/test/document-nodejs.test.ts | 288 ++++++++++++++++---- 2 files changed, 249 insertions(+), 63 deletions(-) diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 6adab462ee..9198a7e229 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -142,13 +142,13 @@ export class RRDocument element = new RRIFrameElement(upperTagName); break; case 'IMG': - element = new RRImageElement('IMG'); + element = new RRImageElement(upperTagName); break; case 'CANVAS': - element = new RRCanvasElement('CANVAS'); + element = new RRCanvasElement(upperTagName); break; case 'STYLE': - element = new RRStyleElement('STYLE'); + element = new RRStyleElement(upperTagName); break; default: element = new RRElement(upperTagName); @@ -158,10 +158,7 @@ export class RRDocument return element; } - createElementNS( - _namespaceURI: 'http://www.w3.org/2000/svg', - qualifiedName: string, - ) { + createElementNS(_namespaceURI: string, qualifiedName: string) { return this.createElement(qualifiedName as keyof HTMLElementTagNameMap); } @@ -228,10 +225,6 @@ export class RRElement extends BaseRRElementImpl(RRNode) { this.attributes[name.toLowerCase()] = attribute; } - hasAttribute(name: string) { - return name.toLowerCase() in this.attributes; - } - removeAttribute(name: string) { delete this.attributes[name.toLowerCase()]; } @@ -253,13 +246,18 @@ export class RRElement extends BaseRRElementImpl(RRNode) { } querySelectorAll(selectors: string): RRNode[] { + const result: RRElement[] = []; if (this.ownerDocument !== null) { - return ((this.ownerDocument as RRDocument).nwsapi.select( + ((this.ownerDocument as RRDocument).nwsapi.select( selectors, (this as unknown) as Element, + (element) => { + if (((element as unknown) as RRElement) !== this) + result.push((element as unknown) as RRElement); + }, ) as unknown) as RRNode[]; } - return []; + return result; } getElementById(elementId: string): RRElement | null { diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index e4074248d4..57a83d0e0c 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -3,7 +3,19 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { RRDocument, RRElement, RRStyleElement } from '../src/document-nodejs'; +import { NodeType } from 'rrweb-snapshot'; +import { + RRCanvasElement, + RRCDATASection, + RRComment, + RRDocument, + RRElement, + RRIFrameElement, + RRImageElement, + RRMediaElement, + RRStyleElement, + RRText, +} from '../src/document-nodejs'; import { buildFromDom } from '../src/virtual-dom'; describe('RRDocument for nodejs environment', () => { @@ -16,37 +28,75 @@ describe('RRDocument for nodejs environment', () => { buildFromDom(document, rrdom); }); - it('get className', () => { - expect(rrdom.getElementsByTagName('DIV')[0].className).toEqual( - 'blocks blocks1', - ); - expect(rrdom.getElementsByTagName('DIV')[1].className).toEqual( - 'blocks blocks1 :hover', + it('can create different type of RRNodes', () => { + const document = rrdom.createDocument('', ''); + expect(document).toBeInstanceOf(RRDocument); + const audio = rrdom.createElement('audio'); + expect(audio).toBeInstanceOf(RRMediaElement); + const video = rrdom.createElement('video'); + expect(video).toBeInstanceOf(RRMediaElement); + const iframe = rrdom.createElement('iframe'); + expect(iframe).toBeInstanceOf(RRIFrameElement); + const image = rrdom.createElement('img'); + expect(image).toBeInstanceOf(RRImageElement); + const canvas = rrdom.createElement('canvas'); + expect(canvas).toBeInstanceOf(RRCanvasElement); + const style = rrdom.createElement('style'); + expect(style).toBeInstanceOf(RRStyleElement); + const elementNS = rrdom.createElementNS( + 'http://www.w3.org/2000/svg', + 'div', ); + expect(elementNS).toBeInstanceOf(RRElement); + expect(elementNS.tagName).toEqual('DIV'); + const text = rrdom.createTextNode('text'); + expect(text).toBeInstanceOf(RRText); + expect(text.textContent).toEqual('text'); + const comment = rrdom.createComment('comment'); + expect(comment).toBeInstanceOf(RRComment); + expect(comment.textContent).toEqual('comment'); + const CDATA = rrdom.createCDATASection('data'); + expect(CDATA).toBeInstanceOf(RRCDATASection); + expect(CDATA.data).toEqual('data'); }); - it('get id', () => { - expect(rrdom.getElementsByTagName('DIV')[0].id).toEqual('block1'); - expect(rrdom.getElementsByTagName('DIV')[1].id).toEqual('block2'); - expect(rrdom.getElementsByTagName('DIV')[2].id).toEqual('block3'); + it('can get head element', () => { + expect(rrdom.head).toBeDefined(); + expect(rrdom.head!.tagName).toBe('HEAD'); + expect(rrdom.head!.parentElement).toBe(rrdom.documentElement); }); - it('get attribute name', () => { - expect( - rrdom.getElementsByTagName('DIV')[0].getAttribute('class'), - ).toEqual('blocks blocks1'); - expect( - rrdom.getElementsByTagName('dIv')[0].getAttribute('cLaSs'), - ).toEqual('blocks blocks1'); - expect(rrdom.getElementsByTagName('DIV')[0].getAttribute('id')).toEqual( - 'block1', + it('can get body element', () => { + expect(rrdom.body).toBeDefined(); + expect(rrdom.body!.tagName).toBe('BODY'); + expect(rrdom.body!.parentElement).toBe(rrdom.documentElement); + }); + + it('can get implementation', () => { + expect(rrdom.implementation).toBeDefined(); + expect(rrdom.implementation).toBe(rrdom); + }); + + it('can insert elements', () => { + expect(() => + rrdom.insertBefore(rrdom.createDocumentType('', '', ''), null), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRDoctype on RRDocument allowed."`, ); - expect(rrdom.getElementsByTagName('div')[0].getAttribute('iD')).toEqual( - 'block1', + expect(() => + rrdom.insertBefore(rrdom.createElement('div'), null), + ).toThrowErrorMatchingInlineSnapshot( + `"RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one RRElement on RRDocument allowed."`, ); - expect( - rrdom.getElementsByTagName('p')[0].getAttribute('class'), - ).toBeNull(); + const node = new RRDocument(); + const doctype = rrdom.createDocumentType('', '', ''); + const documentElement = node.createElement('html'); + node.insertBefore(documentElement, null); + node.insertBefore(doctype, documentElement); + expect(node.childNodes.length).toEqual(2); + expect(node.childNodes[0]).toBe(doctype); + expect(node.childNodes[1]).toBe(documentElement); + expect(node.documentElement).toBe(documentElement); }); it('get firstElementChild', () => { @@ -61,31 +111,6 @@ describe('RRDocument for nodejs environment', () => { expect(div2!.firstElementChild!.id).toEqual('block3'); }); - it('get nextElementSibling', () => { - expect(rrdom.documentElement!.firstElementChild).not.toBeNull(); - expect(rrdom.documentElement!.firstElementChild!.tagName).toEqual('HEAD'); - expect( - rrdom.documentElement!.firstElementChild!.nextElementSibling, - ).not.toBeNull(); - expect( - rrdom.documentElement!.firstElementChild!.nextElementSibling!.tagName, - ).toEqual('BODY'); - expect( - rrdom.documentElement!.firstElementChild!.nextElementSibling! - .nextElementSibling, - ).toBeNull(); - - expect(rrdom.getElementsByTagName('h1').length).toEqual(2); - const element1 = rrdom.getElementsByTagName('h1')[0]; - const element2 = rrdom.getElementsByTagName('h1')[1]; - expect(element1.tagName).toEqual('H1'); - expect(element2.tagName).toEqual('H1'); - expect(element1.nextElementSibling).toEqual(element2); - expect(element2.nextElementSibling).not.toBeNull(); - expect(element2.nextElementSibling!.id).toEqual('block1'); - expect(element2.nextElementSibling!.nextElementSibling).toBeNull(); - }); - it('getElementsByTagName', () => { for (let tagname of [ 'HTML', @@ -102,6 +127,8 @@ describe('RRDocument for nodejs environment', () => { 'BUTTON', 'IMG', 'CANVAS', + 'FORM', + 'INPUT', ]) { const expectedResult = document.getElementsByTagName(tagname).length; expect(rrdom.getElementsByTagName(tagname).length).toEqual( @@ -114,6 +141,8 @@ describe('RRDocument for nodejs environment', () => { expect(node.tagName).toEqual(tagname); } } + const node = new RRDocument(); + expect(node.getElementsByTagName('h2').length).toEqual(0); }); it('getElementsByClassName', () => { @@ -136,6 +165,8 @@ describe('RRDocument for nodejs environment', () => { result: document.getElementsByClassName(className).length, }); } + const node = new RRDocument(); + expect(node.getElementsByClassName('block').length).toEqual(0); }); it('getElementById', () => { @@ -145,6 +176,8 @@ describe('RRDocument for nodejs environment', () => { } for (let elementId of ['block', 'blocks', 'blocks1']) expect(rrdom.getElementById(elementId)).toBeNull(); + const node = new RRDocument(); + expect(node.getElementById('id')).toBeNull(); }); it('querySelectorAll querying tag name', () => { @@ -208,6 +241,82 @@ describe('RRDocument for nodejs environment', () => { expect(rrdom.querySelectorAll('.blocks#block1').length).toEqual(1); expect(rrdom.querySelectorAll('.blocks#block3').length).toEqual(0); }); + }); + + describe('RRElement API', () => { + let rrdom: RRDocument; + beforeAll(() => { + // initialize rrdom + document.write(getHtml('main.html')); + rrdom = new RRDocument(); + buildFromDom(document, rrdom); + }); + + it('get attribute name', () => { + expect( + rrdom.getElementsByTagName('DIV')[0].getAttribute('class'), + ).toEqual('blocks blocks1'); + expect( + rrdom.getElementsByTagName('dIv')[0].getAttribute('cLaSs'), + ).toEqual('blocks blocks1'); + expect(rrdom.getElementsByTagName('DIV')[0].getAttribute('id')).toEqual( + 'block1', + ); + expect(rrdom.getElementsByTagName('div')[0].getAttribute('iD')).toEqual( + 'block1', + ); + expect( + rrdom.getElementsByTagName('p')[0].getAttribute('class'), + ).toBeNull(); + }); + + it('can set attribute', () => { + const node = rrdom.createElement('div'); + expect(node.getAttribute('class')).toEqual(null); + node.setAttribute('class', 'className'); + expect(node.getAttribute('cLass')).toEqual('className'); + expect(node.getAttribute('iD')).toEqual(null); + node.setAttribute('iD', 'id'); + expect(node.getAttribute('id')).toEqual('id'); + }); + + it('can remove attribute', () => { + const node = rrdom.createElement('div'); + node.setAttribute('Class', 'className'); + expect(node.getAttribute('class')).toEqual('className'); + node.removeAttribute('clAss'); + expect(node.getAttribute('class')).toEqual(null); + node.removeAttribute('Id'); + expect(node.getAttribute('id')).toEqual(null); + }); + + it('get nextElementSibling', () => { + expect(rrdom.documentElement!.firstElementChild).not.toBeNull(); + expect(rrdom.documentElement!.firstElementChild!.tagName).toEqual('HEAD'); + expect( + rrdom.documentElement!.firstElementChild!.nextElementSibling, + ).not.toBeNull(); + expect( + rrdom.documentElement!.firstElementChild!.nextElementSibling!.tagName, + ).toEqual('BODY'); + expect( + rrdom.documentElement!.firstElementChild!.nextElementSibling! + .nextElementSibling, + ).toBeNull(); + + expect(rrdom.getElementsByTagName('h1').length).toEqual(2); + const element1 = rrdom.getElementsByTagName('h1')[0]; + const element2 = rrdom.getElementsByTagName('h1')[1]; + expect(element1.tagName).toEqual('H1'); + expect(element2.tagName).toEqual('H1'); + expect(element1.nextElementSibling).toEqual(element2); + expect(element2.nextElementSibling).not.toBeNull(); + expect(element2.nextElementSibling!.id).toEqual('block1'); + expect(element2.nextElementSibling!.nextElementSibling).toBeNull(); + + const node = rrdom.createElement('div'); + expect(node.nextElementSibling).toBeNull(); + }); it('can get CSS style declaration', () => { const node = rrdom.createElement('div'); @@ -317,6 +426,55 @@ describe('RRDocument for nodejs environment', () => { expect(node.style.overflow).toEqual(''); }); + it('querySelectorAll', () => { + const element = rrdom.getElementById('block2')!; + expect(element).toBeDefined(); + expect(element.id).toEqual('block2'); + + const result = element.querySelectorAll('div'); + expect(result.length).toBe(1); + expect((result[0]! as RRElement).tagName).toEqual('DIV'); + expect(element.querySelectorAll('.blocks').length).toEqual(0); + + const element2 = rrdom.getElementById('block1')!; + expect(element2).toBeDefined(); + expect(element2.id).toEqual('block1'); + expect(element2.querySelectorAll('div').length).toEqual(2); + expect(element2.querySelectorAll('.blocks').length).toEqual(1); + }); + + it('can attach shadow dom', () => { + const node = rrdom.createElement('div'); + expect(node.shadowRoot).toBeNull(); + node.attachShadow({ mode: 'open' }); + expect(node.shadowRoot).not.toBeNull(); + expect(node.shadowRoot!.RRNodeType).toBe(NodeType.Element); + expect(node.shadowRoot!.tagName).toBe('SHADOWROOT'); + expect(node.parentNode).toBeNull(); + }); + + it('can insert new child before an existing child', () => { + const node = rrdom.createElement('div'); + const child1 = rrdom.createElement('h1'); + const child2 = rrdom.createElement('h2'); + expect(() => + node.insertBefore(node, child1), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."`, + ); + expect(node.insertBefore(child1, null)).toBe(child1); + expect(node.childNodes[0]).toBe(child1); + expect(child1.parentNode).toBe(node); + expect(child1.parentElement).toBe(node); + + expect(node.insertBefore(child2, child1)).toBe(child2); + expect(node.childNodes.length).toBe(2); + expect(node.childNodes[0]).toBe(child2); + expect(node.childNodes[1]).toBe(child1); + expect(child2.parentNode).toBe(node); + expect(child2.parentElement).toBe(node); + }); + it('style element', () => { expect(rrdom.getElementsByTagName('style').length).not.toEqual(0); expect(rrdom.getElementsByTagName('style')[0].tagName).toEqual('STYLE'); @@ -346,6 +504,36 @@ describe('RRDocument for nodejs environment', () => { expect(rules[5]).toBeUndefined(); expect(rules[4].cssText).toEqual(`@import url(main.css);`); }); + + it('can create an RRIframeElement', () => { + const iframe = rrdom.createElement('iframe'); + expect(iframe.tagName).toEqual('IFRAME'); + expect(iframe.width).toEqual(''); + expect(iframe.height).toEqual(''); + expect(iframe.contentDocument).toBeDefined(); + expect(iframe.contentDocument!.childNodes.length).toBe(1); + expect(iframe.contentDocument!.documentElement).toBeDefined(); + expect(iframe.contentDocument!.head).toBeDefined(); + expect(iframe.contentDocument!.body).toBeDefined(); + expect(iframe.contentWindow).toBeDefined(); + expect(iframe.contentWindow!.scrollTop).toEqual(0); + expect(iframe.contentWindow!.scrollLeft).toEqual(0); + expect(iframe.contentWindow!.scrollTo).toBeDefined(); + + // empty parameter and did nothing + iframe.contentWindow!.scrollTo(); + expect(iframe.contentWindow!.scrollTop).toEqual(0); + expect(iframe.contentWindow!.scrollLeft).toEqual(0); + + iframe.contentWindow!.scrollTo({ top: 10, left: 20 }); + expect(iframe.contentWindow!.scrollTop).toEqual(10); + expect(iframe.contentWindow!.scrollLeft).toEqual(20); + }); + + it('should have a RRCanvasElement', () => { + const canvas = rrdom.createElement('canvas'); + expect(canvas.getContext()).toBeNull(); + }); }); }); From 81eac971f423b8798f41b88ca34b240c2cbcb6e9 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Mar 2022 02:33:51 +1100 Subject: [PATCH 38/79] enable to diff scroll positions of an element --- packages/rrdom/src/diff.ts | 3 +++ packages/rrdom/test/diff.test.ts | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index dab9e07b0b..a3a30374b2 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -222,6 +222,9 @@ function diffProps(oldTree: HTMLElement, newTree: IRRElement) { for (const { name } of Array.from(oldAttributes)) if (!(name in newAttributes)) oldTree.removeAttribute(name); + + newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); + newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); } function diffChildren( diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index a0786eefab..9ba4c6d4b9 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -238,7 +238,7 @@ describe('diff algorithm for rrdom', () => { }); describe('diff properties', () => { - it('add new properties', () => { + it('can add new properties', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; node.__sn = Object.assign({}, elementSn, { tagName }); @@ -251,7 +251,7 @@ describe('diff algorithm for rrdom', () => { expect(((node as Node) as HTMLElement).className).toBe('node'); }); - it('update exist properties', () => { + it('can update exist properties', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; node.__sn = Object.assign({}, elementSn, { tagName }); @@ -276,7 +276,7 @@ describe('diff algorithm for rrdom', () => { expect(((node as Node) as HTMLElement).getAttribute('style')).toBe(null); }); - it('delete old properties', () => { + it('can delete old properties', () => { const tagName = 'DIV'; const node = (document.createElement(tagName) as unknown) as INode; node.__sn = Object.assign({}, elementSn, { tagName }); @@ -299,6 +299,22 @@ describe('diff algorithm for rrdom', () => { expect(((node as Node) as HTMLElement).getAttribute('src')).toBe('link'); }); + it('can diff scroll positions', () => { + const tagName = 'DIV'; + const node = (document.createElement(tagName) as unknown) as INode; + node.__sn = Object.assign({}, elementSn, { tagName }); + expect(((node as Node) as HTMLElement).scrollLeft).toEqual(0); + expect(((node as Node) as HTMLElement).scrollTop).toEqual(0); + const rrDocument = new RRDocument(); + const rrNode = rrDocument.createElement(tagName); + rrNode.__sn = Object.assign({}, elementSn, { tagName }); + rrNode.scrollLeft = 100; + rrNode.scrollTop = 200; + diff(node, rrNode, replayer); + expect(((node as Node) as HTMLElement).scrollLeft).toEqual(100); + expect(((node as Node) as HTMLElement).scrollTop).toEqual(200); + }); + it('can diff properties for SVG elements', () => { const element = (document.createElement('svg') as unknown) as INode; const rrDocument = new RRDocument(); From 46a44d29dcf4a505e565a1804583b0b0820c7c1e Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Mar 2022 02:59:12 +1100 Subject: [PATCH 39/79] rename rrdom to virtualDom for more readability and make the tree public --- packages/rrweb/src/replay/index.ts | 107 ++++++++++++++++------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b419a73138..8c76ecd803 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -108,6 +108,9 @@ export class Replayer { public config: playerConfig; + public usingVirtualDom = false; + public virtualDom: RRDocument = new RRDocument(); + private mouse: HTMLDivElement; private mouseTail: HTMLCanvasElement | null = null; private tailPositions: Array<{ x: number; y: number }> = []; @@ -132,8 +135,6 @@ export class Replayer { private mousePos: mouseMovePos | null = null; private touchActive: boolean | null = null; - private usingRRDom = false; - private rrdom: RRDocument = new RRDocument(); constructor( events: Array, @@ -168,30 +169,34 @@ export class Replayer { this.setupDom(); this.emitter.on(ReplayerEvents.Flush, () => { - if (this.usingRRDom) { - diff((this.iframe.contentDocument! as unknown) as INode, this.rrdom, { - mirror: this.mirror, - applyCanvas: ( - canvasEvent: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; + if (this.usingVirtualDom) { + diff( + (this.iframe.contentDocument! as unknown) as INode, + this.virtualDom, + { + mirror: this.mirror, + applyCanvas: ( + canvasEvent: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => { + canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); }, - canvasMutationData: canvasMutationData, - target: HTMLCanvasElement, - ) => { - canvasMutation({ - event: canvasEvent, - mutation: canvasMutationData, - target, - imageMap: this.imageMap, - errorHandler: this.warnCanvasMutationFailed.bind(this), - }); + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), }, - applyInput: this.applyInput.bind(this), - applyScroll: this.applyScroll.bind(this), - }); - this.rrdom.destroyTree(); - this.usingRRDom = false; + ); + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; } if (this.mousePos) { @@ -688,14 +693,14 @@ export class Replayer { 'html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }', ); } - if (this.usingRRDom) { - const styleEl = this.rrdom.createElement('style') as RRStyleElement; + if (this.usingVirtualDom) { + const styleEl = this.virtualDom.createElement('style') as RRStyleElement; styleEl.__sn = { type: NodeType.Element, tagName: 'style', childNodes: [], attributes: {}, - id: this.rrdom.notSerializedId, + id: this.virtualDom.notSerializedId, }; (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { @@ -728,8 +733,8 @@ export class Replayer { const collected: AppendedIframe[] = []; buildNodeWithSN(mutation.node, { doc: (iframeEl.contentDocument! as unknown) as Document, - map: this.usingRRDom - ? ((this.rrdom.mirror.map as unknown) as idNodeMap) + map: this.usingVirtualDom + ? ((this.virtualDom.mirror.map as unknown) as idNodeMap) : this.mirror.map, hackCss: true, skipChild: false, @@ -1037,15 +1042,15 @@ export class Replayer { if (d.id === -1) { break; } - if (this.usingRRDom) { - const target = this.rrdom.mirror.getNode(d.id) as RRElement; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRElement; if (!target) { return this.debugNodeNotFound(d, d.id); } target.scrollData = d; break; } - // Use isSync rather than this.usingRRDom because not every fast-forward process uses virtual dom optimization. + // Use isSync rather than this.usingVirtualDom because not every fast-forward process uses virtual dom optimization. this.applyScroll(d, isSync); break; } @@ -1065,8 +1070,8 @@ export class Replayer { if (d.id === -1) { break; } - if (this.usingRRDom) { - const target = this.rrdom.mirror.getNode(d.id) as RRElement; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRElement; if (!target) { return this.debugNodeNotFound(d, d.id); } @@ -1077,8 +1082,8 @@ export class Replayer { break; } case IncrementalSource.MediaInteraction: { - const target = this.usingRRDom - ? this.rrdom.mirror.getNode(d.id) + const target = this.usingVirtualDom + ? this.virtualDom.mirror.getNode(d.id) : this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); @@ -1114,8 +1119,8 @@ export class Replayer { break; } case IncrementalSource.StyleSheetRule: { - if (this.usingRRDom) { - const target = this.rrdom.mirror.getNode(d.id) as RRStyleElement; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; if (!target) { return this.debugNodeNotFound(d, d.id); } @@ -1186,8 +1191,8 @@ export class Replayer { break; } case IncrementalSource.StyleDeclaration: { - if (this.usingRRDom) { - const target = this.rrdom.mirror.getNode(d.id) as RRStyleElement; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRStyleElement; if (!target) { return this.debugNodeNotFound(d, d.id); } @@ -1234,8 +1239,10 @@ export class Replayer { if (!this.config.UNSAFE_replayCanvas) { return; } - if (this.usingRRDom) { - const target = this.rrdom.mirror.getNode(d.id) as RRCanvasElement; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode( + d.id, + ) as RRCanvasElement; if (!target) { return this.debugNodeNotFound(d, d.id); } @@ -1275,12 +1282,16 @@ export class Replayer { } private applyMutation(d: mutationData, useVirtualParent: boolean) { - // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a rrdom tree and executing the diff algorithm is usually higher than directly applying other kind of events. - if (!this.usingRRDom && useVirtualParent) { - this.usingRRDom = true; - buildFromDom(this.iframe.contentDocument!, this.rrdom, this.rrdom.mirror); + // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events. + if (!this.usingVirtualDom && useVirtualParent) { + this.usingVirtualDom = true; + buildFromDom( + this.iframe.contentDocument!, + this.virtualDom, + this.virtualDom.mirror, + ); } - const mirror = useVirtualParent ? this.rrdom.mirror : this.mirror; + const mirror = useVirtualParent ? this.virtualDom.mirror : this.mirror; d.removes.forEach((mutation) => { let target = mirror.getNode(mutation.id); if (!target) { @@ -1380,7 +1391,7 @@ export class Replayer { const targetDoc = mutation.node.rootId ? mirror.getNode(mutation.node.rootId) : useVirtualParent - ? this.rrdom + ? this.virtualDom : this.iframe.contentDocument; if (isIframeINode(parent) || isRRIFrameElement(parent)) { this.attachDocumentToIframe(mutation, parent); From af762d01bd485c35d9a9bee885f27fc9f1ffd873 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Mar 2022 03:33:40 +1100 Subject: [PATCH 40/79] revert unknown change --- packages/rrweb-snapshot/src/rebuild.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index f0e430df8b..d4ec23f55b 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -228,25 +228,23 @@ function buildNode( } else { // handle internal attributes if (tagName === 'canvas' && name === 'rr_dataURL') { - const ctx = (node as HTMLCanvasElement).getContext('2d'); - // If node is a RRCanvasElement, keep 'rr_dataURL' until the diff stage. - if (!ctx) { - node.setAttribute(name, value); - } else { - const image = document.createElement('img'); - image.src = value; - image.onload = () => + const image = document.createElement('img'); + image.src = value; + image.onload = () => { + const ctx = (node as HTMLCanvasElement).getContext('2d'); + if (ctx) { ctx.drawImage(image, 0, 0, image.width, image.height); - } + } + }; } else if (tagName === 'img' && name === 'rr_dataURL') { const image = node as HTMLImageElement; - if (!image.getAttribute('src')?.startsWith('data:')) { + if (!image.currentSrc.startsWith('data:')) { // Backup original img src. It may not have been set yet. image.setAttribute( 'rrweb-original-src', n.attributes.src as string, ); - image.setAttribute('src', value); + image.src = value; } } From 555c9379d8b1e7511b3b42f8712f1bdc4c66c4de Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 4 Mar 2022 21:24:27 +1100 Subject: [PATCH 41/79] improve the css style parser for comments --- packages/rrdom/src/style.ts | 2 +- packages/rrdom/test/document.test.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/rrdom/src/style.ts b/packages/rrdom/src/style.ts index e4dd6bbf64..ec97685615 100644 --- a/packages/rrdom/src/style.ts +++ b/packages/rrdom/src/style.ts @@ -2,7 +2,7 @@ export function parseCSSText(cssText: string): Record { const res: Record = {}; const listDelimiter = /;(?![^(]*\))/g; const propertyDelimiter = /:(.+)/; - const comment = /\/\*.*\*\//; + const comment = /\/\*.*?\*\//g; cssText .replace(comment, '') .split(listDelimiter) diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 10fefba905..d41e992466 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -628,9 +628,13 @@ describe('Basic RRDocument implementation', () => { expect(style.borderRadius).toEqual('10px 5px'); // comment - node.attributes.style = 'top: 0; /* comment */ bottom: 42rem;'; + node.attributes.style = + 'top: 0; /* comment1 */ bottom: /* comment2 */42rem;'; expect(node.style.top).toEqual('0'); expect(node.style.bottom).toEqual('42rem'); + // empty comment + node.attributes.style = 'top: /**/0;'; + expect(node.style.top).toEqual('0'); // incomplete node.attributes.style = 'overflow:'; From 086bc6f7f5deed81d735342018b87c7c9b9cd583 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 5 Mar 2022 00:49:19 +1100 Subject: [PATCH 42/79] improve code style --- packages/rrdom/src/virtual-dom.ts | 10 +++++----- packages/rrdom/test/document-nodejs.test.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 475443ccfb..53eab1fa0e 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -163,13 +163,13 @@ export type RRComment = typeof RRComment; export const RRCDATASection = BaseRRCDATASectionImpl(RRNode); export type RRCDATASection = typeof RRCDATASection; -type Mirror = { +export type Mirror = { map: { - [key: number]: IRRNode; + [key: number]: RRNode; }; - getId(n: IRRNode): number; - getNode(id: number): IRRNode | null; - removeNodeFromMap(n: IRRNode): void; + getId(n: RRNode): number; + getNode(id: number): RRNode | null; + removeNodeFromMap(n: RRNode): void; has(id: number): boolean; reset(): void; }; diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index 57a83d0e0c..ec7453f6f6 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -252,7 +252,7 @@ describe('RRDocument for nodejs environment', () => { buildFromDom(document, rrdom); }); - it('get attribute name', () => { + it('can get attribute', () => { expect( rrdom.getElementsByTagName('DIV')[0].getAttribute('class'), ).toEqual('blocks blocks1'); @@ -417,9 +417,13 @@ describe('RRDocument for nodejs environment', () => { ); // comment - node.attributes.style = 'top: 0; /* comment */ bottom: 42rem;'; + node.attributes.style = + 'top: 0; /* comment1 */ bottom: /* comment2 */42rem;'; expect(node.style.top).toEqual('0px'); expect(node.style.bottom).toEqual('42rem'); + // empty comment + node.attributes.style = 'top: /**/0;'; + expect(node.style.top).toEqual('0px'); // incomplete node.attributes.style = 'overflow:'; From 37ee65ea71fe4450f348edc682fdb11b419401b1 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 5 Mar 2022 01:01:04 +1100 Subject: [PATCH 43/79] update typings --- packages/rrweb/typings/replay/index.d.ts | 5 +++-- packages/rrweb/typings/types.d.ts | 2 +- packages/rrweb/typings/utils.d.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index c08dc1eff9..7ad37461e9 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -1,3 +1,4 @@ +import { RRDocument } from 'rrdom/es/virtual-dom'; import { Timer } from './timer'; import { createPlayerService, createSpeedService } from './machine'; import { eventWithTime, playerConfig, playerMetaData, Handler, Mirror } from '../types'; @@ -9,6 +10,8 @@ export declare class Replayer { speedService: ReturnType; get timer(): Timer; config: playerConfig; + usingVirtualDom: boolean; + virtualDom: RRDocument; private mouse; private mouseTail; private tailPositions; @@ -22,8 +25,6 @@ export declare class Replayer { private newDocumentQueue; private mousePos; private touchActive; - private usingRRDom; - private rrdom; constructor(events: Array, config?: Partial); on(event: string, handler: Handler): this; off(event: string, handler: Handler): this; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 46dbefd644..6b96f76e38 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -4,7 +4,7 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; -import type { RRNode } from 'rrdom/es/document-browser'; +import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export declare enum EventType { DomContentLoaded = 0, diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index d93a0b992a..4fcef991f1 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,6 +1,6 @@ import type { Mirror, throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow } from './types'; import { INode, serializedNodeWithId } from 'rrweb-snapshot'; -import { RRNode, RRIFrameElement } from 'rrdom/es/document-browser'; +import { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler; export declare function createMirror(): Mirror; export declare let _mirror: Mirror; From 8544074b25732562c13e231701c8976b6bd59fce Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 5 Mar 2022 21:07:54 +1100 Subject: [PATCH 44/79] add handling for the case where legacy_missingNodeRetryMap is not empty --- packages/rrdom/src/diff.ts | 6 +- packages/rrdom/src/virtual-dom.ts | 204 ++++++++++-------- .../__snapshots__/virtual-dom.test.ts.snap | 7 + packages/rrdom/test/diff.test.ts | 11 +- packages/rrdom/test/virtual-dom.test.ts | 160 +++++++++++++- packages/rrweb/src/replay/index.ts | 85 ++++++-- 6 files changed, 356 insertions(+), 117 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index a3a30374b2..e71ceabeec 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -335,6 +335,9 @@ export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { let node = mirror.getNode(rrNode.__sn.id); if (node !== null) return node; switch (rrNode.RRNodeType) { + case NodeType.Document: + node = (new Document() as unknown) as INode; + break; case NodeType.DocumentType: node = (document.implementation.createDocumentType( (rrNode as IRRDocumentType).name, @@ -371,8 +374,7 @@ export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { ) as unknown) as INode; break; default: - // RRDocument of RRIFrameElement won't be created here because it's automatically generated when RRIFrameElement is created. - throw new Error('Unknown rrNode type ' + rrNode.toString()); + throw new Error('Unknown RRNode type ' + rrNode.toString()); } node.__sn = { ...rrNode.__sn }; mirror.map[rrNode.__sn.id] = node; diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 53eab1fa0e..be183ea653 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -186,8 +186,101 @@ type RRElementType< K extends keyof HTMLElementTagNameMap > = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; +const NodeTypeMap: Record = {}; +NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; +NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; +NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; +NodeTypeMap[document.TEXT_NODE] = NodeType.Text; +NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; +NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; + +function getValidTagName(element: HTMLElement): string { + // https://github.com/rrweb-io/rrweb-snapshot/issues/56 + if (element instanceof HTMLFormElement) { + return 'FORM'; + } + return element.tagName.toUpperCase(); +} + +/** + * Build a RRNode from a real Node. + * @param node the real Node + * @param rrdom the RRDocument + * @returns the built RRNode + */ +export function buildFromNode( + node: INode, + rrdom: IRRDocument, + parentRRNode?: IRRNode | null, +): IRRNode | null { + let serializedNodeWithId = node.__sn; + let rrNode: IRRNode; + if (!serializedNodeWithId || serializedNodeWithId.id < 0) { + serializedNodeWithId = { + type: NodeTypeMap[node.nodeType], + textContent: '', + id: rrdom.notSerializedId, + }; + node.__sn = serializedNodeWithId; + } + + switch (node.nodeType) { + case node.DOCUMENT_NODE: + if ( + parentRRNode && + parentRRNode.RRNodeType === NodeType.Element && + (parentRRNode as IRRElement).tagName === 'IFRAME' + ) + rrNode = (parentRRNode as RRIFrameElement).contentDocument; + else rrNode = rrdom; + break; + case node.DOCUMENT_TYPE_NODE: + const documentType = (node as Node) as DocumentType; + rrNode = rrdom.createDocumentType( + documentType.name, + documentType.publicId, + documentType.systemId, + ); + break; + case node.ELEMENT_NODE: + const elementNode = (node as Node) as HTMLElement; + const tagName = getValidTagName(elementNode); + rrNode = rrdom.createElement(tagName); + const rrElement = rrNode as IRRElement; + for (const { name, value } of Array.from(elementNode.attributes)) { + rrElement.attributes[name] = value; + } + elementNode.scrollLeft && (rrElement.scrollLeft = elementNode.scrollLeft); + elementNode.scrollTop && (rrElement.scrollTop = elementNode.scrollTop); + /** + * We don't have to record special values of input elements at the beginning. + * Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed. + */ + break; + case node.TEXT_NODE: + rrNode = rrdom.createTextNode(((node as Node) as Text).textContent || ''); + break; + case node.CDATA_SECTION_NODE: + rrNode = rrdom.createCDATASection(((node as Node) as CDATASection).data); + break; + case node.COMMENT_NODE: + rrNode = rrdom.createComment( + ((node as Node) as Comment).textContent || '', + ); + break; + // if node is a shadow root + case node.DOCUMENT_FRAGMENT_NODE: + rrNode = (parentRRNode as IRRElement).attachShadow({ mode: 'open' }); + break; + default: + return null; + } + rrNode.__sn = serializedNodeWithId; + return rrNode; +} + /** - * Build a rrdom from a real document tree. + * Build a RRDocument from a real document tree. * @param dom the real document tree * @param rrdomToBuild the rrdom object to be constructed * @returns the build rrdom @@ -199,96 +292,20 @@ export function buildFromDom( ) { let rrdom = rrdomToBuild || new RRDocument(); - const NodeTypeMap: Record = {}; - NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; - NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; - NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; - NodeTypeMap[document.TEXT_NODE] = NodeType.Text; - NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; - NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; - - function getValidTagName(element: HTMLElement): string { - // https://github.com/rrweb-io/rrweb-snapshot/issues/56 - if (element instanceof HTMLFormElement) { - return 'FORM'; - } - return element.tagName.toUpperCase(); - } - - const walk = function (node: INode, parentRRNode: IRRNode | null) { - let serializedNodeWithId = node.__sn; - let rrNode: IRRNode; - if (!serializedNodeWithId || serializedNodeWithId.id < 0) { - serializedNodeWithId = { - type: NodeTypeMap[node.nodeType], - textContent: '', - id: rrdom.notSerializedId, - }; - node.__sn = serializedNodeWithId; - } - - switch (node.nodeType) { - case node.DOCUMENT_NODE: - if ( - parentRRNode && - parentRRNode.RRNodeType === NodeType.Element && - (parentRRNode as IRRElement).tagName === 'IFRAME' - ) - rrNode = (parentRRNode as RRIFrameElement).contentDocument; - else rrNode = rrdom; - break; - case node.DOCUMENT_TYPE_NODE: - const documentType = (node as Node) as DocumentType; - rrNode = rrdom.createDocumentType( - documentType.name, - documentType.publicId, - documentType.systemId, - ); - break; - case node.ELEMENT_NODE: - const elementNode = (node as Node) as HTMLElement; - const tagName = getValidTagName(elementNode); - rrNode = rrdom.createElement(tagName); - const rrElement = rrNode as RRElement; - for (const { name, value } of Array.from(elementNode.attributes)) { - rrElement.attributes[name] = value; - } - /** - * We don't have to record special values of input elements at the beginning. - * Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed. - */ - break; - case node.TEXT_NODE: - rrNode = rrdom.createTextNode( - ((node as Node) as Text).textContent || '', - ); - break; - case node.CDATA_SECTION_NODE: - rrNode = rrdom.createCDATASection(''); - break; - case node.COMMENT_NODE: - rrNode = rrdom.createComment( - ((node as Node) as Comment).textContent || '', - ); - break; - // if node is a shadow root - case node.DOCUMENT_FRAGMENT_NODE: - rrNode = (parentRRNode as RRElement).attachShadow({ mode: 'open' }); - break; - default: - return; - } - rrNode.__sn = serializedNodeWithId; - mirror && (mirror.map[serializedNodeWithId.id] = rrNode); + function walk(node: INode, parentRRNode: IRRNode | null) { + const rrNode = buildFromNode(node, rrdom, parentRRNode); + if (rrNode === null) return; + mirror && (mirror.map[rrNode.__sn.id] = rrNode); if ( - parentRRNode?.RRNodeType === NodeType.Element && - (parentRRNode as IRRElement).tagName === 'IFRAME' + // if the parentRRNode isn't a RRIFrameElement + !( + parentRRNode?.RRNodeType === NodeType.Element && + (parentRRNode as IRRElement).tagName === 'IFRAME' + ) && + // if node isn't a shadow root + node.nodeType !== node.DOCUMENT_FRAGMENT_NODE ) { - (parentRRNode as RRIFrameElement).contentDocument = rrNode as RRDocument; - } - // if node isn't a shadow root - else if (node.nodeType !== node.DOCUMENT_FRAGMENT_NODE) { parentRRNode?.appendChild(rrNode); rrNode.parentNode = parentRRNode; rrNode.parentElement = parentRRNode as RRElement; @@ -308,6 +325,7 @@ export function buildFromDom( node.nodeType === node.ELEMENT_NODE || node.nodeType === node.DOCUMENT_FRAGMENT_NODE ) { + // if the node is a shadow dom if ( node.nodeType === Node.ELEMENT_NODE && ((node as Node) as HTMLElement).shadowRoot @@ -320,9 +338,15 @@ export function buildFromDom( walk((node as unknown) as INode, rrNode), ); } - }; + } walk((dom as unknown) as INode, null); return rrdom; } + export { RRNode }; -export { diff, StyleRuleType, VirtualStyleRules } from './diff'; +export { + diff, + createOrGetNode, + StyleRuleType, + VirtualStyleRules, +} from './diff'; diff --git a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap index 71f639094a..c1dc00248b 100644 --- a/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap +++ b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap @@ -89,6 +89,13 @@ exports[`RRDocument for browser environment create a RRDocument from a html docu " `; +exports[`RRDocument for browser environment create a RRDocument from a html document can build from a xml page 1`] = ` +"-1 RRDocument + -2 XML + -3 RRCDATASection data=\\"Some data & then some\\" +" +`; + exports[`RRDocument for browser environment create a RRDocument from a html document can build from an iframe html 1`] = ` "-1 RRDocument -2 RRDocumentType diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 9ba4c6d4b9..ffd72f5d4c 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -958,10 +958,19 @@ describe('diff algorithm for rrdom', () => { it('create a node from RRNode', () => { const rrDocument = new RRDocument(); + rrDocument.__sn = { + type: NodeType.Document, + childNodes: [], + id: 0, + }; + let result = createOrGetNode(rrDocument, mirror); + expect(result).toBeInstanceOf(Document); + expect(result.__sn.id).toBe(0); + const textContent = 'Text Content'; let rrNode: RRNode = rrDocument.createTextNode(textContent); rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; - let result = createOrGetNode(rrNode, mirror); + result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Text); expect(result.__sn.id).toBe(0); expect(((result as Node) as Text).textContent).toBe(textContent); diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 8f64b0dac7..34a03c7df2 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -7,11 +7,13 @@ import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import * as typescript from 'rollup-plugin-typescript2'; -import { NodeType } from 'rrweb-snapshot'; +import { NodeType, INode } from 'rrweb-snapshot'; import { buildFromDom, + buildFromNode, RRCanvasElement, RRDocument, + RRElement, RRNode, } from '../src/virtual-dom'; @@ -38,6 +40,148 @@ function walk(node, blankSpace) { `; describe('RRDocument for browser environment', () => { + describe('create a RRNode from a real Node', () => { + it('can patch serialized ID for an unserialized node', () => { + // build from document + expect(((document as unknown) as INode).__sn).toBeUndefined(); + const rrdom = new RRDocument(); + let rrNode = buildFromNode((document as unknown) as INode, rrdom)!; + expect(((document as unknown) as INode).__sn).toBeDefined(); + expect(((document as unknown) as INode).__sn.id).toEqual(-1); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.Document); + expect(rrNode.__sn.id).toEqual(-1); + expect(rrNode).toBe(rrdom); + + // build from document type + expect(((document.doctype as unknown) as INode).__sn).toBeUndefined(); + rrNode = buildFromNode((document.doctype as unknown) as INode, rrdom)!; + expect(((document.doctype as unknown) as INode).__sn).toBeDefined(); + expect(((document.doctype as unknown) as INode).__sn.id).toEqual(-2); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.DocumentType); + expect(rrNode.__sn.id).toEqual(-2); + + // build from element + expect( + ((document.documentElement as unknown) as INode).__sn, + ).toBeUndefined(); + rrNode = buildFromNode( + (document.documentElement as unknown) as INode, + rrdom, + )!; + expect( + ((document.documentElement as unknown) as INode).__sn, + ).toBeDefined(); + expect(((document.documentElement as unknown) as INode).__sn.id).toEqual( + -3, + ); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.Element); + expect(rrNode.__sn.id).toEqual(-3); + + // build from text + const text = document.createTextNode('text'); + expect(((text as unknown) as INode).__sn).toBeUndefined(); + rrNode = buildFromNode((text as unknown) as INode, rrdom)!; + expect(((text as unknown) as INode).__sn).toBeDefined(); + expect(((text as unknown) as INode).__sn.id).toEqual(-4); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.Text); + expect(rrNode.__sn.id).toEqual(-4); + + // build from comment + const comment = document.createComment('comment'); + expect(((comment as unknown) as INode).__sn).toBeUndefined(); + rrNode = buildFromNode((comment as unknown) as INode, rrdom)!; + expect(((comment as unknown) as INode).__sn).toBeDefined(); + expect(((comment as unknown) as INode).__sn.id).toEqual(-5); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.Comment); + expect(rrNode.__sn.id).toEqual(-5); + + // build from CDATASection + const xmlDoc = new DOMParser().parseFromString( + '', + 'application/xml', + ); + const cdata = 'Some data & then some'; + var cdataSection = xmlDoc.createCDATASection(cdata); + expect(((cdataSection as unknown) as INode).__sn).toBeUndefined(); + rrNode = buildFromNode((cdataSection as unknown) as INode, rrdom)!; + expect(((cdataSection as unknown) as INode).__sn).toBeDefined(); + expect(((cdataSection as unknown) as INode).__sn.id).toEqual(-6); + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.CDATA); + expect(rrNode.textContent).toEqual(cdata); + expect(rrNode.__sn.id).toEqual(-6); + }); + + it('can record scroll position from HTMLElements', () => { + expect(document.body.scrollLeft).toEqual(0); + expect(document.body.scrollTop).toEqual(0); + const rrdom = new RRDocument(); + let rrNode = buildFromNode((document.body as unknown) as INode, rrdom)!; + expect((rrNode as RRElement).scrollLeft).toBeUndefined(); + expect((rrNode as RRElement).scrollTop).toBeUndefined(); + + document.body.scrollLeft = 100; + document.body.scrollTop = 200; + expect(document.body.scrollLeft).toEqual(100); + expect(document.body.scrollTop).toEqual(200); + rrNode = buildFromNode((document.body as unknown) as INode, rrdom)!; + expect((rrNode as RRElement).scrollLeft).toEqual(100); + expect((rrNode as RRElement).scrollTop).toEqual(200); + }); + + it('can build contentDocument from an iframe element', () => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + expect(iframe.contentDocument).not.toBeNull(); + const rrdom = new RRDocument(); + const RRIFrame = rrdom.createElement('iframe'); + const rrNode = buildFromNode( + (iframe.contentDocument as unknown) as INode, + rrdom, + RRIFrame, + )!; + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.type).toEqual(NodeType.Document); + expect(rrNode.__sn.id).toEqual(-1); + expect(((iframe.contentDocument as unknown) as INode).__sn.id).toEqual( + -1, + ); + expect(rrNode).toBe(RRIFrame.contentDocument); + }); + + it('can build from a shadow dom', () => { + const div = document.createElement('div'); + div.attachShadow({ mode: 'open' }); + expect(div.shadowRoot).toBeDefined(); + const rrdom = new RRDocument(); + const parentRRNode = rrdom.createElement('div'); + const rrNode = buildFromNode( + (div.shadowRoot as unknown) as INode, + rrdom, + parentRRNode, + )!; + expect(rrNode).not.toBeNull(); + expect(rrNode.__sn).toBeDefined(); + expect(rrNode.__sn.id).toEqual(-1); + expect(((div.shadowRoot as unknown) as INode).__sn.id).toEqual(-1); + expect(rrNode.RRNodeType).toEqual(NodeType.Element); + expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT'); + expect(rrNode).toBe(parentRRNode.shadowRoot); + }); + }); + describe('create a RRDocument from a html document', () => { let browser: puppeteer.Browser; let code: string; @@ -104,6 +248,20 @@ describe('RRDocument for browser environment', () => { `); expect(result).toMatchSnapshot(); }); + + it('can build from a xml page', async () => { + const result = await page.evaluate(` + var docu = new DOMParser().parseFromString('', 'application/xml'); + var cdata = docu.createCDATASection('Some data & then some'); + docu.getElementsByTagName('xml')[0].appendChild(cdata); + // Displays: data & then some]]> + + const doc = new rrdom.RRDocument(); + rrdom.buildFromDom(docu, doc, doc.mirror); + printRRDom(doc); + `); + expect(result).toMatchSnapshot(); + }); }); describe('RRDocument build for virtual dom', () => { diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 8c76ecd803..3112f7f1b6 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -17,6 +17,8 @@ import { RRCanvasElement, StyleRuleType, VirtualStyleRules, + createOrGetNode, + buildFromNode, buildFromDom, diff, } from 'rrdom/es/virtual-dom'; @@ -170,33 +172,53 @@ export class Replayer { this.emitter.on(ReplayerEvents.Flush, () => { if (this.usingVirtualDom) { + const replayerHandler = { + mirror: this.mirror, + applyCanvas: ( + canvasEvent: incrementalSnapshotEvent & { + timestamp: number; + delay?: number | undefined; + }, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => { + canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + }, + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), + }; diff( (this.iframe.contentDocument! as unknown) as INode, this.virtualDom, - { - mirror: this.mirror, - applyCanvas: ( - canvasEvent: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; - }, - canvasMutationData: canvasMutationData, - target: HTMLCanvasElement, - ) => { - canvasMutation({ - event: canvasEvent, - mutation: canvasMutationData, - target, - imageMap: this.imageMap, - errorHandler: this.warnCanvasMutationFailed.bind(this), - }); - }, - applyInput: this.applyInput.bind(this), - applyScroll: this.applyScroll.bind(this), - }, + replayerHandler, ); this.virtualDom.destroyTree(); this.usingVirtualDom = false; + + // If these legacy missing nodes haven't been resolved, they should be converted to real Nodes. + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const realNode = createOrGetNode( + value.node as RRNode, + this.mirror, + ); + diff(realNode, value.node as RRNode, replayerHandler); + value.node = realNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } } if (this.mousePos) { @@ -1290,6 +1312,23 @@ export class Replayer { this.virtualDom, this.virtualDom.mirror, ); + // If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes. + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const virtualNode = buildFromNode( + value.node as INode, + this.virtualDom, + ); + if (virtualNode) value.node = virtualNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } } const mirror = useVirtualParent ? this.virtualDom.mirror : this.mirror; d.removes.forEach((mutation) => { @@ -1642,7 +1681,7 @@ export class Replayer { delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; if (mutation.previousId || mutation.nextId) { - this.legacy_resolveMissingNode(map, parent, node as Node, mutation); + this.legacy_resolveMissingNode(map, parent, node, mutation); } } if (nextInMap) { @@ -1654,7 +1693,7 @@ export class Replayer { delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; if (mutation.previousId || mutation.nextId) { - this.legacy_resolveMissingNode(map, parent, node as Node, mutation); + this.legacy_resolveMissingNode(map, parent, node, mutation); } } } From f780e58be94b7599cd9339e4eb618542e8941cb0 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 8 Mar 2022 10:39:27 +1100 Subject: [PATCH 45/79] only import types from rrweb into rrdom --- packages/rrdom/src/diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index e71ceabeec..da6f8aec88 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,5 +1,5 @@ import { elementNode, INode, NodeType } from 'rrweb-snapshot'; -import { +import type { canvasMutationData, incrementalSnapshotEvent, inputData, From 1cc430e2cc792370c2fa3014e1572eaad3282730 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 8 Mar 2022 22:15:34 +1100 Subject: [PATCH 46/79] Apply suggestions from code review Co-authored-by: Justin Halsall --- packages/rrdom/src/virtual-dom.ts | 2 +- packages/rrdom/test/document.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index be183ea653..e44a677998 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -141,7 +141,7 @@ export class RRCanvasElement extends RRElement implements IRRElement { /** * This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement. */ - getContext(): CanvasRenderingContext2D | null { + getContext(): RenderingContext | null { return null; } } diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index d41e992466..7e2c0f2d67 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -163,7 +163,7 @@ describe('Basic RRDocument implementation', () => { expect(node.toString()).toEqual('1 RRDocument'); }); - it('can access the a unique notSerializedId every time', () => { + it('can access a unique, decremented notSerializedId every time', () => { const node = new RRDocument(); for (let i = 1; i <= 100; i++) expect(node.notSerializedId).toBe(-i); }); From f2311017e80940722c4471b71f7bd3d8aca85032 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 8 Mar 2022 23:08:48 +1100 Subject: [PATCH 47/79] Apply suggestions from code review --- packages/rrdom/src/diff.ts | 2 +- packages/rrdom/src/virtual-dom.ts | 2 +- packages/rrdom/test/diff.test.ts | 24 ++++++-------------- packages/rrweb/test/replayer.test.ts | 33 ++++++++++++++-------------- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index da6f8aec88..906c736ba2 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -119,7 +119,7 @@ export function diff( oldMediaElement.currentTime = newMediaRRElement.currentTime; break; case 'CANVAS': - (newTree as RRCanvasElement).canvasMutation.forEach( + (newTree as RRCanvasElement).canvasMutations.forEach( (canvasMutation) => replayer.applyCanvas( canvasMutation.event, diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index e44a677998..8670bc4401 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -131,7 +131,7 @@ export class RRElement extends BaseRRElementImpl(RRNode) { export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} export class RRCanvasElement extends RRElement implements IRRElement { - public canvasMutation: { + public canvasMutations: { event: incrementalSnapshotEvent & { timestamp: number; delay?: number | undefined; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index ffd72f5d4c..9fb9c94caf 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -181,7 +181,7 @@ describe('diff algorithm for rrdom', () => { } as canvasMutationData; const MutationNumber = 100; Array.from(Array(MutationNumber)).forEach(() => - rrCanvas.canvasMutation.push({ + rrCanvas.canvasMutations.push({ event: { timestamp: Date.now(), type: EventType.IncrementalSnapshot, @@ -323,22 +323,17 @@ describe('diff algorithm for rrdom', () => { const value = 'http://www.w3.org/2000/svg'; node.attributes.xmlns = value; - const setAttributeNS = jest.fn(); - const originalFun = SVGElement.prototype.setAttributeNS; - HTMLElement.prototype.setAttributeNS = function () { - setAttributeNS(...arguments); - return originalFun.apply(this, arguments); - }; - + jest.spyOn(Element.prototype, 'setAttributeNS'); diff(element, node, replayer); expect(((element as Node) as SVGElement).getAttribute('xmlns')).toBe( value, ); - expect(setAttributeNS).toHaveBeenCalledWith( + expect(SVGElement.prototype.setAttributeNS).toHaveBeenCalledWith( 'http://www.w3.org/2000/xmlns/', 'xmlns', value, ); + jest.restoreAllMocks(); }); it('can diff properties for canvas', async () => { @@ -348,16 +343,11 @@ describe('diff algorithm for rrdom', () => { rrCanvas.__sn = Object.assign({}, elementSn, { tagName: 'canvas' }); rrCanvas.attributes['rr_dataURL'] = 'data:image/png;base64,'; - // Patch the function to detect whether an img element is created. - const originalFn = document.createElement; - const createImageElement = jest.fn(); - document.createElement = function () { - createImageElement(...arguments); - return originalFn.apply(this, arguments); - }; + jest.spyOn(document, 'createElement'); diff(element, rrCanvas, replayer); - expect(createImageElement).toHaveBeenCalledWith('img'); + expect(document.createElement).toHaveBeenCalledWith('img'); + jest.restoreAllMocks(); }); }); diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index f5255bb435..2790f9f865 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -8,6 +8,7 @@ import { launchPuppeteer, sampleEvents as events, sampleStyleSheetRemoveEvents as stylesheetRemoveEvents, + waitForRAF, } from './utils'; import styleSheetRuleEvents from './events/style-sheet-rule-events'; import orderingEvents from './events/ordering'; @@ -248,7 +249,7 @@ describe('replayer', function () { const delay = 50; // restart the replayer await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1050);'); // scroll the "#container" div' at 1000 @@ -260,7 +261,7 @@ describe('replayer', function () { ).toEqual(2500); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1550);'); // scroll the document at 1500 expect( @@ -272,7 +273,7 @@ describe('replayer', function () { ).toEqual(250); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2050);'); // remove the "#container" element at 2000 expect(await contentDocument!.$('#container')).toBeNull(); @@ -303,10 +304,9 @@ describe('replayer', function () { ), ).toEqual('valueB'); // the default value - const delay = 50; // restart the replayer await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1550);'); // the value get changed to 'valueA' at 1500 @@ -318,7 +318,7 @@ describe('replayer', function () { ).toEqual('valueA'); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2050);'); // the value get changed to 'valueC' at 2000 expect( @@ -329,7 +329,7 @@ describe('replayer', function () { ).toEqual('valueC'); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2550);'); // add a new input element at 2500 expect( @@ -340,7 +340,7 @@ describe('replayer', function () { ).toEqual(''); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(3050);'); // set the value 'test input' for the input element at 3000 expect( @@ -351,13 +351,13 @@ describe('replayer', function () { ).toEqual('test input'); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(3550);'); // remove the select element at 3500 expect(await contentDocument!.$('select')).toBeNull(); await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(4050);'); // remove the input element at 4000 expect(await contentDocument!.$('input')).toBeNull(); @@ -374,10 +374,9 @@ describe('replayer', function () { const contentDocument = await iframe!.contentFrame()!; expect(await contentDocument!.$('iframe')).toBeNull(); - const delay = 50; // restart the replayer await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(550);'); // add 'iframe one' at 500 expect(await contentDocument!.$('iframe')).not.toBeNull(); let iframeOneDocument = await (await contentDocument!.$( @@ -396,7 +395,7 @@ describe('replayer', function () { // add 'iframe two' and 'iframe three' at 1000 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1050);'); // check the inserted style of iframe 'one' again iframeOneDocument = await (await contentDocument!.$( @@ -423,7 +422,7 @@ describe('replayer', function () { // add 'iframe four' at 1500 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1550);'); iframeTwoDocument = await ( await contentDocument!.$$('iframe') @@ -438,7 +437,7 @@ describe('replayer', function () { // add 'iframe five' at 2000 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2050);'); iframeTwoDocument = await ( await contentDocument!.$$('iframe') @@ -463,7 +462,7 @@ describe('replayer', function () { // remove the html element of 'iframe four' at 2500 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2550);'); iframeTwoDocument = await ( await contentDocument!.$$('iframe') @@ -506,7 +505,7 @@ describe('replayer', function () { // add shadow dom 'two' at 1000 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(50); + await waitForRAF(page); await page.evaluate('replayer.pause(1050);'); expect( await contentDocument!.evaluate( From e745322c9d488f893e7b3a6eaf85ae0a4fda57d9 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 8 Mar 2022 23:20:03 +1100 Subject: [PATCH 48/79] fix building error in rrweb --- packages/rrweb/src/replay/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 3112f7f1b6..b50c618ec2 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1268,7 +1268,7 @@ export class Replayer { if (!target) { return this.debugNodeNotFound(d, d.id); } - target.canvasMutation.push({ event: e, mutation: d }); + target.canvasMutations.push({ event: e, mutation: d }); } else { const target = this.mirror.getNode(d.id); if (!target) { From 3e12f66293b9f193c40f15beb3c194daa2cf1fb4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 9 Mar 2022 01:08:15 +1100 Subject: [PATCH 49/79] add a method setDefaultSN to set a default value for a RRNode's __sn --- packages/rrdom/src/diff.ts | 2 - packages/rrdom/src/document.ts | 58 ++++++++++++++ packages/rrdom/src/virtual-dom.ts | 24 ++---- packages/rrdom/test/diff.test.ts | 28 ++----- packages/rrdom/test/document.test.ts | 102 ++++++++++++++---------- packages/rrdom/test/virtual-dom.test.ts | 22 +---- packages/rrweb/src/replay/index.ts | 8 +- 7 files changed, 134 insertions(+), 110 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 906c736ba2..61cf56b2d1 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -373,8 +373,6 @@ export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { (rrNode as IRRCDATASection).data, ) as unknown) as INode; break; - default: - throw new Error('Unknown RRNode type ' + rrNode.toString()); } node.__sn = { ...rrNode.__sn }; mirror.map[rrNode.__sn.id] = node; diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 641331a447..e4c6f2b351 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -27,6 +27,12 @@ export interface IRRNode { removeChild(node: IRRNode): IRRNode; + /** + * Set a default value for RRNode's __sn property. + * @param id the serialized id to assign + */ + setDefaultSN(id: number): void; + toString(nodeName?: string): string; } export interface IRRDocument extends IRRNode { @@ -167,6 +173,58 @@ export class BaseRRNode implements IRRNode { ); } + public setDefaultSN(id: number) { + switch (this.RRNodeType) { + case NodeType.Document: + this.__sn = { + id, + type: this.RRNodeType, + childNodes: [], + }; + break; + case NodeType.DocumentType: + const doctype = (this as unknown) as IRRDocumentType; + this.__sn = { + id, + type: this.RRNodeType, + name: doctype.name, + publicId: doctype.publicId, + systemId: doctype.systemId, + }; + break; + case NodeType.Element: + this.__sn = { + id, + type: this.RRNodeType, + tagName: ((this as unknown) as IRRElement).tagName.toLowerCase(), // In rrweb data, all tagNames are lowercase. + attributes: {}, + childNodes: [], + }; + break; + case NodeType.Text: + this.__sn = { + id, + type: this.RRNodeType, + textContent: ((this as unknown) as IRRText).textContent || '', + }; + break; + case NodeType.Comment: + this.__sn = { + id, + type: this.RRNodeType, + textContent: ((this as unknown) as IRRComment).textContent || '', + }; + break; + case NodeType.CDATA: + this.__sn = { + id, + type: this.RRNodeType, + textContent: '', + }; + break; + } + } + public toString(nodeName?: string) { return `${this.__sn?.id || ''} ${nodeName}`; } diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 8670bc4401..9bb5e57367 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -186,14 +186,6 @@ type RRElementType< K extends keyof HTMLElementTagNameMap > = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement; -const NodeTypeMap: Record = {}; -NodeTypeMap[document.DOCUMENT_NODE] = NodeType.Document; -NodeTypeMap[document.DOCUMENT_TYPE_NODE] = NodeType.DocumentType; -NodeTypeMap[document.ELEMENT_NODE] = NodeType.Element; -NodeTypeMap[document.TEXT_NODE] = NodeType.Text; -NodeTypeMap[document.CDATA_SECTION_NODE] = NodeType.CDATA; -NodeTypeMap[document.COMMENT_NODE] = NodeType.Comment; - function getValidTagName(element: HTMLElement): string { // https://github.com/rrweb-io/rrweb-snapshot/issues/56 if (element instanceof HTMLFormElement) { @@ -213,16 +205,7 @@ export function buildFromNode( rrdom: IRRDocument, parentRRNode?: IRRNode | null, ): IRRNode | null { - let serializedNodeWithId = node.__sn; let rrNode: IRRNode; - if (!serializedNodeWithId || serializedNodeWithId.id < 0) { - serializedNodeWithId = { - type: NodeTypeMap[node.nodeType], - textContent: '', - id: rrdom.notSerializedId, - }; - node.__sn = serializedNodeWithId; - } switch (node.nodeType) { case node.DOCUMENT_NODE: @@ -275,7 +258,12 @@ export function buildFromNode( default: return null; } - rrNode.__sn = serializedNodeWithId; + + if (!node.__sn || node.__sn.id < 0) { + rrNode.setDefaultSN(rrdom.notSerializedId); + node.__sn = rrNode.__sn; + } else rrNode.__sn = node.__sn; + return rrNode; } diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 9fb9c94caf..069e7d490f 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -915,11 +915,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement('iframe'); - rrNode.contentDocument.__sn = { - type: NodeType.Document, - childNodes: [], - id: 1, - }; + rrNode.contentDocument.setDefaultSN(1); const childElement = rrNode.contentDocument.createElement('div'); childElement.__sn = Object.assign({}, elementSn, { tagName: 'div', @@ -948,32 +944,28 @@ describe('diff algorithm for rrdom', () => { it('create a node from RRNode', () => { const rrDocument = new RRDocument(); - rrDocument.__sn = { - type: NodeType.Document, - childNodes: [], - id: 0, - }; + rrDocument.setDefaultSN(0); let result = createOrGetNode(rrDocument, mirror); expect(result).toBeInstanceOf(Document); expect(result.__sn.id).toBe(0); const textContent = 'Text Content'; let rrNode: RRNode = rrDocument.createTextNode(textContent); - rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; + rrNode.setDefaultSN(0); result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Text); expect(result.__sn.id).toBe(0); expect(((result as Node) as Text).textContent).toBe(textContent); rrNode = rrDocument.createComment(textContent); - rrNode.__sn = { id: 0, type: NodeType.Comment, textContent }; + rrNode.setDefaultSN(0); result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Comment); expect(result.__sn.id).toBe(0); expect(((result as Node) as Comment).textContent).toBe(textContent); rrNode = rrDocument.createCDATASection(''); - rrNode.__sn = { id: 0, type: NodeType.CDATA, textContent: '' }; + rrNode.setDefaultSN(0); expect(() => createOrGetNode(rrNode, mirror), ).toThrowErrorMatchingInlineSnapshot( @@ -985,13 +977,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; let rrNode: RRNode = rrDocument.createDocumentType('html', publicId, ''); - rrNode.__sn = { - id: 0, - type: NodeType.DocumentType, - name: 'html', - publicId, - systemId: '', - }; + rrNode.setDefaultSN(0); let result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(DocumentType); expect(result.__sn.id).toBe(0); @@ -1015,7 +1001,7 @@ describe('diff algorithm for rrdom', () => { // Add the text node to the mirror to make it look like already existing. mirror.map[0] = (text as unknown) as INode; const rrNode: RRNode = rrDocument.createTextNode(textContent); - rrNode.__sn = { id: 0, type: NodeType.Text, textContent }; + rrNode.setDefaultSN(0); let result = createOrGetNode(rrNode, mirror); expect(result).toBeInstanceOf(Text); diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 7e2c0f2d67..1962bb46da 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -1,7 +1,15 @@ /** * @jest-environment jsdom */ -import { NodeType } from 'rrweb-snapshot'; +import { + cdataNode, + commentNode, + documentNode, + documentTypeNode, + elementNode, + NodeType, + textNode, +} from 'rrweb-snapshot'; import { BaseRRDocumentImpl, BaseRRDocumentTypeImpl, @@ -23,7 +31,7 @@ describe('Basic RRDocument implementation', () => { const node = new RRNode(); node.__sn = { type: NodeType.Element, - tagName: 'DIV', + tagName: 'div', attributes: {}, childNodes: [], id: 1, @@ -44,6 +52,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.toString('RRNode')).toEqual('1 RRNode'); }); @@ -115,6 +124,13 @@ describe('Basic RRDocument implementation', () => { `"RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method."`, ); }); + + it("can set a default value for RRNode's __sn property", () => { + const node = new RRNode(); + node.setDefaultSN(0); + // Because RRNode's type hasn't been specified. + expect(node.__sn).toBeUndefined(); + }); }); describe('Basic RRDocument implementation', () => { @@ -122,11 +138,10 @@ describe('Basic RRDocument implementation', () => { const node = new RRDocument(); expect(node.toString()).toEqual(' RRDocument'); - node.__sn = { - type: NodeType.Document, - id: 1, - childNodes: [], - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.Document); + expect((node.__sn as documentNode).childNodes).toBeInstanceOf(Array); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); @@ -144,6 +159,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.notSerializedId).toBe(-1); expect(node.documentElement).toBeNull(); expect(node.body).toBeNull(); @@ -359,13 +375,13 @@ describe('Basic RRDocument implementation', () => { publicId = 'publicId', systemId = 'systemId'; const node = new RRDocumentType(name, publicId, systemId); - node.__sn = { - type: NodeType.DocumentType, - name, - publicId, - systemId, - id: 1, - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.DocumentType); + expect((node.__sn as documentTypeNode).name).toEqual(name); + expect((node.__sn as documentTypeNode).publicId).toEqual(publicId); + expect((node.__sn as documentTypeNode).systemId).toEqual(systemId); + expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -382,6 +398,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.name).toBe(name); expect(node.publicId).toBe(publicId); expect(node.systemId).toBe(systemId); @@ -394,13 +411,13 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = document.createElement('div'); - node.__sn = { - type: NodeType.Element, - tagName: 'div', - attributes: {}, - childNodes: [], - id: 1, - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.Element); + expect((node.__sn as elementNode).tagName).toEqual('div'); + expect((node.__sn as elementNode).attributes).toBeDefined(); + expect((node.__sn as elementNode).childNodes).toBeInstanceOf(Array); + node.scrollLeft = 100; node.scrollTop = 200; node.attributes.id = 'id'; @@ -421,6 +438,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.tagName).toEqual('DIV'); expect(node.attributes).toEqual({ id: 'id', class: 'className' }); expect(node.shadowRoot).toBeNull(); @@ -762,11 +780,11 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createTextNode('text'); - node.__sn = { - type: NodeType.Text, - textContent: 'text', - id: 1, - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.Text); + expect((node.__sn as textNode).textContent).toEqual('text'); + expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -783,6 +801,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.toString()).toEqual('1 RRText text="text"'); }); @@ -799,11 +818,11 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createComment('comment'); - node.__sn = { - type: NodeType.Comment, - textContent: 'comment', - id: 1, - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.Comment); + expect((node.__sn as commentNode).textContent).toEqual('comment'); + expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -820,6 +839,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.toString()).toEqual('1 RRComment text="comment"'); }); @@ -836,11 +856,11 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createCDATASection('data'); - node.__sn = { - type: NodeType.CDATA, - textContent: '', - id: 1, - }; + node.setDefaultSN(1); + expect(node.__sn).toBeDefined(); + expect(node.__sn.type).toEqual(NodeType.CDATA); + expect((node.__sn as cdataNode).textContent).toEqual(''); + expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -857,6 +877,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.toString()).toEqual('1 RRCDATASection data="data"'); }); @@ -871,13 +892,7 @@ describe('Basic RRDocument implementation', () => { describe('Basic RRMediaElement implementation', () => { it('should have basic properties', () => { const node = new RRMediaElement('video'); - node.__sn = { - type: NodeType.Element, - tagName: 'video', - attributes: {}, - childNodes: [], - id: 1, - }; + node.setDefaultSN(1); node.scrollLeft = 100; node.scrollTop = 200; expect(node.parentNode).toEqual(null); @@ -896,6 +911,7 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); + expect(node.setDefaultSN).toBeDefined(); expect(node.tagName).toEqual('VIDEO'); expect(node.attributes).toEqual({}); expect(node.shadowRoot).toBeNull(); diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 34a03c7df2..703d2eb729 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -324,13 +324,7 @@ describe('RRDocument for browser environment', () => { it('can get node id', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); - node1.__sn = { - type: NodeType.Element, - tagName: 'div', - childNodes: [], - attributes: {}, - id: 0, - }; + node1.setDefaultSN(0); dom.mirror.map[0] = node1; expect(dom.mirror.getId(node1)).toEqual(0); const node2 = dom.createTextNode('text'); @@ -354,19 +348,9 @@ describe('RRDocument for browser environment', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); dom.mirror.map[0] = node1; - node1.__sn = { - type: NodeType.Element, - tagName: 'div', - childNodes: [], - attributes: {}, - id: 0, - }; + node1.setDefaultSN(0); const node2 = dom.createTextNode('text'); - node2.__sn = { - type: NodeType.Text, - textContent: 'text', - id: 1, - }; + node2.setDefaultSN(1); node1.appendChild(node2); dom.mirror.map[1] = node2; expect(dom.mirror.has(0)).toBeTruthy(); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b50c618ec2..8ab8f22fbe 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -717,13 +717,7 @@ export class Replayer { } if (this.usingVirtualDom) { const styleEl = this.virtualDom.createElement('style') as RRStyleElement; - styleEl.__sn = { - type: NodeType.Element, - tagName: 'style', - childNodes: [], - attributes: {}, - id: this.virtualDom.notSerializedId, - }; + styleEl.setDefaultSN(this.virtualDom.notSerializedId); (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { // push virtual styles From 3f9e638836ac6e07c77e3219bb25af8b04ad2924 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 9 Mar 2022 17:53:56 +1100 Subject: [PATCH 50/79] fix rrweb test error and bump up other packages --- packages/rrdom/package.json | 10 +- packages/rrweb/jest.config.js | 11 +- packages/rrweb/typings/types.d.ts | 1 + yarn.lock | 773 +++++------------------------- 4 files changed, 119 insertions(+), 676 deletions(-) diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 645f6ed77e..24c2e52276 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -28,18 +28,18 @@ "@rollup/plugin-node-resolve": "^13.0.4", "@types/cssom": "^0.4.1", "@types/cssstyle": "^2.2.1", - "@types/jest": "^27.0.1", + "@types/jest": "^27.4.1", "@types/nwsapi": "^2.2.2", "@types/puppeteer": "^5.4.4", "compare-versions": "^4.1.3", - "jest": "^27.1.1", + "jest": "^27.5.1", "puppeteer": "^9.1.1", "rollup": "^2.56.3", "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.30.0", + "rollup-plugin-typescript2": "^0.31.2", "rrweb-snapshot": "^1.1.13", - "ts-jest": "^27.0.5", - "typescript": "^3.9.5" + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" }, "dependencies": { "cssom": "^0.5.0", diff --git a/packages/rrweb/jest.config.js b/packages/rrweb/jest.config.js index 7403e73daa..c1f271c9b7 100644 --- a/packages/rrweb/jest.config.js +++ b/packages/rrweb/jest.config.js @@ -5,15 +5,6 @@ module.exports = { testMatch: ['**/**.test.ts'], moduleNameMapper: { '\\.css$': 'identity-obj-proxy', - }, - transform: { - '^.+\\.[tj]s$': 'ts-jest', - }, - globals: { - 'ts-jest': { - tsconfig: { - allowJs: true, - }, - }, + 'rrdom/es/(.*)': 'rrdom/lib/$1', }, }; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 4134bdaf15..a5484efa48 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -492,4 +492,5 @@ declare global { } } export declare type IWindow = Window & typeof globalThis; +export declare type Optional = Pick, K> & Omit; export {}; diff --git a/yarn.lock b/yarn.lock index e455e8128b..bb3b3f494d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -571,18 +571,6 @@ jest-util "^27.2.4" slash "^3.0.0" -"@jest/console@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107" - integrity sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA== - dependencies: - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^27.4.6" - jest-util "^27.4.2" - slash "^3.0.0" - "@jest/console@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" @@ -629,40 +617,6 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/core@^27.4.7": - version "27.4.7" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.4.7.tgz#84eabdf42a25f1fa138272ed229bcf0a1b5e6913" - integrity sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg== - dependencies: - "@jest/console" "^27.4.6" - "@jest/reporters" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^27.4.2" - jest-config "^27.4.7" - jest-haste-map "^27.4.6" - jest-message-util "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-resolve-dependencies "^27.4.6" - jest-runner "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - jest-validate "^27.4.6" - jest-watcher "^27.4.6" - micromatch "^4.0.4" - rimraf "^3.0.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - "@jest/core@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" @@ -707,16 +661,6 @@ "@types/node" "*" jest-mock "^27.2.4" -"@jest/environment@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.4.6.tgz#1e92885d64f48c8454df35ed9779fbcf31c56d8b" - integrity sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg== - dependencies: - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - jest-mock "^27.4.6" - "@jest/environment@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" @@ -739,18 +683,6 @@ jest-mock "^27.2.4" jest-util "^27.2.4" -"@jest/fake-timers@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.4.6.tgz#e026ae1671316dbd04a56945be2fa251204324e8" - integrity sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A== - dependencies: - "@jest/types" "^27.4.2" - "@sinonjs/fake-timers" "^8.0.1" - "@types/node" "*" - jest-message-util "^27.4.6" - jest-mock "^27.4.6" - jest-util "^27.4.2" - "@jest/fake-timers@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" @@ -772,15 +704,6 @@ "@jest/types" "^27.2.4" expect "^27.2.4" -"@jest/globals@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.4.6.tgz#3f09bed64b0fd7f5f996920258bd4be8f52f060a" - integrity sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/types" "^27.4.2" - expect "^27.4.6" - "@jest/globals@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" @@ -820,37 +743,6 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/reporters@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.4.6.tgz#b53dec3a93baf9b00826abf95b932de919d6d8dd" - integrity sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^5.1.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-haste-map "^27.4.6" - jest-resolve "^27.4.6" - jest-util "^27.4.2" - jest-worker "^27.4.6" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^8.1.0" - "@jest/reporters@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" @@ -891,15 +783,6 @@ graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/source-map@^27.4.0": - version "27.4.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.4.0.tgz#2f0385d0d884fb3e2554e8f71f8fa957af9a74b6" - integrity sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ== - dependencies: - callsites "^3.0.0" - graceful-fs "^4.2.4" - source-map "^0.6.0" - "@jest/source-map@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" @@ -919,16 +802,6 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-result@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.4.6.tgz#b3df94c3d899c040f602cea296979844f61bdf69" - integrity sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ== - dependencies: - "@jest/console" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - "@jest/test-result@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" @@ -949,16 +822,6 @@ jest-haste-map "^27.2.4" jest-runtime "^27.2.4" -"@jest/test-sequencer@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz#447339b8a3d7b5436f50934df30854e442a9d904" - integrity sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw== - dependencies: - "@jest/test-result" "^27.4.6" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-runtime "^27.4.6" - "@jest/test-sequencer@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" @@ -990,27 +853,6 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/transform@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.4.6.tgz#153621940b1ed500305eacdb31105d415dc30231" - integrity sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw== - dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^27.4.2" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-regex-util "^27.4.0" - jest-util "^27.4.2" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" - "@jest/transform@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" @@ -1043,17 +885,6 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jest/types@^27.4.2": - version "27.4.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" - integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^16.0.0" - chalk "^4.0.0" - "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" @@ -1999,7 +1830,7 @@ "@rollup/pluginutils" "^3.1.0" resolve "^1.17.0" -"@rollup/pluginutils@4", "@rollup/pluginutils@^4.1.0": +"@rollup/pluginutils@4": version "4.1.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ== @@ -2016,6 +1847,14 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^4.1.2": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.0.tgz#a14bbd058fdbba0a5647143b16ed0d86fb60bd08" + integrity sha512-2WUyJNRkyH5p487pGnn4tWAsxhEFKN/pT8CMgHshd5H+IXkOnKvKZwsz5ZWz+YCXkleZRAU5kwbfgF8CPfDRqA== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -2179,12 +2018,12 @@ jest-diff "^27.0.0" pretty-format "^27.0.0" -"@types/jest@^27.0.1": - version "27.4.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed" - integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== +"@types/jest@^27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== dependencies: - jest-diff "^27.0.0" + jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" "@types/jsdom@^16.2.4": @@ -2510,6 +2349,15 @@ resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== +"@yarn-tool/resolve-package@^1.0.40": + version "1.0.45" + resolved "https://registry.yarnpkg.com/@yarn-tool/resolve-package/-/resolve-package-1.0.45.tgz#4d9716a67903f46a76c8691eff546dafe55bf66f" + integrity sha512-xnfY8JceApkSTliZtr7X6yl1wZYhGbRp0beBMi1OtmvTVTm/ZSt3881Fw1M3ZwhHqr7OEfl8828LJK2q62BvoQ== + dependencies: + pkg-dir "< 6 >= 5" + tslib "^2.3.1" + upath2 "^3.1.12" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2569,6 +2417,11 @@ acorn@^8.2.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -2870,20 +2723,6 @@ babel-jest@^27.2.4: graceful-fs "^4.2.4" slash "^3.0.0" -babel-jest@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.4.6.tgz#4d024e69e241cdf4f396e453a07100f44f7ce314" - integrity sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg== - dependencies: - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^27.4.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - slash "^3.0.0" - babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -2930,16 +2769,6 @@ babel-plugin-jest-hoist@^27.2.0: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-jest-hoist@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz#d7831fc0f93573788d80dee7e682482da4c730d6" - integrity sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" @@ -2976,14 +2805,6 @@ babel-preset-jest@^27.2.0: babel-plugin-jest-hoist "^27.2.0" babel-preset-current-node-syntax "^1.0.0" -babel-preset-jest@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz#70d0e676a282ccb200fbabd7f415db5fdf393bca" - integrity sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg== - dependencies: - babel-plugin-jest-hoist "^27.4.0" - babel-preset-current-node-syntax "^1.0.0" - babel-preset-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" @@ -4243,11 +4064,6 @@ diff-sequences@^27.0.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== -diff-sequences@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" - integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== - diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -4776,16 +4592,6 @@ expect@^27.2.4: jest-message-util "^27.2.4" jest-regex-util "^27.0.6" -expect@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.4.6.tgz#f335e128b0335b6ceb4fcab67ece7cbd14c942e6" - integrity sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag== - dependencies: - "@jest/types" "^27.4.2" - jest-get-type "^27.4.0" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - expect@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" @@ -5028,7 +4834,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^3.3.1: +find-cache-dir@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== @@ -5052,6 +4858,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -5129,14 +4943,14 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +fs-extra@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== dependencies: graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" + jsonfile "^6.0.1" + universalify "^2.0.0" fs-extra@^9.1.0: version "9.1.0" @@ -6361,15 +6175,6 @@ jest-changed-files@^27.2.4: execa "^5.0.0" throat "^6.0.1" -jest-changed-files@^27.4.2: - version "27.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.4.2.tgz#da2547ea47c6e6a5f6ed336151bd2075736eb4a5" - integrity sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A== - dependencies: - "@jest/types" "^27.4.2" - execa "^5.0.0" - throat "^6.0.1" - jest-changed-files@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" @@ -6404,31 +6209,6 @@ jest-circus@^27.2.4: stack-utils "^2.0.3" throat "^6.0.1" -jest-circus@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.4.6.tgz#d3af34c0eb742a967b1919fbb351430727bcea6c" - integrity sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - expect "^27.4.6" - is-generator-fn "^2.0.0" - jest-each "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - pretty-format "^27.4.6" - slash "^3.0.0" - stack-utils "^2.0.3" - throat "^6.0.1" - jest-circus@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" @@ -6472,24 +6252,6 @@ jest-cli@^27.2.4: prompts "^2.0.1" yargs "^16.2.0" -jest-cli@^27.4.7: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.4.7.tgz#d00e759e55d77b3bcfea0715f527c394ca314e5a" - integrity sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw== - dependencies: - "@jest/core" "^27.4.7" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - import-local "^3.0.2" - jest-config "^27.4.7" - jest-util "^27.4.2" - jest-validate "^27.4.6" - prompts "^2.0.1" - yargs "^16.2.0" - jest-cli@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" @@ -6535,34 +6297,6 @@ jest-config@^27.2.4: micromatch "^4.0.4" pretty-format "^27.2.4" -jest-config@^27.4.7: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.4.7.tgz#4f084b2acbd172c8b43aa4cdffe75d89378d3972" - integrity sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw== - dependencies: - "@babel/core" "^7.8.0" - "@jest/test-sequencer" "^27.4.6" - "@jest/types" "^27.4.2" - babel-jest "^27.4.6" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - jest-circus "^27.4.6" - jest-environment-jsdom "^27.4.6" - jest-environment-node "^27.4.6" - jest-get-type "^27.4.0" - jest-jasmine2 "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-runner "^27.4.6" - jest-util "^27.4.2" - jest-validate "^27.4.6" - micromatch "^4.0.4" - pretty-format "^27.4.6" - slash "^3.0.0" - jest-config@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" @@ -6613,16 +6347,6 @@ jest-diff@^27.0.0, jest-diff@^27.2.4: jest-get-type "^27.0.6" pretty-format "^27.2.4" -jest-diff@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d" - integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w== - dependencies: - chalk "^4.0.0" - diff-sequences "^27.4.0" - jest-get-type "^27.4.0" - pretty-format "^27.4.6" - jest-diff@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" @@ -6640,13 +6364,6 @@ jest-docblock@^27.0.6: dependencies: detect-newline "^3.0.0" -jest-docblock@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.4.0.tgz#06c78035ca93cbbb84faf8fce64deae79a59f69f" - integrity sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg== - dependencies: - detect-newline "^3.0.0" - jest-docblock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" @@ -6665,17 +6382,6 @@ jest-each@^27.2.4: jest-util "^27.2.4" pretty-format "^27.2.4" -jest-each@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.4.6.tgz#e7e8561be61d8cc6dbf04296688747ab186c40ff" - integrity sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA== - dependencies: - "@jest/types" "^27.4.2" - chalk "^4.0.0" - jest-get-type "^27.4.0" - jest-util "^27.4.2" - pretty-format "^27.4.6" - jest-each@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" @@ -6700,19 +6406,6 @@ jest-environment-jsdom@^27.2.4: jest-util "^27.2.4" jsdom "^16.6.0" -jest-environment-jsdom@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz#c23a394eb445b33621dfae9c09e4c8021dea7b36" - integrity sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - jest-mock "^27.4.6" - jest-util "^27.4.2" - jsdom "^16.6.0" - jest-environment-jsdom@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" @@ -6738,18 +6431,6 @@ jest-environment-node@^27.2.4: jest-mock "^27.2.4" jest-util "^27.2.4" -jest-environment-node@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.4.6.tgz#ee8cd4ef458a0ef09d087c8cd52ca5856df90242" - integrity sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - jest-mock "^27.4.6" - jest-util "^27.4.2" - jest-environment-node@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" @@ -6772,11 +6453,6 @@ jest-get-type@^27.0.6: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== -jest-get-type@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" - integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== - jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" @@ -6802,26 +6478,6 @@ jest-haste-map@^27.2.4: optionalDependencies: fsevents "^2.3.2" -jest-haste-map@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.4.6.tgz#c60b5233a34ca0520f325b7e2cc0a0140ad0862a" - integrity sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ== - dependencies: - "@jest/types" "^27.4.2" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.4.0" - jest-serializer "^27.4.0" - jest-util "^27.4.2" - jest-worker "^27.4.6" - micromatch "^4.0.4" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.3.2" - jest-haste-map@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" @@ -6881,29 +6537,6 @@ jest-jasmine2@^27.2.4: pretty-format "^27.2.4" throat "^6.0.1" -jest-jasmine2@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz#109e8bc036cb455950ae28a018f983f2abe50127" - integrity sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/source-map" "^27.4.0" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^27.4.6" - is-generator-fn "^2.0.0" - jest-each "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - pretty-format "^27.4.6" - throat "^6.0.1" - jest-jasmine2@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" @@ -6935,14 +6568,6 @@ jest-leak-detector@^27.2.4: jest-get-type "^27.0.6" pretty-format "^27.2.4" -jest-leak-detector@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz#ed9bc3ce514b4c582637088d9faf58a33bd59bf4" - integrity sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA== - dependencies: - jest-get-type "^27.4.0" - pretty-format "^27.4.6" - jest-leak-detector@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" @@ -6980,16 +6605,6 @@ jest-matcher-utils@^27.2.4: jest-get-type "^27.0.6" pretty-format "^27.2.4" -jest-matcher-utils@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz#53ca7f7b58170638590e946f5363b988775509b8" - integrity sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA== - dependencies: - chalk "^4.0.0" - jest-diff "^27.4.6" - jest-get-type "^27.4.0" - pretty-format "^27.4.6" - jest-message-util@^23.4.0: version "23.4.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f" @@ -7016,21 +6631,6 @@ jest-message-util@^27.2.4: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.4.6.tgz#9fdde41a33820ded3127465e1a5896061524da31" - integrity sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.4.2" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.4" - pretty-format "^27.4.6" - slash "^3.0.0" - stack-utils "^2.0.3" - jest-message-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" @@ -7054,14 +6654,6 @@ jest-mock@^27.2.4: "@jest/types" "^27.2.4" "@types/node" "*" -jest-mock@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.4.6.tgz#77d1ba87fbd33ccb8ef1f061697e7341b7635195" - integrity sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw== - dependencies: - "@jest/types" "^27.4.2" - "@types/node" "*" - jest-mock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" @@ -7080,11 +6672,6 @@ jest-regex-util@^27.0.6: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== -jest-regex-util@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca" - integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== - jest-regex-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" @@ -7099,15 +6686,6 @@ jest-resolve-dependencies@^27.2.4: jest-regex-util "^27.0.6" jest-snapshot "^27.2.4" -jest-resolve-dependencies@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz#fc50ee56a67d2c2183063f6a500cc4042b5e2327" - integrity sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw== - dependencies: - "@jest/types" "^27.4.2" - jest-regex-util "^27.4.0" - jest-snapshot "^27.4.6" - jest-resolve-dependencies@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" @@ -7142,22 +6720,6 @@ jest-resolve@^27.2.4: resolve "^1.20.0" slash "^3.0.0" -jest-resolve@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.4.6.tgz#2ec3110655e86d5bfcfa992e404e22f96b0b5977" - integrity sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw== - dependencies: - "@jest/types" "^27.4.2" - chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-pnp-resolver "^1.2.2" - jest-util "^27.4.2" - jest-validate "^27.4.6" - resolve "^1.20.0" - resolve.exports "^1.1.0" - slash "^3.0.0" - jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" @@ -7202,34 +6764,6 @@ jest-runner@^27.2.4: source-map-support "^0.5.6" throat "^6.0.1" -jest-runner@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.4.6.tgz#1d390d276ec417e9b4d0d081783584cbc3e24773" - integrity sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg== - dependencies: - "@jest/console" "^27.4.6" - "@jest/environment" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.4.0" - jest-environment-jsdom "^27.4.6" - jest-environment-node "^27.4.6" - jest-haste-map "^27.4.6" - jest-leak-detector "^27.4.6" - jest-message-util "^27.4.6" - jest-resolve "^27.4.6" - jest-runtime "^27.4.6" - jest-util "^27.4.2" - jest-worker "^27.4.6" - source-map-support "^0.5.6" - throat "^6.0.1" - jest-runner@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" @@ -7290,34 +6824,6 @@ jest-runtime@^27.2.4: strip-bom "^4.0.0" yargs "^16.2.0" -jest-runtime@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.4.6.tgz#83ae923818e3ea04463b22f3597f017bb5a1cffa" - integrity sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/globals" "^27.4.6" - "@jest/source-map" "^27.4.0" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - execa "^5.0.0" - glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-message-util "^27.4.6" - jest-mock "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - slash "^3.0.0" - strip-bom "^4.0.0" - jest-runtime@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" @@ -7354,14 +6860,6 @@ jest-serializer@^27.0.6: "@types/node" "*" graceful-fs "^4.2.4" -jest-serializer@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.4.0.tgz#34866586e1cae2388b7d12ffa2c7819edef5958a" - integrity sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.4" - jest-serializer@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" @@ -7416,34 +6914,6 @@ jest-snapshot@^27.2.4: pretty-format "^27.2.4" semver "^7.3.2" -jest-snapshot@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.4.6.tgz#e2a3b4fff8bdce3033f2373b2e525d8b6871f616" - integrity sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ== - dependencies: - "@babel/core" "^7.7.2" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.0.0" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^27.4.6" - graceful-fs "^4.2.4" - jest-diff "^27.4.6" - jest-get-type "^27.4.0" - jest-haste-map "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-util "^27.4.2" - natural-compare "^1.4.0" - pretty-format "^27.4.6" - semver "^7.3.2" - jest-snapshot@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" @@ -7484,18 +6954,6 @@ jest-util@^27.0.0, jest-util@^27.2.4: is-ci "^3.0.0" picomatch "^2.2.3" -jest-util@^27.4.2: - version "27.4.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.4.2.tgz#ed95b05b1adfd761e2cda47e0144c6a58e05a621" - integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA== - dependencies: - "@jest/types" "^27.4.2" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.4" - picomatch "^2.2.3" - jest-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" @@ -7520,18 +6978,6 @@ jest-validate@^27.2.4: leven "^3.1.0" pretty-format "^27.2.4" -jest-validate@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.4.6.tgz#efc000acc4697b6cf4fa68c7f3f324c92d0c4f1f" - integrity sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ== - dependencies: - "@jest/types" "^27.4.2" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^27.4.0" - leven "^3.1.0" - pretty-format "^27.4.6" - jest-validate@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" @@ -7557,19 +7003,6 @@ jest-watcher@^27.2.4: jest-util "^27.2.4" string-length "^4.0.1" -jest-watcher@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.4.6.tgz#673679ebeffdd3f94338c24f399b85efc932272d" - integrity sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw== - dependencies: - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - jest-util "^27.4.2" - string-length "^4.0.1" - jest-watcher@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" @@ -7601,15 +7034,6 @@ jest-worker@^27.2.4: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.6.tgz#5d2d93db419566cb680752ca0792780e71b3273e" - integrity sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -7619,15 +7043,6 @@ jest-worker@^27.5.1: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.1.1: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.7.tgz#87f74b9026a1592f2da05b4d258e57505f28eca4" - integrity sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg== - dependencies: - "@jest/core" "^27.4.7" - import-local "^3.0.2" - jest-cli "^27.4.7" - jest@^27.2.4: version "27.2.4" resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.4.tgz#70e27bef873138afc123aa4769f7124c50ad3efb" @@ -7789,13 +7204,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -7979,6 +7387,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -8933,6 +8348,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -8947,6 +8369,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -9128,6 +8557,13 @@ path-is-inside@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= +path-is-network-drive@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/path-is-network-drive/-/path-is-network-drive-1.0.13.tgz#c9aa0183eb72c328aa83f43def93ddcb9d7ec4d4" + integrity sha512-Hg74mRN6mmXV+gTm3INjFK40ncAmC/Lo4qoQaSZ+GT3hZzlKdWQSqAjqyPeW0SvObP2W073WyYEBWY9d3wOm3A== + dependencies: + tslib "^2.3.1" + path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -9143,6 +8579,13 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-strip-sep@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/path-strip-sep/-/path-strip-sep-1.0.10.tgz#2be4e789406b298af8709ff79af716134b733b98" + integrity sha512-JpCy+8LAJQQTO1bQsb/84s1g+/Stm3h39aOpPRBQ/paMUGVPPZChLTOTKHoaCkc/6sKuF7yVsnq5Pe1S6xQGcA== + dependencies: + tslib "^2.3.1" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -9236,6 +8679,13 @@ pixelmatch@^5.1.0: dependencies: pngjs "^4.0.1" +"pkg-dir@< 6 >= 5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -9670,15 +9120,6 @@ pretty-format@^27.0.0, pretty-format@^27.2.4: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" - integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -10163,7 +9604,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.20.0, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.1, resolve@^1.14.2, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.1, resolve@^1.14.2, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -10290,16 +9731,17 @@ rollup-plugin-terser@^7.0.2: serialize-javascript "^4.0.0" terser "^5.0.0" -rollup-plugin-typescript2@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.30.0.tgz#1cc99ac2309bf4b9d0a3ebdbc2002aecd56083d3" - integrity sha512-NUFszIQyhgDdhRS9ya/VEmsnpTe+GERDMmFo0Y+kf8ds51Xy57nPNGglJY+W6x1vcouA7Au7nsTgsLFj2I0PxQ== +rollup-plugin-typescript2@^0.31.2: + version "0.31.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.31.2.tgz#463aa713a7e2bf85b92860094b9f7fb274c5a4d8" + integrity sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q== dependencies: - "@rollup/pluginutils" "^4.1.0" - find-cache-dir "^3.3.1" - fs-extra "8.1.0" - resolve "1.20.0" - tslib "2.1.0" + "@rollup/pluginutils" "^4.1.2" + "@yarn-tool/resolve-package" "^1.0.40" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + resolve "^1.20.0" + tslib "^2.3.1" rollup-pluginutils@^2.8.2: version "2.8.2" @@ -11284,11 +10726,6 @@ ts-node@^7.0.1: source-map-support "^0.5.6" yn "^2.0.0" -tslib@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== - tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -11448,7 +10885,7 @@ typescript@*: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== -typescript@^3.9.5, typescript@^3.9.7: +typescript@^3.9.7: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== @@ -11527,7 +10964,7 @@ universal-user-agent@^6.0.0: resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== -universalify@^0.1.0, universalify@^0.1.2: +universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== @@ -11552,6 +10989,15 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= +upath2@^3.1.12: + version "3.1.12" + resolved "https://registry.yarnpkg.com/upath2/-/upath2-3.1.12.tgz#441b3dfbadde21731017bd1b7beb169498efd0a9" + integrity sha512-yC3eZeCyCXFWjy7Nu4pgjLhXNYjuzuUmJiRgSSw6TJp8Emc+E4951HGPJf+bldFC5SL7oBLeNbtm1fGzXn2gxw== + dependencies: + path-is-network-drive "^1.0.13" + path-strip-sep "^1.0.10" + tslib "^2.3.1" + upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -11990,3 +11436,8 @@ yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From b6810f66221e10df54ac32a530ba309b891baba1 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 12 Mar 2022 00:49:22 +1100 Subject: [PATCH 51/79] add support for custom property of css styles --- packages/rrdom/src/style.ts | 4 +++- packages/rrdom/test/document.test.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/rrdom/src/style.ts b/packages/rrdom/src/style.ts index ec97685615..a85f7598f0 100644 --- a/packages/rrdom/src/style.ts +++ b/packages/rrdom/src/style.ts @@ -29,8 +29,10 @@ export function toCSSText(style: Record): string { /** * Camelize a hyphen-delimited string. */ -const camelizeRE = /-(\w)/g; +const camelizeRE = /-([a-z])/g; +const CUSTOM_PROPERTY_REGEX = /^--[a-zA-Z0-9-]+$/; export const camelize = (str: string): string => { + if (CUSTOM_PROPERTY_REGEX.test(str)) return str; return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); }; diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 1962bb46da..3ec1048efc 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -654,6 +654,10 @@ describe('Basic RRDocument implementation', () => { node.attributes.style = 'top: /**/0;'; expect(node.style.top).toEqual('0'); + // custom property (variable) + node.attributes.style = '--custom-property: value'; + expect(node.style['--custom-property']).toEqual('value'); + // incomplete node.attributes.style = 'overflow:'; expect(node.style.overflow).toBeUndefined(); From ab540bbe6acec1f3d1e892bd77ea37edce995437 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 13 Mar 2022 20:39:05 +1100 Subject: [PATCH 52/79] add a switch for virtual-dom optimization --- packages/rrweb/src/replay/index.ts | 9 +++++---- packages/rrweb/src/types.ts | 1 + packages/rrweb/typings/types.d.ts | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 8ab8f22fbe..51c8c95fed 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -160,6 +160,7 @@ export class Replayer { UNSAFE_replayCanvas: false, pauseAnimation: true, mouseTail: defaultMouseTailConfig, + useVirtualDom: true, }; this.config = Object.assign({}, defaultConfig, config); @@ -1297,9 +1298,9 @@ export class Replayer { } } - private applyMutation(d: mutationData, useVirtualParent: boolean) { + private applyMutation(d: mutationData, isSync: boolean) { // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events. - if (!this.usingVirtualDom && useVirtualParent) { + if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) { this.usingVirtualDom = true; buildFromDom( this.iframe.contentDocument!, @@ -1324,7 +1325,7 @@ export class Replayer { } } } - const mirror = useVirtualParent ? this.virtualDom.mirror : this.mirror; + const mirror = this.usingVirtualDom ? this.virtualDom.mirror : this.mirror; d.removes.forEach((mutation) => { let target = mirror.getNode(mutation.id); if (!target) { @@ -1423,7 +1424,7 @@ export class Replayer { const targetDoc = mutation.node.rootId ? mirror.getNode(mutation.node.rootId) - : useVirtualParent + : this.usingVirtualDom ? this.virtualDom : this.iframe.contentDocument; if (isIframeINode(parent) || isRRIFrameElement(parent)) { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 8baeebc000..8109d7a647 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -619,6 +619,7 @@ export type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; + useVirtualDom: boolean; plugins?: ReplayPlugin[]; }; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index a5484efa48..e5cf553e7f 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -442,6 +442,7 @@ export declare type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; + useVirtualDom: boolean; plugins?: ReplayPlugin[]; }; export declare type playerMetaData = { From d69970dbd2ab99bb26816b65a0a55b00b3510cf2 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Tue, 22 Mar 2022 00:36:33 +1100 Subject: [PATCH 53/79] Apply suggestions from code review 1. add an enum type for NodeType 2. rename nodeType from rrweb-snapshot to RRNodeType 3. rename notSerializedId to unserializedId 4. add comments for some confusing variables --- packages/rrdom/src/diff.ts | 30 ++--- packages/rrdom/src/document-nodejs.ts | 22 ++-- packages/rrdom/src/document.ts | 112 +++++++++++------- packages/rrdom/src/virtual-dom.ts | 35 +++--- packages/rrdom/test/diff.test.ts | 10 +- packages/rrdom/test/document-nodejs.test.ts | 4 +- packages/rrdom/test/document.test.ts | 60 +++++----- packages/rrdom/test/virtual-dom.test.ts | 18 +-- packages/rrdom/tsconfig.json | 2 +- packages/rrweb/src/replay/index.ts | 5 +- .../typings/record/shadow-dom-manager.d.ts | 3 + packages/rrweb/typings/utils.d.ts | 2 +- 12 files changed, 166 insertions(+), 137 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 61cf56b2d1..90497ee575 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,4 +1,4 @@ -import { elementNode, INode, NodeType } from 'rrweb-snapshot'; +import { elementNode, INode, NodeType as RRNodeType } from 'rrweb-snapshot'; import type { canvasMutationData, incrementalSnapshotEvent, @@ -92,11 +92,11 @@ export function diff( let inputDataToApply = null, scrollDataToApply = null; switch (newTree.RRNodeType) { - case NodeType.Document: + case RRNodeType.Document: const newRRDocument = newTree as IRRDocument; scrollDataToApply = (newRRDocument as RRDocument).scrollData; break; - case NodeType.Element: + case RRNodeType.Element: const oldElement = (oldTree as Node) as HTMLElement; const newRRElement = newTree as IRRElement; diffProps(oldElement, newRRElement); @@ -148,9 +148,9 @@ export function diff( ); } break; - case NodeType.Text: - case NodeType.Comment: - case NodeType.CDATA: + case RRNodeType.Text: + case RRNodeType.Comment: + case RRNodeType.CDATA: if ( oldTree.textContent !== (newTree as IRRText | IRRComment | IRRCDATASection).data @@ -174,7 +174,7 @@ export function diff( } // IFrame element doesn't have child nodes. if ( - newTree.RRNodeType === NodeType.Element && + newTree.RRNodeType === RRNodeType.Element && (newTree as IRRElement).tagName === 'IFRAME' ) { const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) @@ -288,8 +288,8 @@ function diffChildren( * We should delete it before insert a serialized one. Otherwise, an error 'Only one element on document allowed' will be thrown. */ if ( - parentNode.__sn.type === NodeType.Document && - newNode.__sn.type === NodeType.Element && + parentNode.__sn.type === RRNodeType.Document && + newNode.__sn.type === RRNodeType.Element && ((parentNode as Node) as Document).documentElement ) { parentNode.removeChild( @@ -335,17 +335,17 @@ export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { let node = mirror.getNode(rrNode.__sn.id); if (node !== null) return node; switch (rrNode.RRNodeType) { - case NodeType.Document: + case RRNodeType.Document: node = (new Document() as unknown) as INode; break; - case NodeType.DocumentType: + case RRNodeType.DocumentType: node = (document.implementation.createDocumentType( (rrNode as IRRDocumentType).name, (rrNode as IRRDocumentType).publicId, (rrNode as IRRDocumentType).systemId, ) as unknown) as INode; break; - case NodeType.Element: + case RRNodeType.Element: let tagName = (rrNode as IRRElement).tagName.toLowerCase(); tagName = SVGTagMap[tagName] || tagName; if ((rrNode.__sn as elementNode).isSVG) { @@ -358,17 +358,17 @@ export function createOrGetNode(rrNode: IRRNode, mirror: Mirror): INode { (rrNode as IRRElement).tagName, ) as unknown) as INode; break; - case NodeType.Text: + case RRNodeType.Text: node = (document.createTextNode( (rrNode as IRRText).data, ) as unknown) as INode; break; - case NodeType.Comment: + case RRNodeType.Comment: node = (document.createComment( (rrNode as IRRComment).data, ) as unknown) as INode; break; - case NodeType.CDATA: + case RRNodeType.CDATA: node = (document.createCDATASection( (rrNode as IRRCDATASection).data, ) as unknown) as INode; diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 9198a7e229..9de237f0f4 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -1,4 +1,4 @@ -import { NodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { NWSAPI } from 'nwsapi'; import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle'; import { @@ -53,15 +53,13 @@ export class RRDocument } get documentElement(): RRElement | null { - return super.documentElement as RRElement | null; - } - - get body(): RRElement | null { - return super.body as RRElement | null; - } - - get head() { - return super.head as RRElement | null; + return ( + (this.childNodes.find( + (node) => + node.RRNodeType === RRNodeType.Element && + (node as RRElement).tagName === 'HTML', + ) as RRElement) || null + ); } get implementation(): RRDocument { @@ -231,7 +229,7 @@ export class RRElement extends BaseRRElementImpl(RRNode) { get firstElementChild(): RRElement | null { for (let child of this.childNodes) - if (child.RRNodeType === NodeType.Element) return child as RRElement; + if (child.RRNodeType === RRNodeType.Element) return child as RRElement; return null; } @@ -327,7 +325,7 @@ export class RRStyleElement extends RRElement { if (!this._sheet) { let result = ''; for (let child of this.childNodes) - if (child.nodeType === NodeType.Text) + if (child.RRNodeType === RRNodeType.Text) result += (child as RRText).textContent; this._sheet = cssom.parse(result); } diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index e4c6f2b351..1e4df458d3 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -1,4 +1,4 @@ -import { NodeType, serializedNodeWithId } from 'rrweb-snapshot'; +import { NodeType as RRNodeType, serializedNodeWithId } from 'rrweb-snapshot'; import { parseCSSText, camelize, toCSSText } from './style'; export interface IRRNode { @@ -7,11 +7,11 @@ export interface IRRNode { parentNode: IRRNode | null; childNodes: IRRNode[]; ownerDocument: IRRDocument; - ELEMENT_NODE: 1; - TEXT_NODE: 3; + readonly ELEMENT_NODE: number; + readonly TEXT_NODE: number; // corresponding nodeType value of standard HTML Node readonly nodeType: number; - readonly RRNodeType: NodeType; + readonly RRNodeType: RRNodeType; firstChild: IRRNode | null; @@ -36,7 +36,7 @@ export interface IRRNode { toString(nodeName?: string): string; } export interface IRRDocument extends IRRNode { - notSerializedId: number; + unserializedId: number; documentElement: IRRElement | null; @@ -120,6 +120,9 @@ export interface IRRCDATASection extends IRRNode { type ConstrainedConstructor = new (...args: any[]) => T; +/** + * This is designed as an abstract class so it should never be instantiated. + */ export class BaseRRNode implements IRRNode { public __sn: serializedNodeWithId; public childNodes: IRRNode[] = []; @@ -127,11 +130,11 @@ export class BaseRRNode implements IRRNode { public parentNode: IRRNode | null = null; public textContent: string | null; public ownerDocument: IRRDocument; - public ELEMENT_NODE: 1 = 1; - public TEXT_NODE: 3 = 3; + public readonly ELEMENT_NODE: number = NodeType.ELEMENT_NODE; + public readonly TEXT_NODE: number = NodeType.TEXT_NODE; // corresponding nodeType value of standard HTML Node public readonly nodeType: number; - public readonly RRNodeType: NodeType; + public readonly RRNodeType: RRNodeType; constructor(...args: any[]) {} @@ -175,14 +178,14 @@ export class BaseRRNode implements IRRNode { public setDefaultSN(id: number) { switch (this.RRNodeType) { - case NodeType.Document: + case RRNodeType.Document: this.__sn = { id, type: this.RRNodeType, childNodes: [], }; break; - case NodeType.DocumentType: + case RRNodeType.DocumentType: const doctype = (this as unknown) as IRRDocumentType; this.__sn = { id, @@ -192,7 +195,7 @@ export class BaseRRNode implements IRRNode { systemId: doctype.systemId, }; break; - case NodeType.Element: + case RRNodeType.Element: this.__sn = { id, type: this.RRNodeType, @@ -201,21 +204,21 @@ export class BaseRRNode implements IRRNode { childNodes: [], }; break; - case NodeType.Text: + case RRNodeType.Text: this.__sn = { id, type: this.RRNodeType, textContent: ((this as unknown) as IRRText).textContent || '', }; break; - case NodeType.Comment: + case RRNodeType.Comment: this.__sn = { id, type: this.RRNodeType, textContent: ((this as unknown) as IRRComment).textContent || '', }; break; - case NodeType.CDATA: + case RRNodeType.CDATA: this.__sn = { id, type: this.RRNodeType, @@ -225,7 +228,7 @@ export class BaseRRNode implements IRRNode { } } - public toString(nodeName?: string) { + public toString(nodeName: string) { return `${this.__sn?.id || ''} ${nodeName}`; } } @@ -234,23 +237,26 @@ export function BaseRRDocumentImpl< RRNode extends ConstrainedConstructor >(RRNodeClass: RRNode) { return class BaseRRDocument extends RRNodeClass implements IRRDocument { - public readonly nodeType = 9; - public readonly RRNodeType = NodeType.Document; + public readonly nodeType: number = NodeType.DOCUMENT_NODE; + public readonly RRNodeType = RRNodeType.Document; public textContent: string | null = null; - _notSerializedId = -1; // used as an id to identify not serialized node + // In the rrweb replayer, there are some unserialized nodes like the element that stores the injected style rules. + // These unserialized nodes may interfere the execution of the diff algorithm. + // The id of serialized node is larger than 0. So this value ​​less than 0 is used as id for these unserialized nodes. + _unserializedId = -1; /** * Every time the id is used, it will minus 1 automatically to avoid collisions. */ - public get notSerializedId(): number { - return this._notSerializedId--; + public get unserializedId(): number { + return this._unserializedId--; } public get documentElement(): IRRElement | null { return ( (this.childNodes.find( (node) => - node.RRNodeType === NodeType.Element && + node.RRNodeType === RRNodeType.Element && (node as IRRElement).tagName === 'HTML', ) as IRRElement) || null ); @@ -260,7 +266,7 @@ export function BaseRRDocumentImpl< return ( (this.documentElement?.childNodes.find( (node) => - node.RRNodeType === NodeType.Element && + node.RRNodeType === RRNodeType.Element && (node as IRRElement).tagName === 'BODY', ) as IRRElement) || null ); @@ -270,7 +276,7 @@ export function BaseRRDocumentImpl< return ( (this.documentElement?.childNodes.find( (node) => - node.RRNodeType === NodeType.Element && + node.RRNodeType === RRNodeType.Element && (node as IRRElement).tagName === 'HEAD', ) as IRRElement) || null ); @@ -286,11 +292,14 @@ export function BaseRRDocumentImpl< public appendChild(childNode: IRRNode): IRRNode { const nodeType = childNode.RRNodeType; - if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { + if ( + nodeType === RRNodeType.Element || + nodeType === RRNodeType.DocumentType + ) { if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { throw new Error( `RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${ - nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' + nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype' } on RRDocument allowed.`, ); } @@ -303,11 +312,14 @@ export function BaseRRDocumentImpl< public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { const nodeType = newChild.RRNodeType; - if (nodeType === NodeType.Element || nodeType === NodeType.DocumentType) { + if ( + nodeType === RRNodeType.Element || + nodeType === RRNodeType.DocumentType + ) { if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { throw new Error( `RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one ${ - nodeType === NodeType.Element ? 'RRElement' : 'RRDoctype' + nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype' } on RRDocument allowed.`, ); } @@ -338,7 +350,7 @@ export function BaseRRDocumentImpl< public open() { this.childNodes = []; - this._notSerializedId = -1; + this._unserializedId = -1; } public close() {} @@ -363,13 +375,7 @@ export function BaseRRDocumentImpl< publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; if (publicId) { const doctype = this.createDocumentType('html', publicId, ''); - doctype.__sn = { - type: NodeType.DocumentType, - name: 'html', - publicId: publicId, - systemId: '', - id: this.notSerializedId, - }; + doctype.setDefaultSN(this.unserializedId); this.open(); this.appendChild(doctype); } @@ -438,8 +444,8 @@ export function BaseRRDocumentTypeImpl< return class BaseRRDocumentType extends RRNodeClass implements IRRDocumentType { - public readonly nodeType = 10; - public readonly RRNodeType = NodeType.DocumentType; + public readonly nodeType: number = NodeType.DOCUMENT_TYPE_NODE; + public readonly RRNodeType = RRNodeType.DocumentType; public readonly name: string; public readonly publicId: string; public readonly systemId: string; @@ -463,8 +469,8 @@ export function BaseRRElementImpl< >(RRNodeClass: RRNode) { // @ts-ignore return class BaseRRElement extends RRNodeClass implements IRRElement { - public readonly nodeType = 1; - public readonly RRNodeType = NodeType.Element; + public readonly nodeType: number = NodeType.ELEMENT_NODE; + public readonly RRNodeType = RRNodeType.Element; public tagName: string; public attributes: Record = {}; public shadowRoot: IRRElement | null = null; @@ -630,8 +636,8 @@ export function BaseRRTextImpl>( ) { // @ts-ignore return class BaseRRText extends RRNodeClass implements IRRText { - public readonly nodeType = 3; - public readonly RRNodeType = NodeType.Text; + public readonly nodeType: number = NodeType.TEXT_NODE; + public readonly RRNodeType = RRNodeType.Text; public data: string; constructor(data: string) { @@ -658,8 +664,8 @@ export function BaseRRCommentImpl< >(RRNodeClass: RRNode) { // @ts-ignore return class BaseRRComment extends RRNodeClass implements IRRComment { - public readonly nodeType = 8; - public readonly RRNodeType = NodeType.Comment; + public readonly nodeType: number = NodeType.COMMENT_NODE; + public readonly RRNodeType = RRNodeType.Comment; public data: string; constructor(data: string) { @@ -688,8 +694,8 @@ export function BaseRRCDATASectionImpl< return class BaseRRCDATASection extends RRNodeClass implements IRRCDATASection { - public readonly nodeType = 4; - public readonly RRNodeType = NodeType.CDATA; + public readonly nodeType: number = NodeType.CDATA_SECTION_NODE; + public readonly RRNodeType = RRNodeType.CDATA; public data: string; constructor(data: string) { @@ -753,3 +759,19 @@ export type CSSStyleDeclaration = Record & { ) => void; removeProperty: (name: string) => string; }; + +// Enumerate nodeType value of standard HTML Node. +export enum NodeType { + PLACEHOLDER, // This isn't a node type. Enum type value starts from zero but NodeType value starts from 1. + ELEMENT_NODE, + ATTRIBUTE_NODE, + TEXT_NODE, + CDATA_SECTION_NODE, + ENTITY_REFERENCE_NODE, + ENTITY_NODE, + PROCESSING_INSTRUCTION_NODE, + COMMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_TYPE_NODE, + DOCUMENT_FRAGMENT_NODE, +} diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 9bb5e57367..f9f6d9dd80 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -1,4 +1,4 @@ -import { INode, NodeType } from 'rrweb-snapshot'; +import { INode, NodeType as RRNodeType } from 'rrweb-snapshot'; import type { canvasMutationData, incrementalSnapshotEvent, @@ -17,6 +17,7 @@ import { IRRDocument, IRRElement, IRRNode, + NodeType, } from './document'; import { VirtualStyleRules } from './diff'; @@ -208,16 +209,16 @@ export function buildFromNode( let rrNode: IRRNode; switch (node.nodeType) { - case node.DOCUMENT_NODE: + case NodeType.DOCUMENT_NODE: if ( parentRRNode && - parentRRNode.RRNodeType === NodeType.Element && + parentRRNode.RRNodeType === RRNodeType.Element && (parentRRNode as IRRElement).tagName === 'IFRAME' ) rrNode = (parentRRNode as RRIFrameElement).contentDocument; else rrNode = rrdom; break; - case node.DOCUMENT_TYPE_NODE: + case NodeType.DOCUMENT_TYPE_NODE: const documentType = (node as Node) as DocumentType; rrNode = rrdom.createDocumentType( documentType.name, @@ -225,7 +226,7 @@ export function buildFromNode( documentType.systemId, ); break; - case node.ELEMENT_NODE: + case NodeType.ELEMENT_NODE: const elementNode = (node as Node) as HTMLElement; const tagName = getValidTagName(elementNode); rrNode = rrdom.createElement(tagName); @@ -240,19 +241,19 @@ export function buildFromNode( * Because if these values are changed later, the mutation will be applied through the batched input events on its RRElement after the diff algorithm is executed. */ break; - case node.TEXT_NODE: + case NodeType.TEXT_NODE: rrNode = rrdom.createTextNode(((node as Node) as Text).textContent || ''); break; - case node.CDATA_SECTION_NODE: + case NodeType.CDATA_SECTION_NODE: rrNode = rrdom.createCDATASection(((node as Node) as CDATASection).data); break; - case node.COMMENT_NODE: + case NodeType.COMMENT_NODE: rrNode = rrdom.createComment( ((node as Node) as Comment).textContent || '', ); break; // if node is a shadow root - case node.DOCUMENT_FRAGMENT_NODE: + case NodeType.DOCUMENT_FRAGMENT_NODE: rrNode = (parentRRNode as IRRElement).attachShadow({ mode: 'open' }); break; default: @@ -260,7 +261,7 @@ export function buildFromNode( } if (!node.__sn || node.__sn.id < 0) { - rrNode.setDefaultSN(rrdom.notSerializedId); + rrNode.setDefaultSN(rrdom.unserializedId); node.__sn = rrNode.__sn; } else rrNode.__sn = node.__sn; @@ -288,11 +289,11 @@ export function buildFromDom( if ( // if the parentRRNode isn't a RRIFrameElement !( - parentRRNode?.RRNodeType === NodeType.Element && + parentRRNode?.RRNodeType === RRNodeType.Element && (parentRRNode as IRRElement).tagName === 'IFRAME' ) && // if node isn't a shadow root - node.nodeType !== node.DOCUMENT_FRAGMENT_NODE + node.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE ) { parentRRNode?.appendChild(rrNode); rrNode.parentNode = parentRRNode; @@ -300,7 +301,7 @@ export function buildFromDom( } if ( - node.nodeType === node.ELEMENT_NODE && + node.nodeType === NodeType.ELEMENT_NODE && ((node as Node) as HTMLElement).tagName === 'IFRAME' ) walk( @@ -309,13 +310,13 @@ export function buildFromDom( rrNode, ); else if ( - node.nodeType === node.DOCUMENT_NODE || - node.nodeType === node.ELEMENT_NODE || - node.nodeType === node.DOCUMENT_FRAGMENT_NODE + node.nodeType === NodeType.DOCUMENT_NODE || + node.nodeType === NodeType.ELEMENT_NODE || + node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE ) { // if the node is a shadow dom if ( - node.nodeType === Node.ELEMENT_NODE && + node.nodeType === NodeType.ELEMENT_NODE && ((node as Node) as HTMLElement).shadowRoot ) walk( diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 069e7d490f..29060fc948 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -14,7 +14,11 @@ import { diff, ReplayerHandler, } from '../src/diff'; -import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot/'; +import { + INode, + NodeType as RRNodeType, + serializedNodeWithId, +} from 'rrweb-snapshot/'; import { IRRNode } from '../src/document'; import { canvasMutationData, @@ -24,7 +28,7 @@ import { } from 'rrweb/src/types'; const elementSn = { - type: NodeType.Element, + type: RRNodeType.Element, tagName: 'DIV', attributes: {}, childNodes: [], @@ -995,7 +999,7 @@ describe('diff algorithm for rrdom', () => { const text = document.createTextNode(textContent); ((text as unknown) as INode).__sn = { id: 0, - type: NodeType.Text, + type: RRNodeType.Text, textContent: 'text of the existed node', }; // Add the text node to the mirror to make it look like already existing. diff --git a/packages/rrdom/test/document-nodejs.test.ts b/packages/rrdom/test/document-nodejs.test.ts index ec7453f6f6..147514e65b 100644 --- a/packages/rrdom/test/document-nodejs.test.ts +++ b/packages/rrdom/test/document-nodejs.test.ts @@ -3,7 +3,7 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { NodeType } from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { RRCanvasElement, RRCDATASection, @@ -452,7 +452,7 @@ describe('RRDocument for nodejs environment', () => { expect(node.shadowRoot).toBeNull(); node.attachShadow({ mode: 'open' }); expect(node.shadowRoot).not.toBeNull(); - expect(node.shadowRoot!.RRNodeType).toBe(NodeType.Element); + expect(node.shadowRoot!.RRNodeType).toBe(RRNodeType.Element); expect(node.shadowRoot!.tagName).toBe('SHADOWROOT'); expect(node.parentNode).toBeNull(); }); diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 3ec1048efc..30f25dc199 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -7,7 +7,7 @@ import { documentNode, documentTypeNode, elementNode, - NodeType, + NodeType as RRNodeType, textNode, } from 'rrweb-snapshot'; import { @@ -30,7 +30,7 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = new RRNode(); node.__sn = { - type: NodeType.Element, + type: RRNodeType.Element, tagName: 'div', attributes: {}, childNodes: [], @@ -140,7 +140,7 @@ describe('Basic RRDocument implementation', () => { node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.Document); + expect(node.__sn.type).toEqual(RRNodeType.Document); expect((node.__sn as documentNode).childNodes).toBeInstanceOf(Array); expect(node.parentNode).toEqual(null); @@ -149,7 +149,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toBeNull(); - expect(node.RRNodeType).toBe(NodeType.Document); + expect(node.RRNodeType).toBe(RRNodeType.Document); expect(node.nodeType).toBe(document.nodeType); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -160,7 +160,7 @@ describe('Basic RRDocument implementation', () => { expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); expect(node.setDefaultSN).toBeDefined(); - expect(node.notSerializedId).toBe(-1); + expect(node.unserializedId).toBe(-1); expect(node.documentElement).toBeNull(); expect(node.body).toBeNull(); expect(node.head).toBeNull(); @@ -179,9 +179,9 @@ describe('Basic RRDocument implementation', () => { expect(node.toString()).toEqual('1 RRDocument'); }); - it('can access a unique, decremented notSerializedId every time', () => { + it('can access a unique, decremented unserializedId every time', () => { const node = new RRDocument(); - for (let i = 1; i <= 100; i++) expect(node.notSerializedId).toBe(-i); + for (let i = 1; i <= 100; i++) expect(node.unserializedId).toBe(-i); }); it('can get documentElement', () => { @@ -311,21 +311,21 @@ describe('Basic RRDocument implementation', () => { it('should implement create node functions', () => { const node = new RRDocument(); expect(node.createDocument(null, '', null).RRNodeType).toEqual( - NodeType.Document, + RRNodeType.Document, ); expect(node.createDocumentType('', '', '').RRNodeType).toEqual( - NodeType.DocumentType, + RRNodeType.DocumentType, ); - expect(node.createElement('html').RRNodeType).toEqual(NodeType.Element); + expect(node.createElement('html').RRNodeType).toEqual(RRNodeType.Element); expect(node.createElementNS('', 'html').RRNodeType).toEqual( - NodeType.Element, + RRNodeType.Element, ); - expect(node.createTextNode('text').RRNodeType).toEqual(NodeType.Text); + expect(node.createTextNode('text').RRNodeType).toEqual(RRNodeType.Text); expect(node.createComment('comment').RRNodeType).toEqual( - NodeType.Comment, + RRNodeType.Comment, ); expect(node.createCDATASection('data').RRNodeType).toEqual( - NodeType.CDATA, + RRNodeType.CDATA, ); }); @@ -334,11 +334,11 @@ describe('Basic RRDocument implementation', () => { const documentType = node.createDocumentType('html', '', ''); node.appendChild(documentType); expect(node.childNodes[0]).toBe(documentType); - expect(node.notSerializedId).toBe(-1); + expect(node.unserializedId).toBe(-1); expect(node.close()); expect(node.open()); expect(node.childNodes.length).toEqual(0); - expect(node.notSerializedId).toBe(-1); + expect(node.unserializedId).toBe(-1); }); it('can cover the usage of write() in rrweb-snapshot', () => { @@ -348,7 +348,7 @@ describe('Basic RRDocument implementation', () => { ); expect(node.childNodes.length).toBe(1); let doctype = node.childNodes[0] as IRRDocumentType; - expect(doctype.RRNodeType).toEqual(NodeType.DocumentType); + expect(doctype.RRNodeType).toEqual(RRNodeType.DocumentType); expect(doctype.parentNode).toEqual(node); expect(doctype.name).toEqual('html'); expect(doctype.publicId).toEqual( @@ -361,7 +361,7 @@ describe('Basic RRDocument implementation', () => { ); expect(node.childNodes.length).toBe(1); doctype = node.childNodes[0] as IRRDocumentType; - expect(doctype.RRNodeType).toEqual(NodeType.DocumentType); + expect(doctype.RRNodeType).toEqual(RRNodeType.DocumentType); expect(doctype.parentNode).toEqual(node); expect(doctype.name).toEqual('html'); expect(doctype.publicId).toEqual('-//W3C//DTD HTML 4.0 Transitional//EN'); @@ -377,7 +377,7 @@ describe('Basic RRDocument implementation', () => { const node = new RRDocumentType(name, publicId, systemId); node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.DocumentType); + expect(node.__sn.type).toEqual(RRNodeType.DocumentType); expect((node.__sn as documentTypeNode).name).toEqual(name); expect((node.__sn as documentTypeNode).publicId).toEqual(publicId); expect((node.__sn as documentTypeNode).systemId).toEqual(systemId); @@ -388,7 +388,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toBeNull(); - expect(node.RRNodeType).toBe(NodeType.DocumentType); + expect(node.RRNodeType).toBe(RRNodeType.DocumentType); expect(node.nodeType).toBe(document.DOCUMENT_TYPE_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -413,7 +413,7 @@ describe('Basic RRDocument implementation', () => { const node = document.createElement('div'); node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.Element); + expect(node.__sn.type).toEqual(RRNodeType.Element); expect((node.__sn as elementNode).tagName).toEqual('div'); expect((node.__sn as elementNode).attributes).toBeDefined(); expect((node.__sn as elementNode).childNodes).toBeInstanceOf(Array); @@ -428,7 +428,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(document); expect(node.textContent).toEqual(''); - expect(node.RRNodeType).toBe(NodeType.Element); + expect(node.RRNodeType).toBe(RRNodeType.Element); expect(node.nodeType).toBe(document.ELEMENT_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -707,7 +707,7 @@ describe('Basic RRDocument implementation', () => { expect(node.shadowRoot).toBeNull(); node.attachShadow({ mode: 'open' }); expect(node.shadowRoot).not.toBeNull(); - expect(node.shadowRoot!.RRNodeType).toBe(NodeType.Element); + expect(node.shadowRoot!.RRNodeType).toBe(RRNodeType.Element); expect(node.shadowRoot!.tagName).toBe('SHADOWROOT'); expect(node.parentNode).toBeNull(); }); @@ -786,7 +786,7 @@ describe('Basic RRDocument implementation', () => { const node = dom.createTextNode('text'); node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.Text); + expect(node.__sn.type).toEqual(RRNodeType.Text); expect((node.__sn as textNode).textContent).toEqual('text'); expect(node.parentNode).toEqual(null); @@ -795,7 +795,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('text'); - expect(node.RRNodeType).toBe(NodeType.Text); + expect(node.RRNodeType).toBe(RRNodeType.Text); expect(node.nodeType).toBe(document.TEXT_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -824,7 +824,7 @@ describe('Basic RRDocument implementation', () => { const node = dom.createComment('comment'); node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.Comment); + expect(node.__sn.type).toEqual(RRNodeType.Comment); expect((node.__sn as commentNode).textContent).toEqual('comment'); expect(node.parentNode).toEqual(null); @@ -833,7 +833,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('comment'); - expect(node.RRNodeType).toBe(NodeType.Comment); + expect(node.RRNodeType).toBe(RRNodeType.Comment); expect(node.nodeType).toBe(document.COMMENT_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -862,7 +862,7 @@ describe('Basic RRDocument implementation', () => { const node = dom.createCDATASection('data'); node.setDefaultSN(1); expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(NodeType.CDATA); + expect(node.__sn.type).toEqual(RRNodeType.CDATA); expect((node.__sn as cdataNode).textContent).toEqual(''); expect(node.parentNode).toEqual(null); @@ -871,7 +871,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBe(dom); expect(node.textContent).toEqual('data'); - expect(node.RRNodeType).toBe(NodeType.CDATA); + expect(node.RRNodeType).toBe(RRNodeType.CDATA); expect(node.nodeType).toBe(document.CDATA_SECTION_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); @@ -905,7 +905,7 @@ describe('Basic RRDocument implementation', () => { expect(node.childNodes.length).toBe(0); expect(node.ownerDocument).toBeUndefined(); expect(node.textContent).toEqual(''); - expect(node.RRNodeType).toBe(NodeType.Element); + expect(node.RRNodeType).toBe(RRNodeType.Element); expect(node.nodeType).toBe(document.ELEMENT_NODE); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 703d2eb729..f6e876c127 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -7,7 +7,7 @@ import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import * as typescript from 'rollup-plugin-typescript2'; -import { NodeType, INode } from 'rrweb-snapshot'; +import { NodeType as RRNodeType, INode } from 'rrweb-snapshot'; import { buildFromDom, buildFromNode, @@ -50,7 +50,7 @@ describe('RRDocument for browser environment', () => { expect(((document as unknown) as INode).__sn.id).toEqual(-1); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.Document); + expect(rrNode.__sn.type).toEqual(RRNodeType.Document); expect(rrNode.__sn.id).toEqual(-1); expect(rrNode).toBe(rrdom); @@ -61,7 +61,7 @@ describe('RRDocument for browser environment', () => { expect(((document.doctype as unknown) as INode).__sn.id).toEqual(-2); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.DocumentType); + expect(rrNode.__sn.type).toEqual(RRNodeType.DocumentType); expect(rrNode.__sn.id).toEqual(-2); // build from element @@ -80,7 +80,7 @@ describe('RRDocument for browser environment', () => { ); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.Element); + expect(rrNode.__sn.type).toEqual(RRNodeType.Element); expect(rrNode.__sn.id).toEqual(-3); // build from text @@ -91,7 +91,7 @@ describe('RRDocument for browser environment', () => { expect(((text as unknown) as INode).__sn.id).toEqual(-4); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.Text); + expect(rrNode.__sn.type).toEqual(RRNodeType.Text); expect(rrNode.__sn.id).toEqual(-4); // build from comment @@ -102,7 +102,7 @@ describe('RRDocument for browser environment', () => { expect(((comment as unknown) as INode).__sn.id).toEqual(-5); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.Comment); + expect(rrNode.__sn.type).toEqual(RRNodeType.Comment); expect(rrNode.__sn.id).toEqual(-5); // build from CDATASection @@ -118,7 +118,7 @@ describe('RRDocument for browser environment', () => { expect(((cdataSection as unknown) as INode).__sn.id).toEqual(-6); expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.CDATA); + expect(rrNode.__sn.type).toEqual(RRNodeType.CDATA); expect(rrNode.textContent).toEqual(cdata); expect(rrNode.__sn.id).toEqual(-6); }); @@ -153,7 +153,7 @@ describe('RRDocument for browser environment', () => { )!; expect(rrNode).not.toBeNull(); expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(NodeType.Document); + expect(rrNode.__sn.type).toEqual(RRNodeType.Document); expect(rrNode.__sn.id).toEqual(-1); expect(((iframe.contentDocument as unknown) as INode).__sn.id).toEqual( -1, @@ -176,7 +176,7 @@ describe('RRDocument for browser environment', () => { expect(rrNode.__sn).toBeDefined(); expect(rrNode.__sn.id).toEqual(-1); expect(((div.shadowRoot as unknown) as INode).__sn.id).toEqual(-1); - expect(rrNode.RRNodeType).toEqual(NodeType.Element); + expect(rrNode.RRNodeType).toEqual(RRNodeType.Element); expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT'); expect(rrNode).toBe(parentRRNode.shadowRoot); }); diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index 1a9974856a..e28961e4c1 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ES5", "module": "commonjs", "noImplicitAny": true, "strictNullChecks": true, diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index c9ac699145..25da8579d6 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -110,6 +110,7 @@ export class Replayer { public config: playerConfig; + // In the fast-forward process, if the virtual-dom optimization is used, this flag value is true. public usingVirtualDom = false; public virtualDom: RRDocument = new RRDocument(); @@ -160,7 +161,7 @@ export class Replayer { UNSAFE_replayCanvas: false, pauseAnimation: true, mouseTail: defaultMouseTailConfig, - useVirtualDom: true, + useVirtualDom: true, // Virtual-dom optimization is enabled by default. }; this.config = Object.assign({}, defaultConfig, config); @@ -718,7 +719,7 @@ export class Replayer { } if (this.usingVirtualDom) { const styleEl = this.virtualDom.createElement('style') as RRStyleElement; - styleEl.setDefaultSN(this.virtualDom.notSerializedId); + styleEl.setDefaultSN(this.virtualDom.unserializedId); (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { // push virtual styles diff --git a/packages/rrweb/typings/record/shadow-dom-manager.d.ts b/packages/rrweb/typings/record/shadow-dom-manager.d.ts index 871fd44e2d..ee8b1ca7e0 100644 --- a/packages/rrweb/typings/record/shadow-dom-manager.d.ts +++ b/packages/rrweb/typings/record/shadow-dom-manager.d.ts @@ -7,6 +7,7 @@ export declare class ShadowDomManager { private scrollCb; private bypassOptions; private mirror; + private restorePatches; constructor(options: { mutationCb: mutationCallBack; scrollCb: scrollCallback; @@ -14,5 +15,7 @@ export declare class ShadowDomManager { mirror: Mirror; }); addShadowRoot(shadowRoot: ShadowRoot, doc: Document): void; + observeAttachShadow(iframeElement: HTMLIFrameElement): void; + reset(): void; } export {}; diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index 4fcef991f1..1bcd5b8a97 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -33,7 +33,7 @@ export declare type AppendedIframe = { export declare function isIframeINode(node: INode | ShadowRoot | RRNode): node is HTMLIFrameINode; export declare function isRRIFrameElement(node: INode | ShadowRoot | RRNode): node is RRIFrameElement; export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension; -export declare function hasShadowRoot(n: T): n is T & { +export declare function hasShadowRoot(n: T): n is T & { shadowRoot: ShadowRoot; }; export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule; From 6096651f478af7c7402cc6b63e91761920cfef8e Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 28 Mar 2022 00:44:22 +1100 Subject: [PATCH 54/79] adapt changes of #865 to virtual-dom and improve the test case for more coverage --- packages/rrdom/src/diff.ts | 59 ++++++++++--------- packages/rrweb/src/replay/index.ts | 9 +++ .../test/events/style-sheet-rule-events.ts | 32 +++++----- packages/rrweb/test/replayer.test.ts | 5 +- 4 files changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 90497ee575..23a6c0e707 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -89,6 +89,36 @@ export function diff( newTree: IRRNode, replayer: ReplayerHandler, ) { + const oldChildren = oldTree.childNodes; + const newChildren = newTree.childNodes; + if (oldChildren.length > 0 || newChildren.length > 0) { + diffChildren( + (Array.from(oldChildren) as unknown) as INode[], + newChildren, + oldTree, + replayer, + ); + } + // IFrame element doesn't have child nodes. + if ( + newTree.RRNodeType === RRNodeType.Element && + (newTree as IRRElement).tagName === 'IFRAME' + ) { + const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) + .contentDocument as unknown) as INode; + const newIFrameElement = newTree as RRIFrameElement; + // If the iframe is cross-origin, the contentDocument will be null. + if (oldContentDocument) { + if (newIFrameElement.contentDocument.__sn) { + oldContentDocument.__sn = newIFrameElement.contentDocument.__sn; + replayer.mirror.map[ + newIFrameElement.contentDocument.__sn.id + ] = oldContentDocument; + } + diff(oldContentDocument, newIFrameElement.contentDocument, replayer); + } + } + let inputDataToApply = null, scrollDataToApply = null; switch (newTree.RRNodeType) { @@ -162,35 +192,6 @@ export function diff( break; default: } - const oldChildren = oldTree.childNodes; - const newChildren = newTree.childNodes; - if (oldChildren.length > 0 || newChildren.length > 0) { - diffChildren( - (Array.from(oldChildren) as unknown) as INode[], - newChildren, - oldTree, - replayer, - ); - } - // IFrame element doesn't have child nodes. - if ( - newTree.RRNodeType === RRNodeType.Element && - (newTree as IRRElement).tagName === 'IFRAME' - ) { - const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) - .contentDocument as unknown) as INode; - const newIFrameElement = newTree as RRIFrameElement; - // If the iframe is cross-origin, the contentDocument will be null. - if (oldContentDocument) { - if (newIFrameElement.contentDocument.__sn) { - oldContentDocument.__sn = newIFrameElement.contentDocument.__sn; - replayer.mirror.map[ - newIFrameElement.contentDocument.__sn.id - ] = oldContentDocument; - } - diff(oldContentDocument, newIFrameElement.contentDocument, replayer); - } - } scrollDataToApply && replayer.applyScroll(scrollDataToApply, true); /** diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 25da8579d6..bfc440f720 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1567,6 +1567,15 @@ export class Replayer { return this.warnNodeNotFound(d, mutation.id); } target.textContent = mutation.value; + + /** + * https://github.com/rrweb-io/rrweb/pull/865 + * Remove any virtual style rules for stylesheets whose contents are replaced. + */ + if (this.usingVirtualDom) { + const parent = target.parentNode as RRStyleElement; + if (parent?.rules?.length > 0) parent.rules = []; + } }); d.attributes.forEach((mutation) => { let target = mirror.getNode(mutation.id); diff --git a/packages/rrweb/test/events/style-sheet-rule-events.ts b/packages/rrweb/test/events/style-sheet-rule-events.ts index 0536ea56e3..e5da5a2769 100644 --- a/packages/rrweb/test/events/style-sheet-rule-events.ts +++ b/packages/rrweb/test/events/style-sheet-rule-events.ts @@ -105,22 +105,6 @@ const events: eventWithTime[] = [ type: EventType.FullSnapshot, timestamp: now + 100, }, - // mutation that adds style rule to existing stylesheet - { - data: { - id: 101, - adds: [ - { - rule: - '.css-added-at-400-overwritten-at-3000 {border: 1px solid blue;}', - index: 1, - }, - ], - source: IncrementalSource.StyleSheetRule, - }, - type: EventType.IncrementalSnapshot, - timestamp: now + 400, - }, // mutation that adds stylesheet { data: { @@ -154,6 +138,22 @@ const events: eventWithTime[] = [ attributes: [], }, type: EventType.IncrementalSnapshot, + timestamp: now + 400, + }, + // mutation that adds style rule to existing stylesheet + { + data: { + id: 101, + adds: [ + { + rule: + '.css-added-at-400-overwritten-at-3000 {border: 1px solid blue;}', + index: 1, + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, timestamp: now + 500, }, // adds StyleSheetRule diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index c8fd3fb493..423997429e 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -251,7 +251,10 @@ describe('replayer', function () { const rules = [...replayer.iframe.contentDocument.styleSheets].map( (sheet) => [...sheet.rules], ).flat(); - rules.some((x) => x.selectorText === '.css-added-at-3100'); + rules.some((x) => x.selectorText === '.css-added-at-3100') && + !rules.some( + (x) => x.selectorText === '.css-added-at-400-overwritten-at-3000', + ); `); expect(result).toEqual(true); From bfbf5d3bb21b3e2d3d893bb8be8726203ce13b60 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 28 Mar 2022 08:50:47 +1100 Subject: [PATCH 55/79] apply review suggestions https://github.com/rrweb-io/rrweb/pull/853#pullrequestreview-922474953 --- packages/rrweb/test/__snapshots__/replayer.test.ts.snap | 4 ++-- packages/rrweb/test/events/style-sheet-rule-events.ts | 4 ++-- packages/rrweb/test/replayer.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap index c471f9d958..45f9b9e3d4 100644 --- a/packages/rrweb/test/__snapshots__/replayer.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/replayer.test.ts.snap @@ -56,7 +56,7 @@ html.rrweb-paused *, html.rrweb-paused ::before, html.rrweb-paused ::after { ani file-cid-1 @charset \\"utf-8\\"; -.css-added-at-500 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } +.css-added-at-400 { padding: 1.3125rem; flex: 0 0 auto; width: 100%; } file-cid-2 @@ -64,7 +64,7 @@ file-cid-2 .css-added-at-200-overwritten-at-3000 { opacity: 1; transform: translateX(0px); } -.css-added-at-400-overwritten-at-3000 { border: 1px solid blue; } +.css-added-at-500-overwritten-at-3000 { border: 1px solid blue; } file-cid-3 diff --git a/packages/rrweb/test/events/style-sheet-rule-events.ts b/packages/rrweb/test/events/style-sheet-rule-events.ts index e5da5a2769..19a80bbd8a 100644 --- a/packages/rrweb/test/events/style-sheet-rule-events.ts +++ b/packages/rrweb/test/events/style-sheet-rule-events.ts @@ -126,7 +126,7 @@ const events: eventWithTime[] = [ type: 3, isStyle: true, textContent: - '\n.css-added-at-500 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n', + '\n.css-added-at-400 {\n padding: 1.3125rem;\n flex: none;\n width: 100%;\n}\n', }, nextId: null, parentId: 255, @@ -147,7 +147,7 @@ const events: eventWithTime[] = [ adds: [ { rule: - '.css-added-at-400-overwritten-at-3000 {border: 1px solid blue;}', + '.css-added-at-500-overwritten-at-3000 {border: 1px solid blue;}', index: 1, }, ], diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index 423997429e..f79e249b6a 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -253,7 +253,7 @@ describe('replayer', function () { ).flat(); rules.some((x) => x.selectorText === '.css-added-at-3100') && !rules.some( - (x) => x.selectorText === '.css-added-at-400-overwritten-at-3000', + (x) => x.selectorText === '.css-added-at-500-overwritten-at-3000', ); `); From 97b373a43a9181618e8d4e1a39d03a1e1c106630 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Wed, 30 Mar 2022 13:23:52 +1100 Subject: [PATCH 56/79] tweak the diff algorithm --- packages/rrdom/src/diff.ts | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 23a6c0e707..5b6bc064af 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -99,25 +99,6 @@ export function diff( replayer, ); } - // IFrame element doesn't have child nodes. - if ( - newTree.RRNodeType === RRNodeType.Element && - (newTree as IRRElement).tagName === 'IFRAME' - ) { - const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) - .contentDocument as unknown) as INode; - const newIFrameElement = newTree as RRIFrameElement; - // If the iframe is cross-origin, the contentDocument will be null. - if (oldContentDocument) { - if (newIFrameElement.contentDocument.__sn) { - oldContentDocument.__sn = newIFrameElement.contentDocument.__sn; - replayer.mirror.map[ - newIFrameElement.contentDocument.__sn.id - ] = oldContentDocument; - } - diff(oldContentDocument, newIFrameElement.contentDocument, replayer); - } - } let inputDataToApply = null, scrollDataToApply = null; @@ -199,6 +180,26 @@ export function diff( * Otherwise when we set a value for a select element whose options are empty, the value won't actually update. */ inputDataToApply && replayer.applyInput(inputDataToApply); + + // IFrame element doesn't have child nodes. + if ( + newTree.RRNodeType === RRNodeType.Element && + (newTree as IRRElement).tagName === 'IFRAME' + ) { + const oldContentDocument = (((oldTree as Node) as HTMLIFrameElement) + .contentDocument as unknown) as INode; + const newIFrameElement = newTree as RRIFrameElement; + // If the iframe is cross-origin, the contentDocument will be null. + if (oldContentDocument) { + if (newIFrameElement.contentDocument.__sn) { + oldContentDocument.__sn = newIFrameElement.contentDocument.__sn; + replayer.mirror.map[ + newIFrameElement.contentDocument.__sn.id + ] = oldContentDocument; + } + diff(oldContentDocument, newIFrameElement.contentDocument, replayer); + } + } } function diffProps(oldTree: HTMLElement, newTree: IRRElement) { From 5743b32f14ff53db0e90af7ae71657f0f6bad7a4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sat, 2 Apr 2022 16:41:41 +1100 Subject: [PATCH 57/79] add description of the flag useVirtualDom and remove outdated logConfig --- guide.md | 79 +++++++++++++++++++++++++------------------------- guide.zh_CN.md | 3 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/guide.md b/guide.md index b95b4c2da3..2c697ebc1f 100644 --- a/guide.md +++ b/guide.md @@ -135,30 +135,30 @@ setInterval(save, 10 * 1000); The parameter of `rrweb.record` accepts the following options. -| key | default | description | -| -------------------- | ------------------ | ------------------------------------------------------------ | -| emit | required | the callback function to get emitted events | -| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | -| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | -| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | -| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | -| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | -| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | -| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | -| maskAllInputs | false | mask all input content as \* | -| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | -| maskInputFn | - | customize mask input content recording logic | -| maskTextFn | - | customize mask text content recording logic | -| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | -| inlineStylesheet | true | whether to inline the stylesheet in the events | -| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | -| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| recordCanvas | false | whether to record the canvas element | -| inlineImages | false | whether to record the image content | -| collectFonts | false | whether to collect fonts in the website | +| key | default | description | +| -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| emit | required | the callback function to get emitted events | +| checkoutEveryNth | - | take a full snapshot after every N events
refer to the [checkout](#checkout) chapter | +| checkoutEveryNms | - | take a full snapshot after every N ms
refer to the [checkout](#checkout) chapter | +| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | +| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | +| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | +| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | +| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | +| maskAllInputs | false | mask all input content as \* | +| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) | +| maskInputFn | - | customize mask input content recording logic | +| maskTextFn | - | customize mask text content recording logic | +| slimDOMOptions | {} | remove unnecessary parts of the DOM
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) | +| inlineStylesheet | true | whether to inline the stylesheet in the events | +| hooks | {} | hooks for events
refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | +| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| recordCanvas | false | whether to record the canvas element | +| inlineImages | false | whether to record the image content | +| collectFonts | false | whether to collect fonts in the website | | userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | -| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | +| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | #### Privacy @@ -286,23 +286,24 @@ replayer.pause(5000); The replayer accepts options as its constructor's second parameter, and it has the following options: -| key | default | description | -| ------------------- | ------------- | ------------------------------------------------------------ | -| speed | 1 | replay speed ratio | -| root | document.body | the root element of replayer | -| loadTimeout | 0 | timeout of loading remote style sheet | -| skipInactive | false | whether to skip inactive time | -| showWarning | true | whether to print warning messages during replay | -| showDebug | false | whether to print debug messages during replay | -| blockClass | 'rr-block' | element with the class name will display as a blocked area | -| liveMode | false | whether to enable live mode | -| insertStyleRules | [] | accepts multiple CSS rule string, which will be injected into the replay iframe | -| triggerFocus | true | whether to trigger focus during replay | -| UNSAFE_replayCanvas | false | whether to replay the canvas element. **Enable this will remove the sandbox, which is unsafe.** | +| key | default | description | +| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| speed | 1 | replay speed ratio | +| root | document.body | the root element of replayer | +| loadTimeout | 0 | timeout of loading remote style sheet | +| skipInactive | false | whether to skip inactive time | +| showWarning | true | whether to print warning messages during replay | +| showDebug | false | whether to print debug messages during replay | +| blockClass | 'rr-block' | element with the class name will display as a blocked area | +| liveMode | false | whether to enable live mode | +| insertStyleRules | [] | accepts multiple CSS rule string, which will be injected into the replay iframe | +| triggerFocus | true | whether to trigger focus during replay | +| UNSAFE_replayCanvas | false | whether to replay the canvas element. **Enable this will remove the sandbox, which is unsafe.** | +| pauseAnimation | true | whether to pause CSS animation when the replayer is paused | | mouseTail | true | whether to show mouse tail during replay. Set to false to disable mouse tail. A complete config can be found in this [type](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) | -| unpackFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | -| logConfig | - | configuration of console output playback, refer to the [console recipe](./docs/recipes/console.md) | -| plugins | [] | load plugins to provide extended replay functions. [What is plugins?](./docs/recipes/plugin.md) | +| unpackFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) | +| plugins | [] | load plugins to provide extended replay functions. [What is plugins?](./docs/recipes/plugin.md) | +| useVirtualDom | true | whether to use Virtual Dom optimization in the process of skipping to a new point of time | #### Use rrweb-player diff --git a/guide.zh_CN.md b/guide.zh_CN.md index bdb0a93305..4bd66e5171 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -295,10 +295,11 @@ replayer.pause(5000); | insertStyleRules | [] | 可以传入多个 CSS rule string,用于自定义回放时 iframe 内的样式 | | triggerFocus | true | 回放时是否回放 focus 交互 | | UNSAFE_replayCanvas | false | 回放时是否回放 canvas 内容,**开启后将会关闭沙盒策略,导致一定风险** | +| pauseAnimation | true | 当播放器停止播放时,是否将 CSS 动画也停止播放 | | mouseTail | true | 是否在回放时增加鼠标轨迹。传入 false 可关闭,传入对象可以定制轨迹持续时间、样式等,配置详见[类型](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L407) | | unpackFn | - | 数据解压缩函数,详见[优化存储策略](./docs/recipes/optimize-storage.zh_CN.md) | -| logConfig | - | console logger 数据播放设置,详见[console 录制和播放](./docs/recipes/console.zh_CN.md) | | plugins | [] | 加载插件以获得额外的回放功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) | +| useVirtualDom | true | 在播放器跳转到一个新的时间点的过程中,是否使用 Virtual Dom 优化 | #### 使用 rrweb-player From 551dd64856ff2cb0621e6746706116760ea52711 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 29 Apr 2022 17:39:38 +0200 Subject: [PATCH 58/79] Remove console.log --- packages/rrdom/test/diff.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index dd48b26cd6..ee10241493 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -1047,10 +1047,7 @@ describe('diff algorithm for rrdom', () => { id: 2, } as serializedNodeWithId); - console.log('pre diff'); diff(node, rrNode, replayer); - console.log('post diff'); - console.log(node.contentDocument, node.contentDocument!.documentElement); expect(node.contentDocument!.childNodes.length).toBe(1); const element = node.contentDocument!.childNodes[0] as HTMLElement; expect(element.tagName).toBe('DIV'); From 8e263d3838ca0c1806e6548d72cbc08bb20c591a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 29 Apr 2022 17:53:47 +0200 Subject: [PATCH 59/79] Contain changes to document --- packages/rrdom/test/virtual-dom.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 98e0bff440..25f7cf68cf 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -7,6 +7,7 @@ import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import * as typescript from 'rollup-plugin-typescript2'; +import { JSDOM } from 'jsdom'; import { Mirror, NodeType, NodeType as RRNodeType } from 'rrweb-snapshot'; import { buildFromDom, @@ -48,8 +49,11 @@ describe('RRDocument for browser environment', () => { describe('create a RRNode from a real Node', () => { it('should support quicksmode documents', () => { - document.doctype?.remove(); - expect(document.compatMode).toBe('BackCompat'); + // seperate jsdom document as changes to the document would otherwise bleed into other tests + const dom = new JSDOM(); + const document = dom.window.document; + + expect(document.doctype).toBeNull(); // confirm compatMode is 'BackCompat' in JSDOM const rrdom = new RRDocument(); let rrNode = buildFromNode(document, rrdom, mirror)!; From b1be81a2a76565935c9dc391f31beb7f64d25956 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 29 Apr 2022 18:11:44 +0200 Subject: [PATCH 60/79] Upgrade rollup to 2.70.2 --- packages/rrweb/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 69731d7027..8d63d7fbdd 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -60,7 +60,7 @@ "jest-snapshot": "^23.6.0", "prettier": "2.2.1", "puppeteer": "^9.1.1", - "rollup": "^2.68.0", + "rollup": "^2.70.2", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-rename-node-modules": "^1.3.1", "rollup-plugin-terser": "^7.0.2", diff --git a/yarn.lock b/yarn.lock index 92c830f0e6..de4c1b6fbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9778,10 +9778,10 @@ rollup@^2.56.3: optionalDependencies: fsevents "~2.3.2" -rollup@^2.68.0: - version "2.68.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.68.0.tgz#6ccabfd649447f8f21d62bf41662e5caece3bd66" - integrity sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA== +rollup@^2.70.2: + version "2.70.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d" + integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg== optionalDependencies: fsevents "~2.3.2" From aded41c58b18d42278b8117ea7f3c72d53ac5326 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 29 Apr 2022 18:24:11 +0200 Subject: [PATCH 61/79] Revert "Upgrade rollup to 2.70.2" This reverts commit b1be81a2a76565935c9dc391f31beb7f64d25956. --- packages/rrweb/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 8d63d7fbdd..69731d7027 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -60,7 +60,7 @@ "jest-snapshot": "^23.6.0", "prettier": "2.2.1", "puppeteer": "^9.1.1", - "rollup": "^2.70.2", + "rollup": "^2.68.0", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-rename-node-modules": "^1.3.1", "rollup-plugin-terser": "^7.0.2", diff --git a/yarn.lock b/yarn.lock index de4c1b6fbe..92c830f0e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9778,10 +9778,10 @@ rollup@^2.56.3: optionalDependencies: fsevents "~2.3.2" -rollup@^2.70.2: - version "2.70.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d" - integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg== +rollup@^2.68.0: + version "2.68.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.68.0.tgz#6ccabfd649447f8f21d62bf41662e5caece3bd66" + integrity sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA== optionalDependencies: fsevents "~2.3.2" From be4c35cb77ef1589503a07ab413df3410426fbc9 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 29 Apr 2022 18:32:03 +0200 Subject: [PATCH 62/79] Fix type checking rrdom --- packages/rrdom/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index cf3825f500..5fb330030b 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -16,5 +16,5 @@ }, "compileOnSave": true, "exclude": ["test"], - "include": ["src", "test.d.ts"] + "include": ["src", "test.d.ts", "../rrweb/src/record/workers/workers.d.ts"] } From c62be20eb3d99135381e0d123fab9117095bdd89 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Sat, 30 Apr 2022 16:11:34 +0200 Subject: [PATCH 63/79] Fix typing error while bundling --- packages/rrdom/package.json | 1 + packages/rrdom/rollup.config.js | 5 ++ packages/rrdom/src/diff.ts | 7 +-- packages/rrdom/src/virtual-dom.ts | 7 +-- packages/rrweb-player/package.json | 12 +++-- packages/rrweb-player/rollup.config.js | 5 ++ packages/rrweb-player/tsconfig.json | 9 +++- packages/rrweb/rollup.config.js | 6 +++ packages/rrweb/src/replay/index.ts | 14 +++--- packages/rrweb/src/types.ts | 5 ++ packages/rrweb/typings/types.d.ts | 4 ++ yarn.lock | 69 +++++++++++++++----------- 12 files changed, 92 insertions(+), 52 deletions(-) diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 9f88f1b56d..bfe3459503 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -38,6 +38,7 @@ "rollup": "^2.56.3", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.2", + "rollup-plugin-web-worker-loader": "^1.6.1", "rrweb-snapshot": "^1.1.14", "ts-jest": "^27.1.3", "typescript": "^4.6.2" diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index ecb174a91c..fe1d74a15b 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -2,6 +2,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; import typescript from 'rollup-plugin-typescript2'; +import webWorkerLoader from 'rollup-plugin-web-worker-loader'; import pkg from './package.json'; function toMinPath(path) { @@ -11,6 +12,10 @@ function toMinPath(path) { const basePlugins = [ resolve({ browser: true }), commonjs(), + + // supports bundling `web-worker:..filename` from rrweb + webWorkerLoader(), + typescript({ tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, }), diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 46017f7c7c..3caed0523b 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -1,7 +1,7 @@ import { NodeType as RRNodeType, Mirror as NodeMirror } from 'rrweb-snapshot'; import type { canvasMutationData, - incrementalSnapshotEvent, + canvasEventWithTime, inputData, scrollData, } from 'rrweb/src/types'; @@ -73,10 +73,7 @@ const SVGTagMap: Record = { export type ReplayerHandler = { mirror: NodeMirror; applyCanvas: ( - canvasEvent: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; - }, + canvasEvent: canvasEventWithTime, canvasMutationData: canvasMutationData, target: HTMLCanvasElement, ) => void; diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index ed2db0c828..d563ccfc2a 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -6,7 +6,7 @@ import { import type { Mirror as NodeMirror } from 'rrweb-snapshot'; import type { canvasMutationData, - incrementalSnapshotEvent, + canvasEventWithTime, inputData, scrollData, } from 'rrweb/src/types'; @@ -126,10 +126,7 @@ export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} export class RRCanvasElement extends RRElement implements IRRElement { public canvasMutations: { - event: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; - }; + event: canvasEventWithTime; mutation: canvasMutationData; }[] = []; /** diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 55a830e42f..2824bb6170 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -2,26 +2,28 @@ "name": "rrweb-player", "version": "0.7.14", "devDependencies": { - "@rollup/plugin-commonjs": "^21.0.2", - "@rollup/plugin-node-resolve": "^7.0.0", - "@rollup/plugin-typescript": "^4.0.0", + "@rollup/plugin-commonjs": "^22.0.0", + "@rollup/plugin-node-resolve": "^13.2.1", + "@rollup/plugin-typescript": "^8.3.2", + "@types/offscreencanvas": "^2019.6.4", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "eslint": "^7.5.0", "eslint-config-google": "^0.11.0", "eslint-plugin-svelte3": "^2.7.3", "postcss-easy-import": "^3.0.0", - "rollup": "^2.45.2", + "rollup": "^2.71.1", "rollup-plugin-css-only": "^3.1.0", "rollup-plugin-livereload": "^2.0.0", "rollup-plugin-svelte": "^7.1.0", "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-web-worker-loader": "^1.6.1", "sirv-cli": "^0.4.4", "svelte": "^3.2.0", "svelte-check": "^1.4.0", "svelte-preprocess": "^4.0.0", "tslib": "^2.0.0", - "typescript": "^3.9.7" + "typescript": "^4.6.4" }, "dependencies": { "@tsconfig/svelte": "^1.0.0", diff --git a/packages/rrweb-player/rollup.config.js b/packages/rrweb-player/rollup.config.js index 313b8e1133..403bf883d2 100644 --- a/packages/rrweb-player/rollup.config.js +++ b/packages/rrweb-player/rollup.config.js @@ -4,6 +4,7 @@ import commonjs from '@rollup/plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; import { terser } from 'rollup-plugin-terser'; import sveltePreprocess from 'svelte-preprocess'; +import webWorkerLoader from 'rollup-plugin-web-worker-loader'; import typescript from '@rollup/plugin-typescript'; import pkg from './package.json'; import css from 'rollup-plugin-css-only'; @@ -64,8 +65,12 @@ export default entries.map((output) => ({ browser: true, dedupe: ['svelte'], }), + commonjs(), + // supports bundling `web-worker:..filename` from rrweb + webWorkerLoader(), + typescript(), css({ diff --git a/packages/rrweb-player/tsconfig.json b/packages/rrweb-player/tsconfig.json index 77db65da2f..b049c15dd8 100644 --- a/packages/rrweb-player/tsconfig.json +++ b/packages/rrweb-player/tsconfig.json @@ -1,5 +1,10 @@ { "extends": "@tsconfig/svelte/tsconfig.json", "include": ["src/**/*"], - "exclude": ["node_modules/*", "__sapper__/*", "public/*"], -} \ No newline at end of file + "exclude": [ + "node_modules/*", + "__sapper__/*", + "public/*", + "../rrweb/src/record/workers/workers.d.ts" + ] +} diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 3c2eff47a1..690c97eada 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -111,7 +111,10 @@ let configs = []; for (const c of baseConfigs) { const basePlugins = [ resolve({ browser: true }), + + // supports bundling `web-worker:..filename` webWorkerLoader(), + typescript(), ]; const plugins = basePlugins.concat( @@ -199,7 +202,10 @@ if (process.env.BROWSER_ONLY) { for (const c of browserOnlyBaseConfigs) { const plugins = [ resolve({ browser: true }), + + // supports bundling `web-worker:..filename` webWorkerLoader(), + typescript({ outDir: null, }), diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index d9721d2caf..9abf1f46b1 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -56,6 +56,7 @@ import { IWindow, canvasMutationCommand, canvasMutationParam, + canvasEventWithTime, } from '../types'; import { polyfill, @@ -72,6 +73,7 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; +import type { ReplayerHandler } from 'rrdom/es/diff'; const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -175,13 +177,10 @@ export class Replayer { this.emitter.on(ReplayerEvents.Flush, () => { if (this.usingVirtualDom) { - const replayerHandler = { + const replayerHandler: ReplayerHandler = { mirror: this.mirror, applyCanvas: ( - canvasEvent: incrementalSnapshotEvent & { - timestamp: number; - delay?: number | undefined; - }, + canvasEvent: canvasEventWithTime, canvasMutationData: canvasMutationData, target: HTMLCanvasElement, ) => { @@ -1310,7 +1309,10 @@ export class Replayer { if (!target) { return this.debugNodeNotFound(d, d.id); } - target.canvasMutations.push({ event: e, mutation: d }); + target.canvasMutations.push({ + event: e as canvasEventWithTime, + mutation: d, + }); } else { const target = this.mirror.getNode(d.id); if (!target) { diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index f0d5914dc9..a74a091c75 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -170,6 +170,11 @@ export type eventWithTime = event & { delay?: number; }; +export type canvasEventWithTime = eventWithTime & { + type: EventType.IncrementalSnapshot; + data: canvasMutationData; +}; + export type blockClass = string | RegExp; export type maskTextClass = string | RegExp; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 8fc6ff7ba4..61f3d8b84c 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -116,6 +116,10 @@ export declare type eventWithTime = event & { timestamp: number; delay?: number; }; +export declare type canvasEventWithTime = eventWithTime & { + type: EventType.IncrementalSnapshot; + data: canvasMutationData; +}; export declare type blockClass = string | RegExp; export declare type maskTextClass = string | RegExp; export declare type SamplingStrategy = Partial<{ diff --git a/yarn.lock b/yarn.lock index 92c830f0e6..7204c4b33f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,10 +1770,10 @@ magic-string "^0.25.7" resolve "^1.17.0" -"@rollup/plugin-commonjs@^21.0.2": - version "21.0.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.2.tgz#0b9c539aa1837c94abfaf87945838b0fc8564891" - integrity sha512-d/OmjaLVO4j/aQX69bwpWPpbvI3TJkQuxoAk7BH8ew1PyoMBLTOuvJTjzG8oEoW7drIIqB0KCJtfFLu/2GClWg== +"@rollup/plugin-commonjs@^22.0.0": + version "22.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.0.tgz#f4d87016e2fbf187a593ab9f46626fe05b59e8bd" + integrity sha512-Ktvf2j+bAO+30awhbYoCaXpBcyPmJbaEUYClQns/+6SNCYFURbvBiNbWgHITEsIgDDWCDUclWRKEuf8cwZCFoQ== dependencies: "@rollup/pluginutils" "^3.1.0" commondir "^1.0.1" @@ -1807,24 +1807,17 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-node-resolve@^7.0.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca" - integrity sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q== +"@rollup/plugin-node-resolve@^13.2.1": + version "13.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.2.1.tgz#cdee815cf02c180ff0a42536ca67a8f67e299f84" + integrity sha512-btX7kzGvp1JwShQI9V6IM841YKNPYjKCvUbNrQ2EcVYbULtUd/GH6wZ/qdqH13j9pOHBER+EZXNN2L8RSJhVRA== dependencies: - "@rollup/pluginutils" "^3.0.8" - "@types/resolve" "0.0.8" + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" builtin-modules "^3.1.0" + deepmerge "^4.2.2" is-module "^1.0.0" - resolve "^1.14.2" - -"@rollup/plugin-typescript@^4.0.0": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-4.1.2.tgz#6f910430276ae3e53a47a12ad65820627e7b6ad9" - integrity sha512-+7UlGat/99e2JbmGNnIauxwEhYLwrL7adO/tSJxUN57xrrS3Ps+ZzYpLCDGPZJ57j+ZJTZLLN89KXW9JMEB+jg== - dependencies: - "@rollup/pluginutils" "^3.0.1" - resolve "^1.14.1" + resolve "^1.19.0" "@rollup/plugin-typescript@^8.2.5": version "8.2.5" @@ -1834,6 +1827,14 @@ "@rollup/pluginutils" "^3.1.0" resolve "^1.17.0" +"@rollup/plugin-typescript@^8.3.2": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.3.2.tgz#e1b719e2ed3e752bbc092001656c48378f2d15f0" + integrity sha512-MtgyR5LNHZr3GyN0tM7gNO9D0CS+Y+vflS4v/PHmrX17JCkHUYKvQ5jN5o3cz1YKllM3duXUqu3yOHwMPUxhDg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + "@rollup/pluginutils@4": version "4.1.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" @@ -1842,7 +1843,7 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^3.0.1", "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": +"@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== @@ -2135,13 +2136,6 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== -"@types/resolve@0.0.8": - version "0.0.8" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" - integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== - dependencies: - "@types/node" "*" - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -3889,7 +3883,12 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4, cssom@^0.5.0: +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== @@ -9613,7 +9612,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.1, resolve@^1.14.2, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.16.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -9785,6 +9784,13 @@ rollup@^2.68.0: optionalDependencies: fsevents "~2.3.2" +rollup@^2.71.1: + version "2.71.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.71.1.tgz#82b259af7733dfd1224a8171013aaaad02971a22" + integrity sha512-lMZk3XfUBGjrrZQpvPSoXcZSfKcJ2Bgn+Z0L1MoW2V8Wh7BVM+LOBJTPo16yul2MwL59cXedzW1ruq3rCjSRgw== + optionalDependencies: + fsevents "~2.3.2" + run-async@^2.2.0, run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -10916,6 +10922,11 @@ typescript@^4.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@^4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== + uglify-js@^3.1.4: version "3.14.1" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06" From 6105a7476fc8b01ce8bf29244999065cffa90c6a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Sun, 1 May 2022 09:06:14 +0200 Subject: [PATCH 64/79] Fix tslib error on build Rollup would output the following error: `semantic error TS2343: This syntax requires an imported helper named '__spreadArray' which does not exist in 'tslib'. Consider upgrading your version of 'tslib'.` --- packages/rrdom/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index 5fb330030b..4a4f18a080 100644 --- a/packages/rrdom/tsconfig.json +++ b/packages/rrdom/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES5", + "target": "ES6", "module": "commonjs", "noImplicitAny": true, "strictNullChecks": true, From 61e1a5d323212ca8fbe0569e0b3062ddd53fc612 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Sun, 1 May 2022 11:31:10 +0200 Subject: [PATCH 65/79] Increase memory limit for rollup --- .travis.yml | 1 + package.json | 5 ++++- yarn.lock | 30 ++++++++++++++++-------------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9d5753868e..7fe24db2e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ node_js: install: - yarn + - yarn increase-memory-limit script: - yarn lerna run prepublish diff --git a/package.json b/package.json index 342273e6c3..45d28a4d56 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "packages/rrdom" ], "devDependencies": { + "cross-env": "^7.0.3", + "increase-memory-limit": "^1.0.7", "lerna": "^4.0.0" }, "scripts": { @@ -29,7 +31,8 @@ "test": "yarn lerna run test", "test:watch": "yarn lerna run test:watch --parallel", "dev": "yarn lerna run dev --parallel", - "repl": "cd packages/rrweb && npm run repl" + "repl": "cd packages/rrweb && npm run repl", + "increase-memory-limit": "cross-env LIMIT=4096 increase-memory-limit" }, "resolutions": { "**/jsdom/cssom": "^0.5.0" diff --git a/yarn.lock b/yarn.lock index 7204c4b33f..0235dfd091 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3684,6 +3684,13 @@ cross-env@^5.2.0: dependencies: cross-spawn "^6.0.5" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3704,7 +3711,7 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3883,12 +3890,7 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@^0.5.0: +cssom@^0.4.4, cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== @@ -5602,6 +5604,13 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +increase-memory-limit@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/increase-memory-limit/-/increase-memory-limit-1.0.7.tgz#80417e736e45fcfd6a3d515b435e2eaea4e62233" + integrity sha512-ozyn+HHAPD9VxMT1U50A7G8XXlktUWhnnEDYSeDYnQjKsNrBWHqq6XfEA0uhMdDPD+q/7rXWlzF1CbXX/c1LiQ== + dependencies: + glob "^7.1.1" + indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -9777,13 +9786,6 @@ rollup@^2.56.3: optionalDependencies: fsevents "~2.3.2" -rollup@^2.68.0: - version "2.68.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.68.0.tgz#6ccabfd649447f8f21d62bf41662e5caece3bd66" - integrity sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA== - optionalDependencies: - fsevents "~2.3.2" - rollup@^2.71.1: version "2.71.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.71.1.tgz#82b259af7733dfd1224a8171013aaaad02971a22" From 85d600a20c56cfa764cf1f858932ba14e67b1d23 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 2 May 2022 11:37:28 +0200 Subject: [PATCH 66/79] Use esbuild for bundling Speeds up bundling significantly --- .travis.yml | 1 - package.json | 5 +- packages/rrdom/package.json | 6 +- packages/rrdom/rollup.config.js | 12 +- packages/rrdom/src/virtual-dom.ts | 7 +- packages/rrdom/test/virtual-dom.test.ts | 10 +- packages/rrweb-player/package.json | 4 +- packages/rrweb-player/rollup.config.js | 7 +- packages/rrweb-snapshot/package.json | 6 +- packages/rrweb-snapshot/rollup.config.js | 21 +- .../rrweb-snapshot/test/integration.test.ts | 10 +- packages/rrweb/package.json | 6 +- packages/rrweb/rollup.config.js | 13 +- packages/rrweb/tsconfig.json | 4 +- yarn.lock | 372 ++++++++---------- 15 files changed, 220 insertions(+), 264 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7fe24db2e6..9d5753868e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ node_js: install: - yarn - - yarn increase-memory-limit script: - yarn lerna run prepublish diff --git a/package.json b/package.json index 45d28a4d56..342273e6c3 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,6 @@ "packages/rrdom" ], "devDependencies": { - "cross-env": "^7.0.3", - "increase-memory-limit": "^1.0.7", "lerna": "^4.0.0" }, "scripts": { @@ -31,8 +29,7 @@ "test": "yarn lerna run test", "test:watch": "yarn lerna run test:watch --parallel", "dev": "yarn lerna run dev --parallel", - "repl": "cd packages/rrweb && npm run repl", - "increase-memory-limit": "cross-env LIMIT=4096 increase-memory-limit" + "repl": "cd packages/rrweb && npm run repl" }, "resolutions": { "**/jsdom/cssom": "^0.5.0" diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index bfe3459503..651491c14e 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -33,11 +33,11 @@ "@types/nwsapi": "^2.2.2", "@types/puppeteer": "^5.4.4", "compare-versions": "^4.1.3", + "esbuild": "^0.14.38", "jest": "^27.5.1", "puppeteer": "^9.1.1", - "rollup": "^2.56.3", - "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.31.2", + "rollup": "^2.71.1", + "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-web-worker-loader": "^1.6.1", "rrweb-snapshot": "^1.1.14", "ts-jest": "^27.1.3", diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index fe1d74a15b..76d69f5515 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -1,7 +1,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import { terser } from 'rollup-plugin-terser'; -import typescript from 'rollup-plugin-typescript2'; +import esbuild, { minify } from 'rollup-plugin-esbuild'; + import webWorkerLoader from 'rollup-plugin-web-worker-loader'; import pkg from './package.json'; @@ -16,9 +16,7 @@ const basePlugins = [ // supports bundling `web-worker:..filename` from rrweb webWorkerLoader(), - typescript({ - tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, - }), + esbuild(), ]; const baseConfigs = [ @@ -70,7 +68,7 @@ for (let config of baseConfigs) { }, { input: config.input, - plugins: basePlugins.concat(terser()), + plugins: basePlugins.concat(minify()), output: [ { name: config.name, @@ -94,7 +92,7 @@ for (let config of baseConfigs) { // ES module (packed) { input: config.input, - plugins: basePlugins.concat(terser()), + plugins: basePlugins.concat(minify()), output: [ { format: 'esm', diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index d563ccfc2a..9d0b64fbf1 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -318,9 +318,4 @@ export function buildFromDom( } export { RRNode }; -export { - diff, - createOrGetNode, - StyleRuleType, - VirtualStyleRules, -} from './diff'; +export { diff, createOrGetNode, StyleRuleType } from './diff'; diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 25f7cf68cf..b9fad03286 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; -import * as typescript from 'rollup-plugin-typescript2'; +import esbuild from 'rollup-plugin-esbuild'; import { JSDOM } from 'jsdom'; import { Mirror, NodeType, NodeType as RRNodeType } from 'rrweb-snapshot'; import { @@ -19,7 +19,6 @@ import { } from '../src/virtual-dom'; import { setDefaultSN } from '../src/document'; -const _typescript = (typescript as unknown) as typeof typescript.default; const printRRDomCode = ` /** * Print the RRDom as a string. @@ -207,12 +206,7 @@ describe('RRDocument for browser environment', () => { browser = await puppeteer.launch(); const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/virtual-dom.ts'), - plugins: [ - resolve(), - _typescript({ - tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, - }), - ], + plugins: [resolve(), esbuild()], }); const { output: [{ code: _code }], diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 2824bb6170..d86775b141 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -4,19 +4,19 @@ "devDependencies": { "@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-node-resolve": "^13.2.1", - "@rollup/plugin-typescript": "^8.3.2", "@types/offscreencanvas": "^2019.6.4", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", + "esbuild": "^0.14.38", "eslint": "^7.5.0", "eslint-config-google": "^0.11.0", "eslint-plugin-svelte3": "^2.7.3", "postcss-easy-import": "^3.0.0", "rollup": "^2.71.1", "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-livereload": "^2.0.0", "rollup-plugin-svelte": "^7.1.0", - "rollup-plugin-terser": "^7.0.2", "rollup-plugin-web-worker-loader": "^1.6.1", "sirv-cli": "^0.4.4", "svelte": "^3.2.0", diff --git a/packages/rrweb-player/rollup.config.js b/packages/rrweb-player/rollup.config.js index 403bf883d2..4af172b5d6 100644 --- a/packages/rrweb-player/rollup.config.js +++ b/packages/rrweb-player/rollup.config.js @@ -2,10 +2,9 @@ import svelte from 'rollup-plugin-svelte'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; -import { terser } from 'rollup-plugin-terser'; import sveltePreprocess from 'svelte-preprocess'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; -import typescript from '@rollup/plugin-typescript'; +import esbuild, { minify } from 'rollup-plugin-esbuild'; import pkg from './package.json'; import css from 'rollup-plugin-css-only'; @@ -71,7 +70,7 @@ export default entries.map((output) => ({ // supports bundling `web-worker:..filename` from rrweb webWorkerLoader(), - typescript(), + esbuild(), css({ // we'll extract any component CSS out into @@ -89,7 +88,7 @@ export default entries.map((output) => ({ // If we're building for production (npm run build // instead of npm run dev), minify - production && terser(), + production && minify(), ], watch: { clearScreen: false, diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index d66d3519ce..19d936625b 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -39,19 +39,19 @@ }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", "devDependencies": { - "@rollup/plugin-typescript": "^8.2.5", "@types/chai": "^4.1.4", "@types/jest": "^27.0.2", "@types/jsdom": "^16.2.4", "@types/node": "^10.11.3", "@types/puppeteer": "^1.12.4", "cross-env": "^5.2.0", + "esbuild": "^0.14.38", "jest": "^27.2.4", "jest-snapshot": "^23.6.0", "jsdom": "^16.4.0", "puppeteer": "^1.15.0", - "rollup": "^2.45.2", - "rollup-plugin-terser": "^7.0.2", + "rollup": "^2.71.1", + "rollup-plugin-esbuild": "^4.9.1", "ts-jest": "^27.0.5", "ts-node": "^7.0.1", "tslib": "^1.9.3", diff --git a/packages/rrweb-snapshot/rollup.config.js b/packages/rrweb-snapshot/rollup.config.js index 6a8b0a0933..8d5b4ff927 100644 --- a/packages/rrweb-snapshot/rollup.config.js +++ b/packages/rrweb-snapshot/rollup.config.js @@ -1,5 +1,4 @@ -import typescript from '@rollup/plugin-typescript'; -import { terser } from 'rollup-plugin-terser'; +import esbuild from 'rollup-plugin-esbuild'; import pkg from './package.json'; function toMinPath(path) { @@ -10,7 +9,7 @@ let configs = [ // ES module - for building rrweb { input: './src/index.ts', - plugins: [typescript()], + plugins: [esbuild()], output: [ { format: 'esm', @@ -23,7 +22,7 @@ let extra_configs = [ // browser { input: './src/index.ts', - plugins: [typescript()], + plugins: [esbuild()], output: [ { name: 'rrwebSnapshot', @@ -34,7 +33,11 @@ let extra_configs = [ }, { input: './src/index.ts', - plugins: [typescript(), terser()], + plugins: [ + esbuild({ + minify: true, + }), + ], output: [ { name: 'rrwebSnapshot', @@ -47,7 +50,7 @@ let extra_configs = [ // CommonJS { input: './src/index.ts', - plugins: [typescript()], + plugins: [esbuild()], output: [ { format: 'cjs', @@ -58,7 +61,11 @@ let extra_configs = [ // ES module (packed) { input: './src/index.ts', - plugins: [typescript(), terser()], + plugins: [ + esbuild({ + minify: true, + }), + ], output: [ { format: 'esm', diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index c34bfefcc1..34db1ee7d7 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -4,11 +4,9 @@ import * as http from 'http'; import * as url from 'url'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; -import * as typescript from '@rollup/plugin-typescript'; +import esbuild from 'rollup-plugin-esbuild'; import * as assert from 'assert'; -const _typescript = (typescript as unknown) as typeof typescript.default; - const htmlFolder = path.join(__dirname, 'html'); const htmls = fs.readdirSync(htmlFolder).map((filePath) => { const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'); @@ -73,7 +71,7 @@ describe('integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [esbuild()], }); const { output: [{ code: _code }], @@ -225,7 +223,7 @@ describe('iframe integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [esbuild()], }); const { output: [{ code: _code }], @@ -274,7 +272,7 @@ describe('shadow DOM integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [_typescript()], + plugins: [esbuild()], }); const { output: [{ code: _code }], diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 69731d7027..7213c9b349 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -51,6 +51,7 @@ "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.4", "cross-env": "^5.2.0", + "esbuild": "^0.14.38", "fast-mhtml": "^1.1.9", "identity-obj-proxy": "^3.0.0", "ignore-styles": "^5.0.1", @@ -60,11 +61,10 @@ "jest-snapshot": "^23.6.0", "prettier": "2.2.1", "puppeteer": "^9.1.1", - "rollup": "^2.68.0", + "rollup": "^2.71.1", + "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-rename-node-modules": "^1.3.1", - "rollup-plugin-terser": "^7.0.2", - "rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-web-worker-loader": "^1.6.1", "ts-jest": "^27.1.3", "ts-node": "^10.7.0", diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 690c97eada..e6405d64bd 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -1,6 +1,5 @@ -import typescript from 'rollup-plugin-typescript2'; +import esbuild, { minify } from 'rollup-plugin-esbuild'; import resolve from '@rollup/plugin-node-resolve'; -import { terser } from 'rollup-plugin-terser'; import postcss from 'rollup-plugin-postcss'; import renameNodeModules from 'rollup-plugin-rename-node-modules'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; @@ -115,7 +114,7 @@ for (const c of baseConfigs) { // supports bundling `web-worker:..filename` webWorkerLoader(), - typescript(), + esbuild(), ]; const plugins = basePlugins.concat( postcss({ @@ -144,7 +143,7 @@ for (const c of baseConfigs) { minimize: true, sourceMap: true, }), - terser(), + minify(), ), output: [ { @@ -206,15 +205,15 @@ if (process.env.BROWSER_ONLY) { // supports bundling `web-worker:..filename` webWorkerLoader(), - typescript({ - outDir: null, + esbuild({ + minify: true, }), + postcss({ extract: false, inject: false, sourceMap: true, }), - terser(), ]; configs.push({ diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index f83abe5354..4be4650eae 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -2,14 +2,14 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "Node", - "target": "ES5", + "target": "ES6", "noImplicitAny": true, "strictNullChecks": true, "removeComments": true, "preserveConstEnums": true, "rootDir": "src", "outDir": "build", - "lib": ["es6", "dom"], + "lib": ["ES6", "DOM"], "downlevelIteration": true, "importsNotUsedAsValues": "error" }, diff --git a/yarn.lock b/yarn.lock index 0235dfd091..58d8434ac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== @@ -1819,22 +1819,6 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-typescript@^8.2.5": - version "8.2.5" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.2.5.tgz#e0319761b2b5105615e5a0c371ae05bc2984b7de" - integrity sha512-QL/LvDol/PAGB2O0S7/+q2HpSUNodpw7z6nGn9BfoVCPOZ0r4EALrojFU29Bkoi2Hr2jgTocTejJ5GGWZfOxbQ== - dependencies: - "@rollup/pluginutils" "^3.1.0" - resolve "^1.17.0" - -"@rollup/plugin-typescript@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.3.2.tgz#e1b719e2ed3e752bbc092001656c48378f2d15f0" - integrity sha512-MtgyR5LNHZr3GyN0tM7gNO9D0CS+Y+vflS4v/PHmrX17JCkHUYKvQ5jN5o3cz1YKllM3duXUqu3yOHwMPUxhDg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - resolve "^1.17.0" - "@rollup/pluginutils@4": version "4.1.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" @@ -1852,10 +1836,10 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^4.1.2": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" - integrity sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ== +"@rollup/pluginutils@^4.1.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== dependencies: estree-walker "^2.0.1" picomatch "^2.2.2" @@ -2352,15 +2336,6 @@ resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== -"@yarn-tool/resolve-package@^1.0.40": - version "1.0.45" - resolved "https://registry.yarnpkg.com/@yarn-tool/resolve-package/-/resolve-package-1.0.45.tgz#4d9716a67903f46a76c8691eff546dafe55bf66f" - integrity sha512-xnfY8JceApkSTliZtr7X6yl1wZYhGbRp0beBMi1OtmvTVTm/ZSt3881Fw1M3ZwhHqr7OEfl8828LJK2q62BvoQ== - dependencies: - pkg-dir "< 6 >= 5" - tslib "^2.3.1" - upath2 "^3.1.12" - JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -3437,7 +3412,7 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.20.0: +commander@^2.12.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3684,13 +3659,6 @@ cross-env@^5.2.0: dependencies: cross-spawn "^6.0.5" -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3711,7 +3679,7 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3954,6 +3922,13 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -4336,6 +4311,11 @@ es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-module-lexer@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4357,6 +4337,132 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +esbuild-android-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64" + integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw== + +esbuild-android-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8" + integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA== + +esbuild-darwin-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46" + integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA== + +esbuild-darwin-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9" + integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ== + +esbuild-freebsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e" + integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig== + +esbuild-freebsd-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6" + integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ== + +esbuild-linux-32@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70" + integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g== + +esbuild-linux-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519" + integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q== + +esbuild-linux-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a" + integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA== + +esbuild-linux-arm@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986" + integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA== + +esbuild-linux-mips64le@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5" + integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ== + +esbuild-linux-ppc64le@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47" + integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q== + +esbuild-linux-riscv64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2" + integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ== + +esbuild-linux-s390x@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0" + integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ== + +esbuild-netbsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95" + integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q== + +esbuild-openbsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd" + integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ== + +esbuild-sunos-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b" + integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA== + +esbuild-windows-32@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1" + integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw== + +esbuild-windows-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107" + integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw== + +esbuild-windows-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54" + integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw== + +esbuild@^0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30" + integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA== + optionalDependencies: + esbuild-android-64 "0.14.38" + esbuild-android-arm64 "0.14.38" + esbuild-darwin-64 "0.14.38" + esbuild-darwin-arm64 "0.14.38" + esbuild-freebsd-64 "0.14.38" + esbuild-freebsd-arm64 "0.14.38" + esbuild-linux-32 "0.14.38" + esbuild-linux-64 "0.14.38" + esbuild-linux-arm "0.14.38" + esbuild-linux-arm64 "0.14.38" + esbuild-linux-mips64le "0.14.38" + esbuild-linux-ppc64le "0.14.38" + esbuild-linux-riscv64 "0.14.38" + esbuild-linux-s390x "0.14.38" + esbuild-netbsd-64 "0.14.38" + esbuild-openbsd-64 "0.14.38" + esbuild-sunos-64 "0.14.38" + esbuild-windows-32 "0.14.38" + esbuild-windows-64 "0.14.38" + esbuild-windows-arm64 "0.14.38" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4844,15 +4950,6 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4868,14 +4965,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -4953,15 +5042,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -5604,13 +5684,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -increase-memory-limit@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/increase-memory-limit/-/increase-memory-limit-1.0.7.tgz#80417e736e45fcfd6a3d515b435e2eaea4e62233" - integrity sha512-ozyn+HHAPD9VxMT1U50A7G8XXlktUWhnnEDYSeDYnQjKsNrBWHqq6XfEA0uhMdDPD+q/7rXWlzF1CbXX/c1LiQ== - dependencies: - glob "^7.1.1" - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -7033,15 +7106,6 @@ jest-watcher@^27.5.1: jest-util "^27.5.1" string-length "^4.0.1" -jest-worker@^26.2.1: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - jest-worker@^27.2.4: version "27.2.4" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.4.tgz#881455df75e22e7726a53f43703ab74d6b36f82d" @@ -7078,6 +7142,11 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +joycon@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -7221,6 +7290,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -7404,13 +7478,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -7513,7 +7580,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2: +make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -8365,13 +8432,6 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -8386,13 +8446,6 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -8574,13 +8627,6 @@ path-is-inside@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-is-network-drive@^1.0.13: - version "1.0.13" - resolved "https://registry.yarnpkg.com/path-is-network-drive/-/path-is-network-drive-1.0.13.tgz#c9aa0183eb72c328aa83f43def93ddcb9d7ec4d4" - integrity sha512-Hg74mRN6mmXV+gTm3INjFK40ncAmC/Lo4qoQaSZ+GT3hZzlKdWQSqAjqyPeW0SvObP2W073WyYEBWY9d3wOm3A== - dependencies: - tslib "^2.3.1" - path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -8596,13 +8642,6 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-strip-sep@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/path-strip-sep/-/path-strip-sep-1.0.10.tgz#2be4e789406b298af8709ff79af716134b733b98" - integrity sha512-JpCy+8LAJQQTO1bQsb/84s1g+/Stm3h39aOpPRBQ/paMUGVPPZChLTOTKHoaCkc/6sKuF7yVsnq5Pe1S6xQGcA== - dependencies: - tslib "^2.3.1" - path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -8696,14 +8735,7 @@ pixelmatch@^5.1.0: dependencies: pngjs "^4.0.1" -"pkg-dir@< 6 >= 5": - version "5.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" - integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== - dependencies: - find-up "^5.0.0" - -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -9318,13 +9350,6 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -9695,6 +9720,17 @@ rollup-plugin-css-only@^3.1.0: dependencies: "@rollup/pluginutils" "4" +rollup-plugin-esbuild@^4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.9.1.tgz#369d137e2b1542c8ee459495fd4f10de812666aa" + integrity sha512-qn/x7Wz9p3Xnva99qcb+nopH0d2VJwVnsxJTGEg+Sh2Z3tqQl33MhOwzekVo1YTKgv+yAmosjcBRJygMfGrtLw== + dependencies: + "@rollup/pluginutils" "^4.1.1" + debug "^4.3.3" + es-module-lexer "^0.9.3" + joycon "^3.0.1" + jsonc-parser "^3.0.0" + rollup-plugin-livereload@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz#4747fa292a2cceb0c972c573d71b3d66b4252b37" @@ -9738,28 +9774,6 @@ rollup-plugin-svelte@^7.1.0: require-relative "^0.8.7" rollup-pluginutils "^2.8.2" -rollup-plugin-terser@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" - integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== - dependencies: - "@babel/code-frame" "^7.10.4" - jest-worker "^26.2.1" - serialize-javascript "^4.0.0" - terser "^5.0.0" - -rollup-plugin-typescript2@^0.31.2: - version "0.31.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.31.2.tgz#463aa713a7e2bf85b92860094b9f7fb274c5a4d8" - integrity sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q== - dependencies: - "@rollup/pluginutils" "^4.1.2" - "@yarn-tool/resolve-package" "^1.0.40" - find-cache-dir "^3.3.2" - fs-extra "^10.0.0" - resolve "^1.20.0" - tslib "^2.3.1" - rollup-plugin-web-worker-loader@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/rollup-plugin-web-worker-loader/-/rollup-plugin-web-worker-loader-1.6.1.tgz#9d7a27575b64b0780fe4e8b3bc87470d217e485f" @@ -9772,20 +9786,6 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@^2.45.2: - version "2.53.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.53.3.tgz#14b0e57f0874d4ad23bdbb13050cf70bcd1eabf7" - integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA== - optionalDependencies: - fsevents "~2.3.2" - -rollup@^2.56.3: - version "2.60.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.2.tgz#3f45ace36a9b10b4297181831ea0719922513463" - integrity sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw== - optionalDependencies: - fsevents "~2.3.2" - rollup@^2.71.1: version "2.71.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.71.1.tgz#82b259af7733dfd1224a8171013aaaad02971a22" @@ -9824,7 +9824,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9894,13 +9894,6 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -10059,7 +10052,7 @@ sort-keys@^4.0.0: dependencies: is-plain-obj "^2.0.0" -source-map-support@^0.5.6, source-map-support@~0.5.19: +source-map-support@^0.5.6: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -10077,7 +10070,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3, source-map@~0.7.2: +source-map@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -10546,15 +10539,6 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser@^5.0.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784" - integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -11023,15 +11007,6 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= -upath2@^3.1.12: - version "3.1.12" - resolved "https://registry.yarnpkg.com/upath2/-/upath2-3.1.12.tgz#441b3dfbadde21731017bd1b7beb169498efd0a9" - integrity sha512-yC3eZeCyCXFWjy7Nu4pgjLhXNYjuzuUmJiRgSSw6TJp8Emc+E4951HGPJf+bldFC5SL7oBLeNbtm1fGzXn2gxw== - dependencies: - path-is-network-drive "^1.0.13" - path-strip-sep "^1.0.10" - tslib "^2.3.1" - upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -11470,8 +11445,3 @@ yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 2 May 2022 14:59:48 +0200 Subject: [PATCH 67/79] Avoid circular dependencies and import un-bundled rrdom --- packages/rrdom/rollup.config.js | 1 - packages/rrdom/src/diff.ts | 33 +----------------------- packages/rrdom/src/types.ts | 33 ++++++++++++++++++++++++ packages/rrdom/src/virtual-dom.ts | 3 +-- packages/rrweb/src/replay/index.ts | 23 ++++++++--------- packages/rrweb/src/types.ts | 9 +++---- packages/rrweb/src/utils.ts | 2 +- packages/rrweb/typings/replay/index.d.ts | 2 +- packages/rrweb/typings/types.d.ts | 9 +++---- packages/rrweb/typings/utils.d.ts | 2 +- 10 files changed, 57 insertions(+), 60 deletions(-) create mode 100644 packages/rrdom/src/types.ts diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index 76d69f5515..90dac8eaed 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -1,7 +1,6 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import esbuild, { minify } from 'rollup-plugin-esbuild'; - import webWorkerLoader from 'rollup-plugin-web-worker-loader'; import pkg from './package.json'; diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 3caed0523b..da44ed06fc 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -15,6 +15,7 @@ import type { IRRText, Mirror, } from './document'; +import { StyleRuleType, VirtualStyleRules } from './types'; import type { RRCanvasElement, RRElement, @@ -418,38 +419,6 @@ export function getNestedRule( } } -export enum StyleRuleType { - Insert, - Remove, - Snapshot, - SetProperty, - RemoveProperty, -} -type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; - -export type VirtualStyleRules = Array< - InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule ->; export function getPositionsAndIndex(nestedIndex: number[]) { const positions = [...nestedIndex]; diff --git a/packages/rrdom/src/types.ts b/packages/rrdom/src/types.ts new file mode 100644 index 0000000000..01362263d6 --- /dev/null +++ b/packages/rrdom/src/types.ts @@ -0,0 +1,33 @@ +export enum StyleRuleType { + Insert, + Remove, + Snapshot, + SetProperty, + RemoveProperty, +} + +type InsertRule = { + cssText: string; + type: StyleRuleType.Insert; + index?: number | number[]; +}; +type RemoveRule = { + type: StyleRuleType.Remove; + index: number | number[]; +}; +type SetPropertyRule = { + type: StyleRuleType.SetProperty; + index: number[]; + property: string; + value: string | null; + priority: string | undefined; +}; +type RemovePropertyRule = { + type: StyleRuleType.RemoveProperty; + index: number[]; + property: string; +}; + +export type VirtualStyleRules = Array< + InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule +>; diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 9d0b64fbf1..5980ed53a3 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -26,7 +26,7 @@ import { createMirror, Mirror, } from './document'; -import type { VirtualStyleRules } from './diff'; +import type { VirtualStyleRules } from './types'; export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = createMirror(); @@ -318,4 +318,3 @@ export function buildFromDom( } export { RRNode }; -export { diff, createOrGetNode, StyleRuleType } from './diff'; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 9abf1f46b1..fe18b9a8c1 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -15,14 +15,13 @@ import { RRIFrameElement, RRMediaElement, RRCanvasElement, - StyleRuleType, - VirtualStyleRules, - createOrGetNode, buildFromNode, buildFromDom, - diff, -} from 'rrdom/es/virtual-dom'; -import type { Mirror as RRDOMMirror } from 'rrdom/es/document'; +} from 'rrdom/src/virtual-dom'; +import type { Mirror as RRDOMMirror } from 'rrdom/src/document'; +import type { VirtualStyleRules } from 'rrdom/src/types'; +import { StyleRuleType } from 'rrdom/src/types'; +import { createOrGetNode, diff } from 'rrdom/src/diff'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -73,7 +72,7 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; -import type { ReplayerHandler } from 'rrdom/es/diff'; +import type { ReplayerHandler } from 'rrdom/src/diff'; const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -125,7 +124,7 @@ export class Replayer { private nextUserInteractionEvent: eventWithTime | null; // tslint:disable-next-line: variable-name - private legacy_missingNodeRetryMap: missingNodeMap = {}; + private legacy_missingNodeRetryMap: missingNodeMap = {}; // The replayer uses the cache to speed up replay and scrubbing. private cache: BuildCache = createCache(); @@ -1413,7 +1412,7 @@ export class Replayer { }); // tslint:disable-next-line: variable-name - const legacy_missingNodeMap: missingNodeMap = { + const legacy_missingNodeMap: missingNodeMap = { ...this.legacy_missingNodeRetryMap, }; const queue: addedNodeMutation[] = []; @@ -1733,7 +1732,7 @@ export class Replayer { } private legacy_resolveMissingNode( - map: missingNodeMap, + map: missingNodeMap, parent: Node | RRNode, target: Node | RRNode, targetMutation: addedNodeMutation, @@ -1742,7 +1741,7 @@ export class Replayer { const previousInMap = previousId && map[previousId]; const nextInMap = nextId && map[nextId]; if (previousInMap) { - const { node, mutation } = previousInMap as missingNode; + const { node, mutation } = previousInMap as missingNode; parent.insertBefore(node as Node & RRNode, target as Node & RRNode); delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; @@ -1751,7 +1750,7 @@ export class Replayer { } } if (nextInMap) { - const { node, mutation } = nextInMap as missingNode; + const { node, mutation } = nextInMap as missingNode; parent.insertBefore( node as Node & RRNode, target.nextSibling as Node & RRNode, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index a74a091c75..8e6e4a0c4c 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -11,7 +11,6 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; -import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export enum EventType { @@ -669,12 +668,12 @@ export type playerMetaData = { totalTime: number; }; -export type missingNode = { - node: Node | RRNode; +export type missingNode = { + node: TNode; mutation: addedNodeMutation; }; -export type missingNodeMap = { - [id: number]: missingNode; +export type missingNodeMap = { + [id: number]: missingNode; }; export type actionWithDelay = { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 0d315972d1..5693fb926b 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -10,7 +10,7 @@ import type { } from './types'; import type { IMirror, Mirror } from 'rrweb-snapshot'; import { isShadowRoot, IGNORED_NODE } from 'rrweb-snapshot'; -import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; +import type { RRNode, RRIFrameElement } from 'rrdom/src/virtual-dom'; export function on( type: string, diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index 46b84e58c4..7a87f95e67 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -1,5 +1,5 @@ import { Mirror } from 'rrweb-snapshot'; -import { RRDocument } from 'rrdom/es/virtual-dom'; +import { RRDocument } from 'rrdom/src/virtual-dom'; import { Timer } from './timer'; import { createPlayerService, createSpeedService } from './machine'; import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types'; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 61f3d8b84c..2e53b8756a 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -3,7 +3,6 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; -import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export declare enum EventType { DomContentLoaded = 0, @@ -477,12 +476,12 @@ export declare type playerMetaData = { endTime: number; totalTime: number; }; -export declare type missingNode = { - node: Node | RRNode; +export declare type missingNode = { + node: TNode; mutation: addedNodeMutation; }; -export declare type missingNodeMap = { - [id: number]: missingNode; +export declare type missingNodeMap = { + [id: number]: missingNode; }; export declare type actionWithDelay = { doAction: () => void; diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index 555f948ec0..80373051b3 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,6 +1,6 @@ import type { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow, DeprecatedMirror } from './types'; import type { IMirror, Mirror } from 'rrweb-snapshot'; -import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; +import type { RRNode, RRIFrameElement } from 'rrdom/src/virtual-dom'; export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler; export declare let _mirror: DeprecatedMirror; export declare function throttle(func: (arg: T) => void, wait: number, options?: throttleOptions): (arg: T) => void; From b7b3c8dbaa551a0129da1477136b1baaad28e6e1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 2 May 2022 15:08:22 +0200 Subject: [PATCH 68/79] Fix imports --- packages/rrdom/test/diff.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index ee10241493..f38f78d344 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -7,9 +7,8 @@ import { createOrGetNode, diff, ReplayerHandler, - StyleRuleType, - VirtualStyleRules, } from '../src/diff'; +import { StyleRuleType, VirtualStyleRules } from '../src/types'; import { NodeType as RRNodeType, serializedNodeWithId, From 2d584aa72dd9a697e499f7eb95d2e6f7ba242404 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 2 May 2022 16:24:31 +0200 Subject: [PATCH 69/79] Revert back to pre-esbuild This reverts the following commits: b7b3c8dbaa551a0129da1477136b1baaad28e6e1 72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f 85d600a20c56cfa764cf1f858932ba14e67b1d23 61e1a5d323212ca8fbe0569e0b3062ddd53fc612 --- packages/rrdom/package.json | 6 +- packages/rrdom/rollup.config.js | 11 +- packages/rrdom/src/diff.ts | 33 +- packages/rrdom/src/types.ts | 33 -- packages/rrdom/src/virtual-dom.ts | 8 +- packages/rrdom/test/diff.test.ts | 3 +- packages/rrdom/test/virtual-dom.test.ts | 10 +- packages/rrweb-player/package.json | 4 +- packages/rrweb-player/rollup.config.js | 7 +- packages/rrweb-snapshot/package.json | 6 +- packages/rrweb-snapshot/rollup.config.js | 21 +- .../rrweb-snapshot/test/integration.test.ts | 10 +- packages/rrweb/package.json | 6 +- packages/rrweb/rollup.config.js | 13 +- packages/rrweb/src/replay/index.ts | 23 +- packages/rrweb/src/types.ts | 9 +- packages/rrweb/src/utils.ts | 2 +- packages/rrweb/tsconfig.json | 4 +- packages/rrweb/typings/replay/index.d.ts | 2 +- packages/rrweb/typings/types.d.ts | 9 +- packages/rrweb/typings/utils.d.ts | 2 +- yarn.lock | 370 ++++++++++-------- 22 files changed, 317 insertions(+), 275 deletions(-) delete mode 100644 packages/rrdom/src/types.ts diff --git a/packages/rrdom/package.json b/packages/rrdom/package.json index 651491c14e..bfe3459503 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -33,11 +33,11 @@ "@types/nwsapi": "^2.2.2", "@types/puppeteer": "^5.4.4", "compare-versions": "^4.1.3", - "esbuild": "^0.14.38", "jest": "^27.5.1", "puppeteer": "^9.1.1", - "rollup": "^2.71.1", - "rollup-plugin-esbuild": "^4.9.1", + "rollup": "^2.56.3", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-web-worker-loader": "^1.6.1", "rrweb-snapshot": "^1.1.14", "ts-jest": "^27.1.3", diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index 90dac8eaed..fe1d74a15b 100644 --- a/packages/rrdom/rollup.config.js +++ b/packages/rrdom/rollup.config.js @@ -1,6 +1,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import esbuild, { minify } from 'rollup-plugin-esbuild'; +import { terser } from 'rollup-plugin-terser'; +import typescript from 'rollup-plugin-typescript2'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; import pkg from './package.json'; @@ -15,7 +16,9 @@ const basePlugins = [ // supports bundling `web-worker:..filename` from rrweb webWorkerLoader(), - esbuild(), + typescript({ + tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, + }), ]; const baseConfigs = [ @@ -67,7 +70,7 @@ for (let config of baseConfigs) { }, { input: config.input, - plugins: basePlugins.concat(minify()), + plugins: basePlugins.concat(terser()), output: [ { name: config.name, @@ -91,7 +94,7 @@ for (let config of baseConfigs) { // ES module (packed) { input: config.input, - plugins: basePlugins.concat(minify()), + plugins: basePlugins.concat(terser()), output: [ { format: 'esm', diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index da44ed06fc..3caed0523b 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -15,7 +15,6 @@ import type { IRRText, Mirror, } from './document'; -import { StyleRuleType, VirtualStyleRules } from './types'; import type { RRCanvasElement, RRElement, @@ -419,6 +418,38 @@ export function getNestedRule( } } +export enum StyleRuleType { + Insert, + Remove, + Snapshot, + SetProperty, + RemoveProperty, +} +type InsertRule = { + cssText: string; + type: StyleRuleType.Insert; + index?: number | number[]; +}; +type RemoveRule = { + type: StyleRuleType.Remove; + index: number | number[]; +}; +type SetPropertyRule = { + type: StyleRuleType.SetProperty; + index: number[]; + property: string; + value: string | null; + priority: string | undefined; +}; +type RemovePropertyRule = { + type: StyleRuleType.RemoveProperty; + index: number[]; + property: string; +}; + +export type VirtualStyleRules = Array< + InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule +>; export function getPositionsAndIndex(nestedIndex: number[]) { const positions = [...nestedIndex]; diff --git a/packages/rrdom/src/types.ts b/packages/rrdom/src/types.ts deleted file mode 100644 index 01362263d6..0000000000 --- a/packages/rrdom/src/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -export enum StyleRuleType { - Insert, - Remove, - Snapshot, - SetProperty, - RemoveProperty, -} - -type InsertRule = { - cssText: string; - type: StyleRuleType.Insert; - index?: number | number[]; -}; -type RemoveRule = { - type: StyleRuleType.Remove; - index: number | number[]; -}; -type SetPropertyRule = { - type: StyleRuleType.SetProperty; - index: number[]; - property: string; - value: string | null; - priority: string | undefined; -}; -type RemovePropertyRule = { - type: StyleRuleType.RemoveProperty; - index: number[]; - property: string; -}; - -export type VirtualStyleRules = Array< - InsertRule | RemoveRule | SetPropertyRule | RemovePropertyRule ->; diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 5980ed53a3..d563ccfc2a 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -26,7 +26,7 @@ import { createMirror, Mirror, } from './document'; -import type { VirtualStyleRules } from './types'; +import type { VirtualStyleRules } from './diff'; export class RRDocument extends BaseRRDocumentImpl(RRNode) { public mirror: Mirror = createMirror(); @@ -318,3 +318,9 @@ export function buildFromDom( } export { RRNode }; +export { + diff, + createOrGetNode, + StyleRuleType, + VirtualStyleRules, +} from './diff'; diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index f38f78d344..ee10241493 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -7,8 +7,9 @@ import { createOrGetNode, diff, ReplayerHandler, + StyleRuleType, + VirtualStyleRules, } from '../src/diff'; -import { StyleRuleType, VirtualStyleRules } from '../src/types'; import { NodeType as RRNodeType, serializedNodeWithId, diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index b9fad03286..25f7cf68cf 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; -import esbuild from 'rollup-plugin-esbuild'; +import * as typescript from 'rollup-plugin-typescript2'; import { JSDOM } from 'jsdom'; import { Mirror, NodeType, NodeType as RRNodeType } from 'rrweb-snapshot'; import { @@ -19,6 +19,7 @@ import { } from '../src/virtual-dom'; import { setDefaultSN } from '../src/document'; +const _typescript = (typescript as unknown) as typeof typescript.default; const printRRDomCode = ` /** * Print the RRDom as a string. @@ -206,7 +207,12 @@ describe('RRDocument for browser environment', () => { browser = await puppeteer.launch(); const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/virtual-dom.ts'), - plugins: [resolve(), esbuild()], + plugins: [ + resolve(), + _typescript({ + tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, + }), + ], }); const { output: [{ code: _code }], diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index d86775b141..2824bb6170 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -4,19 +4,19 @@ "devDependencies": { "@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-node-resolve": "^13.2.1", + "@rollup/plugin-typescript": "^8.3.2", "@types/offscreencanvas": "^2019.6.4", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "esbuild": "^0.14.38", "eslint": "^7.5.0", "eslint-config-google": "^0.11.0", "eslint-plugin-svelte3": "^2.7.3", "postcss-easy-import": "^3.0.0", "rollup": "^2.71.1", "rollup-plugin-css-only": "^3.1.0", - "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-livereload": "^2.0.0", "rollup-plugin-svelte": "^7.1.0", + "rollup-plugin-terser": "^7.0.2", "rollup-plugin-web-worker-loader": "^1.6.1", "sirv-cli": "^0.4.4", "svelte": "^3.2.0", diff --git a/packages/rrweb-player/rollup.config.js b/packages/rrweb-player/rollup.config.js index 4af172b5d6..403bf883d2 100644 --- a/packages/rrweb-player/rollup.config.js +++ b/packages/rrweb-player/rollup.config.js @@ -2,9 +2,10 @@ import svelte from 'rollup-plugin-svelte'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; import sveltePreprocess from 'svelte-preprocess'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; -import esbuild, { minify } from 'rollup-plugin-esbuild'; +import typescript from '@rollup/plugin-typescript'; import pkg from './package.json'; import css from 'rollup-plugin-css-only'; @@ -70,7 +71,7 @@ export default entries.map((output) => ({ // supports bundling `web-worker:..filename` from rrweb webWorkerLoader(), - esbuild(), + typescript(), css({ // we'll extract any component CSS out into @@ -88,7 +89,7 @@ export default entries.map((output) => ({ // If we're building for production (npm run build // instead of npm run dev), minify - production && minify(), + production && terser(), ], watch: { clearScreen: false, diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 19d936625b..d66d3519ce 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -39,19 +39,19 @@ }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", "devDependencies": { + "@rollup/plugin-typescript": "^8.2.5", "@types/chai": "^4.1.4", "@types/jest": "^27.0.2", "@types/jsdom": "^16.2.4", "@types/node": "^10.11.3", "@types/puppeteer": "^1.12.4", "cross-env": "^5.2.0", - "esbuild": "^0.14.38", "jest": "^27.2.4", "jest-snapshot": "^23.6.0", "jsdom": "^16.4.0", "puppeteer": "^1.15.0", - "rollup": "^2.71.1", - "rollup-plugin-esbuild": "^4.9.1", + "rollup": "^2.45.2", + "rollup-plugin-terser": "^7.0.2", "ts-jest": "^27.0.5", "ts-node": "^7.0.1", "tslib": "^1.9.3", diff --git a/packages/rrweb-snapshot/rollup.config.js b/packages/rrweb-snapshot/rollup.config.js index 8d5b4ff927..6a8b0a0933 100644 --- a/packages/rrweb-snapshot/rollup.config.js +++ b/packages/rrweb-snapshot/rollup.config.js @@ -1,4 +1,5 @@ -import esbuild from 'rollup-plugin-esbuild'; +import typescript from '@rollup/plugin-typescript'; +import { terser } from 'rollup-plugin-terser'; import pkg from './package.json'; function toMinPath(path) { @@ -9,7 +10,7 @@ let configs = [ // ES module - for building rrweb { input: './src/index.ts', - plugins: [esbuild()], + plugins: [typescript()], output: [ { format: 'esm', @@ -22,7 +23,7 @@ let extra_configs = [ // browser { input: './src/index.ts', - plugins: [esbuild()], + plugins: [typescript()], output: [ { name: 'rrwebSnapshot', @@ -33,11 +34,7 @@ let extra_configs = [ }, { input: './src/index.ts', - plugins: [ - esbuild({ - minify: true, - }), - ], + plugins: [typescript(), terser()], output: [ { name: 'rrwebSnapshot', @@ -50,7 +47,7 @@ let extra_configs = [ // CommonJS { input: './src/index.ts', - plugins: [esbuild()], + plugins: [typescript()], output: [ { format: 'cjs', @@ -61,11 +58,7 @@ let extra_configs = [ // ES module (packed) { input: './src/index.ts', - plugins: [ - esbuild({ - minify: true, - }), - ], + plugins: [typescript(), terser()], output: [ { format: 'esm', diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 34db1ee7d7..c34bfefcc1 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -4,9 +4,11 @@ import * as http from 'http'; import * as url from 'url'; import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; -import esbuild from 'rollup-plugin-esbuild'; +import * as typescript from '@rollup/plugin-typescript'; import * as assert from 'assert'; +const _typescript = (typescript as unknown) as typeof typescript.default; + const htmlFolder = path.join(__dirname, 'html'); const htmls = fs.readdirSync(htmlFolder).map((filePath) => { const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'); @@ -71,7 +73,7 @@ describe('integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [esbuild()], + plugins: [_typescript()], }); const { output: [{ code: _code }], @@ -223,7 +225,7 @@ describe('iframe integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [esbuild()], + plugins: [_typescript()], }); const { output: [{ code: _code }], @@ -272,7 +274,7 @@ describe('shadow DOM integration tests', function (this: ISuite) { const bundle = await rollup.rollup({ input: path.resolve(__dirname, '../src/index.ts'), - plugins: [esbuild()], + plugins: [_typescript()], }); const { output: [{ code: _code }], diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 7213c9b349..69731d7027 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -51,7 +51,6 @@ "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.4", "cross-env": "^5.2.0", - "esbuild": "^0.14.38", "fast-mhtml": "^1.1.9", "identity-obj-proxy": "^3.0.0", "ignore-styles": "^5.0.1", @@ -61,10 +60,11 @@ "jest-snapshot": "^23.6.0", "prettier": "2.2.1", "puppeteer": "^9.1.1", - "rollup": "^2.71.1", - "rollup-plugin-esbuild": "^4.9.1", + "rollup": "^2.68.0", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-rename-node-modules": "^1.3.1", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-web-worker-loader": "^1.6.1", "ts-jest": "^27.1.3", "ts-node": "^10.7.0", diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index e6405d64bd..690c97eada 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -1,5 +1,6 @@ -import esbuild, { minify } from 'rollup-plugin-esbuild'; +import typescript from 'rollup-plugin-typescript2'; import resolve from '@rollup/plugin-node-resolve'; +import { terser } from 'rollup-plugin-terser'; import postcss from 'rollup-plugin-postcss'; import renameNodeModules from 'rollup-plugin-rename-node-modules'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; @@ -114,7 +115,7 @@ for (const c of baseConfigs) { // supports bundling `web-worker:..filename` webWorkerLoader(), - esbuild(), + typescript(), ]; const plugins = basePlugins.concat( postcss({ @@ -143,7 +144,7 @@ for (const c of baseConfigs) { minimize: true, sourceMap: true, }), - minify(), + terser(), ), output: [ { @@ -205,15 +206,15 @@ if (process.env.BROWSER_ONLY) { // supports bundling `web-worker:..filename` webWorkerLoader(), - esbuild({ - minify: true, + typescript({ + outDir: null, }), - postcss({ extract: false, inject: false, sourceMap: true, }), + terser(), ]; configs.push({ diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index fe18b9a8c1..9abf1f46b1 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -15,13 +15,14 @@ import { RRIFrameElement, RRMediaElement, RRCanvasElement, + StyleRuleType, + VirtualStyleRules, + createOrGetNode, buildFromNode, buildFromDom, -} from 'rrdom/src/virtual-dom'; -import type { Mirror as RRDOMMirror } from 'rrdom/src/document'; -import type { VirtualStyleRules } from 'rrdom/src/types'; -import { StyleRuleType } from 'rrdom/src/types'; -import { createOrGetNode, diff } from 'rrdom/src/diff'; + diff, +} from 'rrdom/es/virtual-dom'; +import type { Mirror as RRDOMMirror } from 'rrdom/es/document'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -72,7 +73,7 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; -import type { ReplayerHandler } from 'rrdom/src/diff'; +import type { ReplayerHandler } from 'rrdom/es/diff'; const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_INTERVAL = 5 * 1000; @@ -124,7 +125,7 @@ export class Replayer { private nextUserInteractionEvent: eventWithTime | null; // tslint:disable-next-line: variable-name - private legacy_missingNodeRetryMap: missingNodeMap = {}; + private legacy_missingNodeRetryMap: missingNodeMap = {}; // The replayer uses the cache to speed up replay and scrubbing. private cache: BuildCache = createCache(); @@ -1412,7 +1413,7 @@ export class Replayer { }); // tslint:disable-next-line: variable-name - const legacy_missingNodeMap: missingNodeMap = { + const legacy_missingNodeMap: missingNodeMap = { ...this.legacy_missingNodeRetryMap, }; const queue: addedNodeMutation[] = []; @@ -1732,7 +1733,7 @@ export class Replayer { } private legacy_resolveMissingNode( - map: missingNodeMap, + map: missingNodeMap, parent: Node | RRNode, target: Node | RRNode, targetMutation: addedNodeMutation, @@ -1741,7 +1742,7 @@ export class Replayer { const previousInMap = previousId && map[previousId]; const nextInMap = nextId && map[nextId]; if (previousInMap) { - const { node, mutation } = previousInMap as missingNode; + const { node, mutation } = previousInMap as missingNode; parent.insertBefore(node as Node & RRNode, target as Node & RRNode); delete map[mutation.node.id]; delete this.legacy_missingNodeRetryMap[mutation.node.id]; @@ -1750,7 +1751,7 @@ export class Replayer { } } if (nextInMap) { - const { node, mutation } = nextInMap as missingNode; + const { node, mutation } = nextInMap as missingNode; parent.insertBefore( node as Node & RRNode, target.nextSibling as Node & RRNode, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 8e6e4a0c4c..a74a091c75 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -11,6 +11,7 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; +import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export enum EventType { @@ -668,12 +669,12 @@ export type playerMetaData = { totalTime: number; }; -export type missingNode = { - node: TNode; +export type missingNode = { + node: Node | RRNode; mutation: addedNodeMutation; }; -export type missingNodeMap = { - [id: number]: missingNode; +export type missingNodeMap = { + [id: number]: missingNode; }; export type actionWithDelay = { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 5693fb926b..0d315972d1 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -10,7 +10,7 @@ import type { } from './types'; import type { IMirror, Mirror } from 'rrweb-snapshot'; import { isShadowRoot, IGNORED_NODE } from 'rrweb-snapshot'; -import type { RRNode, RRIFrameElement } from 'rrdom/src/virtual-dom'; +import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; export function on( type: string, diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index 4be4650eae..f83abe5354 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -2,14 +2,14 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "Node", - "target": "ES6", + "target": "ES5", "noImplicitAny": true, "strictNullChecks": true, "removeComments": true, "preserveConstEnums": true, "rootDir": "src", "outDir": "build", - "lib": ["ES6", "DOM"], + "lib": ["es6", "dom"], "downlevelIteration": true, "importsNotUsedAsValues": "error" }, diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index 7a87f95e67..46b84e58c4 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -1,5 +1,5 @@ import { Mirror } from 'rrweb-snapshot'; -import { RRDocument } from 'rrdom/src/virtual-dom'; +import { RRDocument } from 'rrdom/es/virtual-dom'; import { Timer } from './timer'; import { createPlayerService, createSpeedService } from './machine'; import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types'; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 2e53b8756a..61f3d8b84c 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -3,6 +3,7 @@ import type { PackFn, UnpackFn } from './packer/base'; import type { IframeManager } from './record/iframe-manager'; import type { ShadowDomManager } from './record/shadow-dom-manager'; import type { Replayer } from './replay'; +import type { RRNode } from 'rrdom/es/virtual-dom'; import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export declare enum EventType { DomContentLoaded = 0, @@ -476,12 +477,12 @@ export declare type playerMetaData = { endTime: number; totalTime: number; }; -export declare type missingNode = { - node: TNode; +export declare type missingNode = { + node: Node | RRNode; mutation: addedNodeMutation; }; -export declare type missingNodeMap = { - [id: number]: missingNode; +export declare type missingNodeMap = { + [id: number]: missingNode; }; export declare type actionWithDelay = { doAction: () => void; diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index 80373051b3..555f948ec0 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,6 +1,6 @@ import type { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow, DeprecatedMirror } from './types'; import type { IMirror, Mirror } from 'rrweb-snapshot'; -import type { RRNode, RRIFrameElement } from 'rrdom/src/virtual-dom'; +import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | IWindow): listenerHandler; export declare let _mirror: DeprecatedMirror; export declare function throttle(func: (arg: T) => void, wait: number, options?: throttleOptions): (arg: T) => void; diff --git a/yarn.lock b/yarn.lock index 58d8434ac2..7204c4b33f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== @@ -1819,6 +1819,22 @@ is-module "^1.0.0" resolve "^1.19.0" +"@rollup/plugin-typescript@^8.2.5": + version "8.2.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.2.5.tgz#e0319761b2b5105615e5a0c371ae05bc2984b7de" + integrity sha512-QL/LvDol/PAGB2O0S7/+q2HpSUNodpw7z6nGn9BfoVCPOZ0r4EALrojFU29Bkoi2Hr2jgTocTejJ5GGWZfOxbQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + +"@rollup/plugin-typescript@^8.3.2": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.3.2.tgz#e1b719e2ed3e752bbc092001656c48378f2d15f0" + integrity sha512-MtgyR5LNHZr3GyN0tM7gNO9D0CS+Y+vflS4v/PHmrX17JCkHUYKvQ5jN5o3cz1YKllM3duXUqu3yOHwMPUxhDg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + resolve "^1.17.0" + "@rollup/pluginutils@4": version "4.1.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" @@ -1836,10 +1852,10 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^4.1.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" - integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== +"@rollup/pluginutils@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" + integrity sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ== dependencies: estree-walker "^2.0.1" picomatch "^2.2.2" @@ -2336,6 +2352,15 @@ resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== +"@yarn-tool/resolve-package@^1.0.40": + version "1.0.45" + resolved "https://registry.yarnpkg.com/@yarn-tool/resolve-package/-/resolve-package-1.0.45.tgz#4d9716a67903f46a76c8691eff546dafe55bf66f" + integrity sha512-xnfY8JceApkSTliZtr7X6yl1wZYhGbRp0beBMi1OtmvTVTm/ZSt3881Fw1M3ZwhHqr7OEfl8828LJK2q62BvoQ== + dependencies: + pkg-dir "< 6 >= 5" + tslib "^2.3.1" + upath2 "^3.1.12" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -3412,7 +3437,7 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1: +commander@^2.12.1, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3858,7 +3883,12 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4, cssom@^0.5.0: +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== @@ -3922,13 +3952,6 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -4311,11 +4334,6 @@ es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" -es-module-lexer@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== - es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4337,132 +4355,6 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild-android-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64" - integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw== - -esbuild-android-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8" - integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA== - -esbuild-darwin-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46" - integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA== - -esbuild-darwin-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9" - integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ== - -esbuild-freebsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e" - integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig== - -esbuild-freebsd-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6" - integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ== - -esbuild-linux-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70" - integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g== - -esbuild-linux-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519" - integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q== - -esbuild-linux-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a" - integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA== - -esbuild-linux-arm@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986" - integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA== - -esbuild-linux-mips64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5" - integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ== - -esbuild-linux-ppc64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47" - integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q== - -esbuild-linux-riscv64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2" - integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ== - -esbuild-linux-s390x@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0" - integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ== - -esbuild-netbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95" - integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q== - -esbuild-openbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd" - integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ== - -esbuild-sunos-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b" - integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA== - -esbuild-windows-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1" - integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw== - -esbuild-windows-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107" - integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw== - -esbuild-windows-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54" - integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw== - -esbuild@^0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30" - integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA== - optionalDependencies: - esbuild-android-64 "0.14.38" - esbuild-android-arm64 "0.14.38" - esbuild-darwin-64 "0.14.38" - esbuild-darwin-arm64 "0.14.38" - esbuild-freebsd-64 "0.14.38" - esbuild-freebsd-arm64 "0.14.38" - esbuild-linux-32 "0.14.38" - esbuild-linux-64 "0.14.38" - esbuild-linux-arm "0.14.38" - esbuild-linux-arm64 "0.14.38" - esbuild-linux-mips64le "0.14.38" - esbuild-linux-ppc64le "0.14.38" - esbuild-linux-riscv64 "0.14.38" - esbuild-linux-s390x "0.14.38" - esbuild-netbsd-64 "0.14.38" - esbuild-openbsd-64 "0.14.38" - esbuild-sunos-64 "0.14.38" - esbuild-windows-32 "0.14.38" - esbuild-windows-64 "0.14.38" - esbuild-windows-arm64 "0.14.38" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4950,6 +4842,15 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-cache-dir@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4965,6 +4866,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -5042,6 +4951,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -7106,6 +7024,15 @@ jest-watcher@^27.5.1: jest-util "^27.5.1" string-length "^4.0.1" +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + jest-worker@^27.2.4: version "27.2.4" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.4.tgz#881455df75e22e7726a53f43703ab74d6b36f82d" @@ -7142,11 +7069,6 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" -joycon@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" - integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== - js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -7290,11 +7212,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== - jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -7478,6 +7395,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -7580,7 +7504,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -8432,6 +8356,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -8446,6 +8377,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -8627,6 +8565,13 @@ path-is-inside@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= +path-is-network-drive@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/path-is-network-drive/-/path-is-network-drive-1.0.13.tgz#c9aa0183eb72c328aa83f43def93ddcb9d7ec4d4" + integrity sha512-Hg74mRN6mmXV+gTm3INjFK40ncAmC/Lo4qoQaSZ+GT3hZzlKdWQSqAjqyPeW0SvObP2W073WyYEBWY9d3wOm3A== + dependencies: + tslib "^2.3.1" + path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -8642,6 +8587,13 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-strip-sep@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/path-strip-sep/-/path-strip-sep-1.0.10.tgz#2be4e789406b298af8709ff79af716134b733b98" + integrity sha512-JpCy+8LAJQQTO1bQsb/84s1g+/Stm3h39aOpPRBQ/paMUGVPPZChLTOTKHoaCkc/6sKuF7yVsnq5Pe1S6xQGcA== + dependencies: + tslib "^2.3.1" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -8735,7 +8687,14 @@ pixelmatch@^5.1.0: dependencies: pngjs "^4.0.1" -pkg-dir@^4.2.0: +"pkg-dir@< 6 >= 5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -9350,6 +9309,13 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -9720,17 +9686,6 @@ rollup-plugin-css-only@^3.1.0: dependencies: "@rollup/pluginutils" "4" -rollup-plugin-esbuild@^4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.9.1.tgz#369d137e2b1542c8ee459495fd4f10de812666aa" - integrity sha512-qn/x7Wz9p3Xnva99qcb+nopH0d2VJwVnsxJTGEg+Sh2Z3tqQl33MhOwzekVo1YTKgv+yAmosjcBRJygMfGrtLw== - dependencies: - "@rollup/pluginutils" "^4.1.1" - debug "^4.3.3" - es-module-lexer "^0.9.3" - joycon "^3.0.1" - jsonc-parser "^3.0.0" - rollup-plugin-livereload@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz#4747fa292a2cceb0c972c573d71b3d66b4252b37" @@ -9774,6 +9729,28 @@ rollup-plugin-svelte@^7.1.0: require-relative "^0.8.7" rollup-pluginutils "^2.8.2" +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup-plugin-typescript2@^0.31.2: + version "0.31.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.31.2.tgz#463aa713a7e2bf85b92860094b9f7fb274c5a4d8" + integrity sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q== + dependencies: + "@rollup/pluginutils" "^4.1.2" + "@yarn-tool/resolve-package" "^1.0.40" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + resolve "^1.20.0" + tslib "^2.3.1" + rollup-plugin-web-worker-loader@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/rollup-plugin-web-worker-loader/-/rollup-plugin-web-worker-loader-1.6.1.tgz#9d7a27575b64b0780fe4e8b3bc87470d217e485f" @@ -9786,6 +9763,27 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" +rollup@^2.45.2: + version "2.53.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.53.3.tgz#14b0e57f0874d4ad23bdbb13050cf70bcd1eabf7" + integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^2.56.3: + version "2.60.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.2.tgz#3f45ace36a9b10b4297181831ea0719922513463" + integrity sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^2.68.0: + version "2.68.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.68.0.tgz#6ccabfd649447f8f21d62bf41662e5caece3bd66" + integrity sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA== + optionalDependencies: + fsevents "~2.3.2" + rollup@^2.71.1: version "2.71.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.71.1.tgz#82b259af7733dfd1224a8171013aaaad02971a22" @@ -9824,7 +9822,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9894,6 +9892,13 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -10052,7 +10057,7 @@ sort-keys@^4.0.0: dependencies: is-plain-obj "^2.0.0" -source-map-support@^0.5.6: +source-map-support@^0.5.6, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -10070,7 +10075,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: +source-map@^0.7.3, source-map@~0.7.2: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== @@ -10539,6 +10544,15 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +terser@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784" + integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -11007,6 +11021,15 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= +upath2@^3.1.12: + version "3.1.12" + resolved "https://registry.yarnpkg.com/upath2/-/upath2-3.1.12.tgz#441b3dfbadde21731017bd1b7beb169498efd0a9" + integrity sha512-yC3eZeCyCXFWjy7Nu4pgjLhXNYjuzuUmJiRgSSw6TJp8Emc+E4951HGPJf+bldFC5SL7oBLeNbtm1fGzXn2gxw== + dependencies: + path-is-network-drive "^1.0.13" + path-strip-sep "^1.0.10" + tslib "^2.3.1" + upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -11445,3 +11468,8 @@ yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 3c7eb56bcf3150cd235b3951526b4aecd1908da2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 2 May 2022 16:25:05 +0200 Subject: [PATCH 70/79] Set node to lts (12 is no longer supported) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9d5753868e..9ecba610da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ os: linux dist: focal node_js: - - 12 + - lts/* install: - yarn From d50b72cf89196eaf4aab0446acccd55c200b8eb2 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 3 May 2022 15:54:27 +0200 Subject: [PATCH 71/79] Speed up bundling and use less memory This fixes the out of memory errors happening while bundling --- packages/rrdom/test/virtual-dom.test.ts | 4 +- packages/rrweb/package.json | 3 +- packages/rrweb/rollup.config.js | 53 +++--- .../__snapshots__/integration.test.ts.snap | 38 ++-- packages/rrweb/tsconfig.json | 2 +- yarn.lock | 174 +++++++++++++++++- 6 files changed, 217 insertions(+), 57 deletions(-) diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index 25f7cf68cf..d3bf2bfd20 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -209,9 +209,9 @@ describe('RRDocument for browser environment', () => { input: path.resolve(__dirname, '../src/virtual-dom.ts'), plugins: [ resolve(), - _typescript({ + (_typescript({ tsconfigOverride: { compilerOptions: { module: 'ESNext' } }, - }), + }) as unknown) as rollup.Plugin, ], }); const { diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 69731d7027..a7d83c8b3f 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -51,6 +51,7 @@ "@types/prettier": "^2.3.2", "@types/puppeteer": "^5.4.4", "cross-env": "^5.2.0", + "esbuild": "^0.14.38", "fast-mhtml": "^1.1.9", "identity-obj-proxy": "^3.0.0", "ignore-styles": "^5.0.1", @@ -61,9 +62,9 @@ "prettier": "2.2.1", "puppeteer": "^9.1.1", "rollup": "^2.68.0", + "rollup-plugin-esbuild": "^4.9.1", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-rename-node-modules": "^1.3.1", - "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-web-worker-loader": "^1.6.1", "ts-jest": "^27.1.3", diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 690c97eada..fe4ec73171 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -1,6 +1,6 @@ import typescript from 'rollup-plugin-typescript2'; +import esbuild from 'rollup-plugin-esbuild'; import resolve from '@rollup/plugin-node-resolve'; -import { terser } from 'rollup-plugin-terser'; import postcss from 'rollup-plugin-postcss'; import renameNodeModules from 'rollup-plugin-rename-node-modules'; import webWorkerLoader from 'rollup-plugin-web-worker-loader'; @@ -108,6 +108,27 @@ const baseConfigs = [ let configs = []; +function getPlugins(options = {}) { + const { minify = false, sourceMap = false } = options; + return [ + resolve({ browser: true }), + webWorkerLoader({ + targetPlatform: 'browser', + inline: true, + sourceMap, + }), + esbuild({ + minify, + }), + postcss({ + extract: false, + inject: false, + minimize: minify, + sourceMap, + }), + ]; +} + for (const c of baseConfigs) { const basePlugins = [ resolve({ browser: true }), @@ -126,7 +147,7 @@ for (const c of baseConfigs) { // browser configs.push({ input: c.input, - plugins, + plugins: getPlugins(), output: [ { name: c.name, @@ -138,14 +159,7 @@ for (const c of baseConfigs) { // browser + minify configs.push({ input: c.input, - plugins: basePlugins.concat( - postcss({ - extract: true, - minimize: true, - sourceMap: true, - }), - terser(), - ), + plugins: getPlugins({ minify: true, sourceMap: true }), output: [ { name: c.name, @@ -200,26 +214,9 @@ if (process.env.BROWSER_ONLY) { configs = []; for (const c of browserOnlyBaseConfigs) { - const plugins = [ - resolve({ browser: true }), - - // supports bundling `web-worker:..filename` - webWorkerLoader(), - - typescript({ - outDir: null, - }), - postcss({ - extract: false, - inject: false, - sourceMap: true, - }), - terser(), - ]; - configs.push({ input: c.input, - plugins, + plugins: getPlugins({ sourceMap: true, minify: true }), output: [ { name: c.name, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 9a4fa49a15..5af8b6dcf1 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -8362,7 +8362,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"assert\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:2:37\\" + \\"__puppeteer_evaluation_script__:2:21\\" ], \\"payload\\": [ \\"true\\", @@ -8378,7 +8378,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"count\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:3:37\\" + \\"__puppeteer_evaluation_script__:3:21\\" ], \\"payload\\": [ \\"\\\\\\"count\\\\\\"\\" @@ -8393,7 +8393,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"countReset\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:4:37\\" + \\"__puppeteer_evaluation_script__:4:21\\" ], \\"payload\\": [ \\"\\\\\\"count\\\\\\"\\" @@ -8408,7 +8408,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"debug\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:5:37\\" + \\"__puppeteer_evaluation_script__:5:21\\" ], \\"payload\\": [ \\"\\\\\\"debug\\\\\\"\\" @@ -8423,7 +8423,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"dir\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:6:37\\" + \\"__puppeteer_evaluation_script__:6:21\\" ], \\"payload\\": [ \\"\\\\\\"dir\\\\\\"\\" @@ -8438,7 +8438,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"dirxml\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:7:37\\" + \\"__puppeteer_evaluation_script__:7:21\\" ], \\"payload\\": [ \\"\\\\\\"dirxml\\\\\\"\\" @@ -8453,7 +8453,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"group\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:8:37\\" + \\"__puppeteer_evaluation_script__:8:21\\" ], \\"payload\\": [] } @@ -8466,7 +8466,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"groupCollapsed\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:9:37\\" + \\"__puppeteer_evaluation_script__:9:21\\" ], \\"payload\\": [] } @@ -8479,7 +8479,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"info\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:10:37\\" + \\"__puppeteer_evaluation_script__:10:21\\" ], \\"payload\\": [ \\"\\\\\\"info\\\\\\"\\" @@ -8494,7 +8494,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"log\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:11:37\\" + \\"__puppeteer_evaluation_script__:11:21\\" ], \\"payload\\": [ \\"\\\\\\"log\\\\\\"\\" @@ -8509,7 +8509,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"table\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:12:37\\" + \\"__puppeteer_evaluation_script__:12:21\\" ], \\"payload\\": [ \\"\\\\\\"table\\\\\\"\\" @@ -8524,7 +8524,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"time\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:13:37\\" + \\"__puppeteer_evaluation_script__:13:21\\" ], \\"payload\\": [] } @@ -8537,7 +8537,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"timeEnd\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:14:37\\" + \\"__puppeteer_evaluation_script__:14:21\\" ], \\"payload\\": [] } @@ -8550,7 +8550,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"timeLog\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:15:37\\" + \\"__puppeteer_evaluation_script__:15:21\\" ], \\"payload\\": [] } @@ -8563,7 +8563,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"trace\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:16:37\\" + \\"__puppeteer_evaluation_script__:16:21\\" ], \\"payload\\": [ \\"\\\\\\"trace\\\\\\"\\" @@ -8578,7 +8578,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"warn\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:17:37\\" + \\"__puppeteer_evaluation_script__:17:21\\" ], \\"payload\\": [ \\"\\\\\\"warn\\\\\\"\\" @@ -8593,7 +8593,7 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"clear\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:18:37\\" + \\"__puppeteer_evaluation_script__:18:21\\" ], \\"payload\\": [] } @@ -8606,10 +8606,10 @@ exports[`record integration tests should record console messages 1`] = ` \\"payload\\": { \\"level\\": \\"log\\", \\"trace\\": [ - \\"__puppeteer_evaluation_script__:19:37\\" + \\"__puppeteer_evaluation_script__:19:21\\" ], \\"payload\\": [ - \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:41\\\\\\\\nEnd of stack for Error object\\\\\\"\\" + \\"\\\\\\"TypeError: a message\\\\\\\\n at __puppeteer_evaluation_script__:19:25\\\\\\\\nEnd of stack for Error object\\\\\\"\\" ] } } diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index f83abe5354..fe6c5a6848 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "Node", - "target": "ES5", + "target": "ES6", "noImplicitAny": true, "strictNullChecks": true, "removeComments": true, diff --git a/yarn.lock b/yarn.lock index 7204c4b33f..a438248446 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1852,6 +1852,14 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^4.1.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + "@rollup/pluginutils@^4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" @@ -3883,12 +3891,7 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@^0.5.0: +cssom@^0.4.4, cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== @@ -3952,6 +3955,13 @@ debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -4334,6 +4344,11 @@ es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-module-lexer@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4355,6 +4370,132 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +esbuild-android-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64" + integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw== + +esbuild-android-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8" + integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA== + +esbuild-darwin-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46" + integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA== + +esbuild-darwin-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9" + integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ== + +esbuild-freebsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e" + integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig== + +esbuild-freebsd-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6" + integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ== + +esbuild-linux-32@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70" + integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g== + +esbuild-linux-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519" + integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q== + +esbuild-linux-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a" + integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA== + +esbuild-linux-arm@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986" + integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA== + +esbuild-linux-mips64le@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5" + integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ== + +esbuild-linux-ppc64le@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47" + integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q== + +esbuild-linux-riscv64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2" + integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ== + +esbuild-linux-s390x@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0" + integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ== + +esbuild-netbsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95" + integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q== + +esbuild-openbsd-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd" + integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ== + +esbuild-sunos-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b" + integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA== + +esbuild-windows-32@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1" + integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw== + +esbuild-windows-64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107" + integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw== + +esbuild-windows-arm64@0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54" + integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw== + +esbuild@^0.14.38: + version "0.14.38" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30" + integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA== + optionalDependencies: + esbuild-android-64 "0.14.38" + esbuild-android-arm64 "0.14.38" + esbuild-darwin-64 "0.14.38" + esbuild-darwin-arm64 "0.14.38" + esbuild-freebsd-64 "0.14.38" + esbuild-freebsd-arm64 "0.14.38" + esbuild-linux-32 "0.14.38" + esbuild-linux-64 "0.14.38" + esbuild-linux-arm "0.14.38" + esbuild-linux-arm64 "0.14.38" + esbuild-linux-mips64le "0.14.38" + esbuild-linux-ppc64le "0.14.38" + esbuild-linux-riscv64 "0.14.38" + esbuild-linux-s390x "0.14.38" + esbuild-netbsd-64 "0.14.38" + esbuild-openbsd-64 "0.14.38" + esbuild-sunos-64 "0.14.38" + esbuild-windows-32 "0.14.38" + esbuild-windows-64 "0.14.38" + esbuild-windows-arm64 "0.14.38" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -7069,6 +7210,11 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +joycon@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -7212,6 +7358,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -9686,6 +9837,17 @@ rollup-plugin-css-only@^3.1.0: dependencies: "@rollup/pluginutils" "4" +rollup-plugin-esbuild@^4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-esbuild/-/rollup-plugin-esbuild-4.9.1.tgz#369d137e2b1542c8ee459495fd4f10de812666aa" + integrity sha512-qn/x7Wz9p3Xnva99qcb+nopH0d2VJwVnsxJTGEg+Sh2Z3tqQl33MhOwzekVo1YTKgv+yAmosjcBRJygMfGrtLw== + dependencies: + "@rollup/pluginutils" "^4.1.1" + debug "^4.3.3" + es-module-lexer "^0.9.3" + joycon "^3.0.1" + jsonc-parser "^3.0.0" + rollup-plugin-livereload@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz#4747fa292a2cceb0c972c573d71b3d66b4252b37" From de0ef9f20fc5034a8270ffa49493faa71ea41559 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 00:43:37 +1000 Subject: [PATCH 72/79] remove __sn from rrdom --- packages/rrdom/src/diff.ts | 32 ++-- packages/rrdom/src/document-nodejs.ts | 24 ++- packages/rrdom/src/document.ts | 196 ++----------------- packages/rrdom/src/virtual-dom.ts | 177 ++++++++++++++--- packages/rrdom/test/diff.test.ts | 23 ++- packages/rrdom/test/document.test.ts | 117 ++++-------- packages/rrdom/test/virtual-dom.test.ts | 241 ++++++++++++++++++------ packages/rrweb/package.json | 2 +- packages/rrweb/src/replay/index.ts | 8 +- 9 files changed, 441 insertions(+), 379 deletions(-) diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index 3caed0523b..26f2309147 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -13,7 +13,6 @@ import type { IRRElement, IRRNode, IRRText, - Mirror, } from './document'; import type { RRCanvasElement, @@ -22,6 +21,7 @@ import type { RRMediaElement, RRStyleElement, RRDocument, + Mirror, } from './virtual-dom'; const NAMESPACES: Record = { @@ -91,8 +91,8 @@ export function diff( const newChildren = newTree.childNodes; rrnodeMirror = rrnodeMirror || - (newTree as IRRDocument).mirror || - newTree.ownerDocument.mirror; + (newTree as RRDocument).mirror || + (newTree.ownerDocument as RRDocument).mirror; if (oldChildren.length > 0 || newChildren.length > 0) { diffChildren( @@ -187,10 +187,7 @@ export function diff( inputDataToApply && replayer.applyInput(inputDataToApply); // IFrame element doesn't have child nodes. - if ( - newTree.RRNodeType === RRNodeType.Element && - (newTree as IRRElement).tagName === 'IFRAME' - ) { + if (newTree.nodeName === 'IFRAME') { const oldContentDocument = ((oldTree as Node) as HTMLIFrameElement) .contentDocument; const newIFrameElement = newTree as RRIFrameElement; @@ -200,7 +197,12 @@ export function diff( if (sn) { replayer.mirror.add(oldContentDocument, { ...sn }); } - diff(oldContentDocument, newIFrameElement.contentDocument, replayer); + diff( + oldContentDocument, + newIFrameElement.contentDocument, + replayer, + rrnodeMirror, + ); } } } @@ -262,27 +264,27 @@ function diffChildren( } else if ( replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newStartNode) ) { - diff(oldStartNode, newStartNode, replayer); + diff(oldStartNode, newStartNode, replayer, rrnodeMirror); oldStartNode = oldChildren[++oldStartIndex]; newStartNode = newChildren[++newStartIndex]; } else if ( replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newEndNode) ) { - diff(oldEndNode, newEndNode, replayer); + diff(oldEndNode, newEndNode, replayer, rrnodeMirror); oldEndNode = oldChildren[--oldEndIndex]; newEndNode = newChildren[--newEndIndex]; } else if ( replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newEndNode) ) { parentNode.insertBefore(oldStartNode, oldEndNode.nextSibling); - diff(oldStartNode, newEndNode, replayer); + diff(oldStartNode, newEndNode, replayer, rrnodeMirror); oldStartNode = oldChildren[++oldStartIndex]; newEndNode = newChildren[--newEndIndex]; } else if ( replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newStartNode) ) { parentNode.insertBefore(oldEndNode, oldStartNode); - diff(oldEndNode, newStartNode, replayer); + diff(oldEndNode, newStartNode, replayer, rrnodeMirror); oldEndNode = oldChildren[--oldEndIndex]; newStartNode = newChildren[++newStartIndex]; } else { @@ -298,7 +300,7 @@ function diffChildren( if (indexInOld) { const nodeToMove = oldChildren[indexInOld]!; parentNode.insertBefore(nodeToMove, oldStartNode); - diff(nodeToMove, newStartNode, replayer); + diff(nodeToMove, newStartNode, replayer, rrnodeMirror); oldChildren[indexInOld] = undefined; } else { const newNode = createOrGetNode( @@ -323,7 +325,7 @@ function diffChildren( oldStartNode = undefined; } parentNode.insertBefore(newNode, oldStartNode || null); - diff(newNode, newStartNode, replayer); + diff(newNode, newStartNode, replayer, rrnodeMirror); } newStartNode = newChildren[++newStartIndex]; } @@ -345,7 +347,7 @@ function diffChildren( rrnodeMirror, ); parentNode.insertBefore(newNode, referenceNode); - diff(newNode, newChildren[newStartIndex], replayer); + diff(newNode, newChildren[newStartIndex], replayer, rrnodeMirror); } } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { diff --git a/packages/rrdom/src/document-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index 5ee06ec22d..f7523ffefc 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -13,8 +13,6 @@ import { ClassList, IRRDocument, CSSStyleDeclaration, - Mirror, - createMirror, } from './document'; const nwsapi = require('nwsapi'); const cssom = require('cssom'); @@ -37,7 +35,6 @@ export class RRDocument implements IRRDocument { readonly nodeName: '#document' = '#document'; private _nwsapi: NWSAPI; - public mirror: Mirror = createMirror(); get nwsapi() { if (!this._nwsapi) { this._nwsapi = nwsapi({ @@ -57,13 +54,15 @@ export class RRDocument } get documentElement(): RRElement | null { - return ( - (this.childNodes.find( - (node) => - node.RRNodeType === RRNodeType.Element && - (node as RRElement).tagName === 'HTML', - ) as RRElement) || null - ); + return super.documentElement as RRElement | null; + } + + get body(): RRElement | null { + return super.body as RRElement | null; + } + + get head() { + return super.head as RRElement | null; } get implementation(): RRDocument { @@ -141,7 +140,7 @@ export class RRDocument element = new RRMediaElement(upperTagName); break; case 'IFRAME': - element = new RRIFrameElement(upperTagName, this.mirror); + element = new RRIFrameElement(upperTagName); break; case 'IMG': element = new RRImageElement(upperTagName); @@ -344,9 +343,8 @@ export class RRIFrameElement extends RRElement { contentDocument: RRDocument = new RRDocument(); contentWindow: RRWindow = new RRWindow(); - constructor(tagName: string, mirror: Mirror) { + constructor(tagName: string) { super(tagName); - this.contentDocument.mirror = mirror; const htmlElement = this.contentDocument.createElement('HTML'); this.contentDocument.appendChild(htmlElement); htmlElement.appendChild(this.contentDocument.createElement('HEAD')); diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index f25f46ffa8..aec54d9a98 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -1,11 +1,6 @@ -import { - IMirror, - NodeType as RRNodeType, - serializedNodeWithId, -} from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { parseCSSText, camelize, toCSSText } from './style'; export interface IRRNode { - __sn: serializedNodeWithId; parentElement: IRRNode | null; parentNode: IRRNode | null; childNodes: IRRNode[]; @@ -18,6 +13,7 @@ export interface IRRNode { readonly RRNodeType: RRNodeType; firstChild: IRRNode | null; + lastChild: IRRNode | null; nextSibling: IRRNode | null; @@ -32,26 +28,9 @@ export interface IRRNode { removeChild(node: IRRNode): IRRNode; - /** - * @deprecated - * Set a default value for RRNode's __sn property. - * @param id the serialized id to assign - */ - setDefaultSN(id: number): void; - - /** - * Get a default value for RRNode's mirror. - * @param id the serialized id to assign - */ - getDefaultSN(id: number): serializedNodeWithId; - - toString(nodeName?: string): string; + toString(): string; } export interface IRRDocument extends IRRNode { - mirror: Mirror; - - unserializedId: number; - documentElement: IRRElement | null; body: IRRElement | null; @@ -62,7 +41,7 @@ export interface IRRDocument extends IRRNode { firstElementChild: IRRElement | null; - nodeName: '#document'; + readonly nodeName: '#document'; compatMode: 'BackCompat' | 'CSS1Compat'; @@ -127,15 +106,15 @@ export interface IRRDocumentType extends IRRNode { readonly systemId: string; } export interface IRRText extends IRRNode { - nodeName: '#text'; + readonly nodeName: '#text'; data: string; } export interface IRRComment extends IRRNode { - nodeName: '#comment'; + readonly nodeName: '#comment'; data: string; } export interface IRRCDATASection extends IRRNode { - nodeName: '#cdata-section'; + readonly nodeName: '#cdata-section'; data: string; } @@ -145,7 +124,6 @@ type ConstrainedConstructor = new (...args: any[]) => T; * This is designed as an abstract class so it should never be instantiated. */ export class BaseRRNode implements IRRNode { - public __sn: serializedNodeWithId; public childNodes: IRRNode[] = []; public parentElement: IRRNode | null = null; public parentNode: IRRNode | null = null; @@ -202,59 +180,8 @@ export class BaseRRNode implements IRRNode { ); } - public getDefaultSN(id: number): serializedNodeWithId { - switch (this.RRNodeType) { - case RRNodeType.Document: - return { - id, - type: this.RRNodeType, - childNodes: [], - }; - case RRNodeType.DocumentType: - const doctype = (this as unknown) as IRRDocumentType; - return { - id, - type: this.RRNodeType, - name: doctype.name, - publicId: doctype.publicId, - systemId: doctype.systemId, - }; - case RRNodeType.Element: - return { - id, - type: this.RRNodeType, - tagName: ((this as unknown) as IRRElement).tagName.toLowerCase(), // In rrweb data, all tagNames are lowercase. - attributes: {}, - childNodes: [], - }; - case RRNodeType.Text: - return { - id, - type: this.RRNodeType, - textContent: ((this as unknown) as IRRText).textContent || '', - }; - case RRNodeType.Comment: - return { - id, - type: this.RRNodeType, - textContent: ((this as unknown) as IRRComment).textContent || '', - }; - case RRNodeType.CDATA: - return { - id, - type: this.RRNodeType, - textContent: '', - }; - } - } - - // @deprecated - public setDefaultSN(id: number) { - this.__sn = this.getDefaultSN(id); - } - - public toString(nodeName: string) { - return `${this.__sn?.id || ''} ${nodeName}`; + public toString(): string { + return 'RRNode'; } } @@ -262,23 +189,11 @@ export function BaseRRDocumentImpl< RRNode extends ConstrainedConstructor >(RRNodeClass: RRNode) { return class BaseRRDocument extends RRNodeClass implements IRRDocument { - public mirror: Mirror = createMirror(); public readonly nodeType: number = NodeType.DOCUMENT_NODE; public readonly nodeName: '#document' = '#document'; public readonly compatMode: 'BackCompat' | 'CSS1Compat' = 'CSS1Compat'; public readonly RRNodeType = RRNodeType.Document; public textContent: string | null = null; - // In the rrweb replayer, there are some unserialized nodes like the element that stores the injected style rules. - // These unserialized nodes may interfere the execution of the diff algorithm. - // The id of serialized node is larger than 0. So this value ​​less than 0 is used as id for these unserialized nodes. - _unserializedId = -1; - - /** - * Every time the id is used, it will minus 1 automatically to avoid collisions. - */ - public get unserializedId(): number { - return this._unserializedId--; - } public get documentElement(): IRRElement | null { return ( @@ -378,7 +293,6 @@ export function BaseRRDocumentImpl< public open() { this.childNodes = []; - this._unserializedId = -1; } public close() {} @@ -403,7 +317,6 @@ export function BaseRRDocumentImpl< publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; if (publicId) { const doctype = this.createDocumentType('html', publicId, ''); - setDefaultSN(doctype, this.unserializedId, this.mirror); this.open(); this.appendChild(doctype); } @@ -460,7 +373,7 @@ export function BaseRRDocumentImpl< } toString() { - return super.toString('RRDocument'); + return 'RRDocument'; } }; } @@ -474,6 +387,7 @@ export function BaseRRDocumentTypeImpl< implements IRRDocumentType { public readonly nodeType: number = NodeType.DOCUMENT_TYPE_NODE; public readonly RRNodeType = RRNodeType.DocumentType; + public readonly nodeName: string; public readonly name: string; public readonly publicId: string; public readonly systemId: string; @@ -484,10 +398,11 @@ export function BaseRRDocumentTypeImpl< this.name = qualifiedName; this.publicId = publicId; this.systemId = systemId; + this.nodeName = qualifiedName; } toString() { - return super.toString('RRDocumentType'); + return 'RRDocumentType'; } }; } @@ -634,7 +549,7 @@ export function BaseRRElementImpl< for (let attribute in this.attributes) { attributeString += `${attribute}="${this.attributes[attribute]}" `; } - return `${super.toString(this.tagName)} ${attributeString}`; + return `${this.tagName} ${attributeString}`; } }; } @@ -685,7 +600,7 @@ export function BaseRRTextImpl>( } toString() { - return `${super.toString('RRText')} text=${JSON.stringify(this.data)}`; + return `RRText text=${JSON.stringify(this.data)}`; } }; } @@ -714,7 +629,7 @@ export function BaseRRCommentImpl< } toString() { - return `${super.toString('RRComment')} text=${JSON.stringify(this.data)}`; + return `RRComment text=${JSON.stringify(this.data)}`; } }; } @@ -745,9 +660,7 @@ export function BaseRRCDATASectionImpl< } toString() { - return `${super.toString('RRCDATASection')} data=${JSON.stringify( - this.data, - )}`; + return `RRCDATASection data=${JSON.stringify(this.data)}`; } }; } @@ -808,78 +721,3 @@ export enum NodeType { DOCUMENT_TYPE_NODE, DOCUMENT_FRAGMENT_NODE, } - -export function createMirror(): Mirror { - return new Mirror(); -} - -// based on Mirror from rrweb-snapshots -export class Mirror implements IMirror { - private idNodeMap: Map = new Map(); - private nodeMetaMap: WeakMap< - BaseRRNode, - serializedNodeWithId - > = new WeakMap(); - - getId(n: BaseRRNode | undefined | null): number { - if (!n) return -1; - - const id = this.getMeta(n)?.id; - - // if n is not a serialized Node, use -1 as its id. - return id ?? -1; - } - - getNode(id: number): BaseRRNode | null { - return this.idNodeMap.get(id) || null; - } - - getIds(): number[] { - return Array.from(this.idNodeMap.keys()); - } - - getMeta(n: BaseRRNode): serializedNodeWithId | null { - return this.nodeMetaMap.get(n) || null; - } - - // removes the node from idNodeMap - // doesn't remove the node from nodeMetaMap - removeNodeFromMap(n: BaseRRNode) { - const id = this.getId(n); - this.idNodeMap.delete(id); - - if (n.childNodes) { - n.childNodes.forEach((childNode) => - this.removeNodeFromMap((childNode as unknown) as BaseRRNode), - ); - } - } - has(id: number): boolean { - return this.idNodeMap.has(id); - } - - hasNode(node: BaseRRNode): boolean { - return this.nodeMetaMap.has(node); - } - - add(n: BaseRRNode, meta: serializedNodeWithId) { - const id = meta.id; - this.idNodeMap.set(id, n); - this.nodeMetaMap.set(n, meta); - } - - replace(id: number, n: BaseRRNode) { - this.idNodeMap.set(id, n); - } - - reset() { - this.idNodeMap = new Map(); - this.nodeMetaMap = new WeakMap(); - } -} - -export function setDefaultSN(node: IRRNode, id: number, mirror: Mirror) { - const sn = node.getDefaultSN(id); - node.setDefaultSN(id); // DEPRECATED - mirror.add(node, sn); -} diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index d563ccfc2a..52ba178431 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -1,9 +1,12 @@ import { NodeType as RRNodeType, - serializedNodeWithId, createMirror as createNodeMirror, } from 'rrweb-snapshot'; -import type { Mirror as NodeMirror } from 'rrweb-snapshot'; +import type { + Mirror as NodeMirror, + IMirror, + serializedNodeWithId, +} from 'rrweb-snapshot'; import type { canvasMutationData, canvasEventWithTime, @@ -23,15 +26,28 @@ import { IRRElement, IRRNode, NodeType, - createMirror, - Mirror, + IRRDocumentType, + IRRText, + IRRComment, } from './document'; import type { VirtualStyleRules } from './diff'; export class RRDocument extends BaseRRDocumentImpl(RRNode) { + // In the rrweb replayer, there are some unserialized nodes like the element that stores the injected style rules. + // These unserialized nodes may interfere the execution of the diff algorithm. + // The id of serialized node is larger than 0. So this value ​​less than 0 is used as id for these unserialized nodes. + private _unserializedId = -1; + + /** + * Every time the id is used, it will minus 1 automatically to avoid collisions. + */ + public get unserializedId(): number { + return this._unserializedId--; + } + public mirror: Mirror = createMirror(); - scrollData: scrollData | null = null; + public scrollData: scrollData | null = null; constructor(mirror?: Mirror) { super(); @@ -113,6 +129,11 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { this.childNodes = []; this.mirror.reset(); } + + open() { + super.open(); + this._unserializedId = -1; + } } export const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); @@ -192,15 +213,10 @@ export function buildFromNode( parentRRNode?: IRRNode | null, ): IRRNode | null { let rrNode: IRRNode; - const rrdomMirror = rrdom.mirror; switch (node.nodeType) { case NodeType.DOCUMENT_NODE: - if ( - parentRRNode && - parentRRNode.RRNodeType === RRNodeType.Element && - (parentRRNode as IRRElement).tagName === 'IFRAME' - ) + if (parentRRNode && parentRRNode.nodeName === 'IFRAME') rrNode = (parentRRNode as RRIFrameElement).contentDocument; else { rrNode = rrdom; @@ -252,13 +268,14 @@ export function buildFromNode( } let sn: serializedNodeWithId | null = domMirror.getMeta(node); - if (!sn) { - const { unserializedId } = rrdom; - rrNode.setDefaultSN(unserializedId); - sn = rrNode.getDefaultSN(unserializedId); - } else rrNode.__sn = sn; - rrdomMirror.add(rrNode, { ...sn }); + if (rrdom instanceof RRDocument) { + if (!sn) { + sn = getDefaultSN(rrNode, rrdom.unserializedId); + domMirror.add(node, sn); + } + rrdom.mirror.add(rrNode, { ...sn }); + } return rrNode; } @@ -275,17 +292,12 @@ export function buildFromDom( domMirror: NodeMirror = createNodeMirror(), rrdom: IRRDocument = new RRDocument(), ) { - const rrdomMirror = rrdom.mirror; - function walk(node: Node, parentRRNode: IRRNode | null) { const rrNode = buildFromNode(node, rrdom, domMirror, parentRRNode); if (rrNode === null) return; if ( // if the parentRRNode isn't a RRIFrameElement - !( - parentRRNode?.RRNodeType === RRNodeType.Element && - (parentRRNode as IRRElement).tagName === 'IFRAME' - ) && + parentRRNode?.nodeName !== 'IFRAME' && // if node isn't a shadow root node.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE ) { @@ -294,10 +306,7 @@ export function buildFromDom( rrNode.parentElement = parentRRNode as RRElement; } - if ( - node.nodeType === NodeType.ELEMENT_NODE && - (node as HTMLElement).tagName === 'IFRAME' - ) { + if (node.nodeName === 'IFRAME') { walk((node as HTMLIFrameElement).contentDocument!, rrNode); } else if ( node.nodeType === NodeType.DOCUMENT_NODE || @@ -317,6 +326,120 @@ export function buildFromDom( return rrdom; } +export function createMirror(): Mirror { + return new Mirror(); +} + +// based on Mirror from rrweb-snapshots +export class Mirror implements IMirror { + private idNodeMap: Map = new Map(); + private nodeMetaMap: WeakMap = new WeakMap(); + + getId(n: RRNode | undefined | null): number { + if (!n) return -1; + + const id = this.getMeta(n)?.id; + + // if n is not a serialized Node, use -1 as its id. + return id ?? -1; + } + + getNode(id: number): RRNode | null { + return this.idNodeMap.get(id) || null; + } + + getIds(): number[] { + return Array.from(this.idNodeMap.keys()); + } + + getMeta(n: RRNode): serializedNodeWithId | null { + return this.nodeMetaMap.get(n) || null; + } + + // removes the node from idNodeMap + // doesn't remove the node from nodeMetaMap + removeNodeFromMap(n: RRNode) { + const id = this.getId(n); + this.idNodeMap.delete(id); + + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id: number): boolean { + return this.idNodeMap.has(id); + } + + hasNode(node: RRNode): boolean { + return this.nodeMetaMap.has(node); + } + + add(n: RRNode, meta: serializedNodeWithId) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + + replace(id: number, n: RRNode) { + this.idNodeMap.set(id, n); + } + + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} + +/** + * Get a default serializedNodeWithId value for a RRNode. + * @param id the serialized id to assign + */ +export function getDefaultSN(node: IRRNode, id: number): serializedNodeWithId { + switch (node.RRNodeType) { + case RRNodeType.Document: + return { + id, + type: node.RRNodeType, + childNodes: [], + }; + case RRNodeType.DocumentType: + const doctype = node as IRRDocumentType; + return { + id, + type: node.RRNodeType, + name: doctype.name, + publicId: doctype.publicId, + systemId: doctype.systemId, + }; + case RRNodeType.Element: + return { + id, + type: node.RRNodeType, + tagName: (node as IRRElement).tagName.toLowerCase(), // In rrweb data, all tagNames are lowercase. + attributes: {}, + childNodes: [], + }; + case RRNodeType.Text: + return { + id, + type: node.RRNodeType, + textContent: (node as IRRText).textContent || '', + }; + case RRNodeType.Comment: + return { + id, + type: node.RRNodeType, + textContent: (node as IRRComment).textContent || '', + }; + case RRNodeType.CDATA: + return { + id, + type: node.RRNodeType, + textContent: '', + }; + } +} + export { RRNode }; export { diff, diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index ee10241493..22feace950 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ -import { RRDocument, RRMediaElement } from '../src/virtual-dom'; +import { getDefaultSN, RRDocument, RRMediaElement } from '../src/virtual-dom'; import { applyVirtualStyleRulesToNode, createOrGetNode, @@ -16,7 +16,7 @@ import { createMirror, Mirror, } from 'rrweb-snapshot'; -import { IRRNode, setDefaultSN } from '../src/document'; +import type { IRRNode } from '../src/document'; import { canvasMutationData, EventType, @@ -48,7 +48,7 @@ type RRNode = IRRNode; */ function createTree( treeNode: ElementType, - rrDocument?: RRDocument | undefined, + rrDocument?: RRDocument, mirror: Mirror = createMirror(), ): Node | RRNode { type TNode = typeof rrDocument extends RRDocument ? RRNode : Node; @@ -1037,7 +1037,10 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const rrNode = rrDocument.createElement('iframe'); - setDefaultSN(rrNode.contentDocument, 1, rrDocument.mirror); + rrDocument.mirror.add( + rrNode.contentDocument, + getDefaultSN(rrNode.contentDocument, 1), + ); const childElement = rrNode.contentDocument.createElement('div'); rrNode.contentDocument.appendChild(childElement); @@ -1070,28 +1073,28 @@ describe('diff algorithm for rrdom', () => { it('create a node from RRNode', () => { const rrDocument = new RRDocument(); - setDefaultSN(rrDocument, 0, rrDocument.mirror); + rrDocument.mirror.add(rrDocument, getDefaultSN(rrDocument, 0)); let result = createOrGetNode(rrDocument, mirror, rrDocument.mirror); expect(result).toBeInstanceOf(Document); expect(mirror.getId(result)).toBe(0); const textContent = 'Text Content'; let rrNode: RRNode = rrDocument.createTextNode(textContent); - setDefaultSN(rrNode, 1, rrDocument.mirror); + rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 1)); result = createOrGetNode(rrNode, mirror, rrDocument.mirror); expect(result).toBeInstanceOf(Text); expect(mirror.getId(result)).toBe(1); expect(((result as Node) as Text).textContent).toBe(textContent); rrNode = rrDocument.createComment(textContent); - setDefaultSN(rrNode, 2, rrDocument.mirror); + rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 2)); result = createOrGetNode(rrNode, mirror, rrDocument.mirror); expect(result).toBeInstanceOf(Comment); expect(mirror.getId(result)).toBe(2); expect(((result as Node) as Comment).textContent).toBe(textContent); rrNode = rrDocument.createCDATASection(''); - setDefaultSN(rrNode, 3, rrDocument.mirror); + rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 3)); expect(() => createOrGetNode(rrNode, mirror, rrDocument.mirror), ).toThrowErrorMatchingInlineSnapshot( @@ -1103,7 +1106,7 @@ describe('diff algorithm for rrdom', () => { const rrDocument = new RRDocument(); const publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; let rrNode: RRNode = rrDocument.createDocumentType('html', publicId, ''); - setDefaultSN(rrNode, 0, rrDocument.mirror); + rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 0)); let result = createOrGetNode(rrNode, mirror, rrDocument.mirror); expect(result).toBeInstanceOf(DocumentType); expect(mirror.getId(result)).toBe(0); @@ -1124,7 +1127,7 @@ describe('diff algorithm for rrdom', () => { // Add the text node to the mirror to make it look like already existing. mirror.add(text, sn); const rrNode: RRNode = rrDocument.createTextNode(textContent); - setDefaultSN(rrNode, 0, rrDocument.mirror); + rrDocument.mirror.add(rrNode, getDefaultSN(rrNode, 0)); let result = createOrGetNode(rrNode, mirror, rrDocument.mirror); expect(result).toBeInstanceOf(Text); diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 30f25dc199..32e43a9d57 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -1,15 +1,7 @@ /** * @jest-environment jsdom */ -import { - cdataNode, - commentNode, - documentNode, - documentTypeNode, - elementNode, - NodeType as RRNodeType, - textNode, -} from 'rrweb-snapshot'; +import { NodeType as RRNodeType } from 'rrweb-snapshot'; import { BaseRRDocumentImpl, BaseRRDocumentTypeImpl, @@ -29,13 +21,6 @@ describe('Basic RRDocument implementation', () => { describe('Basic RRNode implementation', () => { it('should have basic properties', () => { const node = new RRNode(); - node.__sn = { - type: RRNodeType.Element, - tagName: 'div', - attributes: {}, - childNodes: [], - id: 1, - }; expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -44,16 +29,17 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toBeUndefined(); expect(node.RRNodeType).toBeUndefined(); expect(node.nodeType).toBeUndefined(); + expect(node.nodeName).toBeUndefined(); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); - expect(node.toString('RRNode')).toEqual('1 RRNode'); + expect(node.toString()).toEqual('RRNode'); }); it('can get first child node', () => { @@ -69,6 +55,19 @@ describe('Basic RRDocument implementation', () => { expect(parentNode.firstChild).toBe(childNode2); }); + it('can get last child node', () => { + const parentNode = new RRNode(); + const childNode1 = new RRNode(); + const childNode2 = new RRNode(); + expect(parentNode.lastChild).toBeNull(); + parentNode.childNodes = [childNode1]; + expect(parentNode.lastChild).toBe(childNode1); + parentNode.childNodes = [childNode1, childNode2]; + expect(parentNode.lastChild).toBe(childNode2); + parentNode.childNodes = [childNode2, childNode1]; + expect(parentNode.lastChild).toBe(childNode1); + }); + it('can get nextSibling', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); @@ -124,25 +123,12 @@ describe('Basic RRDocument implementation', () => { `"RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method."`, ); }); - - it("can set a default value for RRNode's __sn property", () => { - const node = new RRNode(); - node.setDefaultSN(0); - // Because RRNode's type hasn't been specified. - expect(node.__sn).toBeUndefined(); - }); }); describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = new RRDocument(); - expect(node.toString()).toEqual(' RRDocument'); - - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.Document); - expect((node.__sn as documentNode).childNodes).toBeInstanceOf(Array); - + expect(node.toString()).toEqual('RRDocument'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); expect(node.childNodes).toBeInstanceOf(Array); @@ -151,16 +137,17 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toBeNull(); expect(node.RRNodeType).toBe(RRNodeType.Document); expect(node.nodeType).toBe(document.nodeType); + expect(node.nodeName).toBe('#document'); + expect(node.compatMode).toBe('CSS1Compat'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); - expect(node.unserializedId).toBe(-1); expect(node.documentElement).toBeNull(); expect(node.body).toBeNull(); expect(node.head).toBeNull(); @@ -176,12 +163,7 @@ describe('Basic RRDocument implementation', () => { expect(node.open).toBeDefined(); expect(node.close).toBeDefined(); expect(node.write).toBeDefined(); - expect(node.toString()).toEqual('1 RRDocument'); - }); - - it('can access a unique, decremented unserializedId every time', () => { - const node = new RRDocument(); - for (let i = 1; i <= 100; i++) expect(node.unserializedId).toBe(-i); + expect(node.toString()).toEqual('RRDocument'); }); it('can get documentElement', () => { @@ -334,11 +316,9 @@ describe('Basic RRDocument implementation', () => { const documentType = node.createDocumentType('html', '', ''); node.appendChild(documentType); expect(node.childNodes[0]).toBe(documentType); - expect(node.unserializedId).toBe(-1); expect(node.close()); expect(node.open()); expect(node.childNodes.length).toEqual(0); - expect(node.unserializedId).toBe(-1); }); it('can cover the usage of write() in rrweb-snapshot', () => { @@ -375,12 +355,6 @@ describe('Basic RRDocument implementation', () => { publicId = 'publicId', systemId = 'systemId'; const node = new RRDocumentType(name, publicId, systemId); - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.DocumentType); - expect((node.__sn as documentTypeNode).name).toEqual(name); - expect((node.__sn as documentTypeNode).publicId).toEqual(publicId); - expect((node.__sn as documentTypeNode).systemId).toEqual(systemId); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); @@ -390,19 +364,20 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toBeNull(); expect(node.RRNodeType).toBe(RRNodeType.DocumentType); expect(node.nodeType).toBe(document.DOCUMENT_TYPE_NODE); + expect(node.nodeName).toBe(name); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); expect(node.name).toBe(name); expect(node.publicId).toBe(publicId); expect(node.systemId).toBe(systemId); - expect(node.toString()).toEqual('1 RRDocumentType'); + expect(node.toString()).toEqual('RRDocumentType'); }); }); @@ -411,12 +386,6 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = document.createElement('div'); - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.Element); - expect((node.__sn as elementNode).tagName).toEqual('div'); - expect((node.__sn as elementNode).attributes).toBeDefined(); - expect((node.__sn as elementNode).childNodes).toBeInstanceOf(Array); node.scrollLeft = 100; node.scrollTop = 200; @@ -430,15 +399,16 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toEqual(''); expect(node.RRNodeType).toBe(RRNodeType.Element); expect(node.nodeType).toBe(document.ELEMENT_NODE); + expect(node.nodeName).toBe('DIV'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); expect(node.tagName).toEqual('DIV'); expect(node.attributes).toEqual({ id: 'id', class: 'className' }); expect(node.shadowRoot).toBeNull(); @@ -455,7 +425,7 @@ describe('Basic RRDocument implementation', () => { expect(node.attachShadow).toBeDefined(); expect(node.dispatchEvent).toBeDefined(); expect(node.dispatchEvent((null as unknown) as Event)).toBeTruthy(); - expect(node.toString()).toEqual('1 DIV id="id" class="className" '); + expect(node.toString()).toEqual('DIV id="id" class="className" '); }); it('can get textContent', () => { @@ -784,10 +754,6 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createTextNode('text'); - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.Text); - expect((node.__sn as textNode).textContent).toEqual('text'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); @@ -797,16 +763,17 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toEqual('text'); expect(node.RRNodeType).toBe(RRNodeType.Text); expect(node.nodeType).toBe(document.TEXT_NODE); + expect(node.nodeName).toBe('#text'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); - expect(node.toString()).toEqual('1 RRText text="text"'); + expect(node.toString()).toEqual('RRText text="text"'); }); it('can set textContent', () => { @@ -822,10 +789,6 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createComment('comment'); - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.Comment); - expect((node.__sn as commentNode).textContent).toEqual('comment'); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); @@ -835,16 +798,17 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toEqual('comment'); expect(node.RRNodeType).toBe(RRNodeType.Comment); expect(node.nodeType).toBe(document.COMMENT_NODE); + expect(node.nodeName).toBe('#comment'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); - expect(node.toString()).toEqual('1 RRComment text="comment"'); + expect(node.toString()).toEqual('RRComment text="comment"'); }); it('can set textContent', () => { @@ -860,10 +824,6 @@ describe('Basic RRDocument implementation', () => { it('should have basic properties', () => { const node = dom.createCDATASection('data'); - node.setDefaultSN(1); - expect(node.__sn).toBeDefined(); - expect(node.__sn.type).toEqual(RRNodeType.CDATA); - expect((node.__sn as cdataNode).textContent).toEqual(''); expect(node.parentNode).toEqual(null); expect(node.parentElement).toEqual(null); @@ -873,16 +833,17 @@ describe('Basic RRDocument implementation', () => { expect(node.textContent).toEqual('data'); expect(node.RRNodeType).toBe(RRNodeType.CDATA); expect(node.nodeType).toBe(document.CDATA_SECTION_NODE); + expect(node.nodeName).toBe('#cdata-section'); expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); - expect(node.toString()).toEqual('1 RRCDATASection data="data"'); + expect(node.toString()).toEqual('RRCDATASection data="data"'); }); it('can set textContent', () => { @@ -896,7 +857,6 @@ describe('Basic RRDocument implementation', () => { describe('Basic RRMediaElement implementation', () => { it('should have basic properties', () => { const node = new RRMediaElement('video'); - node.setDefaultSN(1); node.scrollLeft = 100; node.scrollTop = 200; expect(node.parentNode).toEqual(null); @@ -915,7 +875,6 @@ describe('Basic RRDocument implementation', () => { expect(node.appendChild).toBeDefined(); expect(node.insertBefore).toBeDefined(); expect(node.removeChild).toBeDefined(); - expect(node.setDefaultSN).toBeDefined(); expect(node.tagName).toEqual('VIDEO'); expect(node.attributes).toEqual({}); expect(node.shadowRoot).toBeNull(); @@ -937,7 +896,7 @@ describe('Basic RRDocument implementation', () => { expect(node.muted).toBeUndefined(); expect(node.play).toBeDefined(); expect(node.pause).toBeDefined(); - expect(node.toString()).toEqual('1 VIDEO '); + expect(node.toString()).toEqual('VIDEO '); }); it('can play and pause the media', () => { diff --git a/packages/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts index d3bf2bfd20..4bded073b2 100644 --- a/packages/rrdom/test/virtual-dom.test.ts +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -8,16 +8,27 @@ import * as rollup from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import * as typescript from 'rollup-plugin-typescript2'; import { JSDOM } from 'jsdom'; -import { Mirror, NodeType, NodeType as RRNodeType } from 'rrweb-snapshot'; +import { + cdataNode, + commentNode, + documentNode, + documentTypeNode, + elementNode, + Mirror, + NodeType, + NodeType as RRNodeType, + textNode, +} from 'rrweb-snapshot'; import { buildFromDom, buildFromNode, + createMirror, + getDefaultSN, RRCanvasElement, RRDocument, RRElement, RRNode, } from '../src/virtual-dom'; -import { setDefaultSN } from '../src/document'; const _typescript = (typescript as unknown) as typeof typescript.default; const printRRDomCode = ` @@ -26,17 +37,17 @@ const printRRDomCode = ` * @param rootNode the root node of the RRDom tree * @returns printed string */ -function printRRDom(rootNode) { - return walk(rootNode, ''); +function printRRDom(rootNode, mirror) { + return walk(rootNode, mirror, ''); } -function walk(node, blankSpace) { - let printText = \`\${blankSpace}\${node.toString()}\n\`; +function walk(node, mirror, blankSpace) { + let printText = \`\${blankSpace}\${mirror.getId(node)} \${node.toString()}\n\`; if(node instanceof rrdom.RRElement && node.shadowRoot) - printText += walk(node.shadowRoot, blankSpace + ' '); + printText += walk(node.shadowRoot, mirror, blankSpace + ' '); for (const child of node.childNodes) - printText += walk(child, blankSpace + ' '); + printText += walk(child, mirror, blankSpace + ' '); if (node instanceof rrdom.RRIFrameElement) - printText += walk(node.contentDocument, blankSpace + ' '); + printText += walk(node.contentDocument, mirror, blankSpace + ' '); return printText; } `; @@ -69,20 +80,22 @@ describe('RRDocument for browser environment', () => { expect(mirror.getMeta(document)).toBeDefined(); expect(mirror.getId(document)).toEqual(-1); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.Document); - expect(rrNode.__sn.id).toEqual(-1); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document); + expect(rrdom.mirror.getId(rrNode)).toEqual(-1); expect(rrNode).toBe(rrdom); // build from document type expect(mirror.getMeta(document.doctype!)).toBeNull(); rrNode = buildFromNode(document.doctype!, rrdom, mirror)!; expect(mirror.getMeta(document.doctype!)).toBeDefined(); - expect(mirror.getId(document.doctype)).toEqual(-1); + expect(mirror.getId(document.doctype)).toEqual(-2); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.DocumentType); - expect(rrNode.__sn.id).toEqual(-2); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual( + RRNodeType.DocumentType, + ); + expect(rrdom.mirror.getId(rrNode)).toEqual(-2); // build from element expect(mirror.getMeta(document.documentElement)).toBeNull(); @@ -92,33 +105,33 @@ describe('RRDocument for browser environment', () => { mirror, )!; expect(mirror.getMeta(document.documentElement)).toBeDefined(); - expect(mirror.getId(document.documentElement)).toEqual(-1); + expect(mirror.getId(document.documentElement)).toEqual(-3); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.Element); - expect(rrNode.__sn.id).toEqual(-3); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Element); + expect(rrdom.mirror.getId(rrNode)).toEqual(-3); // build from text const text = document.createTextNode('text'); expect(mirror.getMeta(text)).toBeNull(); rrNode = buildFromNode(text, rrdom, mirror)!; expect(mirror.getMeta(text)).toBeDefined(); - expect(mirror.getId(text)).toEqual(-1); + expect(mirror.getId(text)).toEqual(-4); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.Text); - expect(rrNode.__sn.id).toEqual(-4); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Text); + expect(rrdom.mirror.getId(rrNode)).toEqual(-4); // build from comment const comment = document.createComment('comment'); expect(mirror.getMeta(comment)).toBeNull(); rrNode = buildFromNode(comment, rrdom, mirror)!; expect(mirror.getMeta(comment)).toBeDefined(); - expect(mirror.getId(comment)).toEqual(-1); + expect(mirror.getId(comment)).toEqual(-5); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.Comment); - expect(rrNode.__sn.id).toEqual(-5); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Comment); + expect(rrdom.mirror.getId(rrNode)).toEqual(-5); // build from CDATASection const xmlDoc = new DOMParser().parseFromString( @@ -131,12 +144,12 @@ describe('RRDocument for browser environment', () => { expect(mirror.getMeta(cdataSection)).toBeNull(); rrNode = buildFromNode(cdataSection, rrdom, mirror)!; expect(mirror.getMeta(cdataSection)).toBeDefined(); - expect(mirror.getId(cdataSection)).toEqual(-1); + expect(mirror.getId(cdataSection)).toEqual(-6); expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.CDATA); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.CDATA); + expect(rrdom.mirror.getId(rrNode)).toEqual(-6); expect(rrNode.textContent).toEqual(cdata); - expect(rrNode.__sn.id).toEqual(-6); }); it('can record scroll position from HTMLElements', () => { @@ -169,9 +182,9 @@ describe('RRDocument for browser environment', () => { RRIFrame, )!; expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.type).toEqual(RRNodeType.Document); - expect(rrNode.__sn.id).toEqual(-1); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getMeta(rrNode)!.type).toEqual(RRNodeType.Document); + expect(rrdom.mirror.getId(rrNode)).toEqual(-1); expect(mirror.getId(iframe.contentDocument)).toEqual(-1); expect(rrNode).toBe(RRIFrame.contentDocument); }); @@ -189,8 +202,8 @@ describe('RRDocument for browser environment', () => { parentRRNode, )!; expect(rrNode).not.toBeNull(); - expect(rrNode.__sn).toBeDefined(); - expect(rrNode.__sn.id).toEqual(-1); + expect(rrdom.mirror.getMeta(rrNode)).toBeDefined(); + expect(rrdom.mirror.getId(rrNode)).toEqual(-1); expect(mirror.getId(div.shadowRoot)).toEqual(-1); expect(rrNode.RRNodeType).toEqual(RRNodeType.Element); expect((rrNode as RRElement).tagName).toEqual('SHADOWROOT'); @@ -240,7 +253,7 @@ describe('RRDocument for browser environment', () => { const result = await page.evaluate(` const doc = new rrdom.RRDocument(); rrdom.buildFromDom(document, undefined, doc); - printRRDom(doc); + printRRDom(doc, doc.mirror); `); expect(result).toMatchSnapshot(); }); @@ -250,7 +263,7 @@ describe('RRDocument for browser environment', () => { const result = await page.evaluate(` const doc = new rrdom.RRDocument(); rrdom.buildFromDom(document, undefined, doc); - printRRDom(doc); + printRRDom(doc, doc.mirror); `); expect(result).toMatchSnapshot(); }); @@ -260,7 +273,7 @@ describe('RRDocument for browser environment', () => { const result = await page.evaluate(` const doc = new rrdom.RRDocument(); rrdom.buildFromDom(document, undefined, doc); - printRRDom(doc); + printRRDom(doc, doc.mirror); `); expect(result).toMatchSnapshot(); }); @@ -274,19 +287,32 @@ describe('RRDocument for browser environment', () => { const doc = new rrdom.RRDocument(); rrdom.buildFromDom(docu, undefined, doc); - printRRDom(doc); + printRRDom(doc, doc.mirror); `); expect(result).toMatchSnapshot(); }); }); describe('RRDocument build for virtual dom', () => { + it('can access a unique, decremented unserializedId every time', () => { + const node = new RRDocument(); + for (let i = 1; i <= 100; i++) expect(node.unserializedId).toBe(-i); + }); + it('can create a new RRDocument', () => { const dom = new RRDocument(); const newDom = dom.createDocument('', ''); expect(newDom).toBeInstanceOf(RRDocument); }); + it('can create a new RRDocument receiving a mirror parameter', () => { + const mirror = createMirror(); + const dom = new RRDocument(mirror); + const newDom = dom.createDocument('', ''); + expect(newDom).toBeInstanceOf(RRDocument); + expect(dom.mirror).toBe(mirror); + }); + it('can build a RRDocument from a real Dom', () => { const result = buildFromDom(document, mirror); expect(result.childNodes.length).toBe(2); @@ -326,6 +352,19 @@ describe('RRDocument for browser environment', () => { expect(dom.mirror.has(1)).toBeFalsy(); }); + it('can close and open a RRDocument', () => { + const dom = new RRDocument(); + const documentType = dom.createDocumentType('html', '', ''); + dom.appendChild(documentType); + expect(dom.childNodes[0]).toBe(documentType); + expect(dom.unserializedId).toBe(-1); + expect(dom.unserializedId).toBe(-2); + expect(dom.close()); + expect(dom.open()); + expect(dom.childNodes.length).toEqual(0); + expect(dom.unserializedId).toBe(-1); + }); + it('can execute a dummy getContext function in RRCanvasElement', () => { const canvas = new RRCanvasElement('CANVAS'); expect(canvas.getContext).toBeDefined(); @@ -337,10 +376,10 @@ describe('RRDocument for browser environment', () => { const dom = new RRDocument(); expect(dom.mirror).toBeDefined(); const node1 = dom.createElement('div'); - dom.mirror.add(node1, node1.getDefaultSN(0)); + dom.mirror.add(node1, getDefaultSN(node1, 0)); const node2 = dom.createTextNode('text'); - dom.mirror.add(node2, node2.getDefaultSN(1)); + dom.mirror.add(node2, getDefaultSN(node2, 1)); expect(dom.mirror.getNode(0)).toBe(node1); expect(dom.mirror.getNode(1)).toBe(node2); @@ -351,7 +390,7 @@ describe('RRDocument for browser environment', () => { it('can get node id', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); - setDefaultSN(node1, 0, dom.mirror); + dom.mirror.add(node1, getDefaultSN(node1, 0)); expect(dom.mirror.getId(node1)).toEqual(0); const node2 = dom.createTextNode('text'); @@ -359,12 +398,12 @@ describe('RRDocument for browser environment', () => { expect(dom.mirror.getId((null as unknown) as RRNode)).toEqual(-1); }); - it('has() should return whether the mirror has a node', () => { + it('has() should return whether the mirror has an ID', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); - dom.mirror.add(node1, node1.getDefaultSN(0)); + dom.mirror.add(node1, getDefaultSN(node1, 0)); const node2 = dom.createTextNode('text'); - dom.mirror.add(node2, node2.getDefaultSN(1)); + dom.mirror.add(node2, getDefaultSN(node2, 1)); expect(dom.mirror.has(0)).toBeTruthy(); expect(dom.mirror.has(1)).toBeTruthy(); expect(dom.mirror.has(2)).toBeFalsy(); @@ -374,18 +413,17 @@ describe('RRDocument for browser environment', () => { it('can remove node from the mirror', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); - setDefaultSN(node1, 0, dom.mirror); + dom.mirror.add(node1, getDefaultSN(node1, 0)); const node2 = dom.createTextNode('text'); - node2.setDefaultSN(1); + dom.mirror.add(node2, getDefaultSN(node2, 1)); node1.appendChild(node2); - dom.mirror.add(node2, node2.getDefaultSN(1)); expect(dom.mirror.has(0)).toBeTruthy(); expect(dom.mirror.has(1)).toBeTruthy(); dom.mirror.removeNodeFromMap(node2); expect(dom.mirror.has(0)).toBeTruthy(); expect(dom.mirror.has(1)).toBeFalsy(); - dom.mirror.add(node2, node2.getDefaultSN(1)); + dom.mirror.add(node2, getDefaultSN(node2, 1)); expect(dom.mirror.has(1)).toBeTruthy(); // To remove node1 and its child node2 from the mirror. dom.mirror.removeNodeFromMap(node1); @@ -396,9 +434,9 @@ describe('RRDocument for browser environment', () => { it('can reset the mirror', () => { const dom = new RRDocument(); const node1 = dom.createElement('div'); - dom.mirror.add(node1, node1.getDefaultSN(0)); + dom.mirror.add(node1, getDefaultSN(node1, 0)); const node2 = dom.createTextNode('text'); - dom.mirror.add(node2, node2.getDefaultSN(1)); + dom.mirror.add(node2, getDefaultSN(node2, 1)); expect(dom.mirror.has(0)).toBeTruthy(); expect(dom.mirror.has(1)).toBeTruthy(); @@ -406,6 +444,103 @@ describe('RRDocument for browser environment', () => { expect(dom.mirror.has(0)).toBeFalsy(); expect(dom.mirror.has(1)).toBeFalsy(); }); + + it('hasNode() should return whether the mirror has a node', () => { + const dom = new RRDocument(); + const node1 = dom.createElement('div'); + const node2 = dom.createTextNode('text'); + expect(dom.mirror.hasNode(node1)).toBeFalsy(); + dom.mirror.add(node1, getDefaultSN(node1, 0)); + expect(dom.mirror.hasNode(node1)).toBeTruthy(); + expect(dom.mirror.hasNode(node2)).toBeFalsy(); + dom.mirror.add(node2, getDefaultSN(node2, 1)); + expect(dom.mirror.hasNode(node2)).toBeTruthy(); + }); + + it('can get all IDs from the mirror', () => { + const dom = new RRDocument(); + expect(dom.mirror.getIds().length).toBe(0); + const node1 = dom.createElement('div'); + dom.mirror.add(node1, getDefaultSN(node1, 0)); + const node2 = dom.createTextNode('text'); + dom.mirror.add(node2, getDefaultSN(node2, 1)); + expect(dom.mirror.getIds().length).toBe(2); + expect(dom.mirror.getIds()).toStrictEqual([0, 1]); + }); + + it('can replace nodes', () => { + const dom = new RRDocument(); + expect(dom.mirror.getIds().length).toBe(0); + const node1 = dom.createElement('div'); + dom.mirror.add(node1, getDefaultSN(node1, 0)); + expect(dom.mirror.getNode(0)).toBe(node1); + const node2 = dom.createTextNode('text'); + dom.mirror.replace(0, node2); + expect(dom.mirror.getNode(0)).toBe(node2); + }); + }); + }); + + describe('can get default SN value from a RRNode', () => { + const rrdom = new RRDocument(); + it('can get from RRDocument', () => { + const node = rrdom; + const sn = getDefaultSN(node, 1); + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.Document); + expect((sn as documentNode).childNodes).toBeInstanceOf(Array); + }); + + it('can get from RRDocumentType', () => { + const name = 'name', + publicId = 'publicId', + systemId = 'systemId'; + const node = rrdom.createDocumentType(name, publicId, systemId); + const sn = getDefaultSN(node, 1); + + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.DocumentType); + expect((sn as documentTypeNode).name).toEqual(name); + expect((sn as documentTypeNode).publicId).toEqual(publicId); + expect((sn as documentTypeNode).systemId).toEqual(systemId); + }); + + it('can get from RRElement', () => { + const node = rrdom.createElement('div'); + const sn = getDefaultSN(node, 1); + + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.Element); + expect((sn as elementNode).tagName).toEqual('div'); + expect((sn as elementNode).attributes).toBeDefined(); + expect((sn as elementNode).childNodes).toBeInstanceOf(Array); + }); + + it('can get from RRText', () => { + const node = rrdom.createTextNode('text'); + const sn = getDefaultSN(node, 1); + + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.Text); + expect((sn as textNode).textContent).toEqual('text'); + }); + + it('can get from RRComment', () => { + const node = rrdom.createComment('comment'); + const sn = getDefaultSN(node, 1); + + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.Comment); + expect((sn as commentNode).textContent).toEqual('comment'); + }); + + it('can get from RRCDATASection', () => { + const node = rrdom.createCDATASection('data'); + const sn = getDefaultSN(node, 1); + + expect(sn).toBeDefined(); + expect(sn.type).toEqual(RRNodeType.CDATA); + expect((sn as cdataNode).textContent).toEqual(''); }); }); }); diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index a7d83c8b3f..72b6ffff89 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -79,7 +79,7 @@ "base64-arraybuffer": "^1.0.1", "fflate": "^0.4.4", "mitt": "^1.1.3", - "rrdom": "^0.1.1", + "rrdom": "^0.1.2", "rrweb-snapshot": "^1.1.14" } } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 9abf1f46b1..6cb8bf5c21 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -21,8 +21,9 @@ import { buildFromNode, buildFromDom, diff, + getDefaultSN, } from 'rrdom/es/virtual-dom'; -import type { Mirror as RRDOMMirror } from 'rrdom/es/document'; +import type { Mirror as RRDOMMirror } from 'rrdom/es/virtual-dom'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -728,7 +729,10 @@ export class Replayer { } if (this.usingVirtualDom) { const styleEl = this.virtualDom.createElement('style') as RRStyleElement; - styleEl.setDefaultSN(this.virtualDom.unserializedId); + this.virtualDom.mirror.add( + styleEl, + getDefaultSN(styleEl, this.virtualDom.unserializedId), + ); (documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); for (let idx = 0; idx < injectStylesRules.length; idx++) { // push virtual styles From 15247dcad229fef5d8d909d0a386ff3e790ef9ec Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 01:16:36 +1000 Subject: [PATCH 73/79] fix typo --- packages/rrweb/src/replay/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 6cb8bf5c21..b1a76a75b0 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1353,7 +1353,7 @@ export class Replayer { } private applyMutation(d: mutationData, isSync: boolean) { - // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of cr eating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events. + // Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events. if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) { this.usingVirtualDom = true; buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom); From f6b4456724232c32ab516535e66a36078ad62ea4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 17:53:33 +1000 Subject: [PATCH 74/79] test: add a test case for StyleSheet mutation exceptions while fast-forwarding --- .../test/events/style-sheet-text-mutation.ts | 178 ++++++++++++++++++ packages/rrweb/test/replayer.test.ts | 52 +++++ 2 files changed, 230 insertions(+) create mode 100644 packages/rrweb/test/events/style-sheet-text-mutation.ts diff --git a/packages/rrweb/test/events/style-sheet-text-mutation.ts b/packages/rrweb/test/events/style-sheet-text-mutation.ts new file mode 100644 index 0000000000..c795f6bef2 --- /dev/null +++ b/packages/rrweb/test/events/style-sheet-text-mutation.ts @@ -0,0 +1,178 @@ +import { EventType, eventWithTime, IncrementalSource } from '../../src/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { + id: 101, + type: 2, + tagName: 'style', + attributes: {}, + childNodes: [ + { + id: 102, + type: 3, + isStyle: true, + textContent: '\n.css-added-at-100 {color: yellow;}\n', + }, + ], + }, + ], + }, + { + id: 107, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds an element + { + data: { + adds: [ + { + node: { + id: 108, + type: 2, + tagName: 'div', + attributes: {}, + childNodes: [], + }, + nextId: null, + parentId: 107, + }, + ], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-1000-overwritten-at-1500 {color:red;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 1000, + }, + // adds a StyleSheetRule by adding a text + { + data: { + adds: [ + { + node: { + type: 3, + textContent: '.css-added-at-1500-deleted-at-2500 {color: yellow;}', + id: 109, + }, + nextId: null, + parentId: 101, + }, + ], + texts: [], + source: IncrementalSource.Mutation, + removes: [], + attributes: [], + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 1500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-2000-overwritten-at-2500 {color: blue;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 2000, + }, + // deletes a StyleSheetRule by removing the text + { + data: { + texts: [], + attributes: [], + removes: [{ parentId: 101, id: 109 }], + adds: [], + source: IncrementalSource.Mutation, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 2500, + }, + // adds a StyleSheetRule by inserting + { + data: { + id: 101, + adds: [ + { + rule: '.css-added-at-3000 {color: red;}', + }, + ], + source: IncrementalSource.StyleSheetRule, + }, + type: EventType.IncrementalSnapshot, + timestamp: now + 3000, + }, +]; + +export default events; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index b563732f81..df5af56c4c 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -8,10 +8,12 @@ import { launchPuppeteer, sampleEvents as events, sampleStyleSheetRemoveEvents as stylesheetRemoveEvents, + waitForRAF, } from './utils'; import styleSheetRuleEvents from './events/style-sheet-rule-events'; import orderingEvents from './events/ordering'; import iframeEvents from './events/iframe'; +import StyleSheetTextMutation from './events/style-sheet-text-mutation'; interface ISuite { code: string; @@ -253,6 +255,56 @@ describe('replayer', function () { expect(result).toEqual(true); }); + it('should delete all inserted StyleSheetRules by adding a text to style element while fast-forwarding', async () => { + await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(1600); + let rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-1000-overwritten-at-1500'); + `); + expect(result).toEqual(false); + + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + await page.evaluate(` + replayer.pause(2100); + rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500'); + `); + expect(result).toEqual(true); + }); + + it("should delete all inserted StyleSheetRules by removing style element's textContent while fast-forwarding", async () => { + await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.pause(2600); + let rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500'); + `); + expect(result).toEqual(false); + + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + await page.evaluate(` + replayer.pause(3100); + rules = [...replayer.iframe.contentDocument.styleSheets].map( + (sheet) => [...sheet.rules], + ).flat(); + rules.some((x) => x.selectorText === '.css-added-at-3000'); + `); + expect(result).toEqual(true); + }); + it('can fast-forward mutation events containing nested iframe elements', async () => { await page.evaluate(` events = ${JSON.stringify(iframeEvents)}; From 0f6729d27a323260b36fbe79485a86715c0bc98a Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 19:14:08 +1000 Subject: [PATCH 75/79] rename Array.prototype.slice.call() to Array.from() --- packages/rrweb/src/replay/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b1a76a75b0..933349382a 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1520,8 +1520,9 @@ export class Replayer { parentSn.tagName === 'textarea' && mutation.node.type === NodeType.Text ) { - // ES6-TODO: rename this to Array.from(parent.childNodes) - const childNodeArray = Array.prototype.slice.call(parent.childNodes); + const childNodeArray = Array.from( + parent.childNodes as Iterable, + ); // https://github.com/rrweb-io/rrweb/issues/745 // parent is textarea, will only keep one child node as the value From 70db46b5dfcab9161c9b9414e35d63fe9f47e5f9 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 20:05:07 +1000 Subject: [PATCH 76/79] improve test cases --- packages/rrweb/test/replayer.test.ts | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index df5af56c4c..c8aadec63d 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -255,24 +255,27 @@ describe('replayer', function () { expect(result).toEqual(true); }); - it('should delete all inserted StyleSheetRules by adding a text to style element while fast-forwarding', async () => { + it('should overwrite all StyleSheetRules by appending a text node to stylesheet element while fast-forwarding', async () => { await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); const result = await page.evaluate(` const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.pause(1600); - let rules = [...replayer.iframe.contentDocument.styleSheets].map( + const rules = [...replayer.iframe.contentDocument.styleSheets].map( (sheet) => [...sheet.rules], ).flat(); rules.some((x) => x.selectorText === '.css-added-at-1000-overwritten-at-1500'); `); expect(result).toEqual(false); + }); - await page.evaluate('replayer.play(0);'); - await waitForRAF(page); - await page.evaluate(` + it('should apply fast-forwarded StyleSheetRules that came after appending text node to stylesheet element', async () => { + await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); replayer.pause(2100); - rules = [...replayer.iframe.contentDocument.styleSheets].map( + const rules = [...replayer.iframe.contentDocument.styleSheets].map( (sheet) => [...sheet.rules], ).flat(); rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500'); @@ -280,24 +283,27 @@ describe('replayer', function () { expect(result).toEqual(true); }); - it("should delete all inserted StyleSheetRules by removing style element's textContent while fast-forwarding", async () => { + it('should overwrite all StyleSheetRules by removing text node from stylesheet element while fast-forwarding', async () => { await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); const result = await page.evaluate(` const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.pause(2600); - let rules = [...replayer.iframe.contentDocument.styleSheets].map( + const rules = [...replayer.iframe.contentDocument.styleSheets].map( (sheet) => [...sheet.rules], ).flat(); rules.some((x) => x.selectorText === '.css-added-at-2000-overwritten-at-2500'); `); expect(result).toEqual(false); + }); - await page.evaluate('replayer.play(0);'); - await waitForRAF(page); - await page.evaluate(` + it('should apply fast-forwarded StyleSheetRules that came after removing text node from stylesheet element', async () => { + await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`); + const result = await page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); replayer.pause(3100); - rules = [...replayer.iframe.contentDocument.styleSheets].map( + const rules = [...replayer.iframe.contentDocument.styleSheets].map( (sheet) => [...sheet.rules], ).flat(); rules.some((x) => x.selectorText === '.css-added-at-3000'); From 52b7ec614611f75e6a9c3e0e7c47606ed4a258b6 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 20:50:14 +1000 Subject: [PATCH 77/79] fix: PR #887 in 'virtual-dom' branch --- packages/rrweb/src/replay/index.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 933349382a..b9c63f0737 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1402,6 +1402,17 @@ export class Replayer { if (parent) try { parent.removeChild(target as Node & RRNode); + /** + * https://github.com/rrweb-io/rrweb/pull/887 + * Remove any virtual style rules for stylesheets if a child text node is removed. + */ + if ( + this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + (parent as RRStyleElement).rules?.length > 0 + ) + (parent as RRStyleElement).rules = []; } catch (error) { if (error instanceof DOMException) { this.warn( @@ -1556,6 +1567,17 @@ export class Replayer { (parent as TNode).appendChild(target as TNode); } + /** + * https://github.com/rrweb-io/rrweb/pull/887 + * Remove any virtual style rules for stylesheets if a new text node is appended. + */ + if ( + this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + (parent as RRStyleElement).rules?.length > 0 + ) + (parent as RRStyleElement).rules = []; if (isSerializedIframe(target, this.mirror)) { const targetId = this.mirror.getId(target as HTMLIFrameElement); From 993eff712919aa8f0a636995d543e12f34361d70 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Thu, 5 May 2022 20:53:36 +1000 Subject: [PATCH 78/79] apply justin's suggestion on 'Array.from' refactor related commit 0f6729d27a323260b36fbe79485a86715c0bc98a --- packages/rrweb/src/replay/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index b9c63f0737..4d96c2af89 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1531,15 +1531,15 @@ export class Replayer { parentSn.tagName === 'textarea' && mutation.node.type === NodeType.Text ) { - const childNodeArray = Array.from( - parent.childNodes as Iterable, - ); + const childNodeArray = Array.isArray(parent.childNodes) + ? parent.childNodes + : Array.from(parent.childNodes); // https://github.com/rrweb-io/rrweb/issues/745 // parent is textarea, will only keep one child node as the value for (const c of childNodeArray) { if (c.nodeType === parent.TEXT_NODE) { - parent.removeChild(c); + parent.removeChild(c as Node & RRNode); } } } From b72b065be8ef8fed8a52e79518cfa428affb9b4e Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 6 May 2022 01:03:04 +1000 Subject: [PATCH 79/79] improve import code structure --- packages/rrdom/src/virtual-dom.ts | 1 + packages/rrweb/src/replay/index.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/rrdom/src/virtual-dom.ts b/packages/rrdom/src/virtual-dom.ts index 52ba178431..86f59f85b6 100644 --- a/packages/rrdom/src/virtual-dom.ts +++ b/packages/rrdom/src/virtual-dom.ts @@ -446,4 +446,5 @@ export { createOrGetNode, StyleRuleType, VirtualStyleRules, + ReplayerHandler, } from './diff'; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 4d96c2af89..2420075422 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -8,22 +8,25 @@ import { createMirror, } from 'rrweb-snapshot'; import { - RRNode, RRDocument, - RRElement, - RRStyleElement, - RRIFrameElement, - RRMediaElement, - RRCanvasElement, StyleRuleType, - VirtualStyleRules, createOrGetNode, buildFromNode, buildFromDom, diff, getDefaultSN, } from 'rrdom/es/virtual-dom'; -import type { Mirror as RRDOMMirror } from 'rrdom/es/virtual-dom'; +import type { + RRNode, + RRElement, + RRStyleElement, + RRIFrameElement, + RRMediaElement, + RRCanvasElement, + ReplayerHandler, + Mirror as RRDOMMirror, + VirtualStyleRules, +} from 'rrdom/es/virtual-dom'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; import { Timer } from './timer'; @@ -74,7 +77,6 @@ import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; -import type { ReplayerHandler } from 'rrdom/es/diff'; const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_INTERVAL = 5 * 1000;