Skip to content

Commit 9b4e13a

Browse files
committed
prefer base href for inline style urls, else fallback to doc.location
1 parent 0e48d10 commit 9b4e13a

File tree

5 files changed

+113
-2
lines changed

5 files changed

+113
-2
lines changed

.changeset/twelve-nails-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"rrweb-snapshot": patch
3+
---
4+
5+
Prefer the <base href> for resolving inline <style> URLs, falling back to document location if not present.

packages/rrweb-snapshot/src/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,13 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null {
149149
let sheetHref = s.href;
150150
if (!sheetHref && s.ownerNode && s.ownerNode.ownerDocument) {
151151
// an inline <style> element
152-
sheetHref = s.ownerNode.ownerDocument.location.href;
152+
const doc = s.ownerNode.ownerDocument;
153+
const baseEl = doc.querySelector('base[href]');
154+
if (baseEl && (baseEl as HTMLBaseElement).href) {
155+
sheetHref = (baseEl as HTMLBaseElement).href;
156+
} else {
157+
sheetHref = s.ownerNode.ownerDocument.location.href;
158+
}
153159
}
154160
const stringifiedRules = Array.from(rules, (rule: CSSRule) =>
155161
stringifyRule(rule, sheetHref),

packages/rrweb-snapshot/test/utils.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
replaceChromeGridTemplateAreas,
99
fixSafariColons,
1010
isNodeMetaEqual,
11+
stringifyStylesheet
1112
} from '../src/utils';
1213
import { NodeType } from '@rrweb/types';
1314
import type { serializedNode, serializedNodeWithId } from '@rrweb/types';
@@ -374,4 +375,98 @@ describe('utils', () => {
374375
expect(out3).toEqual('[data-aa\\:other] { color: red; }');
375376
});
376377
});
378+
379+
describe('stringifyStylesheet', () => {
380+
it('returns null if rules are missing', () => {
381+
const mockSheet = {
382+
rules: null,
383+
cssRules: null,
384+
} as unknown as CSSStyleSheet;
385+
expect(stringifyStylesheet(mockSheet)).toBeNull();
386+
});
387+
388+
it('stringifies rules using .rules if present', () => {
389+
const mockRule1 = { cssText: 'body { color: red; }' } as CSSRule;
390+
const mockRule2 = { cssText: 'a { text-decoration: none; }' } as CSSRule;
391+
const mockSheet = {
392+
rules: [mockRule1, mockRule2],
393+
cssRules: null,
394+
href: 'https://example.com/styles.css',
395+
} as unknown as CSSStyleSheet;
396+
expect(stringifyStylesheet(mockSheet)).toBe(
397+
'body { color: red; }a { text-decoration: none; }'
398+
);
399+
});
400+
401+
it('stringifies rules using .cssRules if .rules is missing', () => {
402+
const mockRule1 = { cssText: 'div { margin: 0; }' } as CSSRule;
403+
const mockSheet = {
404+
rules: null,
405+
cssRules: [mockRule1],
406+
href: 'https://example.com/main.css',
407+
} as unknown as CSSStyleSheet;
408+
expect(stringifyStylesheet(mockSheet)).toBe('div { margin: 0; }');
409+
});
410+
411+
it('uses ownerNode.ownerDocument.location.href for inline styles', () => {
412+
const mockRule = { cssText: 'span { font-size: 12px; }' } as CSSRule;
413+
const mockOwnerDocument = {
414+
location: { href: 'https://example.com/page.html' },
415+
querySelector: () => null,
416+
} as unknown as Document;
417+
const mockOwnerNode = {
418+
ownerDocument: mockOwnerDocument,
419+
} as unknown as Node;
420+
const mockSheet = {
421+
rules: [mockRule],
422+
cssRules: null,
423+
href: null,
424+
ownerNode: mockOwnerNode,
425+
} as unknown as CSSStyleSheet;
426+
expect(stringifyStylesheet(mockSheet)).toBe('span { font-size: 12px; }');
427+
});
428+
429+
it('uses <base href> if present for inline styles', () => {
430+
const mockRule = { cssText: 'p { line-height: 1.5; }' } as CSSRule;
431+
const mockBase = { href: '/test/' } as HTMLBaseElement;
432+
const mockOwnerDocument = {
433+
location: { href: 'https://example.com/test/#/page.html' },
434+
querySelector: (selector: string) =>
435+
selector === 'base[href]' ? 'https://example.com/test/' : null,
436+
} as unknown as Document;
437+
const mockOwnerNode = {
438+
ownerDocument: mockOwnerDocument,
439+
} as unknown as Node;
440+
const mockSheet = {
441+
rules: [mockRule],
442+
cssRules: null,
443+
href: null,
444+
ownerNode: mockOwnerNode,
445+
} as unknown as CSSStyleSheet;
446+
expect(stringifyStylesheet(mockSheet)).toBe('p { line-height: 1.5; }');
447+
});
448+
449+
it('returns null if an error is thrown', () => {
450+
const mockSheet = {
451+
get rules() {
452+
throw new Error('Test error');
453+
},
454+
} as unknown as CSSStyleSheet;
455+
expect(stringifyStylesheet(mockSheet)).toBeNull();
456+
});
457+
458+
it('applies fixBrowserCompatibilityIssuesInCSS', () => {
459+
// This test checks that fixBrowserCompatibilityIssuesInCSS is called.
460+
// We'll use a rule that triggers the fix for background-clip.
461+
const mockRule = {
462+
cssText: 'h1 { background-clip: text; }',
463+
} as CSSRule;
464+
const mockSheet = {
465+
rules: [mockRule],
466+
cssRules: null,
467+
href: 'https://example.com/styles.css',
468+
} as unknown as CSSStyleSheet;
469+
expect(stringifyStylesheet(mockSheet)).toContain('-webkit-background-clip: text;');
470+
});
471+
});
377472
});

turbo.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"typings/**",
1919
".svelte-kit/**",
2020
"types/**"
21-
]
21+
],
22+
"cache": false
2223
},
2324
"test": {
2425
"dependsOn": ["^prepublish"]

vite.config.default.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export default function (
140140

141141
sourcemap: true,
142142

143+
rollupOptions: {
144+
external: ['postcss']
145+
},
146+
143147
// rollupOptions: {
144148
// output: {
145149
// manualChunks: {},

0 commit comments

Comments
 (0)