From d97c82c41d9a1193c038da459743cb951d2960e7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] fix: Ensure CSS support is checked more robustly (#1106) * fix: Ensure CSS support is checked more robustly * Apply formatting changes * apply eslint changes * Update packages/rrweb/src/record/observer.ts Co-authored-by: Justin Halsall * fix: do not use window in module scope --------- Co-authored-by: mydea Co-authored-by: Justin Halsall --- packages/rrweb/src/record/observer.ts | 42 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 324342e1..e7284104 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -54,11 +54,6 @@ type WindowWithAngularZone = IWindow & { export const mutationBuffers: MutationBuffer[] = []; export const processedNodeManager = new ProcessedNodeManager(); -const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined'; -const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined'; -const isCSSSupportsRuleSupported = typeof CSSSupportsRule !== 'undefined'; -const isCSSConditionRuleSupported = typeof CSSConditionRule !== 'undefined'; - // Event.path is non-standard and used in some older browsers type NonStandardEvent = Omit & { path: EventTarget[]; @@ -488,13 +483,13 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] { const positions: number[] = []; function recurse(childRule: CSSRule, pos: number[]) { if ( - (isCSSGroupingRuleSupported && + (hasNestedCSSRule('CSSGroupingRule') && childRule.parentRule instanceof CSSGroupingRule) || - (isCSSMediaRuleSupported && + (hasNestedCSSRule('CSSMediaRule') && childRule.parentRule instanceof CSSMediaRule) || - (isCSSSupportsRuleSupported && + (hasNestedCSSRule('CSSSupportsRule') && childRule.parentRule instanceof CSSSupportsRule) || - (isCSSConditionRuleSupported && + (hasNestedCSSRule('CSSConditionRule') && childRule.parentRule instanceof CSSConditionRule) ) { const rules = Array.from( @@ -643,20 +638,20 @@ function initStyleSheetObserver( const supportedNestedCSSRuleTypes: { [key: string]: GroupingCSSRuleTypes; } = {}; - if (isCSSGroupingRuleSupported) { + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; } else { // Some browsers (Safari) don't support CSSGroupingRule // https://caniuse.com/?search=cssgroupingrule // fall back to monkey patching classes that would have inherited from CSSGroupingRule - if (isCSSMediaRuleSupported) { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; } - if (isCSSConditionRuleSupported) { + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; } - if (isCSSSupportsRuleSupported) { + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; } } @@ -1168,3 +1163,24 @@ export function initObservers( pluginHandlers.forEach((h) => h()); }; } + +type CSSGroupingProp = + | 'CSSGroupingRule' + | 'CSSMediaRule' + | 'CSSSupportsRule' + | 'CSSConditionRule'; + +function hasNestedCSSRule(prop: CSSGroupingProp): boolean { + return typeof window[prop] !== 'undefined'; +} + +function canMonkeyPatchNestedCSSRule(prop: CSSGroupingProp): boolean { + return Boolean( + typeof window[prop] !== 'undefined' && + // Note: Generally, this check _shouldn't_ be necessary + // However, in some scenarios (e.g. jsdom) this can sometimes fail, so we check for it here + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype, + ); +}