From d685bc6051c6c6071fa0a1de57340ce28699dfe5 Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Thu, 11 Jul 2024 18:21:35 -0400 Subject: [PATCH 01/15] Replace relative URLs with absolute URLs when stringifying stylesheets --- packages/rrweb-snapshot/src/snapshot.ts | 67 +---------------- packages/rrweb-snapshot/src/utils.ts | 74 +++++++++++++++++-- packages/rrweb-snapshot/test/snapshot.test.ts | 3 +- 3 files changed, 73 insertions(+), 71 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 88b6c8b0ae..44f2a2963b 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -25,6 +25,7 @@ import { getInputType, toLowerCase, extractFileExtension, + absoluteToStylesheet, } from './utils'; let _id = 1; @@ -53,71 +54,9 @@ function getValidTagName(element: HTMLElement): Lowercase { return processedTagName; } -function extractOrigin(url: string): string { - let origin = ''; - if (url.indexOf('//') > -1) { - origin = url.split('/').slice(0, 3).join('/'); - } else { - origin = url.split('/')[0]; - } - origin = origin.split('?')[0]; - return origin; -} - let canvasService: HTMLCanvasElement | null; let canvasCtx: CanvasRenderingContext2D | null; -const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; -const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; -const URL_WWW_MATCH = /^www\..*/i; -const DATA_URI = /^(data:)([^,]*),(.*)/i; -export function absoluteToStylesheet( - cssText: string | null, - href: string, -): string { - return (cssText || '').replace( - URL_IN_CSS_REF, - ( - origin: string, - quote1: string, - path1: string, - quote2: string, - path2: string, - path3: string, - ) => { - const filePath = path1 || path2 || path3; - const maybeQuote = quote1 || quote2 || ''; - if (!filePath) { - return origin; - } - if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { - return `url(${maybeQuote}${filePath}${maybeQuote})`; - } - if (DATA_URI.test(filePath)) { - return `url(${maybeQuote}${filePath}${maybeQuote})`; - } - if (filePath[0] === '/') { - return `url(${maybeQuote}${ - extractOrigin(href) + filePath - }${maybeQuote})`; - } - const stack = href.split('/'); - const parts = filePath.split('/'); - stack.pop(); - for (const part of parts) { - if (part === '.') { - continue; - } else if (part === '..') { - stack.pop(); - } else { - stack.push(part); - } - } - return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; - }, - ); -} - // eslint-disable-next-line no-control-regex const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; // Don't use \s, to avoid matching non-breaking space // eslint-disable-next-line no-control-regex @@ -664,7 +603,7 @@ function serializeElementNode( if (cssText) { delete attributes.rel; delete attributes.href; - attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!); + attributes._cssText = cssText; } } // dynamic stylesheet @@ -678,7 +617,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); + attributes._cssText = cssText; } } // form fields diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 13da20f0f5..6b7700e2a4 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -96,11 +96,13 @@ export function escapeImportStatement(rule: CSSImportRule): string { export function stringifyStylesheet(s: CSSStyleSheet): string | null { try { const rules = s.rules || s.cssRules; - return rules - ? fixBrowserCompatibilityIssuesInCSS( - Array.from(rules, stringifyRule).join(''), - ) - : null; + const stringifiedRules = Array.from(rules, stringifyRule) + .map((rule) => { + return s.href ? absoluteToStylesheet(rule, s.href) : rule; + }) + .join(''); + + return rules ? fixBrowserCompatibilityIssuesInCSS(stringifiedRules) : null; } catch (error) { return null; } @@ -351,3 +353,65 @@ export function extractFileExtension( const match = url.pathname.match(regex); return match?.[1] ?? null; } + +function extractOrigin(url: string): string { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} + +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; +const URL_WWW_MATCH = /^www\..*/i; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +export function absoluteToStylesheet( + cssText: string | null, + href: string, +): string { + return (cssText || '').replace( + URL_IN_CSS_REF, + ( + origin: string, + quote1: string, + path1: string, + quote2: string, + path2: string, + path3: string, + ) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${ + extractOrigin(href) + filePath + }${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } else if (part === '..') { + stack.pop(); + } else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }, + ); +} diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index de1d79eb6d..11f79f2d9e 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -4,13 +4,12 @@ import { JSDOM } from 'jsdom'; import { describe, it, expect } from 'vitest'; import { - absoluteToStylesheet, serializeNodeWithId, _isBlockedElement, } from '../src/snapshot'; import snapshot from '../src/snapshot'; import { serializedNodeWithId, elementNode } from '../src/types'; -import { Mirror } from '../src/utils'; +import { Mirror, absoluteToStylesheet } from '../src/utils'; describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; From c6c0ee38a5395cda8d5a4f91929a9bd69d17e4c9 Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Mon, 15 Jul 2024 22:17:06 -0400 Subject: [PATCH 02/15] Add test to show desired behavior for imported stylesheets from seperate directory --- .../test/__snapshots__/integration.test.ts.snap | 2 +- packages/rrweb-snapshot/test/alt-css/alt-style.css | 12 ++++++++++++ .../rrweb-snapshot/test/css/style-with-import.css | 1 + packages/rrweb-snapshot/test/snapshot.test.ts | 5 +---- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 packages/rrweb-snapshot/test/alt-css/alt-style.css diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index ee9d95245d..7f3a4ac431 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -500,7 +500,7 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`] with style sheet with import - + " `; diff --git a/packages/rrweb-snapshot/test/alt-css/alt-style.css b/packages/rrweb-snapshot/test/alt-css/alt-style.css new file mode 100644 index 0000000000..0f935c87f2 --- /dev/null +++ b/packages/rrweb-snapshot/test/alt-css/alt-style.css @@ -0,0 +1,12 @@ +body { + margin: 0; + background: url('../c.jpg'); + border-image: url('data:image/svg+xml;utf8,'); + } + p { + color: red; + background: url('./d.jpg'); + } + body > p { + color: yellow; + } diff --git a/packages/rrweb-snapshot/test/css/style-with-import.css b/packages/rrweb-snapshot/test/css/style-with-import.css index 5fa59d8039..a24d901947 100644 --- a/packages/rrweb-snapshot/test/css/style-with-import.css +++ b/packages/rrweb-snapshot/test/css/style-with-import.css @@ -1,2 +1,3 @@ @import '//fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Roboto:wght@100;300;400;500;700&display=swap"'; @import './style.css'; +@import '../alt-css/alt-style.css'; diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index 11f79f2d9e..bfab523940 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -3,10 +3,7 @@ */ import { JSDOM } from 'jsdom'; import { describe, it, expect } from 'vitest'; -import { - serializeNodeWithId, - _isBlockedElement, -} from '../src/snapshot'; +import { serializeNodeWithId, _isBlockedElement } from '../src/snapshot'; import snapshot from '../src/snapshot'; import { serializedNodeWithId, elementNode } from '../src/types'; import { Mirror, absoluteToStylesheet } from '../src/utils'; From f69e8f364ef34147c0a955e29fad763e0cb6974d Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Mon, 15 Jul 2024 22:40:02 -0400 Subject: [PATCH 03/15] Add changeset --- .changeset/six-llamas-brush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-llamas-brush.md diff --git a/.changeset/six-llamas-brush.md b/.changeset/six-llamas-brush.md new file mode 100644 index 0000000000..9aa1d08b7f --- /dev/null +++ b/.changeset/six-llamas-brush.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": patch +--- + +Fix `url()` rewrite for nested stylesheets by rewriting during stringification instead of after From 27432e2ec311396d185d265176e805aa286eb511 Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Tue, 16 Jul 2024 12:43:20 -0400 Subject: [PATCH 04/15] Rename `absoluteToStylesheet` to `absolutifyURLs` and call it once after stringifying imported stylesheet --- packages/rrweb-snapshot/src/snapshot.ts | 6 ++-- packages/rrweb-snapshot/src/utils.ts | 20 ++++++----- packages/rrweb-snapshot/test/snapshot.test.ts | 34 +++++++++---------- tsconfig.base.json | 2 +- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 44f2a2963b..7ceea14096 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -25,7 +25,7 @@ import { getInputType, toLowerCase, extractFileExtension, - absoluteToStylesheet, + absolutifyURLs, } from './utils'; let _id = 1; @@ -193,7 +193,7 @@ export function transformAttribute( } else if (name === 'srcset') { return getAbsoluteSrcsetString(doc, value); } else if (name === 'style') { - return absoluteToStylesheet(value, getHref(doc)); + return absolutifyURLs(value, getHref(doc)); } else if (tagName === 'object' && name === 'data') { return absoluteToDoc(doc, value); } @@ -523,7 +523,7 @@ function serializeTextNode( n, ); } - textContent = absoluteToStylesheet(textContent, getHref(options.doc)); + textContent = absolutifyURLs(textContent, getHref(options.doc)); } if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 6b7700e2a4..7c62bdaa87 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -96,10 +96,8 @@ export function escapeImportStatement(rule: CSSImportRule): string { export function stringifyStylesheet(s: CSSStyleSheet): string | null { try { const rules = s.rules || s.cssRules; - const stringifiedRules = Array.from(rules, stringifyRule) - .map((rule) => { - return s.href ? absoluteToStylesheet(rule, s.href) : rule; - }) + const stringifiedRules = [...rules] + .map((rule: CSSRule) => stringifyRule(rule, s.href)) .join(''); return rules ? fixBrowserCompatibilityIssuesInCSS(stringifiedRules) : null; @@ -108,7 +106,10 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null { } } -export function stringifyRule(rule: CSSRule): string { +export function stringifyRule( + rule: CSSRule, + sheetHref?: string | null, +): string { let importStringified; if (isCSSImportRule(rule)) { try { @@ -118,6 +119,10 @@ export function stringifyRule(rule: CSSRule): string { stringifyStylesheet(rule.styleSheet) || // work around browser issues with the raw string `@import url(...)` statement escapeImportStatement(rule); + + if (sheetHref) { + importStringified = absolutifyURLs(importStringified, sheetHref); + } } catch (error) { // ignore } @@ -369,10 +374,7 @@ const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; const URL_WWW_MATCH = /^www\..*/i; const DATA_URI = /^(data:)([^,]*),(.*)/i; -export function absoluteToStylesheet( - cssText: string | null, - href: string, -): string { +export function absolutifyURLs(cssText: string | null, href: string): string { return (cssText || '').replace( URL_IN_CSS_REF, ( diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts index bfab523940..7bf6141e44 100644 --- a/packages/rrweb-snapshot/test/snapshot.test.ts +++ b/packages/rrweb-snapshot/test/snapshot.test.ts @@ -6,56 +6,56 @@ import { describe, it, expect } from 'vitest'; import { serializeNodeWithId, _isBlockedElement } from '../src/snapshot'; import snapshot from '../src/snapshot'; import { serializedNodeWithId, elementNode } from '../src/types'; -import { Mirror, absoluteToStylesheet } from '../src/utils'; +import { Mirror, absolutifyURLs } from '../src/utils'; describe('absolute url to stylesheet', () => { const href = 'http://localhost/css/style.css'; it('can handle relative path', () => { - expect(absoluteToStylesheet('url(a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle same level path', () => { - expect(absoluteToStylesheet('url("./a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("./a.jpg")', href)).toEqual( `url("http://localhost/css/a.jpg")`, ); }); it('can handle parent level path', () => { - expect(absoluteToStylesheet('url("../a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("../a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle absolute path', () => { - expect(absoluteToStylesheet('url("/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle external path', () => { - expect(absoluteToStylesheet('url("http://localhost/a.jpg")', href)).toEqual( + expect(absolutifyURLs('url("http://localhost/a.jpg")', href)).toEqual( `url("http://localhost/a.jpg")`, ); }); it('can handle single quote path', () => { - expect(absoluteToStylesheet(`url('./a.jpg')`, href)).toEqual( + expect(absolutifyURLs(`url('./a.jpg')`, href)).toEqual( `url('http://localhost/css/a.jpg')`, ); }); it('can handle no quote path', () => { - expect(absoluteToStylesheet('url(./a.jpg)', href)).toEqual( + expect(absolutifyURLs('url(./a.jpg)', href)).toEqual( `url(http://localhost/css/a.jpg)`, ); }); it('can handle multiple no quote paths', () => { expect( - absoluteToStylesheet( + absolutifyURLs( 'background-image: url(images/b.jpg);background: #aabbcc url(images/a.jpg) 50% 50% repeat;', href, ), @@ -66,11 +66,11 @@ describe('absolute url to stylesheet', () => { }); it('can handle data url image', () => { + expect(absolutifyURLs('url(data:image/gif;base64,ABC)', href)).toEqual( + 'url(data:image/gif;base64,ABC)', + ); expect( - absoluteToStylesheet('url(data:image/gif;base64,ABC)', href), - ).toEqual('url(data:image/gif;base64,ABC)'); - expect( - absoluteToStylesheet( + absolutifyURLs( 'url(data:application/font-woff;base64,d09GMgABAAAAAAm)', href, ), @@ -79,7 +79,7 @@ describe('absolute url to stylesheet', () => { it('preserves quotes around inline svgs with spaces', () => { expect( - absoluteToStylesheet( + absolutifyURLs( "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", href, ), @@ -87,7 +87,7 @@ describe('absolute url to stylesheet', () => { "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%2328a745' d='M3'/%3E%3C/svg%3E\")", ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url(\'data:image/svg+xml;utf8,\')', href, ), @@ -95,7 +95,7 @@ describe('absolute url to stylesheet', () => { 'url(\'data:image/svg+xml;utf8,\')', ); expect( - absoluteToStylesheet( + absolutifyURLs( 'url("data:image/svg+xml;utf8,")', href, ), @@ -104,7 +104,7 @@ describe('absolute url to stylesheet', () => { ); }); it('can handle empty path', () => { - expect(absoluteToStylesheet(`url('')`, href)).toEqual(`url('')`); + expect(absolutifyURLs(`url('')`, href)).toEqual(`url('')`); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index bcee1c7337..557bfa46ae 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,7 +10,7 @@ "moduleResolution": "Node", "rootDir": "src", "outDir": "dist", - "lib": ["es6", "dom"], + "lib": ["es6", "dom", "dom.iterable"], "sourceMap": true, "skipLibCheck": true, "declaration": true, From 6ca68b853e25be063df217618f5c04fd0c0a53ab Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 17 Jul 2024 11:41:36 +0100 Subject: [PATCH 05/15] Don't create the intermediary array of the spread operator --- packages/rrweb-snapshot/src/utils.ts | 12 +++++++----- tsconfig.base.json | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 7c62bdaa87..3ded01a998 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -96,11 +96,13 @@ export function escapeImportStatement(rule: CSSImportRule): string { export function stringifyStylesheet(s: CSSStyleSheet): string | null { try { const rules = s.rules || s.cssRules; - const stringifiedRules = [...rules] - .map((rule: CSSRule) => stringifyRule(rule, s.href)) - .join(''); - - return rules ? fixBrowserCompatibilityIssuesInCSS(stringifiedRules) : null; + if (!rules) { + return null; + } + const stringifiedRules = Array.from(rules, (rule: CSSRule) => + stringifyRule(rule, s.href), + ).join(''); + return fixBrowserCompatibilityIssuesInCSS(stringifiedRules); } catch (error) { return null; } diff --git a/tsconfig.base.json b/tsconfig.base.json index 557bfa46ae..bcee1c7337 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,7 +10,7 @@ "moduleResolution": "Node", "rootDir": "src", "outDir": "dist", - "lib": ["es6", "dom", "dom.iterable"], + "lib": ["es6", "dom"], "sourceMap": true, "skipLibCheck": true, "declaration": true, From ddebff525b9c93781dedbef806d925a6a1eeaa95 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 17 Jul 2024 12:40:14 +0100 Subject: [PATCH 06/15] Formalize that `stringifyRule` should expect a sheet href --- packages/rrweb-snapshot/src/utils.ts | 2 +- packages/rrweb/src/record/stylesheet-manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 3ded01a998..0ff5fbd3ce 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -110,7 +110,7 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null { export function stringifyRule( rule: CSSRule, - sheetHref?: string | null, + sheetHref: string | null, ): string { let importStringified; if (isCSSImportRule(rule)) { diff --git a/packages/rrweb/src/record/stylesheet-manager.ts b/packages/rrweb/src/record/stylesheet-manager.ts index 5395cde15d..c2bbacc6ff 100644 --- a/packages/rrweb/src/record/stylesheet-manager.ts +++ b/packages/rrweb/src/record/stylesheet-manager.ts @@ -67,7 +67,7 @@ export class StylesheetManager { styles.push({ styleId, rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ - rule: stringifyRule(r), + rule: stringifyRule(r, sheet.href), index, })), }); From 04b3fc69622b68478cc258985b5d66fe59abf754 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 17 Jul 2024 13:01:18 +0100 Subject: [PATCH 07/15] Ensure a + " `; diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html index 6b45f65bc5..35bb9ae657 100644 --- a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html +++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html @@ -7,6 +7,10 @@ with style sheet with import + From 0c0ceb9c0640f26d1dbb293daead991019ee81fa Mon Sep 17 00:00:00 2001 From: eoghanmurray Date: Wed, 17 Jul 2024 13:09:38 +0000 Subject: [PATCH 08/15] Apply formatting changes --- packages/rrweb-snapshot/src/utils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 0ff5fbd3ce..4e6dee0a1f 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -108,10 +108,7 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null { } } -export function stringifyRule( - rule: CSSRule, - sheetHref: string | null, -): string { +export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { let importStringified; if (isCSSImportRule(rule)) { try { From f4bfd7384ebc35e9b8085159e47b1c894476f6fd Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Wed, 17 Jul 2024 10:57:43 -0400 Subject: [PATCH 09/15] Handle case where non imported stylesheet has relative urls that need to be absolutified --- packages/rrweb-snapshot/src/utils.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 4e6dee0a1f..0faf3147c1 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -109,8 +109,8 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null { } export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { - let importStringified; if (isCSSImportRule(rule)) { + let importStringified; try { importStringified = // for same-origin stylesheets, @@ -119,19 +119,31 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { // work around browser issues with the raw string `@import url(...)` statement escapeImportStatement(rule); - if (sheetHref) { - importStringified = absolutifyURLs(importStringified, sheetHref); + if (rule.styleSheet.href) { + importStringified = absolutifyURLs( + importStringified, + rule.styleSheet.href, + ); } + + return importStringified; } catch (error) { // ignore } - } else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + } + + let ruleStringified = rule.cssText; + if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { // Safari does not escape selectors with : properly // see https://bugs.webkit.org/show_bug.cgi?id=184604 - return fixSafariColons(rule.cssText); + ruleStringified = fixSafariColons(ruleStringified); + } + + if (sheetHref) { + return absolutifyURLs(ruleStringified, sheetHref); } - return importStringified || rule.cssText; + return ruleStringified; } export function fixSafariColons(cssStringified: string): string { From 72f6a68a3751e3760b1edf652baa66e2129a8e87 Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Wed, 17 Jul 2024 10:58:53 -0400 Subject: [PATCH 10/15] Clarify in test files where jpegs are expected to appear in absolutified urls --- .../test/__snapshots__/integration.test.ts.snap | 6 +++--- packages/rrweb-snapshot/test/alt-css/alt-style.css | 4 ++-- packages/rrweb-snapshot/test/css/style.css | 4 ++-- .../test/html/with-style-sheet-with-import.html | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 716b9104ec..0bb9c8f860 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -489,7 +489,7 @@ exports[`integration tests > [html file]: with-style-sheet.html 1`] = ` with style sheet - + " `; @@ -500,8 +500,8 @@ exports[`integration tests > [html file]: with-style-sheet-with-import.html 1`] with style sheet with import - - + + " `; diff --git a/packages/rrweb-snapshot/test/alt-css/alt-style.css b/packages/rrweb-snapshot/test/alt-css/alt-style.css index 0f935c87f2..bda88053d8 100644 --- a/packages/rrweb-snapshot/test/alt-css/alt-style.css +++ b/packages/rrweb-snapshot/test/alt-css/alt-style.css @@ -1,11 +1,11 @@ body { margin: 0; - background: url('../c.jpg'); + background: url('../should-be-in-root-folder.jpg'); border-image: url('data:image/svg+xml;utf8,'); } p { color: red; - background: url('./d.jpg'); + background: url('./should-be-in-alt-css-folder.jpg'); } body > p { color: yellow; diff --git a/packages/rrweb-snapshot/test/css/style.css b/packages/rrweb-snapshot/test/css/style.css index 2b3faf2a77..29b1da8ec8 100644 --- a/packages/rrweb-snapshot/test/css/style.css +++ b/packages/rrweb-snapshot/test/css/style.css @@ -1,11 +1,11 @@ body { margin: 0; - background: url('../a.jpg'); + background: url('../should-be-in-root-folder.jpg'); border-image: url('data:image/svg+xml;utf8,'); } p { color: red; - background: url('./b.jpg'); + background: url('./should-be-in-css-folder.jpg'); } body > p { color: yellow; diff --git a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html index 35bb9ae657..d98ff7b9fa 100644 --- a/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html +++ b/packages/rrweb-snapshot/test/html/with-style-sheet-with-import.html @@ -9,7 +9,7 @@ From 772f64c087e6ccd89275f598fe51f66f04a6c21e Mon Sep 17 00:00:00 2001 From: Jeff Nguyen Date: Thu, 18 Jul 2024 09:10:24 -0400 Subject: [PATCH 11/15] Movee absolutifyURLs call for import rules out of trycatch --- packages/rrweb-snapshot/src/utils.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 0faf3147c1..48c1a22feb 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -118,18 +118,18 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { stringifyStylesheet(rule.styleSheet) || // work around browser issues with the raw string `@import url(...)` statement escapeImportStatement(rule); - - if (rule.styleSheet.href) { - importStringified = absolutifyURLs( - importStringified, - rule.styleSheet.href, - ); - } - - return importStringified; } catch (error) { // ignore } + + if (rule.styleSheet.href) { + importStringified = absolutifyURLs( + importStringified || rule.cssText, + rule.styleSheet.href, + ); + } + + return importStringified || rule.cssText; } let ruleStringified = rule.cssText; From 8edb9fa300fed0a575048e4fe6e353d6e0cc62ff Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 19 Jul 2024 15:09:11 +0100 Subject: [PATCH 12/15] Add a benchmarking test for stringifyStylesheet --- packages/rrweb-snapshot/package.json | 1 + .../test/stringify-stylesheet.bench.ts | 37 +++++++++++++++++++ packages/rrweb-snapshot/vitest.config.ts | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 7b8a5d3db4..80bf64902f 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -10,6 +10,7 @@ "test:watch": "vitest watch", "retest:update": "vitest run --update", "test:update": "yarn build && vitest run --update", + "bench": "vite build && vitest bench", "dev": "vite build --watch", "build": "yarn turbo prepublish -F rrweb-snapshot", "check-types": "tsc --noEmit", diff --git a/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts new file mode 100644 index 0000000000..1e42bab1a6 --- /dev/null +++ b/packages/rrweb-snapshot/test/stringify-stylesheet.bench.ts @@ -0,0 +1,37 @@ +/** + * @vitest-environment jsdom + */ +import { bench } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { stringifyStylesheet } from '../src/utils'; +import * as CSSOM from 'cssom'; + +describe('stringifyStylesheet', () => { + let benchmarkStylesheet: CSSStyleSheet; + + const cssText = fs.readFileSync( + path.resolve(__dirname, './css/benchmark.css'), + 'utf8', + ); + benchmarkStylesheet = CSSOM.parse(cssText); + benchmarkStylesheet.href = 'https://example.com/style.css'; + + it.skip('stringify', () => { + // written just to ensure it's working + const cssText = '.x { background: url(./relative.jpg) }'; + const styleSheet = CSSOM.parse(cssText); + styleSheet.href = 'https://example.com/style.css'; + expect(stringifyStylesheet(styleSheet)).toEqual( + 'x {background: url(https://example.com/relative.jpg);}', + ); + }); + + bench( + 'stringify', + () => { + stringifyStylesheet(benchmarkStylesheet); + }, + { time: 1000 }, + ); +}); diff --git a/packages/rrweb-snapshot/vitest.config.ts b/packages/rrweb-snapshot/vitest.config.ts index 39888437cf..1b5a8b7e3e 100644 --- a/packages/rrweb-snapshot/vitest.config.ts +++ b/packages/rrweb-snapshot/vitest.config.ts @@ -6,7 +6,7 @@ export default mergeConfig( configShared, defineProject({ test: { - // ... custom test config here + globals: true, }, }), ); From e4d186163086758666384605167365c0172aa7f5 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 19 Jul 2024 15:17:08 +0100 Subject: [PATCH 13/15] Avoid the duplication on how to fall back --- packages/rrweb-snapshot/src/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 48c1a22feb..45114a5734 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -119,17 +119,17 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { // work around browser issues with the raw string `@import url(...)` statement escapeImportStatement(rule); } catch (error) { - // ignore + importStringified = rule.cssText; } if (rule.styleSheet.href) { importStringified = absolutifyURLs( - importStringified || rule.cssText, + importStringified, rule.styleSheet.href, ); } - return importStringified || rule.cssText; + return importStringified; } let ruleStringified = rule.cssText; From 74f37588d15924c8ed725b55b59956b167d99770 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 19 Jul 2024 17:42:10 +0100 Subject: [PATCH 14/15] Add explanatory comment --- packages/rrweb-snapshot/src/utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 45114a5734..d405b22a0e 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -123,10 +123,8 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { } if (rule.styleSheet.href) { - importStringified = absolutifyURLs( - importStringified, - rule.styleSheet.href, - ); + // url()s within the imported stylesheet are relative to _that_ sheet's href + return absolutifyURLs(importStringified, rule.styleSheet.href); } return importStringified; From acf1cb9e7d8c7c418aa547f969a487619e64d274 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 19 Jul 2024 17:45:40 +0100 Subject: [PATCH 15/15] Code style: more compact --- packages/rrweb-snapshot/src/utils.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index d405b22a0e..79139ba0dc 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -121,27 +121,23 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { } catch (error) { importStringified = rule.cssText; } - if (rule.styleSheet.href) { // url()s within the imported stylesheet are relative to _that_ sheet's href return absolutifyURLs(importStringified, rule.styleSheet.href); } - return importStringified; + } else { + let ruleStringified = rule.cssText; + if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + // Safari does not escape selectors with : properly + // see https://bugs.webkit.org/show_bug.cgi?id=184604 + ruleStringified = fixSafariColons(ruleStringified); + } + if (sheetHref) { + return absolutifyURLs(ruleStringified, sheetHref); + } + return ruleStringified; } - - let ruleStringified = rule.cssText; - if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { - // Safari does not escape selectors with : properly - // see https://bugs.webkit.org/show_bug.cgi?id=184604 - ruleStringified = fixSafariColons(ruleStringified); - } - - if (sheetHref) { - return absolutifyURLs(ruleStringified, sheetHref); - } - - return ruleStringified; } export function fixSafariColons(cssStringified: string): string {