Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvements to <link rel="stylesheet"> + mutations #1483

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f6fa2e
Unrelated, but the presence of a blank `<style></style>` element when…
eoghanmurray Apr 5, 2024
aeaacc8
Add motivating test which can be used against this PR as well as in #…
eoghanmurray Apr 5, 2024
56b6d7e
Recognize that snapshot.ts::serializeTextNode does important work for…
eoghanmurray Apr 12, 2024
b2e432e
Prep PR for async <style> serialization via assets: refactor stringif…
eoghanmurray Apr 12, 2024
05b42b5
Add a test to show how mutations on multiple text nodes within the <s…
eoghanmurray Apr 12, 2024
501d2e9
Multiple <style> text children: Demonstrate a failing test as CSS was…
eoghanmurray Apr 12, 2024
47bc8a1
Add test and remedy for the most trivial type of failure of `findCssT…
eoghanmurray Apr 12, 2024
1ed670c
Missed this which is 'happy path' - add a test to catch catch case
eoghanmurray Apr 12, 2024
16edea2
Create single-style-capture.md
eoghanmurray Apr 12, 2024
1abc7bf
An expected test change as per this PR; must have missed it first tim…
eoghanmurray Apr 15, 2024
247a37a
More expected test changes I missed when authoring PR
eoghanmurray Apr 15, 2024
2f1c59b
New function buildStyleNode to better encapsulate the separate behavi…
eoghanmurray Apr 16, 2024
6f60237
Revert an accidental reordering
eoghanmurray Apr 16, 2024
86cabdb
Add the css text length to the splits array as a method of checking t…
eoghanmurray May 3, 2024
ff5e3ae
Refactor out `applyCssSplits` for clarity and so we can test it
eoghanmurray May 7, 2024
fc7e60d
Add more tests and fix bug that was causing css text to end up in the…
eoghanmurray May 3, 2024
888aebb
The _cssTextSplits should only apply to <style> elements
eoghanmurray May 7, 2024
f75954a
Realize we don't actually need to look at the `.sheet` during a mutat…
eoghanmurray May 7, 2024
716db22
Fix the following typing problem in the tests:
eoghanmurray May 7, 2024
b853378
Highlight that there is a replayer involved in these tests
eoghanmurray May 2, 2024
5314122
Demonstrate that #1374 caused stylesheet contents to be captured twic…
eoghanmurray May 15, 2024
dba6733
Avoid the double call to onStylesheetLoad
eoghanmurray May 15, 2024
8d9de30
Remove `isStylesheetLoaded` function not used since #995
eoghanmurray May 15, 2024
2b32966
Move responsibility for stylesheet onload event from rrweb-snapshot t…
eoghanmurray May 15, 2024
cb6d2df
Add test as I realize that serializeNode was doing more work in terms…
eoghanmurray May 15, 2024
def3730
WIP: An oddity in that it has a .sheet in the mutation , before the l…
eoghanmurray May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/single-style-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb-snapshot": patch
"rrweb": patch
---

Edge case: Provide support for mutations on a <style> element which (unusually) has multiple text nodes
4 changes: 4 additions & 0 deletions packages/rrweb-snapshot/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import snapshot, {
absoluteToStylesheet,
getHref,
serializeNodeWithId,
transformAttribute,
ignoreAttribute,
Expand All @@ -18,6 +20,8 @@ export * from './types';
export * from './utils';

export {
absoluteToStylesheet,
getHref,
snapshot,
serializeNodeWithId,
rebuild,
Expand Down
109 changes: 98 additions & 11 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Rule, Media, NodeWithRules, parse } from './css';
import {
serializedNodeWithId,
serializedElementNodeWithId,
NodeType,
tagMap,
elementNode,
Expand Down Expand Up @@ -145,6 +146,89 @@ export function createCache(): BuildCache {
};
}

/**
* undo findCssTextSplits
* (would move to utils.ts but uses `adaptCssForReplay`)
*/
export function applyCssSplits(
n: serializedElementNodeWithId,
cssText: string,
cssTextSplits: number[],
hackCss: boolean,
cache: BuildCache,
): void {
const lenCheckOk =
cssTextSplits.length &&
cssTextSplits[cssTextSplits.length - 1] === cssText.length;
for (let j = n.childNodes.length - 1; j >= 0; j--) {
const scn = n.childNodes[j];
let ix = 0;
if (cssTextSplits.length > j && j > 0) {
ix = cssTextSplits[j - 1];
}
if (scn.type === NodeType.Text) {
let remainder = '';
if (ix !== 0 && lenCheckOk) {
remainder = cssText.substring(0, ix);
cssText = cssText.substring(ix);
} else if (j > 0) {
continue;
}
if (hackCss) {
cssText = adaptCssForReplay(cssText, cache);
}
// id will be assigned when these child nodes are
// iterated over in buildNodeWithSN
scn.textContent = cssText;
cssText = remainder;
}
}
if (cssText.length) {
// something has gone wrong
console.warn('Leftover css content after applyCssSplits:', cssText);
}
}

/**
* Normally a <style> element has a single textNode containing the rules.
* During serialization, we bypass this (`styleEl.sheet`) to get the rules the
* browser sees and serialize this to a special _cssText attribute, blanking
* out any text nodes. This function reverses that and also handles cases where
* there were no textNode children present (dynamic css/or a <link> element) as
* well as multiple textNodes (`cssTextSplits`), which need to be repopulated
* in case they are modified by subsequent mutations.
*/
export function buildStyleNode(
n: serializedElementNodeWithId,
styleEl: HTMLStyleElement, // when inlined, a <link type="stylesheet"> also gets rebuilt as a <style>
cssText: string,
options: {
doc: Document;
hackCss: boolean;
cache: BuildCache;
},
) {
const { doc, hackCss, cache } = options;
if (n.childNodes.length) {
let cssTextSplits: number[] = [];
if (n.attributes._cssTextSplits) {
cssTextSplits = n.attributes._cssTextSplits
.split(' ')
.map((s) => parseInt(s));
}
applyCssSplits(n, cssText, cssTextSplits, hackCss, cache);
} else {
if (hackCss) {
cssText = adaptCssForReplay(cssText, cache);
}
/**
<link> element or dynamic <style> are serialized without any child nodes
we create the text node without an ID or presence in mirror as it can't
*/
styleEl.appendChild(doc.createTextNode(cssText));
}
}

function buildNode(
n: serializedNodeWithId,
options: {
Expand Down Expand Up @@ -219,14 +303,17 @@ function buildNode(
if (name.startsWith('rr_')) {
specialAttributes[name] = value;
continue;
} else if (name === '_cssTextSplits') {
continue;
}

const isTextarea = tagName === 'textarea' && name === 'value';
const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText';
if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') {
value = adaptCssForReplay(value, cache);
}
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
if (typeof value !== 'string') {
// pass
} else if (tagName === 'style' && name === '_cssText') {
buildStyleNode(n, node as HTMLStyleElement, value, options);
continue; // no need to set _cssText as attribute
} else if (tagName === 'textarea' && name === 'value') {
// create without an ID or presence in mirror
node.appendChild(doc.createTextNode(value));
// https://github.com/rrweb-io/rrweb/issues/112
n.childNodes = []; // value overrides childNodes
Expand Down Expand Up @@ -378,11 +465,11 @@ function buildNode(
return node;
}
case NodeType.Text:
return doc.createTextNode(
n.isStyle && hackCss
? adaptCssForReplay(n.textContent, cache)
: n.textContent,
);
if (n.isStyle && hackCss) {
// support legacy style
return doc.createTextNode(adaptCssForReplay(n.textContent, cache));
}
return doc.createTextNode(n.textContent);
case NodeType.CDATA:
return doc.createCDATASection(n.textContent);
case NodeType.Comment:
Expand Down
Loading
Loading