Skip to content

Commit 9503417

Browse files
authored
revert: "Single style capture (#1437)" (#56)
This reverts commit 5fbb904. it's added only to support the potential new asset pipeline rrweb are building and it's a pain, let's remove it. will consider that asset pipeline rrweb 3
1 parent 12ca16e commit 9503417

16 files changed

+468
-1679
lines changed

CHANGELOG.md

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
## 0.0.10 - 2025-02-28
22

3-
4-
53
## 0.0.9 - 2025-02-27
64

75
## 0.0.8 - 2025-02-04

packages/rrweb-snapshot/src/rebuild.ts

+13-84
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { mediaSelectorPlugin, pseudoClassPlugin } from './css';
22
import safeParser from 'postcss-safe-parser';
33
import {
44
type serializedNodeWithId,
5-
type serializedElementNodeWithId,
65
NodeType,
76
type elementNode,
87
type legacyAttributes,
@@ -90,77 +89,6 @@ export function createCache(): BuildCache {
9089
};
9190
}
9291

93-
/**
94-
* undo splitCssText/markCssSplits
95-
* (would move to utils.ts but uses `adaptCssForReplay`)
96-
*/
97-
export function applyCssSplits(
98-
n: serializedElementNodeWithId,
99-
cssText: string,
100-
hackCss: boolean,
101-
cache: BuildCache,
102-
): void {
103-
const childTextNodes = [];
104-
for (const scn of n.childNodes) {
105-
if (scn.type === NodeType.Text) {
106-
childTextNodes.push(scn);
107-
}
108-
}
109-
const cssTextSplits = cssText.split('/* rr_split */');
110-
while (
111-
cssTextSplits.length > 1 &&
112-
cssTextSplits.length > childTextNodes.length
113-
) {
114-
// unexpected: remerge the last two so that we don't discard any css
115-
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
116-
}
117-
for (let i = 0; i < childTextNodes.length; i++) {
118-
const childTextNode = childTextNodes[i];
119-
const cssTextSection = cssTextSplits[i];
120-
if (childTextNode && cssTextSection) {
121-
// id will be assigned when these child nodes are
122-
// iterated over in buildNodeWithSN
123-
childTextNode.textContent = hackCss
124-
? adaptCssForReplay(cssTextSection, cache)
125-
: cssTextSection;
126-
}
127-
}
128-
}
129-
130-
/**
131-
* Normally a <style> element has a single textNode containing the rules.
132-
* During serialization, we bypass this (`styleEl.sheet`) to get the rules the
133-
* browser sees and serialize this to a special _cssText attribute, blanking
134-
* out any text nodes. This function reverses that and also handles cases where
135-
* there were no textNode children present (dynamic css/or a <link> element) as
136-
* well as multiple textNodes, which need to be repopulated (based on presence of
137-
* a special `rr_split` marker in case they are modified by subsequent mutations.
138-
*/
139-
export function buildStyleNode(
140-
n: serializedElementNodeWithId,
141-
styleEl: HTMLStyleElement, // when inlined, a <link type="stylesheet"> also gets rebuilt as a <style>
142-
cssText: string,
143-
options: {
144-
doc: Document;
145-
hackCss: boolean;
146-
cache: BuildCache;
147-
},
148-
) {
149-
const { doc, hackCss, cache } = options;
150-
if (n.childNodes.length) {
151-
applyCssSplits(n, cssText, hackCss, cache);
152-
} else {
153-
if (hackCss) {
154-
cssText = adaptCssForReplay(cssText, cache);
155-
}
156-
/**
157-
<link> element or dynamic <style> are serialized without any child nodes
158-
we create the text node without an ID or presence in mirror as it can't
159-
*/
160-
styleEl.appendChild(doc.createTextNode(cssText));
161-
}
162-
}
163-
16492
function buildNode(
16593
n: serializedNodeWithId,
16694
options: {
@@ -237,13 +165,14 @@ function buildNode(
237165
continue;
238166
}
239167

240-
if (typeof value !== 'string') {
241-
// pass
242-
} else if (tagName === 'style' && name === '_cssText') {
243-
buildStyleNode(n, node as HTMLStyleElement, value, options);
244-
continue; // no need to set _cssText as attribute
245-
} else if (tagName === 'textarea' && name === 'value') {
246-
// create without an ID or presence in mirror
168+
const isTextarea = tagName === 'textarea' && name === 'value';
169+
const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText';
170+
if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') {
171+
value = adaptCssForReplay(value, cache);
172+
}
173+
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
174+
// https://github.com/rrweb-io/rrweb/issues/112
175+
// https://github.com/rrweb-io/rrweb/pull/1351
247176
node.appendChild(doc.createTextNode(value));
248177
n.childNodes = []; // value overrides childNodes
249178
continue;
@@ -398,11 +327,11 @@ function buildNode(
398327
return node;
399328
}
400329
case NodeType.Text:
401-
if (n.isStyle && hackCss) {
402-
// support legacy style
403-
return doc.createTextNode(adaptCssForReplay(n.textContent, cache));
404-
}
405-
return doc.createTextNode(n.textContent);
330+
return doc.createTextNode(
331+
n.isStyle && hackCss
332+
? adaptCssForReplay(n.textContent, cache)
333+
: n.textContent,
334+
);
406335
case NodeType.CDATA:
407336
return doc.createCDATASection(n.textContent);
408337
case NodeType.Comment:

packages/rrweb-snapshot/src/snapshot.ts

+37-38
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
toLowerCase,
3030
extractFileExtension,
3131
absolutifyURLs,
32-
markCssSplits,
3332
} from './utils';
3433
import dom from '@posthog/rrweb-utils';
3534

@@ -407,7 +406,6 @@ function serializeNode(
407406
* `newlyAddedElement: true` skips scrollTop and scrollLeft check
408407
*/
409408
newlyAddedElement?: boolean;
410-
cssCaptured?: boolean;
411409
},
412410
): serializedNode | false {
413411
const {
@@ -425,7 +423,6 @@ function serializeNode(
425423
recordCanvas,
426424
keepIframeSrcFn,
427425
newlyAddedElement = false,
428-
cssCaptured = false,
429426
} = options;
430427
// Only record root id when document object is not the base document
431428
const rootId = getRootId(doc, mirror);
@@ -472,7 +469,6 @@ function serializeNode(
472469
needsMask,
473470
maskTextFn,
474471
rootId,
475-
cssCaptured,
476472
});
477473
case n.CDATA_SECTION_NODE:
478474
return {
@@ -504,38 +500,48 @@ function serializeTextNode(
504500
needsMask: boolean;
505501
maskTextFn: MaskTextFn | undefined;
506502
rootId: number | undefined;
507-
cssCaptured?: boolean;
508503
},
509504
): serializedNode {
510-
const { needsMask, maskTextFn, rootId, cssCaptured } = options;
505+
const { needsMask, maskTextFn, rootId } = options;
511506
// The parent node may not be a html element which has a tagName attribute.
512507
// So just let it be undefined which is ok in this use case.
513508
const parent = dom.parentNode(n);
514509
const parentTagName = parent && (parent as HTMLElement).tagName;
515-
let textContent: string | null = '';
510+
let text = dom.textContent(n);
516511
const isStyle = parentTagName === 'STYLE' ? true : undefined;
517512
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
518-
if (isScript) {
519-
textContent = 'SCRIPT_PLACEHOLDER';
520-
} else if (!cssCaptured) {
521-
textContent = dom.textContent(n);
522-
if (isStyle && textContent) {
523-
// mutation only: we don't need to use stringifyStylesheet
524-
// as a <style> text node mutation obliterates any previous
525-
// programmatic rule manipulation (.insertRule etc.)
526-
// so the current textContent represents the most up to date state
527-
textContent = absolutifyURLs(textContent, getHref(options.doc));
513+
if (isStyle && text) {
514+
try {
515+
// try to read style sheet
516+
if (n.nextSibling || n.previousSibling) {
517+
// This is not the only child of the stylesheet.
518+
// We can't read all of the sheet's .cssRules and expect them
519+
// to _only_ include the current rule(s) added by the text node.
520+
// So we'll be conservative and keep textContent as-is.
521+
} else if ((parent as HTMLStyleElement).sheet?.cssRules) {
522+
text = stringifyStylesheet((parent as HTMLStyleElement).sheet!);
523+
}
524+
} catch (err) {
525+
console.warn(
526+
`Cannot get CSS styles from text's parentNode. Error: ${err as string}`,
527+
n,
528+
);
528529
}
530+
text = absolutifyURLs(text, getHref(options.doc));
531+
}
532+
if (isScript) {
533+
text = 'SCRIPT_PLACEHOLDER';
529534
}
530-
if (!isStyle && !isScript && textContent && needsMask) {
531-
textContent = maskTextFn
532-
? maskTextFn(textContent, dom.parentElement(n))
533-
: textContent.replace(/[\S]/g, '*');
535+
if (!isStyle && !isScript && text && needsMask) {
536+
text = maskTextFn
537+
? maskTextFn(text, dom.parentElement(n))
538+
: text.replace(/[\S]/g, '*');
534539
}
535540

536541
return {
537542
type: NodeType.Text,
538-
textContent: textContent || '',
543+
textContent: text || '',
544+
isStyle,
539545
rootId,
540546
};
541547
}
@@ -628,14 +634,17 @@ function serializeElementNode(
628634
}
629635
}
630636
}
631-
if (tagName === 'style' && (n as HTMLStyleElement).sheet) {
632-
let cssText = stringifyStylesheet(
637+
// dynamic stylesheet
638+
if (
639+
tagName === 'style' &&
640+
(n as HTMLStyleElement).sheet &&
641+
// TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
642+
!(n.innerText || dom.textContent(n) || '').trim().length
643+
) {
644+
const cssText = stringifyStylesheet(
633645
(n as HTMLStyleElement).sheet as CSSStyleSheet,
634646
);
635647
if (cssText) {
636-
if (n.childNodes.length > 1) {
637-
cssText = markCssSplits(cssText, n as HTMLStyleElement);
638-
}
639648
attributes._cssText = cssText;
640649
}
641650
}
@@ -961,7 +970,6 @@ export function serializeNodeWithId(
961970
node: serializedElementNodeWithId,
962971
) => unknown;
963972
stylesheetLoadTimeout?: number;
964-
cssCaptured?: boolean;
965973
},
966974
): serializedNodeWithId | null {
967975
const {
@@ -987,7 +995,6 @@ export function serializeNodeWithId(
987995
stylesheetLoadTimeout = 5000,
988996
keepIframeSrcFn = () => false,
989997
newlyAddedElement = false,
990-
cssCaptured = false,
991998
} = options;
992999
let { needsMask } = options;
9931000
let { preserveWhiteSpace = true } = options;
@@ -1018,7 +1025,6 @@ export function serializeNodeWithId(
10181025
recordCanvas,
10191026
keepIframeSrcFn,
10201027
newlyAddedElement,
1021-
cssCaptured,
10221028
});
10231029
if (!_serializedNode) {
10241030
// TODO: dev only
@@ -1034,6 +1040,7 @@ export function serializeNodeWithId(
10341040
slimDOMExcluded(_serializedNode, slimDOMOptions) ||
10351041
(!preserveWhiteSpace &&
10361042
_serializedNode.type === NodeType.Text &&
1043+
!_serializedNode.isStyle &&
10371044
!_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)
10381045
) {
10391046
id = IGNORED_NODE;
@@ -1098,7 +1105,6 @@ export function serializeNodeWithId(
10981105
onStylesheetLoad,
10991106
stylesheetLoadTimeout,
11001107
keepIframeSrcFn,
1101-
cssCaptured: false,
11021108
};
11031109

11041110
if (
@@ -1108,13 +1114,6 @@ export function serializeNodeWithId(
11081114
) {
11091115
// value parameter in DOM reflects the correct value, so ignore childNode
11101116
} else {
1111-
if (
1112-
serializedNode.type === NodeType.Element &&
1113-
(serializedNode as elementNode).attributes._cssText !== undefined &&
1114-
typeof serializedNode.attributes._cssText === 'string'
1115-
) {
1116-
bypassOptions.cssCaptured = true;
1117-
}
11181117
for (const childN of Array.from(dom.childNodes(n))) {
11191118
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
11201119
if (serializedChildNode) {

packages/rrweb-snapshot/src/types.ts

+78
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,83 @@
1+
export enum NodeType {
2+
Document,
3+
DocumentType,
4+
Element,
5+
Text,
6+
CDATA,
7+
Comment,
8+
}
9+
10+
export type documentNode = {
11+
type: NodeType.Document;
12+
childNodes: serializedNodeWithId[];
13+
compatMode?: string;
14+
};
15+
16+
export type documentTypeNode = {
17+
type: NodeType.DocumentType;
18+
name: string;
19+
publicId: string;
20+
systemId: string;
21+
};
22+
23+
export type attributes = {
24+
[key: string]: string | number | true | null;
25+
};
26+
export type legacyAttributes = {
27+
/**
28+
* @deprecated old bug in rrweb was causing these to always be set
29+
* @see https://github.com/rrweb-io/rrweb/pull/651
30+
*/
31+
selected: false;
32+
};
33+
34+
export type elementNode = {
35+
type: NodeType.Element;
36+
tagName: string;
37+
attributes: attributes;
38+
childNodes: serializedNodeWithId[];
39+
isSVG?: true;
40+
needBlock?: boolean;
41+
// This is a custom element or not.
42+
isCustom?: true;
43+
};
44+
45+
export type textNode = {
46+
type: NodeType.Text;
47+
textContent: string;
48+
isStyle?: true;
49+
};
50+
51+
export type cdataNode = {
52+
type: NodeType.CDATA;
53+
textContent: '';
54+
};
55+
56+
export type commentNode = {
57+
type: NodeType.Comment;
58+
textContent: string;
59+
};
60+
61+
export type serializedNode = (
62+
| documentNode
63+
| documentTypeNode
64+
| elementNode
65+
| textNode
66+
| cdataNode
67+
| commentNode
68+
) & {
69+
rootId?: number;
70+
isShadowHost?: boolean;
71+
isShadow?: boolean;
72+
};
73+
174
import type { serializedNodeWithId } from '@posthog/rrweb-types';
275

76+
export type serializedElementNodeWithId = Extract<
77+
serializedNodeWithId,
78+
Record<'type', NodeType.Element>
79+
>;
80+
381
export type tagMap = {
482
[key: string]: string;
583
};

0 commit comments

Comments
 (0)