diff --git a/.changeset/itchy-dryers-double.md b/.changeset/itchy-dryers-double.md new file mode 100644 index 00000000..67fc6f6e --- /dev/null +++ b/.changeset/itchy-dryers-double.md @@ -0,0 +1,8 @@ +--- +'rrweb-player': patch +'rrweb-snapshot': patch +'rrweb': patch +'@rrweb/types': patch +--- + +Upgrade all projects to typescript 4.9.5 diff --git a/.changeset/rich-jars-remember.md b/.changeset/rich-jars-remember.md new file mode 100644 index 00000000..efa613f7 --- /dev/null +++ b/.changeset/rich-jars-remember.md @@ -0,0 +1,5 @@ +--- +'rrweb-snapshot': patch +--- + +Add workaround for Chrome/Edge CSS `@import` escaping bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259 diff --git a/package.json b/package.json index 3e7cb602..e6669ce1 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "markdownlint-cli": "^0.31.1", "prettier": "2.8.4", "turbo": "^1.2.4", - "typescript": "^4.7.3" + "typescript": "^4.9.5" }, "scripts": { "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references' 'yarn turbo run prepublish'", @@ -49,7 +49,8 @@ "release": "yarn build:all && changeset publish" }, "resolutions": { - "**/jsdom/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz" + "**/jsdom/cssom": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "**/@types/dom-webcodecs": "0.1.5" }, "browserslist": [ "defaults", diff --git a/packages/rrweb-player/package.json b/packages/rrweb-player/package.json index 99b04790..a6e28964 100644 --- a/packages/rrweb-player/package.json +++ b/packages/rrweb-player/package.json @@ -17,9 +17,9 @@ "rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-web-worker-loader": "^1.6.1", "sirv-cli": "^0.4.4", - "svelte": "^3.2.0", - "svelte-check": "^1.4.0", - "svelte-preprocess": "^4.0.0", + "svelte": "^3.59.2", + "svelte-check": "^3.0.1", + "svelte-preprocess": "^5.0.0", "tslib": "^2.0.0" }, "dependencies": { diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index e646f58d..d7a413eb 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -345,7 +345,7 @@ export function parse(css: string, options: ParserOptions = {}) { whitespace(); comments(rules); while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) { - if (node !== false) { + if (node) { rules.push(node); comments(rules); } @@ -383,7 +383,7 @@ export function parse(css: string, options: ParserOptions = {}) { function comments(rules: Rule[] = []) { let c: Comment | void; while ((c = comment())) { - if (c !== false) { + if (c) { rules.push(c); } c = comment(); diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 6acb1c25..51e764ce 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -19,7 +19,7 @@ import { isShadowRoot, maskInputValue, isNativeShadowDom, - getCssRulesString, + stringifyStylesheet, getInputType, toLowerCase, validateStringifiedCssRule, @@ -554,7 +554,7 @@ function serializeTextNode( // to _only_ include the current rule(s) added by the text node. // So we'll be conservative and keep textContent as-is. } else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) { - textContent = getCssRulesString( + textContent = stringifyStylesheet( (n.parentNode as HTMLStyleElement).sheet!, ); } @@ -644,7 +644,7 @@ function serializeElementNode( }); let cssText: string | null = null; if (stylesheet) { - cssText = getCssRulesString(stylesheet); + cssText = stringifyStylesheet(stylesheet); } if (cssText) { delete attributes.rel; @@ -659,7 +659,7 @@ function serializeElementNode( // TODO: Currently we only try to get dynamic stylesheet when it is an empty style element !(n.innerText || n.textContent || '').trim().length ) { - const cssText = getCssRulesString( + const cssText = stringifyStylesheet( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 06e3b7a0..2b432459 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -54,12 +54,51 @@ function fixBrowserCompatibilityIssuesInCSS(cssText: string): string { return cssText; } -export function getCssRulesString(s: CSSStyleSheet): string | null { +// Remove this declaration once typescript has added `CSSImportRule.supportsText` to the lib. +declare interface CSSImportRule extends CSSRule { + readonly href: string; + readonly layerName: string | null; + readonly media: MediaList; + readonly styleSheet: CSSStyleSheet; + /** + * experimental API, currently only supported in firefox + * https://developer.mozilla.org/en-US/docs/Web/API/CSSImportRule/supportsText + */ + readonly supportsText?: string | null; +} + +/** + * 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 + * @returns `cssText` with browser inconsistencies fixed, or null if not applicable. + */ +export function escapeImportStatement(rule: CSSImportRule): string { + const { cssText } = rule; + if (cssText.split('"').length < 3) return cssText; + + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; +} + +export function stringifyStylesheet(s: CSSStyleSheet): string | null { try { const rules = s.rules || s.cssRules; return rules ? fixBrowserCompatibilityIssuesInCSS( - Array.from(rules).map(getCssRuleString).join(''), + Array.from(rules).map(stringifyRule).join(''), ) : null; } catch (error) { @@ -67,16 +106,22 @@ export function getCssRulesString(s: CSSStyleSheet): string | null { } } -export function getCssRuleString(rule: CSSRule): string { - let cssStringified = rule.cssText; +export function stringifyRule(rule: CSSRule): string { + let importStringified; if (isCSSImportRule(rule)) { try { - cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; - } catch { + importStringified = + // for same-origin stylesheets, + // we can access the imported stylesheet rules directly + stringifyStylesheet(rule.styleSheet) || + // work around browser issues with the raw string `@import url(...)` statement + escapeImportStatement(rule); + } catch (error) { // ignore } } - return validateStringifiedCssRule(cssStringified); + + return validateStringifiedCssRule(importStringified || rule.cssText); } export function validateStringifiedCssRule(cssStringified: string): string { diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 529a51ee..a50f27ce 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -490,7 +490,7 @@ exports[`integration tests [html file]: with-style-sheet-with-import.html 1`] =