Capture stylesheets designated as rel="preload" (#1374)
* feat(Snapshot): Capture stylesheets designated as `rel="preload"` * fix(Snapshot): Harden asset file extension matching * Add changeset * chore: Lint * Tweak regex, add try-catch block on URL constructor
This commit is contained in:
6
.changeset/smooth-papayas-boil.md
Normal file
6
.changeset/smooth-papayas-boil.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'rrweb-snapshot': patch
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Capture stylesheets designated as `rel="preload"`
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
stringifyStylesheet,
|
stringifyStylesheet,
|
||||||
getInputType,
|
getInputType,
|
||||||
toLowerCase,
|
toLowerCase,
|
||||||
|
extractFileExtension,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
let _id = 1;
|
let _id = 1;
|
||||||
@@ -847,7 +848,7 @@ function slimDOMExcluded(
|
|||||||
(sn.tagName === 'link' &&
|
(sn.tagName === 'link' &&
|
||||||
sn.attributes.rel === 'prefetch' &&
|
sn.attributes.rel === 'prefetch' &&
|
||||||
typeof sn.attributes.href === 'string' &&
|
typeof sn.attributes.href === 'string' &&
|
||||||
sn.attributes.href.endsWith('.js')))
|
extractFileExtension(sn.attributes.href) === 'js'))
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
} else if (
|
} else if (
|
||||||
@@ -1177,7 +1178,11 @@ export function serializeNodeWithId(
|
|||||||
if (
|
if (
|
||||||
serializedNode.type === NodeType.Element &&
|
serializedNode.type === NodeType.Element &&
|
||||||
serializedNode.tagName === 'link' &&
|
serializedNode.tagName === 'link' &&
|
||||||
serializedNode.attributes.rel === 'stylesheet'
|
typeof serializedNode.attributes.rel === 'string' &&
|
||||||
|
(serializedNode.attributes.rel === 'stylesheet' ||
|
||||||
|
(serializedNode.attributes.rel === 'preload' &&
|
||||||
|
typeof serializedNode.attributes.href === 'string' &&
|
||||||
|
extractFileExtension(serializedNode.attributes.href) === 'css'))
|
||||||
) {
|
) {
|
||||||
onceStylesheetLoaded(
|
onceStylesheetLoaded(
|
||||||
n as HTMLLinkElement,
|
n as HTMLLinkElement,
|
||||||
|
|||||||
@@ -331,3 +331,23 @@ export function getInputType(element: HTMLElement): Lowercase<string> | null {
|
|||||||
toLowerCase(type)
|
toLowerCase(type)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the file extension from an a path, considering search parameters and fragments.
|
||||||
|
* @param path - Path to file
|
||||||
|
* @param baseURL - [optional] Base URL of the page, used to resolve relative paths. Defaults to current page URL.
|
||||||
|
*/
|
||||||
|
export function extractFileExtension(
|
||||||
|
path: string,
|
||||||
|
baseURL?: string,
|
||||||
|
): string | null {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(path, baseURL ?? window.location.href);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const regex = /\.([0-9a-z]+)(?:$)/i;
|
||||||
|
const match = url.pathname.match(regex);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
import { NodeType, serializedNode } from '../src/types';
|
import { NodeType, serializedNode } from '../src/types';
|
||||||
import { isNodeMetaEqual } from '../src/utils';
|
import { extractFileExtension, isNodeMetaEqual } from '../src/utils';
|
||||||
import { serializedNodeWithId } from 'rrweb-snapshot';
|
import { serializedNodeWithId } from 'rrweb-snapshot';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
@@ -147,4 +147,55 @@ describe('utils', () => {
|
|||||||
expect(isNodeMetaEqual(element2, element3)).toBeFalsy();
|
expect(isNodeMetaEqual(element2, element3)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('extractFileExtension', () => {
|
||||||
|
test('absolute path', () => {
|
||||||
|
const path = 'https://example.com/styles/main.css';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBe('css');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('relative path', () => {
|
||||||
|
const path = 'styles/main.css';
|
||||||
|
const baseURL = 'https://example.com/';
|
||||||
|
const extension = extractFileExtension(path, baseURL);
|
||||||
|
expect(extension).toBe('css');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path with search parameters', () => {
|
||||||
|
const path = 'https://example.com/scripts/app.js?version=1.0';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBe('js');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path with fragment', () => {
|
||||||
|
const path = 'https://example.com/styles/main.css#section1';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBe('css');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path with search parameters and fragment', () => {
|
||||||
|
const path = 'https://example.com/scripts/app.js?version=1.0#section1';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBe('js');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path without extension', () => {
|
||||||
|
const path = 'https://example.com/path/to/directory/';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid URL', () => {
|
||||||
|
const path = '!@#$%^&*()';
|
||||||
|
const baseURL = 'invalid';
|
||||||
|
const extension = extractFileExtension(path, baseURL);
|
||||||
|
expect(extension).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path with multiple dots', () => {
|
||||||
|
const path = 'https://example.com/scripts/app.min.js?version=1.0';
|
||||||
|
const extension = extractFileExtension(path);
|
||||||
|
expect(extension).toBe('js');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user