From a9ef2cfa83ece408e69e913331252f28a8765539 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] refactoring public API --- src/rebuild.ts | 50 +++++++++++------- src/snapshot.ts | 115 +++++++++++++++++++++++++++--------------- test/integration.ts | 16 +++--- typings/rebuild.d.ts | 13 ++++- typings/snapshot.d.ts | 22 +++++++- 5 files changed, 146 insertions(+), 70 deletions(-) diff --git a/src/rebuild.ts b/src/rebuild.ts index f2a8291d..b09d4234 100644 --- a/src/rebuild.ts +++ b/src/rebuild.ts @@ -77,9 +77,12 @@ export function addHoverClass(cssText: string): string { function buildNode( n: serializedNodeWithId, - doc: Document, - HACK_CSS: boolean, + options: { + doc: Document; + hackCss: boolean; + }, ): Node | null { + const { doc, hackCss } = options; switch (n.type) { case NodeType.Document: return doc.implementation.createDocument(null, '', null); @@ -109,7 +112,7 @@ function buildNode( const isTextarea = tagName === 'textarea' && name === 'value'; const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText'; - if (isRemoteOrDynamicCss && HACK_CSS) { + if (isRemoteOrDynamicCss && hackCss) { value = addHoverClass(value); } if (isTextarea || isRemoteOrDynamicCss) { @@ -177,7 +180,7 @@ function buildNode( return node; case NodeType.Text: return doc.createTextNode( - n.isStyle && HACK_CSS ? addHoverClass(n.textContent) : n.textContent, + n.isStyle && hackCss ? addHoverClass(n.textContent) : n.textContent, ); case NodeType.CDATA: return doc.createCDATASection(n.textContent); @@ -190,12 +193,15 @@ function buildNode( export function buildNodeWithSN( n: serializedNodeWithId, - doc: Document, - map: idNodeMap, - skipChild = false, - HACK_CSS = true, + options: { + doc: Document; + map: idNodeMap; + skipChild?: boolean; + hackCss: boolean; + }, ): INode | null { - let node = buildNode(n, doc, HACK_CSS); + const { doc, map, skipChild = false, hackCss = true } = options; + let node = buildNode(n, { doc, hackCss }); if (!node) { return null; } @@ -214,7 +220,12 @@ export function buildNodeWithSN( !skipChild ) { for (const childN of n.childNodes) { - const childNode = buildNodeWithSN(childN, doc, map, false, HACK_CSS); + const childNode = buildNodeWithSN(childN, { + doc, + map, + skipChild: false, + hackCss, + }); if (!childNode) { console.warn('Failed to rebuild', childN); } else { @@ -259,15 +270,20 @@ function handleScroll(node: INode) { function rebuild( n: serializedNodeWithId, - doc: Document, - onVisit?: (node: INode) => unknown, - /** - * This is not a public API yet, just for POC - */ - HACK_CSS: boolean = true, + options: { + doc: Document; + onVisit?: (node: INode) => unknown; + hackCss?: boolean; + }, ): [Node | null, idNodeMap] { + const { doc, onVisit, hackCss = true } = options; const idNodeMap: idNodeMap = {}; - const node = buildNodeWithSN(n, doc, idNodeMap, false, HACK_CSS); + const node = buildNodeWithSN(n, { + doc, + map: idNodeMap, + skipChild: false, + hackCss, + }); visit(idNodeMap, (visitedNode) => { if (onVisit) { onVisit(visitedNode); diff --git a/src/snapshot.ts b/src/snapshot.ts index ae3e5ad4..4c17612f 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -187,13 +187,23 @@ export function _isBlockedElement( function serializeNode( n: Node, - doc: Document, - blockClass: string | RegExp, - blockSelector: string | null, - inlineStylesheet: boolean, - maskInputOptions: MaskInputOptions = {}, - recordCanvas: boolean, + options: { + doc: Document; + blockClass: string | RegExp; + blockSelector: string | null; + inlineStylesheet: boolean; + maskInputOptions: MaskInputOptions; + recordCanvas: boolean; + }, ): serializedNode | false { + const { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions = {}, + recordCanvas, + } = options; switch (n.nodeType) { case n.DOCUMENT_NODE: return { @@ -437,26 +447,39 @@ function slimDOMExcluded( export function serializeNodeWithId( n: Node | INode, - doc: Document, - map: idNodeMap, - blockClass: string | RegExp, - blockSelector: string | null, - skipChild = false, - inlineStylesheet = true, - maskInputOptions?: MaskInputOptions, - slimDOMOptions: SlimDOMOptions = {}, - recordCanvas?: boolean, - preserveWhiteSpace = true, + options: { + doc: Document; + map: idNodeMap; + blockClass: string | RegExp; + blockSelector: string | null; + skipChild: boolean; + inlineStylesheet: boolean; + maskInputOptions?: MaskInputOptions; + slimDOMOptions: SlimDOMOptions; + recordCanvas?: boolean; + preserveWhiteSpace?: boolean; + }, ): serializedNodeWithId | null { - const _serializedNode = serializeNode( - n, + const { + doc, + map, + blockClass, + blockSelector, + skipChild = false, + inlineStylesheet = true, + maskInputOptions = {}, + slimDOMOptions, + recordCanvas = false, + } = options; + let { preserveWhiteSpace = true } = options; + const _serializedNode = serializeNode(n, { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions, - recordCanvas || false, - ); + recordCanvas, + }); if (!_serializedNode) { // TODO: dev only console.warn(n, 'not serialized'); @@ -504,8 +527,7 @@ export function serializeNodeWithId( preserveWhiteSpace = false; } for (const childN of Array.from(n.childNodes)) { - const serializedChildNode = serializeNodeWithId( - childN, + const serializedChildNode = serializeNodeWithId(childN, { doc, map, blockClass, @@ -516,7 +538,7 @@ export function serializeNodeWithId( slimDOMOptions, recordCanvas, preserveWhiteSpace, - ); + }); if (serializedChildNode) { serializedNode.childNodes.push(serializedChildNode); } @@ -527,16 +549,26 @@ export function serializeNodeWithId( function snapshot( n: Document, - blockClass: string | RegExp = 'rr-block', - inlineStylesheet = true, - maskAllInputsOrOptions: boolean | MaskInputOptions, - slimDOMSensibleOrOptions: boolean | SlimDOMOptions, - recordCanvas?: boolean, - blockSelector: string | null = null, + options?: { + blockClass?: string | RegExp; + inlineStylesheet?: boolean; + maskAllInputs?: boolean | MaskInputOptions; + slimDOM?: boolean | SlimDOMOptions; + recordCanvas?: boolean; + blockSelector?: string | null; + }, ): [serializedNodeWithId | null, idNodeMap] { + const { + blockClass = 'rr-block', + inlineStylesheet = true, + recordCanvas = false, + blockSelector = null, + maskAllInputs = false, + slimDOM = false, + } = options || {}; const idNodeMap: idNodeMap = {}; const maskInputOptions: MaskInputOptions = - maskAllInputsOrOptions === true + maskAllInputs === true ? { color: true, date: true, @@ -554,40 +586,39 @@ function snapshot( textarea: true, select: true, } - : maskAllInputsOrOptions === false + : maskAllInputs === false ? {} - : maskAllInputsOrOptions; + : maskAllInputs; const slimDOMOptions: SlimDOMOptions = - slimDOMSensibleOrOptions === true || slimDOMSensibleOrOptions === 'all' + slimDOM === true || slimDOM === 'all' ? // if true: set of sensible options that should not throw away any information { script: true, comment: true, headFavicon: true, headWhitespace: true, - headMetaDescKeywords: slimDOMSensibleOrOptions === 'all', // destructive + headMetaDescKeywords: slimDOM === 'all', // destructive headMetaSocial: true, headMetaRobots: true, headMetaHttpEquiv: true, headMetaAuthorship: true, headMetaVerification: true, } - : slimDOMSensibleOrOptions === false + : slimDOM === false ? {} - : slimDOMSensibleOrOptions; + : slimDOM; return [ - serializeNodeWithId( - n, - n, - idNodeMap, + serializeNodeWithId(n, { + doc: n, + map: idNodeMap, blockClass, blockSelector, - false, + skipChild: false, inlineStylesheet, maskInputOptions, slimDOMOptions, recordCanvas, - ), + }), idNodeMap, ]; } diff --git a/test/integration.ts b/test/integration.ts index c0fdf2af..ddfb5bc8 100644 --- a/test/integration.ts +++ b/test/integration.ts @@ -11,7 +11,7 @@ import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; import { Suite } from 'mocha'; const htmlFolder = path.join(__dirname, 'html'); -const htmls = fs.readdirSync(htmlFolder).map(filePath => { +const htmls = fs.readdirSync(htmlFolder).map((filePath) => { const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'); return { filePath, @@ -24,7 +24,7 @@ interface IMimeType { } const server = () => - new Promise(resolve => { + new Promise((resolve) => { const mimeType: IMimeType = { '.html': 'text/html', '.js': 'text/javascript', @@ -73,7 +73,7 @@ interface ISuite extends Suite { code: string; } -describe('integration tests', function(this: ISuite) { +describe('integration tests', function (this: ISuite) { before(async () => { this.server = await server(); this.browser = await puppeteer.launch({ @@ -102,16 +102,18 @@ describe('integration tests', function(this: ISuite) { const page: puppeteer.Page = await this.browser.newPage(); // console for debug // tslint:disable-next-line: no-console - page.on('console', msg => console.log(msg.text())); + page.on('console', (msg) => console.log(msg.text())); await page.goto(`http://localhost:3030/html`); await page.setContent(html.src, { waitUntil: 'load', }); - const rebuildHtml = (await page.evaluate(`${this.code} + const rebuildHtml = ( + await page.evaluate(`${this.code} const x = new XMLSerializer(); const [snap] = rrweb.snapshot(document); - x.serializeToString(rrweb.rebuild(snap, document)[0]); - `)).replace(/\n\n/g, ''); + x.serializeToString(rrweb.rebuild(snap, { doc: document })[0]); + `) + ).replace(/\n\n/g, ''); const result = matchSnapshot(rebuildHtml, __filename, title); assert(result.pass, result.pass ? '' : result.report()); }).timeout(5000); diff --git a/typings/rebuild.d.ts b/typings/rebuild.d.ts index f61a762a..35eb6020 100644 --- a/typings/rebuild.d.ts +++ b/typings/rebuild.d.ts @@ -1,5 +1,14 @@ import { serializedNodeWithId, idNodeMap, INode } from './types'; export declare function addHoverClass(cssText: string): string; -export declare function buildNodeWithSN(n: serializedNodeWithId, doc: Document, map: idNodeMap, skipChild?: boolean, HACK_CSS?: boolean): INode | null; -declare function rebuild(n: serializedNodeWithId, doc: Document, onVisit?: (node: INode) => unknown, HACK_CSS?: boolean): [Node | null, idNodeMap]; +export declare function buildNodeWithSN(n: serializedNodeWithId, options: { + doc: Document; + map: idNodeMap; + skipChild?: boolean; + hackCss: boolean; +}): INode | null; +declare function rebuild(n: serializedNodeWithId, options: { + doc: Document; + onVisit?: (node: INode) => unknown; + hackCss?: boolean; +}): [Node | null, idNodeMap]; export default rebuild; diff --git a/typings/snapshot.d.ts b/typings/snapshot.d.ts index d99ef4d6..8ae27a30 100644 --- a/typings/snapshot.d.ts +++ b/typings/snapshot.d.ts @@ -4,8 +4,26 @@ export declare function absoluteToStylesheet(cssText: string | null, href: strin export declare function absoluteToDoc(doc: Document, attributeValue: string): string; export declare function transformAttribute(doc: Document, name: string, value: string): string; export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean; -export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, blockSelector: string | null, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null; -declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, blockSelector?: string | null): [serializedNodeWithId | null, idNodeMap]; +export declare function serializeNodeWithId(n: Node | INode, options: { + doc: Document; + map: idNodeMap; + blockClass: string | RegExp; + blockSelector: string | null; + skipChild: boolean; + inlineStylesheet: boolean; + maskInputOptions?: MaskInputOptions; + slimDOMOptions: SlimDOMOptions; + recordCanvas?: boolean; + preserveWhiteSpace?: boolean; +}): serializedNodeWithId | null; +declare function snapshot(n: Document, options?: { + blockClass?: string | RegExp; + inlineStylesheet?: boolean; + maskAllInputs?: boolean | MaskInputOptions; + slimDOM?: boolean | SlimDOMOptions; + recordCanvas?: boolean; + blockSelector?: string | null; +}): [serializedNodeWithId | null, idNodeMap]; export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void; export declare function cleanupSnapshot(): void; export default snapshot;