Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture stylesheets designated as rel="preload" #1374

Merged
merged 6 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/smooth-papayas-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'rrweb-snapshot': patch
'rrweb': patch
---

Capture stylesheets designated as `rel="preload"`
9 changes: 7 additions & 2 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
stringifyStylesheet,
getInputType,
toLowerCase,
extractFileExtension,
} from './utils';

let _id = 1;
Expand Down Expand Up @@ -253,7 +254,7 @@
export function ignoreAttribute(
tagName: string,
name: string,
_value: unknown,

Check warning on line 257 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L257

[@typescript-eslint/no-unused-vars] '_value' is defined but never used.
): boolean {
return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
}
Expand Down Expand Up @@ -391,7 +392,7 @@
iframeEl.addEventListener('load', listener);
}

function isStylesheetLoaded(link: HTMLLinkElement) {

Check warning on line 395 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L395

[@typescript-eslint/no-unused-vars] 'isStylesheetLoaded' is defined but never used.
if (!link.getAttribute('href')) return true; // nothing to load
return link.sheet !== null;
}
Expand Down Expand Up @@ -556,7 +557,7 @@
// So we'll be conservative and keep textContent as-is.
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
textContent = stringifyStylesheet(
(n.parentNode as HTMLStyleElement).sheet!,

Check warning on line 560 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L560

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
);
}
} catch (err) {
Expand Down Expand Up @@ -645,7 +646,7 @@
if (cssText) {
delete attributes.rel;
delete attributes.href;
attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);

Check warning on line 649 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L649

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

Check warning on line 649 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L649

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
// dynamic stylesheet
Expand Down Expand Up @@ -738,10 +739,10 @@
const recordInlineImage = () => {
image.removeEventListener('load', recordInlineImage);
try {
canvasService!.width = image.naturalWidth;

Check warning on line 742 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L742

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasService!.height = image.naturalHeight;

Check warning on line 743 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L743

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasCtx!.drawImage(image, 0, 0);

Check warning on line 744 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L744

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
attributes.rr_dataURL = canvasService!.toDataURL(

Check warning on line 745 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L745

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
dataURLOptions.type,
dataURLOptions.quality,
);
Expand Down Expand Up @@ -847,7 +848,7 @@
(sn.tagName === 'link' &&
sn.attributes.rel === 'prefetch' &&
typeof sn.attributes.href === 'string' &&
sn.attributes.href.endsWith('.js')))
extractFileExtension(sn.attributes.href) === 'js'))
) {
return true;
} else if (
Expand Down Expand Up @@ -1177,7 +1178,11 @@
if (
serializedNode.type === NodeType.Element &&
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(
n as HTMLLinkElement,
Expand Down
20 changes: 20 additions & 0 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
* Browsers sometimes incorrectly escape `@import` on `.cssText` statements.
* This function tries to correct the escaping.
* more info: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259
* @param cssImportRule

Check warning on line 74 in packages/rrweb-snapshot/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/utils.ts#L74

[tsdoc/syntax] tsdoc-param-tag-missing-hyphen: The @param block should be followed by a parameter name and then a hyphen
* @returns `cssText` with browser inconsistencies fixed, or null if not applicable.
*/
export function escapeImportStatement(rule: CSSImportRule): string {
Expand Down Expand Up @@ -331,3 +331,23 @@
toLowerCase(type)
: 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;
}
53 changes: 52 additions & 1 deletion packages/rrweb-snapshot/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import { NodeType, serializedNode } from '../src/types';
import { isNodeMetaEqual } from '../src/utils';
import { extractFileExtension, isNodeMetaEqual } from '../src/utils';
import { serializedNodeWithId } from 'rrweb-snapshot';

describe('utils', () => {
Expand Down Expand Up @@ -147,4 +147,55 @@ describe('utils', () => {
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');
});
});
});
Loading