From 9bae4bb9e3262b22a7bb68ba8a44e8d76cb1076f Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 20 Mar 2024 18:44:38 +0000 Subject: [PATCH 01/11] We weren't recursing into media queries (or @supports etc.) to rewrite hover pseudoclasses --- packages/rrweb-snapshot/src/rebuild.ts | 8 ++++++-- packages/rrweb-snapshot/test/rebuild.test.ts | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 10bf067a76..992b80519c 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -77,7 +77,7 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } const selectors: string[] = []; - ast.stylesheet.rules.forEach((rule) => { + function getSelectors(rule: any) { if ('selectors' in rule) { (rule.selectors || []).forEach((selector: string) => { if (HOVER_SELECTOR.test(selector)) { @@ -85,7 +85,11 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } }); } - }); + if ('rules' in rule) { + (rule.rules || []).forEach(getSelectors); + } + } + getSelectors(ast.stylesheet); if (selectors.length === 0) { return cssText; diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 357cd2fb3c..b2a2dce76c 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -91,6 +91,13 @@ describe('rebuild', function () { ); }); + it('can add hover class within media query', () => { + const cssText = '@media screen { .m:hover { color: white } }'; + expect(addHoverClass(cssText, cache)).toEqual( + '@media screen { .m:hover, .m.\\:hover { color: white } }', + ); + }); + it('can add hover class when there is multi selector', () => { const cssText = '.a, .b:hover, .c { color: white }'; expect(addHoverClass(cssText, cache)).toEqual( From 788ddb5b21a09b4bb47f7cde1e1fa351b6522972 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 21 Mar 2024 10:50:01 +0000 Subject: [PATCH 02/11] The early return meant that the stylesWithHoverClass cache wasn't being populated if there were no hover selectors on the stylesheet - not committing the test, but modifying the existing 'add a hover class to a previously processed css string' as follows shows the problem: --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -151,6 +185,7 @@ describe('rebuild', function () { path.resolve(__dirname, './css/benchmark.css'), 'utf8', ); + cssText = cssText.replace(/:hover/g, ''); const start = process.hrtime(); addHoverClass(cssText, cache); --- packages/rrweb-snapshot/src/rebuild.ts | 37 +++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 992b80519c..2fe50f39a0 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -91,25 +91,26 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } getSelectors(ast.stylesheet); - if (selectors.length === 0) { - return cssText; + let result = cssText; + if (selectors.length > 0) { + const selectorMatcher = new RegExp( + selectors + .filter((selector, index) => selectors.indexOf(selector) === index) + .sort((a, b) => b.length - a.length) + .map((selector) => { + return escapeRegExp(selector); + }) + .join('|'), + 'g', + ); + result = result.replace(selectorMatcher, (selector) => { + const newSelector = selector.replace( + HOVER_SELECTOR_GLOBAL, + '$1.\\:hover', + ); + return `${selector}, ${newSelector}`; + }); } - - const selectorMatcher = new RegExp( - selectors - .filter((selector, index) => selectors.indexOf(selector) === index) - .sort((a, b) => b.length - a.length) - .map((selector) => { - return escapeRegExp(selector); - }) - .join('|'), - 'g', - ); - - const result = cssText.replace(selectorMatcher, (selector) => { - const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover'); - return `${selector}, ${newSelector}`; - }); cache?.stylesWithHoverClass.set(cssText, result); return result; } From f1b2d76382124471482908a1deea1b329bc5fa6e Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 21 Mar 2024 11:03:23 +0000 Subject: [PATCH 03/11] Replace `min-device-width` and similar with `min-width` as the former looks out at the browser viewport whereas we need it to look at the replayer iframe viewport --- packages/rrweb-snapshot/src/rebuild.ts | 23 ++++++++++++++++++++ packages/rrweb-snapshot/test/rebuild.test.ts | 8 +++++++ 2 files changed, 31 insertions(+) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 2fe50f39a0..634b6ff5f1 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -62,6 +62,8 @@ function escapeRegExp(str: string) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } +const MEDIA_SELECTOR = /(max|min)-device-(width|height)/; +const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g'); const HOVER_SELECTOR = /([^\\]):hover/; const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g'); export function addHoverClass(cssText: string, cache: BuildCache): string { @@ -77,6 +79,7 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } const selectors: string[] = []; + const medias: string[] = []; function getSelectors(rule: any) { if ('selectors' in rule) { (rule.selectors || []).forEach((selector: string) => { @@ -85,6 +88,9 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } }); } + if ('media' in rule && MEDIA_SELECTOR.test(rule.media)) { + medias.push(rule.media); + } if ('rules' in rule) { (rule.rules || []).forEach(getSelectors); } @@ -111,6 +117,23 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { return `${selector}, ${newSelector}`; }); } + if (medias.length > 0) { + const mediaMatcher = new RegExp( + medias + .filter((media, index) => medias.indexOf(media) === index) + .sort((a, b) => b.length - a.length) + .map((media) => { + return escapeRegExp(media); + }) + .join('|'), + 'g', + ); + result = result.replace(mediaMatcher, (media) => { + // not attempting to maintain min-device-width along with min-width + // (it's non standard) + return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); + }); + } cache?.stylesWithHoverClass.set(cssText, result); return result; } diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index b2a2dce76c..29b0619466 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -131,6 +131,14 @@ describe('rebuild', function () { expect(addHoverClass(cssText, cache)).toEqual(cssText); }); + it('can adapt media rules to replay context', () => { + const cssText = + '@media only screen and (min-device-width : 1200px) { .a { width: 10px; }}'; + expect(addHoverClass(cssText, cache)).toEqual( + '@media only screen and (min-width : 1200px) { .a { width: 10px; }}', + ); + }); + // this benchmark is unreliable when run in parallel with other tests it.skip('benchmark', () => { const cssText = fs.readFileSync( From 2fb1f37331f268d03e7592b13f6fae09c21d1996 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 21 Mar 2024 11:06:52 +0000 Subject: [PATCH 04/11] Add some tests to show how the hover replacement works against selector lists. I believe these were failing in a previous version of rrweb as I had some local patches that no longer seem to be needed to handle these cases --- packages/rrweb-snapshot/test/rebuild.test.ts | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 29b0619466..22e69cd08a 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -91,6 +91,34 @@ describe('rebuild', function () { ); }); + it('can correctly add hover when in middle of selector', () => { + const cssText = 'ul li a:hover img { color: white }'; + expect(addHoverClass(cssText, cache)).toEqual( + 'ul li a:hover img, ul li a.\\:hover img { color: white }', + ); + }); + + it('can correctly add hover on multiline selector', () => { + const cssText = `ul li.specified a:hover img, +ul li.multiline +b:hover +img, +ul li.specified c:hover img { + color: white +}`; + expect(addHoverClass(cssText, cache)).toEqual( + `ul li.specified a:hover img, ul li.specified a.\\:hover img, +ul li.multiline +b:hover +img, ul li.multiline +b.\\:hover +img, +ul li.specified c:hover img, ul li.specified c.\\:hover img { + color: white +}`, + ); + }); + it('can add hover class within media query', () => { const cssText = '@media screen { .m:hover { color: white } }'; expect(addHoverClass(cssText, cache)).toEqual( From 343ea197bc183c8137ec328e0676dc1bfec05c0e Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 21 Mar 2024 11:18:32 +0000 Subject: [PATCH 05/11] Update name of function to reflect that 'addHoverClass' does more than just :hover. I believe this function is only exported for the purposes of use in the tests --- packages/rrweb-snapshot/src/index.ts | 4 +-- packages/rrweb-snapshot/src/rebuild.ts | 6 ++-- packages/rrweb-snapshot/test/rebuild.test.ts | 30 ++++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/rrweb-snapshot/src/index.ts b/packages/rrweb-snapshot/src/index.ts index ef9d1b19fa..c9f91a9100 100644 --- a/packages/rrweb-snapshot/src/index.ts +++ b/packages/rrweb-snapshot/src/index.ts @@ -11,7 +11,7 @@ import snapshot, { } from './snapshot'; import rebuild, { buildNodeWithSN, - addHoverClass, + adaptCssForReplay, createCache, } from './rebuild'; export * from './types'; @@ -22,7 +22,7 @@ export { serializeNodeWithId, rebuild, buildNodeWithSN, - addHoverClass, + adaptCssForReplay, createCache, transformAttribute, ignoreAttribute, diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 634b6ff5f1..83ab99cea2 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -66,7 +66,7 @@ const MEDIA_SELECTOR = /(max|min)-device-(width|height)/; const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g'); const HOVER_SELECTOR = /([^\\]):hover/; const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g'); -export function addHoverClass(cssText: string, cache: BuildCache): string { +export function adaptCssForReplay(cssText: string, cache: BuildCache): string { const cachedStyle = cache?.stylesWithHoverClass.get(cssText); if (cachedStyle) return cachedStyle; @@ -224,7 +224,7 @@ function buildNode( const isTextarea = tagName === 'textarea' && name === 'value'; const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText'; if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') { - value = addHoverClass(value, cache); + value = adaptCssForReplay(value, cache); } if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') { node.appendChild(doc.createTextNode(value)); @@ -369,7 +369,7 @@ function buildNode( case NodeType.Text: return doc.createTextNode( n.isStyle && hackCss - ? addHoverClass(n.textContent, cache) + ? adaptCssForReplay(n.textContent, cache) : n.textContent, ); case NodeType.CDATA: diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 22e69cd08a..766217da70 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -3,7 +3,7 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { addHoverClass, buildNodeWithSN, createCache } from '../src/rebuild'; +import { adaptCssForReplay, buildNodeWithSN, createCache } from '../src/rebuild'; import { NodeType } from '../src/types'; import { createMirror, Mirror } from '../src/utils'; @@ -81,19 +81,19 @@ describe('rebuild', function () { describe('add hover class to hover selector related rules', function () { it('will do nothing to css text without :hover', () => { const cssText = 'body { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual(cssText); + expect(adaptCssForReplay(cssText, cache)).toEqual(cssText); }); it('can add hover class to css text', () => { const cssText = '.a:hover { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( '.a:hover, .a.\\:hover { color: white }', ); }); it('can correctly add hover when in middle of selector', () => { const cssText = 'ul li a:hover img { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( 'ul li a:hover img, ul li a.\\:hover img { color: white }', ); }); @@ -106,7 +106,7 @@ img, ul li.specified c:hover img { color: white }`; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( `ul li.specified a:hover img, ul li.specified a.\\:hover img, ul li.multiline b:hover @@ -121,48 +121,48 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { it('can add hover class within media query', () => { const cssText = '@media screen { .m:hover { color: white } }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( '@media screen { .m:hover, .m.\\:hover { color: white } }', ); }); it('can add hover class when there is multi selector', () => { const cssText = '.a, .b:hover, .c { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( '.a, .b:hover, .b.\\:hover, .c { color: white }', ); }); it('can add hover class when there is a multi selector with the same prefix', () => { const cssText = '.a:hover, .a:hover::after { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( '.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }', ); }); it('can add hover class when :hover is not the end of selector', () => { const cssText = 'div:hover::after { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( 'div:hover::after, div.\\:hover::after { color: white }', ); }); it('can add hover class when the selector has multi :hover', () => { const cssText = 'a:hover b:hover { color: white }'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( 'a:hover b:hover, a.\\:hover b.\\:hover { color: white }', ); }); it('will ignore :hover in css value', () => { const cssText = '.a::after { content: ":hover" }'; - expect(addHoverClass(cssText, cache)).toEqual(cssText); + expect(adaptCssForReplay(cssText, cache)).toEqual(cssText); }); it('can adapt media rules to replay context', () => { const cssText = '@media only screen and (min-device-width : 1200px) { .a { width: 10px; }}'; - expect(addHoverClass(cssText, cache)).toEqual( + expect(adaptCssForReplay(cssText, cache)).toEqual( '@media only screen and (min-width : 1200px) { .a { width: 10px; }}', ); }); @@ -174,7 +174,7 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { 'utf8', ); const start = process.hrtime(); - addHoverClass(cssText, cache); + adaptCssForReplay(cssText, cache); const end = process.hrtime(start); const duration = getDuration(end); expect(duration).toBeLessThan(100); @@ -189,11 +189,11 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { ); const start = process.hrtime(); - addHoverClass(cssText, cache); + adaptCssForReplay(cssText, cache); const end = process.hrtime(start); const cachedStart = process.hrtime(); - addHoverClass(cssText, cache); + adaptCssForReplay(cssText, cache); const cachedEnd = process.hrtime(cachedStart); expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end)); From c48ab8e80c29534a61c939d2da111491bbf57850 Mon Sep 17 00:00:00 2001 From: eoghanmurray Date: Thu, 21 Mar 2024 11:25:15 +0000 Subject: [PATCH 06/11] Apply formatting changes --- packages/rrweb-snapshot/test/rebuild.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 766217da70..097ff0989a 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -3,7 +3,11 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import { adaptCssForReplay, buildNodeWithSN, createCache } from '../src/rebuild'; +import { + adaptCssForReplay, + buildNodeWithSN, + createCache, +} from '../src/rebuild'; import { NodeType } from '../src/types'; import { createMirror, Mirror } from '../src/utils'; From 0d9103c799c06ff57dea15e200d3e08845c49b6f Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 21 Mar 2024 11:27:29 +0000 Subject: [PATCH 07/11] Create rotten-spies-enjoy.md --- .changeset/rotten-spies-enjoy.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/rotten-spies-enjoy.md diff --git a/.changeset/rotten-spies-enjoy.md b/.changeset/rotten-spies-enjoy.md new file mode 100644 index 0000000000..724e37fa3d --- /dev/null +++ b/.changeset/rotten-spies-enjoy.md @@ -0,0 +1,7 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Ensure :hover works on replayer, even if a rule is behind a media query +Respect the intent behind max-device-width and min-device-width media queries so that their effects are apparent in the replayer context From 8f6c1eff6ac8821c0e08504ae4a3becd7525e152 Mon Sep 17 00:00:00 2001 From: eoghanmurray Date: Thu, 21 Mar 2024 11:28:33 +0000 Subject: [PATCH 08/11] Apply formatting changes --- .changeset/rotten-spies-enjoy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/rotten-spies-enjoy.md b/.changeset/rotten-spies-enjoy.md index 724e37fa3d..5f5954fcac 100644 --- a/.changeset/rotten-spies-enjoy.md +++ b/.changeset/rotten-spies-enjoy.md @@ -1,6 +1,6 @@ --- -"rrweb-snapshot": patch -"rrweb": patch +'rrweb-snapshot': patch +'rrweb': patch --- Ensure :hover works on replayer, even if a rule is behind a media query From 53b85225438ad7afc3d612a883077a797c5d8936 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 25 Mar 2024 14:25:52 +0100 Subject: [PATCH 09/11] Add correct typing on `getSelectors` --- packages/rrweb-snapshot/src/css.ts | 4 ++-- packages/rrweb-snapshot/src/rebuild.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index d7a413eb67..ad56ae5e5e 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -224,7 +224,7 @@ export interface Stylesheet extends Node { // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; -export function parse(css: string, options: ParserOptions = {}) { +export function parse(css: string, options: ParserOptions = {}): Stylesheet { /** * Positional. */ @@ -882,7 +882,7 @@ function trim(str: string) { * Adds non-enumerable parent node reference to each node. */ -function addParent(obj: Stylesheet, parent?: Stylesheet) { +function addParent(obj: Stylesheet, parent?: Stylesheet): Stylesheet { const isNode = obj && typeof obj.type === 'string'; const childParent = isNode ? obj : parent; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 83ab99cea2..7e3fef546f 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,4 +1,4 @@ -import { parse } from './css'; +import { StyleRules, Rule, Media, parse } from './css'; import { serializedNodeWithId, NodeType, @@ -80,7 +80,7 @@ export function adaptCssForReplay(cssText: string, cache: BuildCache): string { const selectors: string[] = []; const medias: string[] = []; - function getSelectors(rule: any) { + function getSelectors(rule: StyleRules | Rule | Media) { if ('selectors' in rule) { (rule.selectors || []).forEach((selector: string) => { if (HOVER_SELECTOR.test(selector)) { @@ -88,7 +88,7 @@ export function adaptCssForReplay(cssText: string, cache: BuildCache): string { } }); } - if ('media' in rule && MEDIA_SELECTOR.test(rule.media)) { + if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { medias.push(rule.media); } if ('rules' in rule) { From 6cb07d3fc88747fa8e754180d21b931000c8a35a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 25 Mar 2024 15:04:54 +0100 Subject: [PATCH 10/11] Refactor CSS interfaces to include optional rules --- packages/rrweb-snapshot/src/css.ts | 22 +++++++++------------- packages/rrweb-snapshot/src/rebuild.ts | 14 ++++++++------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index ad56ae5e5e..29e96593b4 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -56,6 +56,11 @@ export interface Node { }; } +export interface NodeWithOptionalRules extends Node { + /** Array of nodes with the types rule, comment and any of the at-rule types. */ + rules?: Array; +} + export interface Rule extends Node { /** The list of selectors of the rule, split on commas. Each selector is trimmed from whitespace and comments. */ selectors?: string[]; @@ -98,13 +103,11 @@ export interface CustomMedia extends Node { /** * The @document at-rule. */ -export interface Document extends Node { +export interface Document extends NodeWithOptionalRules { /** The part following @document. */ document?: string; /** The vendor prefix in @document, or undefined if there is none. */ vendor?: string; - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules?: Array; } /** @@ -118,10 +121,7 @@ export interface FontFace extends Node { /** * The @host at-rule. */ -export interface Host extends Node { - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules?: Array; -} +export type Host = NodeWithOptionalRules; /** * The @import at-rule. @@ -153,11 +153,9 @@ export interface KeyFrame extends Node { /** * The @media at-rule. */ -export interface Media extends Node { +export interface Media extends NodeWithOptionalRules { /** The part following @media. */ media?: string; - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules?: Array; } /** @@ -181,11 +179,9 @@ export interface Page extends Node { /** * The @supports at-rule. */ -export interface Supports extends Node { +export interface Supports extends NodeWithOptionalRules { /** The part following @supports. */ supports?: string; - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules?: Array; } /** All at-rules. */ diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 7e3fef546f..98a1ad8f06 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,4 +1,4 @@ -import { StyleRules, Rule, Media, parse } from './css'; +import { StyleRules, Rule, Media, NodeWithOptionalRules, parse } from './css'; import { serializedNodeWithId, NodeType, @@ -80,9 +80,11 @@ export function adaptCssForReplay(cssText: string, cache: BuildCache): string { const selectors: string[] = []; const medias: string[] = []; - function getSelectors(rule: StyleRules | Rule | Media) { - if ('selectors' in rule) { - (rule.selectors || []).forEach((selector: string) => { + function getSelectors( + rule: StyleRules | Rule | Media | NodeWithOptionalRules, + ) { + if ('selectors' in rule && rule.selectors) { + rule.selectors.forEach((selector: string) => { if (HOVER_SELECTOR.test(selector)) { selectors.push(selector); } @@ -91,8 +93,8 @@ export function adaptCssForReplay(cssText: string, cache: BuildCache): string { if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { medias.push(rule.media); } - if ('rules' in rule) { - (rule.rules || []).forEach(getSelectors); + if ('rules' in rule && rule.rules) { + rule.rules.forEach(getSelectors); } } getSelectors(ast.stylesheet); From 681342f3abd750732544370bc8724ceb691e9b89 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 25 Mar 2024 15:08:23 +0100 Subject: [PATCH 11/11] Change `rules` to be non optional --- packages/rrweb-snapshot/src/css.ts | 16 +++++++--------- packages/rrweb-snapshot/src/rebuild.ts | 6 ++---- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 29e96593b4..522e257daf 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -56,9 +56,9 @@ export interface Node { }; } -export interface NodeWithOptionalRules extends Node { +export interface NodeWithRules extends Node { /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules?: Array; + rules: Array; } export interface Rule extends Node { @@ -103,7 +103,7 @@ export interface CustomMedia extends Node { /** * The @document at-rule. */ -export interface Document extends NodeWithOptionalRules { +export interface Document extends NodeWithRules { /** The part following @document. */ document?: string; /** The vendor prefix in @document, or undefined if there is none. */ @@ -121,7 +121,7 @@ export interface FontFace extends Node { /** * The @host at-rule. */ -export type Host = NodeWithOptionalRules; +export type Host = NodeWithRules; /** * The @import at-rule. @@ -153,7 +153,7 @@ export interface KeyFrame extends Node { /** * The @media at-rule. */ -export interface Media extends NodeWithOptionalRules { +export interface Media extends NodeWithRules { /** The part following @media. */ media?: string; } @@ -179,7 +179,7 @@ export interface Page extends Node { /** * The @supports at-rule. */ -export interface Supports extends NodeWithOptionalRules { +export interface Supports extends NodeWithRules { /** The part following @supports. */ supports?: string; } @@ -201,10 +201,8 @@ export type AtRule = /** * A collection of rules */ -export interface StyleRules { +export interface StyleRules extends NodeWithRules { source?: string; - /** Array of nodes with the types rule, comment and any of the at-rule types. */ - rules: Array; /** Array of Errors. Errors collected during parsing when option silent is true. */ parsingErrors?: ParserError[]; } diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 98a1ad8f06..dc9b6c3b9f 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -1,4 +1,4 @@ -import { StyleRules, Rule, Media, NodeWithOptionalRules, parse } from './css'; +import { Rule, Media, NodeWithRules, parse } from './css'; import { serializedNodeWithId, NodeType, @@ -80,9 +80,7 @@ export function adaptCssForReplay(cssText: string, cache: BuildCache): string { const selectors: string[] = []; const medias: string[] = []; - function getSelectors( - rule: StyleRules | Rule | Media | NodeWithOptionalRules, - ) { + function getSelectors(rule: Rule | Media | NodeWithRules) { if ('selectors' in rule && rule.selectors) { rule.selectors.forEach((selector: string) => { if (HOVER_SELECTOR.test(selector)) {