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 diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace index 79849c8fe9..1d100ed367 100644 --- a/.vscode/rrweb-monorepo.code-workspace +++ b/.vscode/rrweb-monorepo.code-workspace @@ -24,8 +24,7 @@ "settings": { "jest.disabledWorkspaceFolders": [ " rrweb monorepo", - "rrweb-player (package)", - "rrdom (package)" + "rrweb-player (package)" ] } } diff --git a/guide.md b/guide.md index 3b2e058df1..1c52dcb8c0 100644 --- a/guide.md +++ b/guide.md @@ -299,10 +299,12 @@ The replayer accepts options as its constructor's second parameter, and it has t | 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) | +| 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 a987934518..054fcb8b12 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 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/package.json b/packages/rrdom/package.json index c7ce3a9c6b..bfe3459503 100644 --- a/packages/rrdom/package.json +++ b/packages/rrdom/package.json @@ -5,6 +5,7 @@ "dev": "rollup -c -w", "bundle": "rollup --config", "bundle:es-only": "cross-env ES_ONLY=true rollup --config", + "check-types": "tsc -noEmit", "test": "jest", "prepublish": "npm run bundle" }, @@ -27,18 +28,24 @@ "@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-node-resolve": "^13.0.4", "@types/cssom": "^0.4.1", - "@types/jest": "^27.0.1", + "@types/cssstyle": "^2.2.1", + "@types/jest": "^27.4.1", "@types/nwsapi": "^2.2.2", - "jest": "^27.1.1", + "@types/puppeteer": "^5.4.4", + "compare-versions": "^4.1.3", + "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", + "rollup-plugin-web-worker-loader": "^1.6.1", "rrweb-snapshot": "^1.1.14", - "ts-jest": "^27.0.5", - "typescript": "^3.9.5" + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" }, "dependencies": { "cssom": "^0.5.0", + "cssstyle": "^2.3.0", "nwsapi": "^2.2.0" } } diff --git a/packages/rrdom/rollup.config.js b/packages/rrdom/rollup.config.js index 80baed3473..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' } }, }), @@ -27,6 +32,11 @@ const baseConfigs = [ name: 'RRDocument', path: 'document-nodejs', }, + { + input: './src/virtual-dom.ts', + name: 'RRDocument', + path: 'virtual-dom', + }, ]; let configs = []; diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts new file mode 100644 index 0000000000..26f2309147 --- /dev/null +++ b/packages/rrdom/src/diff.ts @@ -0,0 +1,513 @@ +import { NodeType as RRNodeType, Mirror as NodeMirror } from 'rrweb-snapshot'; +import type { + canvasMutationData, + canvasEventWithTime, + inputData, + scrollData, +} from 'rrweb/src/types'; +import type { + IRRCDATASection, + IRRComment, + IRRDocument, + IRRDocumentType, + IRRElement, + IRRNode, + IRRText, +} from './document'; +import type { + RRCanvasElement, + RRElement, + RRIFrameElement, + RRMediaElement, + RRStyleElement, + RRDocument, + Mirror, +} from './virtual-dom'; + +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 type ReplayerHandler = { + mirror: NodeMirror; + applyCanvas: ( + canvasEvent: canvasEventWithTime, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => void; + applyInput: (data: inputData) => void; + applyScroll: (data: scrollData, isSync: boolean) => void; +}; + +export function diff( + oldTree: Node, + newTree: IRRNode, + replayer: ReplayerHandler, + rrnodeMirror?: Mirror, +) { + const oldChildren = oldTree.childNodes; + const newChildren = newTree.childNodes; + rrnodeMirror = + rrnodeMirror || + (newTree as RRDocument).mirror || + (newTree.ownerDocument as RRDocument).mirror; + + if (oldChildren.length > 0 || newChildren.length > 0) { + diffChildren( + Array.from(oldChildren), + newChildren, + oldTree, + replayer, + rrnodeMirror, + ); + } + + let inputDataToApply = null, + scrollDataToApply = null; + switch (newTree.RRNodeType) { + case RRNodeType.Document: + const newRRDocument = newTree as IRRDocument; + scrollDataToApply = (newRRDocument as RRDocument).scrollData; + break; + case RRNodeType.Element: + const oldElement = (oldTree as Node) as HTMLElement; + const newRRElement = newTree as IRRElement; + diffProps(oldElement, newRRElement, rrnodeMirror); + 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 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).canvasMutations.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' }); + const oldChildren = oldElement.shadowRoot!.childNodes; + const newChildren = newRRElement.shadowRoot.childNodes; + if (oldChildren.length > 0 || newChildren.length > 0) + diffChildren( + Array.from(oldChildren), + newChildren, + oldElement.shadowRoot!, + replayer, + rrnodeMirror, + ); + } + break; + case RRNodeType.Text: + case RRNodeType.Comment: + case RRNodeType.CDATA: + if ( + oldTree.textContent !== + (newTree as IRRText | IRRComment | IRRCDATASection).data + ) + oldTree.textContent = (newTree as + | IRRText + | IRRComment + | IRRCDATASection).data; + break; + default: + } + + 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); + + // IFrame element doesn't have child nodes. + if (newTree.nodeName === 'IFRAME') { + const oldContentDocument = ((oldTree as Node) as HTMLIFrameElement) + .contentDocument; + const newIFrameElement = newTree as RRIFrameElement; + // If the iframe is cross-origin, the contentDocument will be null. + if (oldContentDocument) { + const sn = rrnodeMirror.getMeta(newIFrameElement.contentDocument); + if (sn) { + replayer.mirror.add(oldContentDocument, { ...sn }); + } + diff( + oldContentDocument, + newIFrameElement.contentDocument, + replayer, + rrnodeMirror, + ); + } + } +} + +function diffProps( + oldTree: HTMLElement, + newTree: IRRElement, + rrnodeMirror: Mirror, +) { + const oldAttributes = oldTree.attributes; + const newAttributes = newTree.attributes; + + for (const name in newAttributes) { + const newValue = newAttributes[name]; + const sn = rrnodeMirror.getMeta(newTree); + if (sn && 'isSVG' in sn && sn.isSVG && NAMESPACES[name]) + oldTree.setAttributeNS(NAMESPACES[name], name, newValue); + else if (newTree.tagName === 'CANVAS' && 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)) + if (!(name in newAttributes)) oldTree.removeAttribute(name); + + newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); + newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); +} + +function diffChildren( + oldChildren: (Node | undefined)[], + newChildren: IRRNode[], + parentNode: Node, + replayer: ReplayerHandler, + rrnodeMirror: Mirror, +) { + 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 ( + replayer.mirror.getId(oldStartNode) === rrnodeMirror.getId(newStartNode) + ) { + diff(oldStartNode, newStartNode, replayer, rrnodeMirror); + oldStartNode = oldChildren[++oldStartIndex]; + newStartNode = newChildren[++newStartIndex]; + } else if ( + replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newEndNode) + ) { + 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, rrnodeMirror); + oldStartNode = oldChildren[++oldStartIndex]; + newEndNode = newChildren[--newEndIndex]; + } else if ( + replayer.mirror.getId(oldEndNode) === rrnodeMirror.getId(newStartNode) + ) { + parentNode.insertBefore(oldEndNode, oldStartNode); + diff(oldEndNode, newStartNode, replayer, rrnodeMirror); + oldEndNode = oldChildren[--oldEndIndex]; + newStartNode = newChildren[++newStartIndex]; + } else { + if (!oldIdToIndex) { + oldIdToIndex = {}; + for (let i = oldStartIndex; i <= oldEndIndex; i++) { + const oldChild = oldChildren[i]; + if (oldChild && replayer.mirror.hasNode(oldChild)) + oldIdToIndex[replayer.mirror.getId(oldChild)] = i; + } + } + indexInOld = oldIdToIndex[rrnodeMirror.getId(newStartNode)]; + if (indexInOld) { + const nodeToMove = oldChildren[indexInOld]!; + parentNode.insertBefore(nodeToMove, oldStartNode); + diff(nodeToMove, newStartNode, replayer, rrnodeMirror); + oldChildren[indexInOld] = undefined; + } else { + const newNode = createOrGetNode( + newStartNode, + replayer.mirror, + rrnodeMirror, + ); + + /** + * 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 ( + replayer.mirror.getMeta(parentNode)?.type === RRNodeType.Document && + replayer.mirror.getMeta(newNode)?.type === RRNodeType.Element && + ((parentNode as Node) as Document).documentElement + ) { + parentNode.removeChild( + ((parentNode as Node) as Document).documentElement, + ); + oldChildren[oldStartIndex] = undefined; + oldStartNode = undefined; + } + parentNode.insertBefore(newNode, oldStartNode || null); + diff(newNode, newStartNode, replayer, rrnodeMirror); + } + newStartNode = newChildren[++newStartIndex]; + } + } + if (oldStartIndex > oldEndIndex) { + const referenceRRNode = newChildren[newEndIndex + 1]; + let referenceNode = null; + if (referenceRRNode) + parentNode.childNodes.forEach((child) => { + if ( + replayer.mirror.getId(child) === rrnodeMirror.getId(referenceRRNode) + ) + referenceNode = child; + }); + for (; newStartIndex <= newEndIndex; ++newStartIndex) { + const newNode = createOrGetNode( + newChildren[newStartIndex], + replayer.mirror, + rrnodeMirror, + ); + parentNode.insertBefore(newNode, referenceNode); + diff(newNode, newChildren[newStartIndex], replayer, rrnodeMirror); + } + } else if (newStartIndex > newEndIndex) { + for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { + const node = oldChildren[oldStartIndex]; + if (node) { + parentNode.removeChild(node); + replayer.mirror.removeNodeFromMap(node); + } + } + } +} + +export function createOrGetNode( + rrNode: IRRNode, + domMirror: NodeMirror, + rrnodeMirror: Mirror, +): Node { + let node = domMirror.getNode(rrnodeMirror.getId(rrNode)); + const sn = rrnodeMirror.getMeta(rrNode); + if (node !== null) return node; + switch (rrNode.RRNodeType) { + case RRNodeType.Document: + node = new Document(); + break; + case RRNodeType.DocumentType: + node = document.implementation.createDocumentType( + (rrNode as IRRDocumentType).name, + (rrNode as IRRDocumentType).publicId, + (rrNode as IRRDocumentType).systemId, + ); + break; + case RRNodeType.Element: + let tagName = (rrNode as IRRElement).tagName.toLowerCase(); + tagName = SVGTagMap[tagName] || tagName; + if (sn && 'isSVG' in sn && sn?.isSVG) { + node = document.createElementNS( + NAMESPACES['svg'], + (rrNode as IRRElement).tagName.toLowerCase(), + ); + } else node = document.createElement((rrNode as IRRElement).tagName); + break; + case RRNodeType.Text: + node = document.createTextNode((rrNode as IRRText).data); + break; + case RRNodeType.Comment: + node = document.createComment((rrNode as IRRComment).data); + break; + case RRNodeType.CDATA: + node = document.createCDATASection((rrNode as IRRCDATASection).data); + break; + } + + if (sn) domMirror.add(node, { ...sn }); + 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 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(); + 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-nodejs.ts b/packages/rrdom/src/document-nodejs.ts index aff8d90512..f7523ffefc 100644 --- a/packages/rrdom/src/document-nodejs.ts +++ b/packages/rrdom/src/document-nodejs.ts @@ -1,68 +1,24 @@ -import { INode, NodeType, serializedNodeWithId } from 'rrweb-snapshot'; -import { NWSAPI } from 'nwsapi'; -import { parseCSSText, camelize, toCSSText } from './style'; +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 abstract class RRNode { - __sn: serializedNodeWithId | undefined; - children: Array = []; - parentElement: RRElement | null = null; - parentNode: RRNode | null = null; - 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 RRNode extends BaseRRNode {} export class RRWindow { scrollLeft = 0; @@ -74,8 +30,10 @@ export class RRWindow { } } -export class RRDocument extends RRNode { - private mirror: Map = new Map(); +export class RRDocument + extends BaseRRDocumentImpl(RRNode) + implements IRRDocument { + readonly nodeName: '#document' = '#document'; private _nwsapi: NWSAPI; get nwsapi() { if (!this._nwsapi) { @@ -95,66 +53,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[] { @@ -216,16 +140,16 @@ 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'); + 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); @@ -235,10 +159,7 @@ export class RRDocument extends RRNode { return element; } - createElementNS( - _namespaceURI: 'http://www.w3.org/2000/svg', - qualifiedName: string, - ) { + createElementNS(_namespaceURI: string, qualifiedName: string) { return this.createElement(qualifiedName as keyof HTMLElementTagNameMap); } @@ -259,266 +180,40 @@ 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() {} - - 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; +export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} +export class RRElement extends BaseRRElementImpl(RRNode) { + private _style: CSSStyleDeclarationType; constructor(tagName: string) { - super(); - this.tagName = tagName; - } - - get classList() { - return new ClassList( - this.attributes.class as string | undefined, - (newClassName) => { - this.attributes.class = newClassName; + super(tagName); + this._style = new cssstyle.CSSStyleDeclaration(); + const style = this._style; + Object.defineProperty(this.attributes, 'style', { + get() { + return style.cssText; }, - ); - } - - get id() { - return this.attributes.id; - } - - get className() { - return this.attributes.class || ''; + set(cssText: string) { + style.cssText = cssText; + }, + }); } - get textContent() { - return ''; + get style() { + return (this._style as unknown) as CSSStyleDeclaration; } - 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; + attachShadow(_init: ShadowRootInit): RRElement { + return super.attachShadow(_init) as RRElement; } - get firstElementChild(): RRElement | null { - for (let child of this.children) - if (child instanceof RRElement) return child; - return null; + appendChild(newChild: RRNode): RRNode { + return super.appendChild(newChild) as RRNode; } - get nextElementSibling(): RRElement | null { - let parentNode = this.parentNode; - if (!parentNode) return null; - const siblings = parentNode.children; - 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; + insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { + return super.insertBefore(newChild, refChild) as RRNode; } getAttribute(name: string) { @@ -531,57 +226,44 @@ export class RRElement extends RRNode { 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]; + delete this.attributes[name.toLowerCase()]; } - appendChild(newChild: RRNode): RRNode { - this.children.push(newChild); - newChild.parentNode = this; - newChild.parentElement = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; + get firstElementChild(): RRElement | null { + for (let child of this.childNodes) + if (child.RRNodeType === RRNodeType.Element) return child as RRElement; + return null; } - 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; + 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) { - return (this.ownerDocument.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 { - if (this instanceof RRElement && this.id === elementId) return this; - for (const child of this.children) { + 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; @@ -596,12 +278,12 @@ export class RRElement extends 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.children) { + for (const child of this.childNodes) { if (child instanceof RRElement) elements = elements.concat(child.getElementsByClassName(className)); } @@ -613,32 +295,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 { @@ -648,16 +310,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 { /** @@ -675,7 +328,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); } @@ -683,7 +336,7 @@ export class RRStyleElement extends RRElement { } } -export class RRIframeElement extends RRElement { +export class RRIFrameElement extends RRElement { width: string = ''; height: string = ''; src: string = ''; @@ -699,89 +352,27 @@ export class RRIframeElement extends RRElement { } } -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 RRText extends BaseRRTextImpl(RRNode) { + readonly nodeName: '#text' = '#text'; } -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 RRComment extends BaseRRCommentImpl(RRNode) { + readonly nodeName: '#comment' = '#comment'; } -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) { + readonly nodeName: '#cdata-section' = '#cdata-section'; } 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 new file mode 100644 index 0000000000..aec54d9a98 --- /dev/null +++ b/packages/rrdom/src/document.ts @@ -0,0 +1,723 @@ +import { NodeType as RRNodeType } from 'rrweb-snapshot'; +import { parseCSSText, camelize, toCSSText } from './style'; +export interface IRRNode { + parentElement: IRRNode | null; + parentNode: IRRNode | null; + childNodes: IRRNode[]; + ownerDocument: IRRDocument; + readonly ELEMENT_NODE: number; + readonly TEXT_NODE: number; + // corresponding nodeType value of standard HTML Node + readonly nodeType: number; + readonly nodeName: string; // https://dom.spec.whatwg.org/#dom-node-nodename + readonly RRNodeType: RRNodeType; + + firstChild: IRRNode | null; + + lastChild: 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): IRRNode; + + toString(): string; +} +export interface IRRDocument extends IRRNode { + documentElement: IRRElement | null; + + body: IRRElement | null; + + head: IRRElement | null; + + implementation: IRRDocument; + + firstElementChild: IRRElement | null; + + readonly nodeName: '#document'; + + compatMode: 'BackCompat' | 'CSS1Compat'; + + 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: string, qualifiedName: string): IRRElement; + + createTextNode(data: string): IRRText; + + createComment(data: string): IRRComment; + + createCDATASection(data: string): IRRCDATASection; + + open(): void; + + close(): void; + + write(content: string): void; +} +export interface IRRElement extends IRRNode { + tagName: string; + attributes: Record; + 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; + readonly publicId: string; + readonly systemId: string; +} +export interface IRRText extends IRRNode { + readonly nodeName: '#text'; + data: string; +} +export interface IRRComment extends IRRNode { + readonly nodeName: '#comment'; + data: string; +} +export interface IRRCDATASection extends IRRNode { + readonly nodeName: '#cdata-section'; + data: string; +} + +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 childNodes: IRRNode[] = []; + public parentElement: IRRNode | null = null; + public parentNode: IRRNode | null = null; + public textContent: string | null; + public ownerDocument: IRRDocument; + 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 nodeName: string; + public readonly RRNodeType: RRNodeType; + + constructor(...args: any[]) {} + + public get firstChild(): IRRNode | null { + return this.childNodes[0] || null; + } + + public get lastChild(): IRRNode | null { + return this.childNodes[this.childNodes.length - 1] || null; + } + + public get nextSibling(): IRRNode | null { + let parentNode = this.parentNode; + if (!parentNode) return null; + const siblings = parentNode.childNodes; + let index = siblings.indexOf(this); + return siblings[index + 1] || null; + } + + public contains(node: IRRNode) { + if (node === this) return true; + for (const child of this.childNodes) { + if (child.contains(node)) return true; + } + return false; + } + + public appendChild(_newChild: IRRNode): IRRNode { + throw new Error( + `RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`, + ); + } + + 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.`, + ); + } + + 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(): string { + return 'RRNode'; + } +} + +export function BaseRRDocumentImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + return class BaseRRDocument extends RRNodeClass implements IRRDocument { + 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; + + public get documentElement(): IRRElement | null { + return ( + (this.childNodes.find( + (node) => + node.RRNodeType === RRNodeType.Element && + (node as IRRElement).tagName === 'HTML', + ) as IRRElement) || null + ); + } + + public get body(): IRRElement | null { + return ( + (this.documentElement?.childNodes.find( + (node) => + node.RRNodeType === RRNodeType.Element && + (node as IRRElement).tagName === 'BODY', + ) as IRRElement) || null + ); + } + + public get head(): IRRElement | null { + return ( + (this.documentElement?.childNodes.find( + (node) => + node.RRNodeType === RRNodeType.Element && + (node as IRRElement).tagName === 'HEAD', + ) as IRRElement) || null + ); + } + + public get implementation(): IRRDocument { + return this; + } + + public get firstElementChild(): IRRElement | null { + return this.documentElement; + } + + public appendChild(childNode: IRRNode): IRRNode { + const nodeType = childNode.RRNodeType; + 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 === RRNodeType.Element ? 'RRElement' : 'RRDoctype' + } on RRDocument allowed.`, + ); + } + } + childNode.parentElement = null; + childNode.parentNode = this; + this.childNodes.push(childNode); + return childNode; + } + + public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + const nodeType = newChild.RRNodeType; + 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 === RRNodeType.Element ? 'RRElement' : 'RRDoctype' + } on RRDocument allowed.`, + ); + } + } + if (refChild === null) return this.appendChild(newChild); + 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.childNodes.splice(childIndex, 0, newChild); + newChild.parentElement = null; + newChild.parentNode = this; + 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 = []; + } + + public 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('') + */ + public 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, ''); + this.open(); + this.appendChild(doctype); + } + } + + createDocument( + _namespace: string | null, + _qualifiedName: string | null, + _doctype?: DocumentType | null, + ): IRRDocument { + return new BaseRRDocument(); + } + + createDocumentType( + qualifiedName: string, + publicId: string, + systemId: string, + ): IRRDocumentType { + const doctype = new (BaseRRDocumentTypeImpl(BaseRRNode))( + qualifiedName, + publicId, + systemId, + ); + doctype.ownerDocument = this; + return doctype; + } + + createElement(tagName: string): IRRElement { + const element = new (BaseRRElementImpl(BaseRRNode))(tagName); + element.ownerDocument = this; + return element; + } + + createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement { + return this.createElement(qualifiedName); + } + + createTextNode(data: string): IRRText { + const text = new (BaseRRTextImpl(BaseRRNode))(data); + text.ownerDocument = this; + return text; + } + + createComment(data: string): IRRComment { + const comment = new (BaseRRCommentImpl(BaseRRNode))(data); + comment.ownerDocument = this; + return comment; + } + + createCDATASection(data: string): IRRCDATASection { + const CDATASection = new (BaseRRCDATASectionImpl(BaseRRNode))(data); + CDATASection.ownerDocument = this; + return CDATASection; + } + + toString() { + return 'RRDocument'; + } + }; +} + +export function BaseRRDocumentTypeImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRDocumentType + extends RRNodeClass + 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; + public textContent: string | null = null; + + constructor(qualifiedName: string, publicId: string, systemId: string) { + super(); + this.name = qualifiedName; + this.publicId = publicId; + this.systemId = systemId; + this.nodeName = qualifiedName; + } + + toString() { + return 'RRDocumentType'; + } + }; +} + +export function BaseRRElementImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRElement extends RRNodeClass implements IRRElement { + public readonly nodeType: number = NodeType.ELEMENT_NODE; + public readonly RRNodeType = RRNodeType.Element; + public readonly nodeName: string; + public tagName: string; + public attributes: Record = {}; + public shadowRoot: IRRElement | null = null; + public scrollLeft?: number; + public scrollTop?: number; + + constructor(tagName: string) { + super(); + this.tagName = tagName.toUpperCase(); + this.nodeName = tagName.toUpperCase(); + } + + public get textContent(): string { + let result = ''; + this.childNodes.forEach((node) => (result += node.textContent)); + return result; + } + + public set textContent(textContent: string) { + this.childNodes = [this.ownerDocument.createTextNode(textContent)]; + } + + public get classList(): ClassList { + return new ClassList( + this.attributes.class as string | undefined, + (newClassName) => { + this.attributes.class = newClassName; + }, + ); + } + + public get id() { + return this.attributes.id || ''; + } + + public get className() { + return this.attributes.class || ''; + } + + public get style() { + 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; + if (priority === 'important') style[normalizedName] += ' !important'; + 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]; + this.attributes.style = toCSSText(style); + return value; + }; + return style; + } + + public getAttribute(name: string) { + return this.attributes[name] || null; + } + + public setAttribute(name: string, attribute: string) { + this.attributes[name] = attribute; + } + + public setAttributeNS( + _namespace: string | null, + qualifiedName: string, + value: string, + ): void { + this.setAttribute(qualifiedName, value); + } + + public removeAttribute(name: string) { + delete this.attributes[name]; + } + + public appendChild(newChild: IRRNode): IRRNode { + this.childNodes.push(newChild); + newChild.parentNode = this; + newChild.parentElement = this; + return newChild; + } + + public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { + if (refChild === null) return this.appendChild(newChild); + 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.childNodes.splice(childIndex, 0, newChild); + newChild.parentElement = this; + newChild.parentNode = this; + 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; + } + + toString() { + let attributeString = ''; + for (let attribute in this.attributes) { + attributeString += `${attribute}="${this.attributes[attribute]}" `; + } + return `${this.tagName} ${attributeString}`; + } + }; +} + +export function BaseRRMediaElementImpl< + RRElement extends ConstrainedConstructor +>(RRElementClass: RRElement) { + return class BaseRRMediaElement extends RRElementClass { + public currentTime?: number; + public volume?: number; + public paused?: boolean; + public muted?: boolean; + attachShadow(_init: ShadowRootInit): IRRElement { + throw new Error( + `RRDomException: 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: RRNode, +) { + // @ts-ignore + return class BaseRRText extends RRNodeClass implements IRRText { + public readonly nodeType: number = NodeType.TEXT_NODE; + public readonly nodeName: '#text' = '#text'; + public readonly RRNodeType = RRNodeType.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 `RRText text=${JSON.stringify(this.data)}`; + } + }; +} + +export function BaseRRCommentImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRComment extends RRNodeClass implements IRRComment { + public readonly nodeType: number = NodeType.COMMENT_NODE; + public readonly nodeName: '#comment' = '#comment'; + public readonly RRNodeType = RRNodeType.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 `RRComment text=${JSON.stringify(this.data)}`; + } + }; +} + +export function BaseRRCDATASectionImpl< + RRNode extends ConstrainedConstructor +>(RRNodeClass: RRNode) { + // @ts-ignore + return class BaseRRCDATASection + extends RRNodeClass + implements IRRCDATASection { + public readonly nodeName: '#cdata-section' = '#cdata-section'; + public readonly nodeType: number = NodeType.CDATA_SECTION_NODE; + public readonly RRNodeType = RRNodeType.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 `RRCDATASection data=${JSON.stringify(this.data)}`; + } + }; +} + +export class ClassList { + private onChange: ((newClassText: string) => void) | undefined; + classes: string[] = []; + + constructor( + classText?: string, + onChange?: ((newClassText: string) => void) | undefined, + ) { + if (classText) { + const classes = classText.trim().split(/\s+/); + this.classes.push(...classes); + } + this.onChange = onChange; + } + + add = (...classNames: string[]) => { + for (const item of classNames) { + const className = String(item); + if (this.classes.indexOf(className) >= 0) continue; + this.classes.push(className); + } + this.onChange && this.onChange(this.classes.join(' ')); + }; + + remove = (...classNames: string[]) => { + 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; +}; + +// 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/polyfill.ts b/packages/rrdom/src/polyfill.ts index 271af35f56..b93c556542 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; @@ -80,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/src/style.ts b/packages/rrdom/src/style.ts index 1e49b83619..a85f7598f0 100644 --- a/packages/rrdom/src/style.ts +++ b/packages/rrdom/src/style.ts @@ -1,40 +1,45 @@ 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 = /\/\*.*?\*\//g; + 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 = /-([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() : '')); +}; + +/** + * 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 new file mode 100644 index 0000000000..86f59f85b6 --- /dev/null +++ b/packages/rrdom/src/virtual-dom.ts @@ -0,0 +1,450 @@ +import { + NodeType as RRNodeType, + createMirror as createNodeMirror, +} from 'rrweb-snapshot'; +import type { + Mirror as NodeMirror, + IMirror, + serializedNodeWithId, +} from 'rrweb-snapshot'; +import type { + canvasMutationData, + canvasEventWithTime, + inputData, + scrollData, +} from 'rrweb/src/types'; +import { + BaseRRNode as RRNode, + BaseRRCDATASectionImpl, + BaseRRCommentImpl, + BaseRRDocumentImpl, + BaseRRDocumentTypeImpl, + BaseRRElementImpl, + BaseRRMediaElementImpl, + BaseRRTextImpl, + IRRDocument, + IRRElement, + IRRNode, + NodeType, + 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(); + + public scrollData: scrollData | null = null; + + constructor(mirror?: Mirror) { + super(); + if (mirror) { + this.mirror = mirror; + } + } + + 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, this.mirror); + 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; + } + + 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; + } + + destroyTree() { + this.childNodes = []; + this.mirror.reset(); + } + + open() { + super.open(); + this._unserializedId = -1; + } +} + +export const RRDocumentType = BaseRRDocumentTypeImpl(RRNode); + +export class RRElement extends BaseRRElementImpl(RRNode) { + inputData: inputData | null = null; + scrollData: scrollData | null = null; +} + +export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} + +export class RRCanvasElement extends RRElement implements IRRElement { + public canvasMutations: { + event: canvasEventWithTime; + mutation: canvasMutationData; + }[] = []; + /** + * This is a dummy implementation to distinguish RRCanvasElement from real HTMLCanvasElement. + */ + getContext(): RenderingContext | null { + return null; + } +} + +export class RRStyleElement extends RRElement { + public rules: VirtualStyleRules = []; +} + +export class RRIFrameElement extends RRElement { + contentDocument: RRDocument = new RRDocument(); + constructor(upperTagName: string, mirror: Mirror) { + super(upperTagName); + this.contentDocument.mirror = mirror; + } +} + +export const RRText = BaseRRTextImpl(RRNode); +export type RRText = typeof RRText; + +export const RRComment = BaseRRCommentImpl(RRNode); +export type RRComment = typeof RRComment; + +export const RRCDATASection = BaseRRCDATASectionImpl(RRNode); +export type RRCDATASection = typeof RRCDATASection; + +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; + +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 + * @param domMirror the NodeMirror that records the real document tree + * @returns the built RRNode + */ +export function buildFromNode( + node: Node, + rrdom: IRRDocument, + domMirror: NodeMirror, + parentRRNode?: IRRNode | null, +): IRRNode | null { + let rrNode: IRRNode; + + switch (node.nodeType) { + case NodeType.DOCUMENT_NODE: + if (parentRRNode && parentRRNode.nodeName === 'IFRAME') + rrNode = (parentRRNode as RRIFrameElement).contentDocument; + else { + rrNode = rrdom; + (rrNode as IRRDocument).compatMode = (node as Document).compatMode as + | 'BackCompat' + | 'CSS1Compat'; + } + break; + case NodeType.DOCUMENT_TYPE_NODE: + const documentType = (node as Node) as DocumentType; + rrNode = rrdom.createDocumentType( + documentType.name, + documentType.publicId, + documentType.systemId, + ); + break; + case NodeType.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 NodeType.TEXT_NODE: + rrNode = rrdom.createTextNode(((node as Node) as Text).textContent || ''); + break; + case NodeType.CDATA_SECTION_NODE: + rrNode = rrdom.createCDATASection(((node as Node) as CDATASection).data); + break; + case NodeType.COMMENT_NODE: + rrNode = rrdom.createComment( + ((node as Node) as Comment).textContent || '', + ); + break; + // if node is a shadow root + case NodeType.DOCUMENT_FRAGMENT_NODE: + rrNode = (parentRRNode as IRRElement).attachShadow({ mode: 'open' }); + break; + default: + return null; + } + + let sn: serializedNodeWithId | null = domMirror.getMeta(node); + + if (rrdom instanceof RRDocument) { + if (!sn) { + sn = getDefaultSN(rrNode, rrdom.unserializedId); + domMirror.add(node, sn); + } + rrdom.mirror.add(rrNode, { ...sn }); + } + + return rrNode; +} + +/** + * Build a RRDocument from a real document tree. + * @param dom the real document tree + * @param domMirror the NodeMirror that records the real document tree + * @param rrdom the rrdom object to be constructed + * @returns the build rrdom + */ +export function buildFromDom( + dom: Document, + domMirror: NodeMirror = createNodeMirror(), + rrdom: IRRDocument = new RRDocument(), +) { + 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?.nodeName !== 'IFRAME' && + // if node isn't a shadow root + node.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE + ) { + parentRRNode?.appendChild(rrNode); + rrNode.parentNode = parentRRNode; + rrNode.parentElement = parentRRNode as RRElement; + } + + if (node.nodeName === 'IFRAME') { + walk((node as HTMLIFrameElement).contentDocument!, rrNode); + } else if ( + 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 === NodeType.ELEMENT_NODE && + ((node as Node) as HTMLElement).shadowRoot + ) + walk(((node as Node) as HTMLElement).shadowRoot!, rrNode); + node.childNodes.forEach((childNode) => walk(childNode, rrNode)); + } + } + walk(dom, null); + 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, + createOrGetNode, + StyleRuleType, + VirtualStyleRules, + ReplayerHandler, +} from './diff'; 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/__snapshots__/virtual-dom.test.ts.snap b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap new file mode 100644 index 0000000000..c1dc00248b --- /dev/null +++ b/packages/rrdom/test/__snapshots__/virtual-dom.test.ts.snap @@ -0,0 +1,160 @@ +// 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 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\\" +" +`; + +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 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 + -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.
+ + + + 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/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/rrdom/test/polyfill.test.ts b/packages/rrdom/test/polyfill.test.ts index 6222f7de3f..a9d5f381f1 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(); @@ -20,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(); @@ -59,12 +73,32 @@ 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', '<')) + expect(global.Event).toBeUndefined(); polyfillEvent(); expect(global.Event).toBeDefined(); 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(); @@ -73,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(); @@ -80,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/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/rrdom/test/virtual-dom.test.ts b/packages/rrdom/test/virtual-dom.test.ts new file mode 100644 index 0000000000..4bded073b2 --- /dev/null +++ b/packages/rrdom/test/virtual-dom.test.ts @@ -0,0 +1,550 @@ +/** + * @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 { JSDOM } from 'jsdom'; +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'; + +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, mirror) { + return walk(rootNode, mirror, ''); +} +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, mirror, blankSpace + ' '); + for (const child of node.childNodes) + printText += walk(child, mirror, blankSpace + ' '); + if (node instanceof rrdom.RRIFrameElement) + printText += walk(node.contentDocument, mirror, blankSpace + ' '); + return printText; +} +`; + +describe('RRDocument for browser environment', () => { + let mirror: Mirror; + beforeEach(() => { + mirror = new Mirror(); + }); + + describe('create a RRNode from a real Node', () => { + it('should support quicksmode documents', () => { + // 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)!; + + expect((rrNode as RRDocument).compatMode).toBe('BackCompat'); + }); + + it('can patch serialized ID for an unserialized node', () => { + // build from document + expect(mirror.getMeta(document)).toBeNull(); + const rrdom = new RRDocument(); + let rrNode = buildFromNode(document, rrdom, mirror)!; + expect(mirror.getMeta(document)).toBeDefined(); + expect(mirror.getId(document)).toEqual(-1); + expect(rrNode).not.toBeNull(); + 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(-2); + expect(rrNode).not.toBeNull(); + 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(); + rrNode = buildFromNode( + (document.documentElement as unknown) as Node, + rrdom, + mirror, + )!; + expect(mirror.getMeta(document.documentElement)).toBeDefined(); + expect(mirror.getId(document.documentElement)).toEqual(-3); + expect(rrNode).not.toBeNull(); + 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(-4); + expect(rrNode).not.toBeNull(); + 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(-5); + expect(rrNode).not.toBeNull(); + 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( + '', + 'application/xml', + ); + const cdata = 'Some data & then some'; + var cdataSection = xmlDoc.createCDATASection(cdata); + expect(mirror.getMeta(cdataSection)).toBeNull(); + expect(mirror.getMeta(cdataSection)).toBeNull(); + rrNode = buildFromNode(cdataSection, rrdom, mirror)!; + expect(mirror.getMeta(cdataSection)).toBeDefined(); + expect(mirror.getId(cdataSection)).toEqual(-6); + expect(rrNode).not.toBeNull(); + 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); + }); + + 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, rrdom, mirror)!; + 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, rrdom, mirror)!; + 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!, + rrdom, + mirror, + RRIFrame, + )!; + expect(rrNode).not.toBeNull(); + 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); + }); + + 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!, + rrdom, + mirror, + parentRRNode, + )!; + expect(rrNode).not.toBeNull(); + 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'); + expect(rrNode).toBe(parentRRNode.shadowRoot); + }); + }); + + 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' } }, + }) as unknown) as rollup.Plugin, + ], + }); + const { + output: [{ code: _code }], + } = await bundle.generate({ + name: 'rrdom', + format: 'iife', + }); + code = _code; + }); + afterAll(async () => { + await browser.close(); + }); + + 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, undefined, doc); + printRRDom(doc, doc.mirror); + `); + 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, undefined, doc); + printRRDom(doc, doc.mirror); + `); + expect(result).toMatchSnapshot(); + }); + + it('can build from a html containing nested shadow doms', async () => { + await page.setContent(getHtml('shadow-dom.html')); + const result = await page.evaluate(` + const doc = new rrdom.RRDocument(); + rrdom.buildFromDom(document, undefined, doc); + printRRDom(doc, doc.mirror); + `); + 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, undefined, 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); + 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.add(node1, { + id: 0, + type: NodeType.DocumentType, + name: '', + publicId: '', + systemId: '', + }); + const node2 = dom.createElement('html'); + dom.appendChild(node2); + dom.mirror.add(node1, { + id: 1, + type: NodeType.Document, + childNodes: [], + }); + + 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 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(); + expect(canvas.getContext()).toBeNull(); + }); + + describe('Mirror in the RRDocument', () => { + it('should have a mirror to store id and node', () => { + const dom = new RRDocument(); + expect(dom.mirror).toBeDefined(); + 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.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'); + dom.mirror.add(node1, getDefaultSN(node1, 0)); + + 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 an ID', () => { + const dom = new RRDocument(); + 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.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.add(node1, getDefaultSN(node1, 0)); + const node2 = dom.createTextNode('text'); + dom.mirror.add(node2, getDefaultSN(node2, 1)); + node1.appendChild(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.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); + 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.add(node1, getDefaultSN(node1, 0)); + const node2 = dom.createTextNode('text'); + dom.mirror.add(node2, getDefaultSN(node2, 1)); + 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(); + }); + + 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(''); + }); + }); +}); +function getHtml(fileName: string) { + const filePath = path.resolve(__dirname, `./html/${fileName}`); + return fs.readFileSync(filePath, 'utf8'); +} diff --git a/packages/rrdom/tsconfig.json b/packages/rrdom/tsconfig.json index 15e1aac33c..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, @@ -11,9 +11,10 @@ "outDir": "build", "lib": ["es6", "dom"], "skipLibCheck": true, - "declaration": true + "declaration": true, + "importsNotUsedAsValues": "error" }, "compileOnSave": true, "exclude": ["test"], - "include": ["src", "test.d.ts"] + "include": ["src", "test.d.ts", "../rrweb/src/record/workers/workers.d.ts"] } 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-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index cf818fc7e3..9e7440cba6 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -330,7 +330,7 @@ export function buildNodeWithSN( if (n.rootId) { console.assert( (mirror.getNode(n.rootId) as Document) === doc, - 'Target document should has the same root id.', + 'Target document should have the same root id.', ); } // use target document as root document diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 89b4e1ac12..6a09c633b4 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -76,6 +76,28 @@ export interface ICanvas extends HTMLCanvasElement { __context: string; } +export interface IMirror { + getId(n: TNode | undefined | null): number; + + getNode(id: number): TNode | null; + + getIds(): number[]; + + getMeta(n: TNode): serializedNodeWithId | null; + + removeNodeFromMap(n: TNode): void; + + has(id: number): boolean; + + hasNode(node: TNode): boolean; + + add(n: TNode, meta: serializedNodeWithId): void; + + replace(id: number, n: TNode): void; + + reset(): void; +} + export type idNodeMap = Map; export type nodeMetaMap = WeakMap; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 5eba92f806..a2bde1570c 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -3,6 +3,7 @@ import { MaskInputFn, MaskInputOptions, nodeMetaMap, + IMirror, serializedNodeWithId, } from './types'; @@ -15,7 +16,7 @@ export function isShadowRoot(n: Node): n is ShadowRoot { return Boolean(host && host.shadowRoot && host.shadowRoot === n); } -export class Mirror { +export class Mirror implements IMirror { private idNodeMap: idNodeMap = new Map(); private nodeMetaMap: nodeMetaMap = new WeakMap(); @@ -47,7 +48,9 @@ export class Mirror { this.idNodeMap.delete(id); if (n.childNodes) { - n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + n.childNodes.forEach((childNode) => + this.removeNodeFromMap((childNode as unknown) as Node), + ); } } has(id: number): boolean { diff --git a/packages/rrweb-snapshot/typings/types.d.ts b/packages/rrweb-snapshot/typings/types.d.ts index 9ffd0ea8ec..bac3fcfb1f 100644 --- a/packages/rrweb-snapshot/typings/types.d.ts +++ b/packages/rrweb-snapshot/typings/types.d.ts @@ -58,6 +58,18 @@ export interface INode extends Node { export interface ICanvas extends HTMLCanvasElement { __context: string; } +export interface IMirror { + getId(n: TNode | undefined | null): number; + getNode(id: number): TNode | null; + getIds(): number[]; + getMeta(n: TNode): serializedNodeWithId | null; + removeNodeFromMap(n: TNode): void; + has(id: number): boolean; + hasNode(node: TNode): boolean; + add(n: TNode, meta: serializedNodeWithId): void; + replace(id: number, n: TNode): void; + reset(): void; +} export declare type idNodeMap = Map; export declare type nodeMetaMap = WeakMap; export declare type MaskInputOptions = Partial<{ diff --git a/packages/rrweb-snapshot/typings/utils.d.ts b/packages/rrweb-snapshot/typings/utils.d.ts index c3bf88f824..23ae46f7ea 100644 --- a/packages/rrweb-snapshot/typings/utils.d.ts +++ b/packages/rrweb-snapshot/typings/utils.d.ts @@ -1,7 +1,7 @@ -import { MaskInputFn, MaskInputOptions, serializedNodeWithId } from './types'; +import { MaskInputFn, MaskInputOptions, IMirror, serializedNodeWithId } from './types'; export declare function isElement(n: Node): n is Element; export declare function isShadowRoot(n: Node): n is ShadowRoot; -export declare class Mirror { +export declare class Mirror implements IMirror { private idNodeMap; private nodeMetaMap; getId(n: Node | undefined | null): number; diff --git a/packages/rrweb/jest.config.js b/packages/rrweb/jest.config.js index 29db4e7fa0..c1f271c9b7 100644 --- a/packages/rrweb/jest.config.js +++ b/packages/rrweb/jest.config.js @@ -5,5 +5,6 @@ module.exports = { testMatch: ['**/**.test.ts'], moduleNameMapper: { '\\.css$': 'identity-obj-proxy', + 'rrdom/es/(.*)': 'rrdom/lib/$1', }, }; diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 18b32fe1b4..72b6ffff89 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -46,12 +46,12 @@ "@types/inquirer": "0.0.43", "@types/jest": "^27.4.1", "@types/jest-image-snapshot": "^4.3.1", - "@types/jsdom": "^16.2.14", "@types/node": "^17.0.21", "@types/offscreencanvas": "^2019.6.4", "@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", @@ -59,14 +59,12 @@ "jest": "^27.5.1", "jest-image-snapshot": "^4.5.1", "jest-snapshot": "^23.6.0", - "jsdom": "^17.0.0", - "jsdom-global": "^3.0.2", "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", @@ -81,6 +79,7 @@ "base64-arraybuffer": "^1.0.1", "fflate": "^0.4.4", "mitt": "^1.1.3", + "rrdom": "^0.1.2", "rrweb-snapshot": "^1.1.14" } } diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 3c2eff47a1..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,10 +108,34 @@ 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 }), + + // supports bundling `web-worker:..filename` webWorkerLoader(), + typescript(), ]; const plugins = basePlugins.concat( @@ -123,7 +147,7 @@ for (const c of baseConfigs) { // browser configs.push({ input: c.input, - plugins, + plugins: getPlugins(), output: [ { name: c.name, @@ -135,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, @@ -197,23 +214,9 @@ if (process.env.BROWSER_ONLY) { configs = []; for (const c of browserOnlyBaseConfigs) { - const plugins = [ - resolve({ browser: true }), - 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/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/packer/unpack.ts b/packages/rrweb/src/packer/unpack.ts index 9211d7b593..c224e09372 100644 --- a/packages/rrweb/src/packer/unpack.ts +++ b/packages/rrweb/src/packer/unpack.ts @@ -1,6 +1,6 @@ import { strFromU8, strToU8, unzlibSync } from 'fflate'; import { UnpackFn, eventWithTimeAndPacker, MARK } from './base'; -import { eventWithTime } from '../types'; +import type { eventWithTime } from '../types'; export const unpack: UnpackFn = (raw: string) => { if (typeof raw !== 'string') { @@ -16,7 +16,7 @@ export const unpack: UnpackFn = (raw: string) => { } try { const e: eventWithTimeAndPacker = JSON.parse( - strFromU8(unzlibSync(strToU8(raw, true))) + strFromU8(unzlibSync(strToU8(raw, true))), ); if (e.v === MARK) { return e; diff --git a/packages/rrweb/src/plugins/console/record/index.ts b/packages/rrweb/src/plugins/console/record/index.ts index 55b81d0f95..914e3057a8 100644 --- a/packages/rrweb/src/plugins/console/record/index.ts +++ b/packages/rrweb/src/plugins/console/record/index.ts @@ -1,4 +1,4 @@ -import { listenerHandler, RecordPlugin, IWindow } from '../../../types'; +import type { listenerHandler, RecordPlugin, IWindow } from '../../../types'; import { patch } from '../../../utils'; import { ErrorStackParser, StackFrame } from './error-stack-parser'; import { stringify } from './stringify'; diff --git a/packages/rrweb/src/plugins/console/record/stringify.ts b/packages/rrweb/src/plugins/console/record/stringify.ts index 3804d48e88..e32069ff9f 100644 --- a/packages/rrweb/src/plugins/console/record/stringify.ts +++ b/packages/rrweb/src/plugins/console/record/stringify.ts @@ -4,7 +4,7 @@ * */ -import { StringifyOptions } from './index'; +import type { StringifyOptions } from './index'; /** * transfer the node path in Event to string diff --git a/packages/rrweb/src/plugins/sequential-id/record/index.ts b/packages/rrweb/src/plugins/sequential-id/record/index.ts index a439831191..fa5a616cae 100644 --- a/packages/rrweb/src/plugins/sequential-id/record/index.ts +++ b/packages/rrweb/src/plugins/sequential-id/record/index.ts @@ -1,4 +1,4 @@ -import { RecordPlugin } from '../../../types'; +import type { RecordPlugin } from '../../../types'; export type SequentialIdOptions = { key: string; diff --git a/packages/rrweb/src/plugins/sequential-id/replay/index.ts b/packages/rrweb/src/plugins/sequential-id/replay/index.ts index 852a02cf36..7e3644afa7 100644 --- a/packages/rrweb/src/plugins/sequential-id/replay/index.ts +++ b/packages/rrweb/src/plugins/sequential-id/replay/index.ts @@ -1,5 +1,5 @@ import type { SequentialIdOptions } from '../record'; -import { ReplayPlugin, eventWithTime } from '../../../types'; +import type { ReplayPlugin, eventWithTime } from '../../../types'; type Options = SequentialIdOptions & { warnOnMissingId: boolean; diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index e4331788d4..db2d85d44d 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -1,5 +1,5 @@ -import { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import { mutationCallBack } from '../types'; +import type { Mirror, serializedNodeWithId } 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 aaa8900550..80718f2589 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -7,7 +7,7 @@ import { maskInputValue, Mirror, } from 'rrweb-snapshot'; -import { +import type { mutationRecord, textCursor, attributeCursor, @@ -298,7 +298,7 @@ export default class MutationBuffer { inlineImages: this.inlineImages, onSerialize: (currentN) => { if (isSerializedIframe(currentN, this.mirror)) { - this.iframeManager.addIframe(currentN); + this.iframeManager.addIframe(currentN as HTMLIFrameElement); } if (hasShadowRoot(n)) { this.shadowDomManager.addShadowRoot(n.shadowRoot, document); @@ -322,7 +322,7 @@ export default class MutationBuffer { this.mirror.removeNodeFromMap(this.mapRemoves.shift()!); } - for (const n of this.movedSet) { + for (const n of Array.from(this.movedSet.values())) { if ( isParentRemoved(this.removes, n, this.mirror) && !this.movedSet.has(n.parentNode!) @@ -332,7 +332,7 @@ export default class MutationBuffer { pushAdd(n); } - for (const n of this.addedSet) { + for (const n of Array.from(this.addedSet.values())) { if ( !isAncestorInSet(this.droppedSet, n) && !isParentRemoved(this.removes, n, this.mirror) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 5504377853..fc1201ced6 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -1,5 +1,5 @@ import { MaskInputOptions, maskInputValue } from 'rrweb-snapshot'; -import { FontFaceSet } from 'css-font-loading-module'; +import type { FontFaceSet } from 'css-font-loading-module'; import { throttle, on, @@ -108,9 +108,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/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts index 86cf0e396a..6309a69e3c 100644 --- a/packages/rrweb/src/record/observers/canvas/2d.ts +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -1,4 +1,4 @@ -import { Mirror } from 'rrweb-snapshot'; +import type { Mirror } 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 79859dbbc8..347be47375 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -1,7 +1,6 @@ -import { ICanvas, Mirror } from 'rrweb-snapshot'; -import { +import type { ICanvas, Mirror } from 'rrweb-snapshot'; +import type { blockClass, - CanvasContext, canvasManagerMutationCallback, canvasMutationCallback, canvasMutationCommand, @@ -10,11 +9,12 @@ import { listenerHandler, CanvasArg, } from '../../../types'; +import { CanvasContext } from '../../../types'; import initCanvas2DMutationObserver from './2d'; import initCanvasContextObserver from './canvas'; import initCanvasWebGLMutationObserver from './webgl'; import ImageBitmapDataURLWorker from 'web-worker:../../workers/image-bitmap-data-url-worker.ts'; -import { ImageBitmapDataURLRequestWorker } from '../../workers/image-bitmap-data-url-worker'; +import type { ImageBitmapDataURLRequestWorker } from '../../workers/image-bitmap-data-url-worker'; export type RafStamps = { latestId: number; invokeId: number | null }; diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts index ff42c7a05d..74a9f8137a 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas.ts @@ -1,5 +1,5 @@ -import { ICanvas } from 'rrweb-snapshot'; -import { blockClass, IWindow, listenerHandler } from '../../../types'; +import type { 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 98095b3a61..f310b8e5d9 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, CanvasArg } from '../../../types'; +import type { IWindow, CanvasArg } from '../../../types'; // TODO: unify with `replay/webgl.ts` type CanvasVarMap = Map; diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts index 1ebe2e19da..f12a104db1 100644 --- a/packages/rrweb/src/record/observers/canvas/webgl.ts +++ b/packages/rrweb/src/record/observers/canvas/webgl.ts @@ -1,4 +1,4 @@ -import { Mirror } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; import { blockClass, CanvasContext, @@ -31,8 +31,8 @@ function patchGLPrototype( return function (this: typeof prototype, ...args: Array) { const result = original.apply(this, args); saveWebGLVar(result, win, prototype); - if (!isBlocked(this.canvas, blockClass)) { - const id = mirror.getId(this.canvas); + if (!isBlocked(this.canvas as HTMLCanvasElement, blockClass)) { + const id = mirror.getId(this.canvas as HTMLCanvasElement); const recordArgs = serializeArgs([...args], win, prototype); const mutation: canvasMutationWithType = { diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 1bc50d0f2e..9e0b91e080 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, scrollCallback, MutationBufferParam, @@ -6,7 +6,7 @@ import { } from '../types'; import { initMutationObserver, initScrollObserver } from './observer'; import { patch } from '../utils'; -import { Mirror } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; type BypassOptions = Omit< MutationBufferParam, diff --git a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts index 28cc5c8b72..f4ffaa5753 100644 --- a/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts +++ b/packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts @@ -1,5 +1,5 @@ import { encode } from 'base64-arraybuffer'; -import { +import type { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse, } from '../../types'; diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts index f535f24bac..53668659fd 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'; import { deserializeArg } from './deserialize-args'; export default async function canvasMutation({ diff --git a/packages/rrweb/src/replay/canvas/deserialize-args.ts b/packages/rrweb/src/replay/canvas/deserialize-args.ts index d5adcbbf7d..27f12b920e 100644 --- a/packages/rrweb/src/replay/canvas/deserialize-args.ts +++ b/packages/rrweb/src/replay/canvas/deserialize-args.ts @@ -1,6 +1,6 @@ import { decode } from 'base64-arraybuffer'; import type { Replayer } from '../'; -import { CanvasArg, SerializedCanvasArg } from '../../types'; +import type { CanvasArg, SerializedCanvasArg } from '../../types'; // TODO: add ability to wipe this list type GLVarMap = Map; diff --git a/packages/rrweb/src/replay/canvas/index.ts b/packages/rrweb/src/replay/canvas/index.ts index 8924b9ec31..b192500a39 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 67bffb6ff8..4480cd9592 100644 --- a/packages/rrweb/src/replay/canvas/webgl.ts +++ b/packages/rrweb/src/replay/canvas/webgl.ts @@ -11,9 +11,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/replay/index.ts b/packages/rrweb/src/replay/index.ts index 9c4fe9751a..1e2ddcc0dd 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -7,6 +7,26 @@ import { Mirror, createMirror, } from 'rrweb-snapshot'; +import { + RRDocument, + StyleRuleType, + createOrGetNode, + buildFromNode, + buildFromDom, + diff, + getDefaultSN, +} 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'; @@ -34,37 +54,28 @@ import { scrollData, inputData, canvasMutationData, - ElementState, styleAttributeValue, styleValueWithPriority, mouseMovePos, IWindow, canvasMutationCommand, canvasMutationParam, - textMutation, + canvasEventWithTime, } from '../types'; import { polyfill, - TreeIndex, queueToResolveTrees, iterateResolveTree, AppendedIframe, getBaseDimension, hasShadowRoot, isSerializedIframe, + getNestedRule, + getPositionsAndIndex, uniqueTextMutations, } from '../utils'; import getInjectStyleRules from './styles/inject-style'; import './styles/style.css'; -import { - applyVirtualStyleRulesToNode, - storeCSSRules, - StyleRuleType, - VirtualStyleRules, - VirtualStyleRulesMap, - getNestedRule, - getPositionsAndIndex, -} from './virtual-styles'; import canvasMutation from './canvas'; import { deserializeArg } from './canvas/deserialize-args'; @@ -105,6 +116,10 @@ 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(); + private mouse: HTMLDivElement; private mouseTail: HTMLCanvasElement | null = null; private tailPositions: Array<{ x: number; y: number }> = []; @@ -116,12 +131,6 @@ export class Replayer { // tslint:disable-next-line: variable-name private legacy_missingNodeRetryMap: missingNodeMap = {}; - 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(); @@ -159,6 +168,7 @@ export class Replayer { UNSAFE_replayCanvas: false, pauseAnimation: true, mouseTail: defaultMouseTailConfig, + useVirtualDom: true, // Virtual-dom optimization is enabled by default. }; this.config = Object.assign({}, defaultConfig, config); @@ -169,38 +179,72 @@ export class Replayer { this.setupDom(); - this.treeIndex = new TreeIndex(); - this.fragmentParentMap = new Map(); - this.elementStateMap = new Map(); - this.virtualStyleRulesMap = new Map(); - this.emitter.on(ReplayerEvents.Flush, () => { - const { scrollMap, inputMap, mutationData } = this.treeIndex.flush(); - - this.fragmentParentMap.forEach((parent, frag) => - this.restoreRealParent(frag, parent), - ); - - // apply text needs to happen before virtual style rules gets applied - // as it can overwrite the contents of a stylesheet - for (const d of uniqueTextMutations(mutationData.texts)) { - this.applyText(d, mutationData); - } + if (this.usingVirtualDom) { + const replayerHandler: ReplayerHandler = { + mirror: this.mirror, + applyCanvas: ( + canvasEvent: canvasEventWithTime, + canvasMutationData: canvasMutationData, + target: HTMLCanvasElement, + ) => { + canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + }, + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), + }; + diff( + this.iframe.contentDocument!, + this.virtualDom, + replayerHandler, + this.virtualDom.mirror, + ); + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; - for (const node of this.virtualStyleRulesMap.keys()) { - // restore css rules of style elements after they are mounted - this.restoreNodeSheet(node); + // 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, + this.virtualDom.mirror, + ); + diff( + realNode, + value.node as RRNode, + replayerHandler, + this.virtualDom.mirror, + ); + value.node = realNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } } - this.fragmentParentMap.clear(); - this.elementStateMap.clear(); - this.virtualStyleRulesMap.clear(); - for (const d of scrollMap.values()) { - this.applyScroll(d, true); - } - for (const d of inputMap.values()) { - this.applyInput(d); + if (this.mousePos) { + this.moveAndHover( + this.mousePos.x, + this.mousePos.y, + this.mousePos.id, + true, + this.mousePos.debugData, + ); } + this.mousePos = null; }); this.emitter.on(ReplayerEvents.PlayBack, () => { this.firstFullSnapshot = null; @@ -371,7 +415,7 @@ export class Replayer { } this.iframe.contentDocument ?.getElementsByTagName('html')[0] - .classList.remove('rrweb-paused'); + ?.classList.remove('rrweb-paused'); this.emitter.emit(ReplayerEvents.Start); } @@ -385,7 +429,7 @@ export class Replayer { } this.iframe.contentDocument ?.getElementsByTagName('html')[0] - .classList.add('rrweb-paused'); + ?.classList.add('rrweb-paused'); this.emitter.emit(ReplayerEvents.Pause); } @@ -488,14 +532,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; @@ -503,16 +540,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) { @@ -692,11 +719,9 @@ export class Replayer { } private insertStyleRules( - documentElement: HTMLElement, - head: HTMLHeadElement, + documentElement: HTMLElement | RRElement, + head: HTMLHeadElement | RRElement, ) { - const styleEl = document.createElement('style'); - documentElement!.insertBefore(styleEl, head); const injectStylesRules = getInjectStyleRules( this.config.blockClass, ).concat(this.config.insertStyleRules); @@ -705,44 +730,64 @@ 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.usingVirtualDom) { + const styleEl = this.virtualDom.createElement('style') as RRStyleElement; + 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 + 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, ) { + const mirror: RRDOMMirror | Mirror = this.usingVirtualDom + ? this.virtualDom.mirror + : this.mirror; + type TNode = typeof mirror extends Mirror ? Node : RRNode; + type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror; + 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)) { - const frag = parent; - const realParent = this.fragmentParentMap.get(frag)!; - this.restoreRealParent(frag, realParent); - break; - } - parent = parent.parentNode; - } - } buildNodeWithSN(mutation.node, { - doc: iframeEl.contentDocument!, - mirror: this.mirror, + doc: iframeEl.contentDocument! as Document, + mirror: mirror as Mirror, hackCss: true, skipChild: false, afterAppend: (builtNode) => { this.collectIframeAndAttachDocument(collected, builtNode); - const sn = this.mirror.getMeta(builtNode); + const sn = (mirror as TMirror).getMeta((builtNode as unknown) as TNode); if ( sn?.type === NodeType.Element && 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, @@ -764,7 +809,10 @@ export class Replayer { (m) => m.parentId === this.mirror.getId(builtNode), ); if (mutationInQueue) { - collected.push({ mutationInQueue, builtNode }); + collected.push({ + mutationInQueue, + builtNode: builtNode as HTMLIFrameElement, + }); } } } @@ -935,21 +983,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) => { - const target = this.mirror.getNode(m.id); - const parent = target?.parentNode; - // remove any style rules that pending - // for stylesheets where the contents get replaced - if (parent && this.virtualStyleRulesMap.has(parent)) - this.virtualStyleRulesMap.delete(parent); - - 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) { @@ -992,7 +1025,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()); @@ -1079,11 +1112,16 @@ export class Replayer { if (d.id === -1) { break; } - if (isSync) { - this.treeIndex.scroll(d); + 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; } - this.applyScroll(d, false); + // Use isSync rather than this.usingVirtualDom because not every fast-forward process uses virtual dom optimization. + this.applyScroll(d, isSync); break; } case IncrementalSource.ViewportResize: @@ -1102,19 +1140,25 @@ export class Replayer { if (d.id === -1) { break; } - if (isSync) { - this.treeIndex.input(d); + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id) as RRElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.inputData = d; break; } this.applyInput(d); break; } case IncrementalSource.MediaInteraction: { - const target = this.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); } - const mediaEl = target as HTMLMediaElement; + const mediaEl = target as HTMLMediaElement | RRMediaElement; try { if (d.currentTime) { mediaEl.currentTime = d.currentTime; @@ -1145,158 +1189,118 @@ export class Replayer { break; } case IncrementalSource.StyleSheetRule: { - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - - const styleEl = target as HTMLStyleElement; - const parent = target.parentNode!; - 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.usingVirtualDom) { + const target = this.virtualDom.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: { - // same with StyleSheetRule - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - - const styleEl = target as HTMLStyleElement; - const parent = target.parentNode!; - 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.usingVirtualDom) { + const target = this.virtualDom.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; @@ -1305,20 +1309,31 @@ export class Replayer { if (!this.config.UNSAFE_replayCanvas) { return; } - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode( + d.id, + ) as RRCanvasElement; + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.canvasMutations.push({ + event: e as canvasEventWithTime, + 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 HTMLCanvasElement, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); } - - canvasMutation({ - event: e, - mutation: d, - target: target as HTMLCanvasElement, - imageMap: this.imageMap, - canvasEventMap: this.canvasEventMap, - errorHandler: this.warnCanvasMutationFailed.bind(this), - }); - break; } case IncrementalSource.Font: { @@ -1340,9 +1355,35 @@ 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.config.useVirtualDom && !this.usingVirtualDom && isSync) { + this.usingVirtualDom = true; + buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom); + // 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 Node, + this.virtualDom, + this.mirror, + ); + if (virtualNode) value.node = virtualNode; + } catch (error) { + if (this.config.showWarning) { + console.warn(error); + } + } + } + } + } + const mirror = this.usingVirtualDom ? this.virtualDom.mirror : this.mirror; + type TNode = typeof mirror extends Mirror ? Node : RRNode; + 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 @@ -1350,53 +1391,43 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } - if (this.virtualStyleRulesMap.has(target)) { - this.virtualStyleRulesMap.delete(target); - } - let parent: Node | null | ShadowRoot = this.mirror.getNode( + let parent: Node | 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 = this.mirror.getMeta(parent) - ? this.fragmentParentMap.get(parent) - : undefined; - if (realParent && realParent.contains(target)) { - parent = realParent; - } else if (this.fragmentParentMap.has(target)) { + mirror.removeNodeFromMap(target as Node & RRNode); + if (parent) + try { + parent.removeChild(target as Node & RRNode); /** - * the target itself is a fragment document and it's not in the dom - * so we should remove the real target from its parent + * https://github.com/rrweb-io/rrweb/pull/887 + * Remove any virtual style rules for stylesheets if a child text node is removed. */ - realTarget = this.fragmentParentMap.get(target)!; - this.fragmentParentMap.delete(target); - target = realTarget; - } - try { - parent.removeChild(target); + 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( 'parent could not remove child in mutation', parent, - realParent, target, - realTarget, d, ); } else { throw error; } } - } }); // tslint:disable-next-line: variable-name @@ -1407,9 +1438,9 @@ export class Replayer { // next not present at this moment const nextNotInDOM = (mutation: addedNodeMutation) => { - let next: Node | null = null; + let next: TNode | null = null; if (mutation.nextId) { - next = this.mirror.getNode(mutation.nextId); + next = mirror.getNode(mutation.nextId) as TNode | null; } // next not present at this moment if ( @@ -1427,7 +1458,7 @@ export class Replayer { if (!this.iframe.contentDocument) { return console.warn('Looks like your replayer has been destroyed.'); } - let parent: Node | null | ShadowRoot = this.mirror.getNode( + let parent: Node | null | ShadowRoot | RRNode = mirror.getNode( mutation.parentId, ); if (!parent) { @@ -1438,78 +1469,49 @@ 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); - } - - const hasIframeChild = - (parent as HTMLElement).getElementsByTagName?.('iframe').length > 0; - /** - * Why !isSerializedIframe(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 && - !isSerializedIframe(parent, this.mirror) && - !hasIframeChild - ) { - const virtualParent = document.createDocumentFragment(); - this.mirror.replace(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) { // If the parent is attached a shadow dom after it's created, it won't have a shadow root. if (!hasShadowRoot(parent)) { - (parent as HTMLElement).attachShadow({ mode: 'open' }); - parent = (parent as HTMLElement).shadowRoot!; - } else parent = parent.shadowRoot; + (parent as Element | RRElement).attachShadow({ mode: 'open' }); + parent = (parent as Element | RRElement).shadowRoot! as Node | RRNode; + } else parent = parent.shadowRoot as Node | RRNode; } - 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); + previous = mirror.getNode(mutation.previousId); } if (mutation.nextId) { - next = this.mirror.getNode(mutation.nextId); + next = mirror.getNode(mutation.nextId); } 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) + : this.usingVirtualDom + ? this.virtualDom : this.iframe.contentDocument; - if (isSerializedIframe(parent, this.mirror)) { - this.attachDocumentToIframe(mutation, parent); + if (isSerializedIframe(parent, mirror)) { + this.attachDocumentToIframe( + mutation, + parent as HTMLIFrameElement | RRIFrameElement, + ); return; } const target = buildNodeWithSN(mutation.node, { - doc: targetDoc as Document, - mirror: this.mirror, + doc: targetDoc as Document, // can be Document or RRDocument + mirror: mirror as Mirror, // can be this.mirror or virtualDom.mirror skipChild: true, hackCss: true, cache: this.cache, - })!; + }) as Node | RRNode; // legacy data, we should not have -1 siblings any more if (mutation.previousId === -1 || mutation.nextId === -1) { @@ -1520,50 +1522,76 @@ export class Replayer { return; } - const parentSn = this.mirror.getMeta(parent); + // Typescripts type system is not smart enough + // to understand what is going on with the types below + type TNode = typeof mirror extends Mirror ? Node : RRNode; + type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror; + + const parentSn = (mirror as TMirror).getMeta(parent as TNode); if ( parentSn && parentSn.type === NodeType.Element && parentSn.tagName === 'textarea' && mutation.node.type === NodeType.Text ) { + 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 Array.from(parent.childNodes)) { + for (const c of childNodeArray) { 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 as TNode).insertBefore( + target as TNode, + previous.nextSibling as TNode, + ); } 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 as TNode).contains(next as TNode) + ? (parent as TNode).insertBefore(target as TNode, next as TNode) + : (parent as TNode).insertBefore(target as TNode, 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 as TNode).removeChild(targetDoc.firstChild as TNode); } } - parent.appendChild(target); + (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); + const targetId = this.mirror.getId(target as HTMLIFrameElement); const mutationInQueue = this.newDocumentQueue.find( (m) => m.parentId === targetId, ); if (mutationInQueue) { - this.attachDocumentToIframe(mutationInQueue, target); + this.attachDocumentToIframe( + mutationInQueue, + target as HTMLIFrameElement, + ); this.newDocumentQueue = this.newDocumentQueue.filter( (m) => m !== mutationInQueue, ); @@ -1597,7 +1625,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.', @@ -1616,7 +1644,7 @@ export class Replayer { } uniqueTextMutations(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 @@ -1624,16 +1652,19 @@ export class Replayer { } return this.warnNodeNotFound(d, mutation.id); } + target.textContent = mutation.value; + /** - * apply text content to real parent directly + * https://github.com/rrweb-io/rrweb/pull/865 + * Remove any virtual style rules for stylesheets whose contents are replaced. */ - if (this.fragmentParentMap.has(target)) { - target = this.fragmentParentMap.get(target)!; + if (this.usingVirtualDom) { + const parent = target.parentNode as RRStyleElement; + if (parent?.rules?.length > 0) parent.rules = []; } - 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 @@ -1641,17 +1672,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 Element).removeAttribute(attributeName); + (target as Element | RRElement).removeAttribute(attributeName); } else if (typeof value === 'string') { try { - (target as Element).setAttribute(attributeName, value); + (target as Element | RRElement).setAttribute( + attributeName, + value, + ); } catch (error) { if (this.config.showWarning) { console.warn( @@ -1662,7 +1693,7 @@ export class Replayer { } } else if (attributeName === 'style') { let styleValues = value as styleAttributeValue; - const targetEl = target as HTMLElement; + const targetEl = target as HTMLElement | RRElement; for (var s in styleValues) { if (styleValues[s] === false) { targetEl.style.removeProperty(s); @@ -1731,22 +1762,10 @@ export class Replayer { } } - private applyText(d: textMutation, mutation: mutationData) { - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(mutation, d.id); - } - try { - (target as HTMLElement).textContent = d.value; - } catch (error) { - // for safe - } - } - private legacy_resolveMissingNode( map: missingNodeMap, - parent: Node, - target: Node, + parent: Node | RRNode, + target: Node | RRNode, targetMutation: addedNodeMutation, ) { const { previousId, nextId } = targetMutation; @@ -1754,7 +1773,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) { @@ -1763,7 +1782,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) { @@ -1868,104 +1890,8 @@ 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: Node, parent: Node) { - const id = this.mirror.getId(frag); - const parentSn = this.mirror.getMeta(parent); - this.mirror.replace(id, parent); - - /** - * If we have already set value attribute on textarea, - * then we could not apply text content as default value any more. - */ - if ( - parentSn?.type === NodeType.Element && - parentSn?.tagName === 'textarea' && - frag.textContent - ) { - (parent as HTMLTextAreaElement).value = frag.textContent; - } - 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: Node) { - if (parent) { - if (parent.nodeType === parent.ELEMENT_NODE) { - const parentElement = parent 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); - } - } - } - } - - /** - * 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: Node) { - if (parent.nodeType === parent.ELEMENT_NODE) { - const parentElement = parent 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); - } - } - } - - private restoreNodeSheet(node: Node) { - const storedRules = this.virtualStyleRulesMap.get(node); - if (node.nodeName !== 'STYLE') { - return; - } - - if (!storedRules) { - return; - } - - const styleNode = node as HTMLStyleElement; - - applyVirtualStyleRulesToNode(storedRules, styleNode); - } - 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( @@ -1982,15 +1908,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/replay/virtual-styles.ts b/packages/rrweb/src/replay/virtual-styles.ts deleted file mode 100644 index ca13344af2..0000000000 --- a/packages/rrweb/src/replay/virtual-styles.ts +++ /dev/null @@ -1,186 +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 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, [ - { - 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 629df4baa5..a74a091c75 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -1,4 +1,4 @@ -import { +import type { serializedNodeWithId, Mirror, INode, @@ -7,11 +7,12 @@ 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 { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { RRNode } from 'rrdom/es/virtual-dom'; +import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export enum EventType { DomContentLoaded, @@ -169,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; @@ -653,6 +659,7 @@ export type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; + useVirtualDom: boolean; plugins?: ReplayPlugin[]; }; @@ -663,7 +670,7 @@ export type playerMetaData = { }; export type missingNode = { - node: Node; + node: Node | RRNode; mutation: addedNodeMutation; }; export type missingNodeMap = { @@ -706,12 +713,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 6a556f4aac..5a1081ec52 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -1,21 +1,17 @@ -import { +import type { throttleOptions, listenerHandler, hookResetter, blockClass, - IncrementalSource, addedNodeMutation, - removedNodeMutation, - textMutation, - attributeMutation, - mutationData, - scrollData, - inputData, DocumentDimension, IWindow, DeprecatedMirror, + textMutation, } from './types'; -import { Mirror, IGNORED_NODE, isShadowRoot } from 'rrweb-snapshot'; +import type { IMirror, Mirror } from 'rrweb-snapshot'; +import { isShadowRoot, IGNORED_NODE } from 'rrweb-snapshot'; +import type { RRNode, RRIFrameElement } from 'rrdom/es/virtual-dom'; export function on( type: string, @@ -280,201 +276,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) => { - if (id === -1) return; - - this.removeIdSet.add(id); - const node = mirror.getNode(id); - node?.childNodes.forEach((childNode) => { - deepRemoveFromMirror(mirror.getId(childNode)); - }); - }; - 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[]; @@ -542,13 +343,13 @@ export function iterateResolveTree( export type AppendedIframe = { mutationInQueue: addedNodeMutation; - builtNode: HTMLIFrameElement; + builtNode: HTMLIFrameElement | RRIFrameElement; }; -export function isSerializedIframe( - n: Node, - mirror: Mirror, -): n is HTMLIFrameElement { +export function isSerializedIframe( + n: TNode, + mirror: IMirror, +): boolean { return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); } @@ -582,12 +383,34 @@ export function getBaseDimension( }; } -export function hasShadowRoot( +export function hasShadowRoot( n: T, ): 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 }; +} + /** * Returns the latest mutation in the queue for each node. * @param {textMutation[]} mutations The text mutations to filter. 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/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/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index 38c4abc880..f77c92b193 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -1,7 +1,7 @@ -import * as http from 'http'; +import type * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; -import * as puppeteer from 'puppeteer'; +import type * as puppeteer from 'puppeteer'; import { startServer, launchPuppeteer, @@ -9,12 +9,7 @@ import { replaceLast, waitForRAF, } from '../utils'; -import { - recordOptions, - eventWithTime, - EventType, - IncrementalSource, -} from '../../src/types'; +import type { recordOptions, eventWithTime } from '../../src/types'; import { toMatchImageSnapshot } from 'jest-image-snapshot'; expect.extend({ toMatchImageSnapshot }); diff --git a/packages/rrweb/test/events/iframe.ts b/packages/rrweb/test/events/iframe.ts index d4110d2070..bfa75822b8 100644 --- a/packages/rrweb/test/events/iframe.ts +++ b/packages/rrweb/test/events/iframe.ts @@ -486,6 +486,30 @@ const events: eventWithTime[] = [ timestamp: now + 1500, }, // add iframe five + { + 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, + }, { type: EventType.IncrementalSnapshot, data: { @@ -550,30 +574,6 @@ const events: eventWithTime[] = [ }, 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, diff --git a/packages/rrweb/test/events/input.ts b/packages/rrweb/test/events/input.ts new file mode 100644 index 0000000000..3820f96533 --- /dev/null +++ b/packages/rrweb/test/events/input.ts @@ -0,0 +1,215 @@ +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: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'select', + 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: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'valueA', + isChecked: false, + id: 26, + }, + timestamp: now + 1500, + }, + // input event + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'valueC', + isChecked: false, + id: 26, + }, + timestamp: now + 2000, + }, + // mutation that adds an input element + { + type: EventType.IncrementalSnapshot, + 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: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'test input', + isChecked: false, + id: 33, + }, + timestamp: now + 3000, + }, + // remove the select element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 26 }], + adds: [], + }, + timestamp: now + 3500, + }, + // remove the input element + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [{ parentId: 5, id: 33 }], + adds: [], + }, + timestamp: now + 4000, + }, +]; + +export default events; 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/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/events/style-sheet-rule-events.ts b/packages/rrweb/test/events/style-sheet-rule-events.ts index 0536ea56e3..19a80bbd8a 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: { @@ -142,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, @@ -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-500-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/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/integration.test.ts b/packages/rrweb/test/integration.test.ts index 3fa0f0a34d..4c92be6218 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as http from 'http'; -import * as puppeteer from 'puppeteer'; +import type * as http from 'http'; +import type * as puppeteer from 'puppeteer'; import { assertSnapshot, startServer, diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index d213760485..1d36fa5ac1 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as puppeteer from 'puppeteer'; +import type * as puppeteer from 'puppeteer'; import { recordOptions, listenerHandler, diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts index 9280e7fb60..143dda2373 100644 --- a/packages/rrweb/test/record/webgl.test.ts +++ b/packages/rrweb/test/record/webgl.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as puppeteer from 'puppeteer'; +import type * as puppeteer from 'puppeteer'; import { recordOptions, listenerHandler, @@ -17,7 +17,7 @@ import { stripBase64, waitForRAF, } from '../utils'; -import { ICanvas } from 'rrweb-snapshot'; +import type { ICanvas } from 'rrweb-snapshot'; interface ISuite { code: string; diff --git a/packages/rrweb/test/replay/preload-all-images.test.ts b/packages/rrweb/test/replay/preload-all-images.test.ts index de7ab4e2ee..6fd9701d07 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, CanvasArg, 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/test/replay/webgl.test.ts b/packages/rrweb/test/replay/webgl.test.ts index f7d0498f17..754f092623 100644 --- a/packages/rrweb/test/replay/webgl.test.ts +++ b/packages/rrweb/test/replay/webgl.test.ts @@ -1,8 +1,8 @@ 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 type * as puppeteer from 'puppeteer'; import events from '../events/webgl'; interface ISuite { diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index b563732f81..bf1a5fc2d3 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -2,16 +2,21 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as puppeteer from 'puppeteer'; +import type * as puppeteer from 'puppeteer'; import { assertDomSnapshot, launchPuppeteer, sampleEvents as events, sampleStyleSheetRemoveEvents as stylesheetRemoveEvents, + waitForRAF, } 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'; import iframeEvents from './events/iframe'; +import shadowDomEvents from './events/shadow-dom'; +import StyleSheetTextMutation from './events/style-sheet-text-mutation'; interface ISuite { code: string; @@ -247,12 +252,206 @@ 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-500-overwritten-at-3000', + ); `); expect(result).toEqual(true); }); + 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); + 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); + }); + + 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); + 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(true); + }); + + 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); + 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); + }); + + 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); + const 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 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); + + // restart the replayer + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + + 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 waitForRAF(page); + 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 waitForRAF(page); + 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,{showDebug:true}); + replayer.pause(1050); + `); + const iframe = await page.$('iframe'); + const 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 + + // restart the replayer + await page.evaluate('replayer.play(0);'); + await waitForRAF(page); + + 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 waitForRAF(page); + 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 waitForRAF(page); + 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 waitForRAF(page); + 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 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 waitForRAF(page); + await page.evaluate('replayer.pause(4050);'); + // remove the input element at 4000 + expect(await contentDocument!.$('input')).toBeNull(); + }); + it('can fast-forward mutation events containing nested iframe elements', async () => { await page.evaluate(` events = ${JSON.stringify(iframeEvents)}; @@ -264,13 +463,12 @@ 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(); - const iframeOneDocument = await (await contentDocument!.$( + let iframeOneDocument = await (await contentDocument!.$( 'iframe', ))!.contentFrame(); expect(iframeOneDocument).not.toBeNull(); @@ -286,14 +484,21 @@ 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!.$( + '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(); @@ -301,25 +506,27 @@ 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 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(1550);'); 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 await page.evaluate('replayer.play(0);'); - await page.waitForTimeout(delay); + await waitForRAF(page); await page.evaluate('replayer.pause(2050);'); iframeTwoDocument = await ( await contentDocument!.$$('iframe') @@ -327,6 +534,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', @@ -343,7 +551,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') @@ -362,6 +570,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 waitForRAF(page); + 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; diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index 6ac48c750c..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, @@ -10,7 +10,8 @@ "rootDir": "src", "outDir": "build", "lib": ["es6", "dom"], - "downlevelIteration": true + "downlevelIteration": true, + "importsNotUsedAsValues": "error" }, "exclude": ["test"], "include": [ 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/plugins/console/record/index.d.ts b/packages/rrweb/typings/plugins/console/record/index.d.ts index 0c3bc2fd61..dc8744ab08 100644 --- a/packages/rrweb/typings/plugins/console/record/index.d.ts +++ b/packages/rrweb/typings/plugins/console/record/index.d.ts @@ -1,4 +1,4 @@ -import { RecordPlugin } from '../../../types'; +import type { RecordPlugin } from '../../../types'; export declare type StringifyOptions = { stringLengthLimit?: number; numOfKeysLimit: number; diff --git a/packages/rrweb/typings/plugins/console/record/stringify.d.ts b/packages/rrweb/typings/plugins/console/record/stringify.d.ts index 213bbf3511..c1f8c9945b 100644 --- a/packages/rrweb/typings/plugins/console/record/stringify.d.ts +++ b/packages/rrweb/typings/plugins/console/record/stringify.d.ts @@ -1,2 +1,2 @@ -import { StringifyOptions } from './index'; +import type { StringifyOptions } from './index'; export declare function stringify(obj: any, stringifyOptions?: StringifyOptions): string; diff --git a/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts b/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts index 3311e19b3c..a2f86c3cee 100644 --- a/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts +++ b/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts @@ -1,4 +1,4 @@ -import { RecordPlugin } from '../../../types'; +import type { RecordPlugin } from '../../../types'; export declare type SequentialIdOptions = { key: string; }; diff --git a/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts b/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts index a1eee69e1b..a8f7e80c0e 100644 --- a/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts +++ b/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts @@ -1,5 +1,5 @@ import type { SequentialIdOptions } from '../record'; -import { ReplayPlugin } from '../../../types'; +import type { ReplayPlugin } from '../../../types'; declare type Options = SequentialIdOptions & { warnOnMissingId: boolean; }; diff --git a/packages/rrweb/typings/record/iframe-manager.d.ts b/packages/rrweb/typings/record/iframe-manager.d.ts index 9fbf136cc4..4dc8c120b3 100644 --- a/packages/rrweb/typings/record/iframe-manager.d.ts +++ b/packages/rrweb/typings/record/iframe-manager.d.ts @@ -1,5 +1,5 @@ -import { Mirror, serializedNodeWithId } from 'rrweb-snapshot'; -import { mutationCallBack } from '../types'; +import type { Mirror, serializedNodeWithId } 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 5f88a664f9..930d247848 100644 --- a/packages/rrweb/typings/record/mutation.d.ts +++ b/packages/rrweb/typings/record/mutation.d.ts @@ -1,4 +1,4 @@ -import { mutationRecord, MutationBufferParam } from '../types'; +import type { mutationRecord, MutationBufferParam } from '../types'; export default class MutationBuffer { private frozen; private locked; diff --git a/packages/rrweb/typings/record/observers/canvas/2d.d.ts b/packages/rrweb/typings/record/observers/canvas/2d.d.ts index fee00970e9..febe6b226d 100644 --- a/packages/rrweb/typings/record/observers/canvas/2d.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/2d.d.ts @@ -1,3 +1,3 @@ -import { Mirror } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types'; export default function initCanvas2DMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): 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 46635a453a..d0ba11718f 100644 --- a/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/canvas-manager.d.ts @@ -1,5 +1,5 @@ -import { Mirror } from 'rrweb-snapshot'; -import { blockClass, canvasMutationCallback, IWindow } from '../../../types'; +import type { Mirror } from 'rrweb-snapshot'; +import type { blockClass, canvasMutationCallback, IWindow } 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 10337d4e2c..b4d17f897b 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, CanvasArg } from '../../../types'; +import type { IWindow, CanvasArg } from '../../../types'; export declare function variableListFor(ctx: RenderingContext, ctor: string): any[]; export declare const saveWebGLVar: (value: any, win: IWindow, ctx: RenderingContext) => number | void; export declare function serializeArg(value: any, win: IWindow, ctx: RenderingContext): CanvasArg; diff --git a/packages/rrweb/typings/record/observers/canvas/webgl.d.ts b/packages/rrweb/typings/record/observers/canvas/webgl.d.ts index 7f866cd8e0..f5bceaeb22 100644 --- a/packages/rrweb/typings/record/observers/canvas/webgl.d.ts +++ b/packages/rrweb/typings/record/observers/canvas/webgl.d.ts @@ -1,3 +1,3 @@ -import { Mirror } from 'rrweb-snapshot'; +import type { Mirror } from 'rrweb-snapshot'; import { blockClass, canvasManagerMutationCallback, IWindow, listenerHandler } from '../../../types'; export default function initCanvasWebGLMutationObserver(cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler; diff --git a/packages/rrweb/typings/record/shadow-dom-manager.d.ts b/packages/rrweb/typings/record/shadow-dom-manager.d.ts index 7f973a93d8..bf6a306c8f 100644 --- a/packages/rrweb/typings/record/shadow-dom-manager.d.ts +++ b/packages/rrweb/typings/record/shadow-dom-manager.d.ts @@ -1,5 +1,5 @@ -import { mutationCallBack, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types'; -import { Mirror } from 'rrweb-snapshot'; +import type { mutationCallBack, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types'; +import type { Mirror } from 'rrweb-snapshot'; declare type BypassOptions = Omit & { sampling: SamplingStrategy; }; diff --git a/packages/rrweb/typings/record/workers/image-bitmap-data-url-worker.d.ts b/packages/rrweb/typings/record/workers/image-bitmap-data-url-worker.d.ts index 71ca36c5b2..ca58dc6b17 100644 --- a/packages/rrweb/typings/record/workers/image-bitmap-data-url-worker.d.ts +++ b/packages/rrweb/typings/record/workers/image-bitmap-data-url-worker.d.ts @@ -1,4 +1,4 @@ -import { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types'; +import type { ImageBitmapDataURLWorkerParams, ImageBitmapDataURLWorkerResponse } from '../../types'; export interface ImageBitmapDataURLRequestWorker { postMessage: (message: ImageBitmapDataURLWorkerParams, transfer?: [ImageBitmap]) => void; onmessage: (message: MessageEvent) => void; diff --git a/packages/rrweb/typings/replay/canvas/2d.d.ts b/packages/rrweb/typings/replay/canvas/2d.d.ts index d780a7b981..ca7485046a 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/deserialize-args.d.ts b/packages/rrweb/typings/replay/canvas/deserialize-args.d.ts index 4d0c00e708..dd7d5a6268 100644 --- a/packages/rrweb/typings/replay/canvas/deserialize-args.d.ts +++ b/packages/rrweb/typings/replay/canvas/deserialize-args.d.ts @@ -1,5 +1,5 @@ import type { Replayer } from '../'; -import { CanvasArg, SerializedCanvasArg } from '../../types'; +import type { CanvasArg, SerializedCanvasArg } from '../../types'; export declare function variableListFor(ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext, ctor: string): any[]; export declare function isSerializedArg(arg: unknown): arg is SerializedCanvasArg; export declare function deserializeArg(imageMap: Replayer['imageMap'], ctx: CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext | null, preload?: { diff --git a/packages/rrweb/typings/replay/canvas/index.d.ts b/packages/rrweb/typings/replay/canvas/index.d.ts index 95b43accae..e316e3ea51 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, canvasEventMap, errorHandler, }: { event: Parameters[0]; diff --git a/packages/rrweb/typings/replay/index.d.ts b/packages/rrweb/typings/replay/index.d.ts index f298737c52..46b84e58c4 100644 --- a/packages/rrweb/typings/replay/index.d.ts +++ b/packages/rrweb/typings/replay/index.d.ts @@ -1,4 +1,5 @@ import { Mirror } from 'rrweb-snapshot'; +import { RRDocument } from 'rrdom/es/virtual-dom'; import { Timer } from './timer'; import { createPlayerService, createSpeedService } from './machine'; import { eventWithTime, playerConfig, playerMetaData, Handler } from '../types'; @@ -10,16 +11,14 @@ export declare class Replayer { speedService: ReturnType; get timer(): Timer; config: playerConfig; + usingVirtualDom: boolean; + virtualDom: RRDocument; private mouse; private mouseTail; private tailPositions; private emitter; private nextUserInteractionEvent; private legacy_missingNodeRetryMap; - private treeIndex; - private fragmentParentMap; - private elementStateMap; - private virtualStyleRulesMap; private cache; private imageMap; private canvasEventMap; @@ -62,17 +61,12 @@ export declare class Replayer { private applyMutation; private applyScroll; private applyInput; - private applyText; private legacy_resolveMissingNode; private moveAndHover; private drawMouseTail; private hoverElements; 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 ad37eed73c..0000000000 --- a/packages/rrweb/typings/replay/virtual-styles.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 03ce628bd6..61f3d8b84c 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -1,9 +1,10 @@ -import { serializedNodeWithId, Mirror, 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, Mirror, 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 { CanvasManager } from './record/observers/canvas/canvas-manager'; +import type { RRNode } from 'rrdom/es/virtual-dom'; +import type { CanvasManager } from './record/observers/canvas/canvas-manager'; export declare enum EventType { DomContentLoaded = 0, Load = 1, @@ -115,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<{ @@ -464,6 +469,7 @@ export declare type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; + useVirtualDom: boolean; plugins?: ReplayPlugin[]; }; export declare type playerMetaData = { @@ -472,7 +478,7 @@ export declare type playerMetaData = { totalTime: number; }; export declare type missingNode = { - node: Node; + node: Node | RRNode; mutation: addedNodeMutation; }; export declare type missingNodeMap = { @@ -507,9 +513,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 fb6dcaba94..0dacfb8245 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -1,5 +1,6 @@ -import { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, removedNodeMutation, textMutation, attributeMutation, mutationData, scrollData, inputData, DocumentDimension, IWindow, DeprecatedMirror } from './types'; -import { Mirror } from 'rrweb-snapshot'; +import type { throttleOptions, listenerHandler, hookResetter, blockClass, addedNodeMutation, DocumentDimension, IWindow, DeprecatedMirror, textMutation } from './types'; +import type { IMirror, Mirror } from 'rrweb-snapshot'; +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; @@ -15,38 +16,6 @@ export declare function isIgnored(n: Node, mirror: Mirror): boolean; export declare function isAncestorRemoved(target: Node, 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[]; @@ -56,12 +25,17 @@ export declare function queueToResolveTrees(queue: addedNodeMutation[]): Resolve export declare function iterateResolveTree(tree: ResolveTree, cb: (mutation: addedNodeMutation) => unknown): void; export declare type AppendedIframe = { mutationInQueue: addedNodeMutation; - builtNode: HTMLIFrameElement; + builtNode: HTMLIFrameElement | RRIFrameElement; }; -export declare function isSerializedIframe(n: Node, mirror: Mirror): n is HTMLIFrameElement; +export declare function isSerializedIframe(n: TNode, mirror: IMirror): boolean; 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 getUniqueTextMutations(mutations: textMutation[]): textMutation[]; +export declare function getNestedRule(rules: CSSRuleList, position: number[]): CSSGroupingRule; +export declare function getPositionsAndIndex(nestedIndex: number[]): { + positions: number[]; + index: number | undefined; +}; +export declare function uniqueTextMutations(mutations: textMutation[]): textMutation[]; export {}; diff --git a/yarn.lock b/yarn.lock index 19feb977df..a438248446 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" @@ -1939,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" @@ -1976,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" @@ -2003,7 +1827,15 @@ "@rollup/pluginutils" "^3.1.0" resolve "^1.17.0" -"@rollup/pluginutils@4", "@rollup/pluginutils@^4.1.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" integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ== @@ -2011,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== @@ -2020,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" @@ -2120,6 +1960,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" @@ -2186,14 +2031,6 @@ 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== - dependencies: - jest-diff "^27.0.0" - pretty-format "^27.0.0" - "@types/jest@^27.4.1": version "27.4.1" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" @@ -2202,15 +2039,6 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/jsdom@^16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.14.tgz#26fe9da6a8870715b154bb84cd3b2e53433d8720" - integrity sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w== - dependencies: - "@types/node" "*" - "@types/parse5" "*" - "@types/tough-cookie" "*" - "@types/jsdom@^16.2.4": version "16.2.13" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f" @@ -2316,13 +2144,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" @@ -2602,11 +2423,16 @@ 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== +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" @@ -2908,20 +2734,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" @@ -2968,16 +2780,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" @@ -3014,14 +2816,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" @@ -3669,6 +3463,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" @@ -4092,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== @@ -4135,15 +3929,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" @@ -4170,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" @@ -4188,7 +3980,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== @@ -4290,11 +4082,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" @@ -4557,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" @@ -4578,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" @@ -4823,16 +4741,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" @@ -5075,7 +4983,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: +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== @@ -5160,15 +5068,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" @@ -5193,15 +5092,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@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== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" @@ -6434,15 +6324,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" @@ -6477,31 +6358,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" @@ -6545,24 +6401,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" @@ -6608,34 +6446,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" @@ -6686,16 +6496,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" @@ -6713,13 +6513,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" @@ -6738,17 +6531,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" @@ -6773,19 +6555,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" @@ -6811,18 +6580,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" @@ -6845,11 +6602,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" @@ -6875,26 +6627,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" @@ -6954,29 +6686,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" @@ -7008,14 +6717,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" @@ -7053,16 +6754,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" @@ -7089,21 +6780,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" @@ -7127,14 +6803,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" @@ -7153,11 +6821,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" @@ -7172,15 +6835,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" @@ -7215,22 +6869,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" @@ -7275,34 +6913,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" @@ -7363,34 +6973,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" @@ -7427,14 +7009,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" @@ -7489,34 +7063,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" @@ -7557,18 +7103,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" @@ -7593,18 +7127,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" @@ -7630,19 +7152,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" @@ -7674,15 +7183,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" @@ -7692,15 +7192,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" @@ -7719,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" @@ -7742,11 +7238,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" @@ -7813,39 +7304,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" @@ -7900,12 +7358,10 @@ 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" +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" @@ -9823,15 +9279,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" @@ -10316,7 +9763,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.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== @@ -10390,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" @@ -10443,17 +9901,6 @@ 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== - 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-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" @@ -10499,6 +9946,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" @@ -11461,11 +10915,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" @@ -11625,7 +11074,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== @@ -11635,6 +11084,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" @@ -11704,7 +11158,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== @@ -11941,14 +11395,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" @@ -12091,11 +11537,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"