Files
rrweb/packages/rrdom/src/document-nodejs.ts
Justin Halsall de755ae577 #853 Second try: fast-forward implementation v2: virtual dom optimization (#895)
* rrdom: add a diff function for properties

* implement diffChildren function and unit tests

* finish basic functions of diff algorithm

* fix several bugs in the diff algorithm

* replace the virtual parent optimization in applyMutation()

* fix: moveAndHover after the diff algorithm is executed

* replace virtual style map with rrdom

cssom version has to be above 0.5.0 to pass virtual style tests

* fix: failed virtual style tests in replayer.test.ts

* fix: failed polyfill tests caused by nodejs compatibility of different versions

* fix: svg viewBox attribute doesn't work

Cause the attribute viewBox is case sensitive, set value for viewbox doesn't work

* feat: replace treeIndex optimization with rrdom

* fix bug of diffProps and disable smooth scrolling animation in fast-forward mode

* feat: add iframe support

* 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'

* fix: bug when fast-forward input events and add test for it

* add test for fast-forward scroll events

* fix: custom style rules don't get inserted into some iframe elements

* code style tweak

* fix: enable to diff iframe elements

* fix  the jest error "Unexpected token 'export'"

* try to fix build error of rrweb-player

* correct the attributes definition in rrdom

* fix: custom style rules are not inserted in some iframes

* add support for shadow dom

* add support for MediaInteraction

* add canvas support

* fix unit test error in rrdom

* add support for Text, Comment

* try to refactor RRDom

* refactor RRDom to reduce duplicate code

* rename document-browser to virtual-dom

* increase the test coverage for document.ts and add ownerDocument for it

* Merge branch 'master' into virtual-dom

* add more test for virtual-dom.ts

* use cssstyle in document-nodejs

* fix: bundle error

* improve document-nodejs

* enable to diff scroll positions of an element

* rename rrdom to virtualDom for more readability and make the tree public

* revert unknown change

* improve the css style parser for comments

* improve code style

* update typings

* add handling for the case where legacy_missingNodeRetryMap is not empty

* only import types from rrweb into rrdom

* Apply suggestions from code review

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* Apply suggestions from code review

* fix building error in rrweb

* add a method setDefaultSN to set a default value for a RRNode's __sn

* fix rrweb test error and bump up other packages

* add support for custom property of css styles

* add a switch for virtual-dom optimization

* 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

* adapt changes of #865 to virtual-dom and improve the test case for more coverage

* apply review suggestions

https://github.com/rrweb-io/rrweb/pull/853#pullrequestreview-922474953

* tweak the diff algorithm

* add description of the flag useVirtualDom and remove outdated logConfig

* Remove console.log

* Contain changes to document

* Upgrade rollup to 2.70.2

* Revert "Upgrade rollup to 2.70.2"

This reverts commit b1be81a2a76565935c9dc391f31beb7f64d25956.

* Fix type checking rrdom

* Fix typing error while bundling

* 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'.`

* Increase memory limit for rollup

* Use esbuild for bundling

Speeds up bundling significantly

* Avoid circular dependencies and import un-bundled rrdom

* Fix imports

* Revert back to pre-esbuild

This reverts the following commits:
b7b3c8dbaa551a0129da1477136b1baaad28e6e1
72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f
85d600a20c56cfa764cf1f858932ba14e67b1d23
61e1a5d323212ca8fbe0569e0b3062ddd53fc612

* Set node to lts (12 is no longer supported)

* Speed up bundling and use less memory

This fixes the out of memory errors happening while bundling

* remove __sn from rrdom

* fix typo

* test: add a test case for StyleSheet mutation exceptions while fast-forwarding

* rename Array.prototype.slice.call() to Array.from()

* improve test cases

* fix: PR #887 in 'virtual-dom' branch

* apply justin's suggestion on 'Array.from' refactor

related commit 0f6729d27a323260b36fbe79485a86715c0bc98a

* improve import code structure

Co-authored-by: Yun Feng <yun.feng@anu.edu.au>
2022-05-12 12:01:13 +08:00

379 lines
10 KiB
TypeScript

import { NodeType as RRNodeType } from 'rrweb-snapshot';
import type { NWSAPI } from 'nwsapi';
import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle';
import {
BaseRRCDATASectionImpl,
BaseRRCommentImpl,
BaseRRDocumentImpl,
BaseRRDocumentTypeImpl,
BaseRRElementImpl,
BaseRRMediaElementImpl,
BaseRRNode,
BaseRRTextImpl,
ClassList,
IRRDocument,
CSSStyleDeclaration,
} from './document';
const nwsapi = require('nwsapi');
const cssom = require('cssom');
const cssstyle = require('cssstyle');
export class RRNode extends BaseRRNode {}
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 BaseRRDocumentImpl(RRNode)
implements IRRDocument {
readonly nodeName: '#document' = '#document';
private _nwsapi: NWSAPI;
get nwsapi() {
if (!this._nwsapi) {
this._nwsapi = nwsapi({
document: (this as unknown) as Document,
DOMException: (null as unknown) as new (
message?: string,
name?: string,
) => DOMException,
});
this._nwsapi.configure({
LOGERRORS: false,
IDS_DUPES: true,
MIXEDCASE: true,
});
}
return this._nwsapi;
}
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;
}
get implementation(): RRDocument {
return this;
}
get firstElementChild(): RRElement | null {
return this.documentElement as RRElement | null;
}
appendChild(childNode: RRNode) {
return super.appendChild(childNode);
}
insertBefore(newChild: RRNode, refChild: RRNode | null) {
return super.insertBefore(newChild, refChild);
}
querySelectorAll(selectors: string): RRNode[] {
return (this.nwsapi.select(selectors) as unknown) as RRNode[];
}
getElementsByTagName(tagName: string): RRElement[] {
if (this.documentElement)
return (this.documentElement as RRElement).getElementsByTagName(tagName);
return [];
}
getElementsByClassName(className: string): RRElement[] {
if (this.documentElement)
return (this.documentElement as RRElement).getElementsByClassName(
className,
);
return [];
}
getElementById(elementId: string): RRElement | null {
if (this.documentElement)
return (this.documentElement as RRElement).getElementById(elementId);
return null;
}
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<K extends keyof HTMLElementTagNameMap>(
tagName: K,
): RRElementType<K>;
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(upperTagName);
break;
case 'CANVAS':
element = new RRCanvasElement(upperTagName);
break;
case 'STYLE':
element = new RRStyleElement(upperTagName);
break;
default:
element = new RRElement(upperTagName);
break;
}
element.ownerDocument = this;
return element;
}
createElementNS(_namespaceURI: string, 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;
}
}
export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {}
export class RRElement extends BaseRRElementImpl(RRNode) {
private _style: CSSStyleDeclarationType;
constructor(tagName: string) {
super(tagName);
this._style = new cssstyle.CSSStyleDeclaration();
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;
}
appendChild(newChild: RRNode): RRNode {
return super.appendChild(newChild) as RRNode;
}
insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode {
return super.insertBefore(newChild, refChild) as RRNode;
}
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;
}
removeAttribute(name: string) {
delete this.attributes[name.toLowerCase()];
}
get firstElementChild(): RRElement | null {
for (let child of this.childNodes)
if (child.RRNodeType === RRNodeType.Element) return child as RRElement;
return null;
}
get nextElementSibling(): RRElement | null {
let parentNode = this.parentNode;
if (!parentNode) return null;
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;
}
querySelectorAll(selectors: string): RRNode[] {
const result: RRElement[] = [];
if (this.ownerDocument !== null) {
((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 result;
}
getElementById(elementId: string): RRElement | null {
if (this.id === elementId) return this;
for (const child of this.childNodes) {
if (child instanceof RRElement) {
const result = child.getElementById(elementId);
if (result !== null) return result;
}
}
return null;
}
getElementsByClassName(className: string): RRElement[] {
let elements: RRElement[] = [];
const queryClassList = new ClassList(className);
// Make sure this element has all queried class names.
if (
this instanceof RRElement &&
queryClassList.classes.filter((queriedClassName) =>
this.classList.classes.some((name) => name === queriedClassName),
).length == queryClassList.classes.length
)
elements.push(this);
for (const child of this.childNodes) {
if (child instanceof RRElement)
elements = elements.concat(child.getElementsByClassName(className));
}
return elements;
}
getElementsByTagName(tagName: string): RRElement[] {
let elements: RRElement[] = [];
const normalizedTagName = tagName.toUpperCase();
if (this instanceof RRElement && this.tagName === normalizedTagName)
elements.push(this);
for (const child of this.childNodes) {
if (child instanceof RRElement)
elements = elements.concat(child.getElementsByTagName(tagName));
}
return elements;
}
}
export class RRImageElement extends RRElement {
src: string;
width: number;
height: number;
onload: ((this: GlobalEventHandlers, ev: Event) => any) | null;
}
export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {}
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 {
private _sheet: CSSStyleSheet | null = null;
get sheet() {
if (!this._sheet) {
let result = '';
for (let child of this.childNodes)
if (child.RRNodeType === RRNodeType.Text)
result += (child as RRText).textContent;
this._sheet = cssom.parse(result);
}
return this._sheet;
}
}
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 BaseRRTextImpl(RRNode) {
readonly nodeName: '#text' = '#text';
}
export class RRComment extends BaseRRCommentImpl(RRNode) {
readonly nodeName: '#comment' = '#comment';
}
export class RRCDATASection extends BaseRRCDATASectionImpl(RRNode) {
readonly nodeName: '#cdata-section' = '#cdata-section';
}
interface RRElementTagNameMap {
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;