From 42b19519afa908bda4762c6bf15a1e0df20fb474 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 29 Nov 2024 20:11:55 +0000 Subject: [PATCH 1/5] Attempt 1 --- packages/rrweb-snapshot/src/rebuild.ts | 58 ++++++++++++++++++++++-- packages/rrweb-snapshot/test/css.test.ts | 45 ++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index e4a4c9df4f..7d92ba8199 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -104,15 +104,65 @@ export function applyCssSplits( // unexpected: remerge the last two so that we don't discard any css cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join('')); } + let adaptionFailures = []; for (let i = 0; i < childTextNodes.length; i++) { const childTextNode = childTextNodes[i]; - const cssTextSection = cssTextSplits[i]; + let cssTextSection = cssTextSplits[i]; if (childTextNode && cssTextSection) { // id will be assigned when these child nodes are // iterated over in buildNodeWithSN - childTextNode.textContent = hackCss - ? adaptCssForReplay(cssTextSection, cache) - : cssTextSection; + if (hackCss) { + try { + cssTextSection = adaptCssForReplay(cssTextSection, cache); + } catch (e) { + // css section might not have been valid on it's own + adaptionFailures.push(i); + } + } + childTextNode.textContent = cssTextSection; + } + } + if (adaptionFailures.length) { + // this time, can throw an exception + const fullAdaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache); + let ix_start = 0; + for (let i = 0; i < childTextNodes.length; i++) { + const childTextNode = childTextNodes[i]; + if (adaptionFailures.includes(i)) { + if (i === childTextNodes.length - 1) { + console.log('he' + i, ix_start, fullAdaptedCss.substring(ix_start)); + childTextNode.textContent = fullAdaptedCss.substring(ix_start); + } else { + let ix_end = -1; + let end_search = childTextNodes[i + 1].textContent.length; + while (ix_end === -1) { + let search_bit = childTextNodes[i + 1].textContent.substring( + 0, + end_search, + ); + ix_end = + ix_start + fullAdaptedCss.substring(ix_start).indexOf(search_bit); + end_search -= 1; + if (end_search <= 2) { + break; + } + } + console.log( + 're' + i, + ix_start, + ix_end, + fullAdaptedCss.substring(ix_start, ix_end), + ); + if (ix_end !== -1) { + childTextNode.textContent = fullAdaptedCss.substring( + ix_start, + ix_end, + ); + } else { + } + } + } + ix_start += childTextNode.textContent.length; } } } diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index 75e261c102..0909e2161c 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -231,6 +231,51 @@ describe('applyCssSplits css rejoiner', function () { expect((sn3.childNodes[2] as textNode).textContent).toEqual(''); }); + it('applies css splits correctly when split parts are invalid by themselves', () => { + const badFirstHalf = 'a:hov'; + const badSecondHalf = 'er { color: red; }'; + const markedCssText = [badFirstHalf, badSecondHalf].join('/* rr_split */'); + applyCssSplits(sn, markedCssText, true, mockLastUnusedArg); + expect((sn.childNodes[1] as textNode).textContent).toEqual( + 'er,\na.\\:hover { color: red; }', + ); + }); + + it('applies css splits correctly when split parts are invalid by themselves x3', () => { + let sn3 = { + type: NodeType.Element, + tagName: 'style', + childNodes: [ + { + type: NodeType.Text, + textContent: '', + }, + { + type: NodeType.Text, + textContent: '', + }, + { + type: NodeType.Text, + textContent: '', + }, + ], + } as serializedElementNodeWithId; + const badStartThird = '.a:hover { background-color'; + const badMidThird = ': red; } input:hover {'; + const badEndThird = 'border: 1px solid purple; }'; + const markedCssText = [badStartThird, badMidThird, badEndThird].join( + '/* rr_split */', + ); + applyCssSplits(sn3, markedCssText, true, mockLastUnusedArg); + expect((sn3.childNodes[0] as textNode).textContent).toEqual( + badStartThird.replace('.a:hover', '.a:hover,\n.a.\\:hover'), + ); + expect((sn3.childNodes[1] as textNode).textContent).toEqual( + badMidThird.replace('input:hover', 'input:hover,\ninput.\\:hover'), + ); + expect((sn3.childNodes[2] as textNode).textContent).toEqual(badEndThird); + }); + it('maintains entire css text when there are too few child nodes', () => { let sn1 = { type: NodeType.Element, From 87560305ec271fee51fd1afcf9238a7e31767a54 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 29 Nov 2024 20:20:10 +0000 Subject: [PATCH 2/5] Apply adaption even if split is within something that needs to be adapted --- .changeset/fix-adapt-css.md | 6 ++ packages/rrweb-snapshot/src/rebuild.ts | 82 ++++++++---------------- packages/rrweb-snapshot/test/css.test.ts | 7 +- 3 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 .changeset/fix-adapt-css.md diff --git a/.changeset/fix-adapt-css.md b/.changeset/fix-adapt-css.md new file mode 100644 index 0000000000..3564086a8c --- /dev/null +++ b/.changeset/fix-adapt-css.md @@ -0,0 +1,6 @@ +--- +'rrweb': patch +'rrweb-snapshot': patch +--- + +#1575 Fix that postcss could fall over when trying to process css content split arbitrarily diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 7d92ba8199..74cae08d55 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -104,65 +104,39 @@ export function applyCssSplits( // unexpected: remerge the last two so that we don't discard any css cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join('')); } - let adaptionFailures = []; + let adaptedCss = ''; + if (hackCss) { + adaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache); + } + let ix_start = 0; for (let i = 0; i < childTextNodes.length; i++) { const childTextNode = childTextNodes[i]; - let cssTextSection = cssTextSplits[i]; - if (childTextNode && cssTextSection) { - // id will be assigned when these child nodes are - // iterated over in buildNodeWithSN - if (hackCss) { - try { - cssTextSection = adaptCssForReplay(cssTextSection, cache); - } catch (e) { - // css section might not have been valid on it's own - adaptionFailures.push(i); - } + if (!hackCss) { + if (i === cssTextSplits.length) { + break; } - childTextNode.textContent = cssTextSection; - } - } - if (adaptionFailures.length) { - // this time, can throw an exception - const fullAdaptedCss = adaptCssForReplay(cssTextSplits.join(''), cache); - let ix_start = 0; - for (let i = 0; i < childTextNodes.length; i++) { - const childTextNode = childTextNodes[i]; - if (adaptionFailures.includes(i)) { - if (i === childTextNodes.length - 1) { - console.log('he' + i, ix_start, fullAdaptedCss.substring(ix_start)); - childTextNode.textContent = fullAdaptedCss.substring(ix_start); - } else { - let ix_end = -1; - let end_search = childTextNodes[i + 1].textContent.length; - while (ix_end === -1) { - let search_bit = childTextNodes[i + 1].textContent.substring( - 0, - end_search, - ); - ix_end = - ix_start + fullAdaptedCss.substring(ix_start).indexOf(search_bit); - end_search -= 1; - if (end_search <= 2) { - break; - } - } - console.log( - 're' + i, - ix_start, - ix_end, - fullAdaptedCss.substring(ix_start, ix_end), - ); - if (ix_end !== -1) { - childTextNode.textContent = fullAdaptedCss.substring( - ix_start, - ix_end, - ); - } else { - } + childTextNode.textContent = cssTextSplits[i]; + } else if (i < childTextNodes.length - 1) { + let ix_end = -1; + let end_search = cssTextSplits[i + 1].length; + while (ix_end === -1) { + let search_bit = cssTextSplits[i + 1].substring(0, end_search); + ix_end = ix_start + adaptedCss.substring(ix_start).indexOf(search_bit); + if (ix_end === -1) { + end_search -= 1; + continue; + } else if (ix_end <= 2) { + break; } } - ix_start += childTextNode.textContent.length; + if (ix_end === -1) { + // something went wrong, put a similar sized chunk in the right place + ix_end = ix_start + cssTextSplits[i].length; + } + childTextNode.textContent = adaptedCss.substring(ix_start, ix_end); + ix_start = ix_end; + } else { + childTextNode.textContent = adaptedCss.substring(ix_start); } } } diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index 0909e2161c..d4bf522949 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -236,9 +236,10 @@ describe('applyCssSplits css rejoiner', function () { const badSecondHalf = 'er { color: red; }'; const markedCssText = [badFirstHalf, badSecondHalf].join('/* rr_split */'); applyCssSplits(sn, markedCssText, true, mockLastUnusedArg); - expect((sn.childNodes[1] as textNode).textContent).toEqual( - 'er,\na.\\:hover { color: red; }', - ); + expect( + (sn.childNodes[0] as textNode).textContent + + (sn.childNodes[1] as textNode).textContent, + ).toEqual('a:hover,\na.\\:hover { color: red; }'); }); it('applies css splits correctly when split parts are invalid by themselves x3', () => { From 2d1be32189ec1d9619cd99311eb8742652264587 Mon Sep 17 00:00:00 2001 From: eoghanmurray Date: Fri, 29 Nov 2024 20:41:15 +0000 Subject: [PATCH 3/5] Apply formatting changes --- .changeset/fix-adapt-css.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fix-adapt-css.md b/.changeset/fix-adapt-css.md index 3564086a8c..428f5833d2 100644 --- a/.changeset/fix-adapt-css.md +++ b/.changeset/fix-adapt-css.md @@ -1,6 +1,6 @@ --- -'rrweb': patch -'rrweb-snapshot': patch +"rrweb": patch +"rrweb-snapshot": patch --- #1575 Fix that postcss could fall over when trying to process css content split arbitrarily From 21f2a971a12418c2a1cd38e8ccef1337c8fe7713 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Mon, 2 Dec 2024 11:46:04 +0000 Subject: [PATCH 4/5] Fix algorithm; checks against `ix_end` within loop were incorrect when `ix_start` was bigger than zero. Also impose an upper bound of 30 iterations on these substring searches --- packages/rrweb-snapshot/src/rebuild.ts | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 74cae08d55..d06d116e27 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -110,28 +110,33 @@ export function applyCssSplits( } let ix_start = 0; for (let i = 0; i < childTextNodes.length; i++) { + if (i === cssTextSplits.length) { + break; + } const childTextNode = childTextNodes[i]; if (!hackCss) { - if (i === cssTextSplits.length) { - break; - } childTextNode.textContent = cssTextSplits[i]; } else if (i < childTextNodes.length - 1) { - let ix_end = -1; + let ix_end = ix_start; let end_search = cssTextSplits[i + 1].length; - while (ix_end === -1) { + + // don't do hundreds of searches, in case a mismatch + // is caused close to start of string + end_search = Math.min(end_search, 30); + + let found = false; + for (; end_search > 2; end_search--) { let search_bit = cssTextSplits[i + 1].substring(0, end_search); - ix_end = ix_start + adaptedCss.substring(ix_start).indexOf(search_bit); - if (ix_end === -1) { - end_search -= 1; - continue; - } else if (ix_end <= 2) { + let search_ix = adaptedCss.substring(ix_start).indexOf(search_bit); + found = search_ix !== -1; + if (found) { + ix_end += search_ix; break; } } - if (ix_end === -1) { + if (!found) { // something went wrong, put a similar sized chunk in the right place - ix_end = ix_start + cssTextSplits[i].length; + ix_end += cssTextSplits[i].length; } childTextNode.textContent = adaptedCss.substring(ix_start, ix_end); ix_start = ix_end; From 4f056fac901a7d528606231fc8d9fea28f905524 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Mon, 2 Dec 2024 11:54:42 +0000 Subject: [PATCH 5/5] These commands are handy within a package --- packages/rrweb-snapshot/package.json | 2 ++ packages/rrweb/package.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 70d5a104a0..f213f6a5e2 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -3,6 +3,8 @@ "version": "2.0.0-alpha.17", "description": "rrweb's component to take a snapshot of DOM, aka DOM serializer", "scripts": { + "format": "yarn prettier --write '**/*.{ts,md}'", + "format:head": "git diff --name-only HEAD^ |grep '/rrweb-snapshot/.*\\.ts$\\|\\.md$' |sed 's|packages/rrweb-snapshot/||'|xargs yarn prettier --write", "prepare": "npm run prepack", "prepack": "npm run build", "retest": "vitest run", diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 2623a517ff..3ff0f3416c 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -3,6 +3,8 @@ "version": "2.0.0-alpha.17", "description": "record and replay the web", "scripts": { + "format": "yarn prettier --write '**/*.{ts,md}'", + "format:head": "git diff --name-only HEAD^ |grep '/rrweb/.*\\.ts$\\|\\.md$' |sed 's|packages/rrweb/||'|xargs yarn prettier --write", "prepare": "npm run prepack", "prepack": "npm run build", "retest": "cross-env PUPPETEER_HEADLESS=true yarn retest:headful",