diff --git a/.changeset/curvy-balloons-brake.md b/.changeset/curvy-balloons-brake.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/curvy-balloons-brake.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/eleven-bobcats-peel.md b/.changeset/eleven-bobcats-peel.md index 75f7263e1f..e3dbcf911e 100644 --- a/.changeset/eleven-bobcats-peel.md +++ b/.changeset/eleven-bobcats-peel.md @@ -1,6 +1,6 @@ --- -'rrweb-snapshot': patch -'rrweb': patch +"rrweb-snapshot": patch +"rrweb": patch --- better support for coexistence with older libraries (e.g. MooTools & Prototype.js) which modify the in-built `Array.from` function diff --git a/.changeset/fair-ducks-clean.md b/.changeset/fair-ducks-clean.md new file mode 100644 index 0000000000..19269db760 --- /dev/null +++ b/.changeset/fair-ducks-clean.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Fix and test for bug #1457 which was affecting replay of complex tailwind css diff --git a/.changeset/fast-pets-exist.md b/.changeset/fast-pets-exist.md new file mode 100644 index 0000000000..82d5216379 --- /dev/null +++ b/.changeset/fast-pets-exist.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Fixup for multiple background-clip replacement diff --git a/.changeset/format-head-prettier.md b/.changeset/format-head-prettier.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/format-head-prettier.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/hungry-dodos-taste.md b/.changeset/hungry-dodos-taste.md new file mode 100644 index 0000000000..76217d072f --- /dev/null +++ b/.changeset/hungry-dodos-taste.md @@ -0,0 +1,10 @@ +--- +'rrweb-snapshot': patch +--- + +Avoid recreating the same element every time, instead, we cache and we just update the element. + +Before: 779k ops/s +After: 860k ops/s + +Benchmark: https://jsbench.me/ktlqztuf95/1 diff --git a/.changeset/nervous-kiwis-nail.md b/.changeset/nervous-kiwis-nail.md new file mode 100644 index 0000000000..897df7ed5f --- /dev/null +++ b/.changeset/nervous-kiwis-nail.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': patch +'rrweb': patch +--- + +Bugfix after #1434 perf improvements: fix that blob urls persist on the shared anchor element and can't be later modified diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..098f1c28c7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +# initialized from https://prettier.io/docs/en/configuration.html#editorconfig +[*] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 2 +max_line_length = 80 +quote_type = single + +[.changeset/*.md] +quote_type = double diff --git a/.gitignore b/.gitignore index 02a1c32fa7..97ae63a201 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ dist .turbo +# emacs working files end in a tilde +*~ + # `.yarn/install-state.gz` is an optimization file that you shouldn't ever have to commit. # It simply stores the exact state of your project so that the next commands can boot without having to resolve your workspaces all over again. .yarn/install-state.gz \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..d380c1457f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,107 @@ +# list of old changeset files that were mutated to use single quotes by a previous version of prettier: +.changeset/attribute-text-reductions.md +.changeset/avoid-costly-createlement.md +.changeset/brave-numbers-joke.md +.changeset/breezy-cats-heal.md +.changeset/breezy-mice-breathe.md +.changeset/calm-bulldogs-speak.md +.changeset/calm-oranges-sin.md +.changeset/chatty-cherries-train.md +.changeset/clean-plants-play.md +.changeset/clean-shrimps-lay.md +.changeset/cold-eyes-hunt.md +.changeset/cold-hounds-teach.md +.changeset/controller-finish-flag.md +.changeset/cool-grapes-hug.md +.changeset/cuddly-readers-warn.md +.changeset/curvy-apples-lay.md +.changeset/curvy-balloons-brake.md +.changeset/date-now-guard.md +.changeset/dirty-rules-dress.md +.changeset/eight-terms-hunt.md +.changeset/empty-bikes-cheer.md +.changeset/event-single-wrap.md +.changeset/fair-dragons-greet.md +.changeset/fast-chefs-smell.md +.changeset/few-rockets-travel.md +.changeset/few-turkeys-reflect.md +.changeset/five-peas-lay.md +.changeset/fluffy-planes-retire.md +.changeset/forty-elephants-attack.md +.changeset/fresh-cars-impress.md +.changeset/fresh-spoons-drive.md +.changeset/friendly-numbers-leave.md +.changeset/gold-apples-joke.md +.changeset/gold-terms-look.md +.changeset/grumpy-ways-own.md +.changeset/hip-worms-relax.md +.changeset/hungry-dodos-taste.md +.changeset/itchy-dryers-double.md +.changeset/khaki-dots-bathe.md +.changeset/large-ants-prove.md +.changeset/lazy-squids-draw.md +.changeset/lazy-toes-confess.md +.changeset/lemon-lamps-switch.md +.changeset/light-fireants-exercise.md +.changeset/little-radios-thank.md +.changeset/little-suits-leave.md +.changeset/loud-seals-raise.md +.changeset/lovely-pears-cross.md +.changeset/lovely-students-boil.md +.changeset/mean-tips-impress.md +.changeset/mighty-ads-worry.md +.changeset/mighty-bulldogs-begin.md +.changeset/mighty-frogs-sparkle.md +.changeset/modern-doors-watch.md +.changeset/moody-dots-refuse.md +.changeset/nervous-buses-pump.md +.changeset/nervous-kiwis-nail.md +.changeset/nervous-mirrors-perform.md +.changeset/nervous-poets-grin.md +.changeset/nervous-tables-travel.md +.changeset/new-snakes-call.md +.changeset/nice-pugs-reply.md +.changeset/old-dryers-hide.md +.changeset/polite-olives-wave.md +.changeset/pretty-plums-rescue.md +.changeset/pretty-schools-remember.md +.changeset/proud-experts-jam.md +.changeset/rare-adults-sneeze.md +.changeset/README.md +.changeset/real-masks-explode.md +.changeset/real-trains-switch.md +.changeset/rich-crews-protect.md +.changeset/rich-dots-lay.md +.changeset/rich-jars-remember.md +.changeset/rotten-spies-enjoy.md +.changeset/serious-ants-juggle.md +.changeset/silver-pots-sit.md +.changeset/silver-windows-float.md +.changeset/sixty-impalas-laugh.md +.changeset/small-olives-arrive.md +.changeset/smart-ears-refuse.md +.changeset/smart-geckos-cover.md +.changeset/smooth-papayas-boil.md +.changeset/smooth-poems-bake.md +.changeset/spotty-bees-destroy.md +.changeset/stupid-ghosts-help.md +.changeset/swift-dancers-rest.md +.changeset/swift-peas-film.md +.changeset/thin-vans-applaud.md +.changeset/thirty-baboons-punch.md +.changeset/three-baboons-bow.md +.changeset/tidy-swans-repair.md +.changeset/tidy-yaks-joke.md +.changeset/tiny-buckets-love.md +.changeset/tiny-candles-whisper.md +.changeset/tiny-chairs-build.md +.changeset/tricky-panthers-guess.md +.changeset/twenty-goats-kneel.md +.changeset/twenty-lies-switch.md +.changeset/twenty-planets-repeat.md +.changeset/violet-melons-itch.md +.changeset/violet-zebras-cry.md +.changeset/wise-spiders-jog.md +.changeset/witty-kids-talk.md +.changeset/yellow-mails-cheat.md +.changeset/young-timers-grow.md diff --git a/.prettierrc b/.prettierrc index a20502b7f0..bf357fbbc0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,3 @@ { - "singleQuote": true, "trailingComma": "all" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 991d5b80ee..e6c87ea457 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,7 @@ clear and has sufficient instructions to be able to reproduce the issue. - Run a cobrowsing/mirroring session locally: `yarn live-stream` - Test: `yarn test` or `yarn test:watch` - Lint: `yarn lint` +- Rewrite files with prettier: `yarn format` or `yarn format:head` ## Coding style diff --git a/docs/development/coding-style.md b/docs/development/coding-style.md index d2f7598833..26227a87ea 100644 --- a/docs/development/coding-style.md +++ b/docs/development/coding-style.md @@ -1,6 +1,6 @@ # Coding Style -These are the style guidelines for coding in Electron. +These have been adapted from the style guidelines for coding in Electron. You can run `yarn lint` to show any style issues detected by `eslint`. @@ -9,6 +9,8 @@ You can run `yarn lint` to show any style issues detected by `eslint`. - End files with a newline. - Using a plain `return` when returning explicitly at the end of a function. - Not `return null`, `return undefined`, `null` or `undefined` +- run `yarn format` to rewrite all files in the standard format +- run `yarn format:head` to rewrite files from your last commit ## Documentation diff --git a/package.json b/package.json index e210dda8ce..702137ea58 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "test:watch": "yarn turbo run test:watch", "test:update": "yarn turbo run test:update", "format": "yarn prettier --write '**/*.{ts,md}'", + "format:head": "git diff --name-only HEAD^ |grep '\\.ts$\\|\\.md$' |xargs yarn prettier --write", "dev": "yarn turbo run dev", "repl": "cd packages/rrweb && npm run repl", "live-stream": "cd packages/rrweb && yarn live-stream", diff --git a/packages/rrweb-snapshot/src/css.ts b/packages/rrweb-snapshot/src/css.ts index 1a3157d40f..220e1a1fce 100644 --- a/packages/rrweb-snapshot/src/css.ts +++ b/packages/rrweb-snapshot/src/css.ts @@ -433,7 +433,7 @@ export function parse(css: string, options: ParserOptions = {}): Stylesheet { } // Use match logic from https://github.com/NxtChg/pieces/blob/3eb39c8287a97632e9347a24f333d52d916bc816/js/css_parser/css_parse.js#L46C1-L47C1 - const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); + const m = match(/^(((? { const pos = position(); const m = match(re); diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 0c80a076cf..c0fb68bfa5 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -194,23 +194,33 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { return output.join(', '); } +const cachedDocument = new WeakMap(); + export function absoluteToDoc(doc: Document, attributeValue: string): string { if (!attributeValue || attributeValue.trim() === '') { return attributeValue; } - const a: HTMLAnchorElement = doc.createElement('a'); - a.href = attributeValue; - return a.href; + + return getHref(doc, attributeValue); } function isSVGElement(el: Element): boolean { return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement); } -function getHref() { - // return a href without hash - const a = document.createElement('a'); - a.href = ''; +function getHref(doc: Document, customHref?: string) { + let a = cachedDocument.get(doc); + if (!a) { + a = doc.createElement('a'); + cachedDocument.set(doc, a); + } + if (!customHref) { + customHref = ''; + } else if (customHref.startsWith('blob:') || customHref.startsWith('data:')) { + return customHref; + } + // note: using `new URL` is slower. See #1434 or https://jsbench.me/uqlud17rxo/1 + a.setAttribute('href', customHref); return a.href; } @@ -242,7 +252,7 @@ export function transformAttribute( } else if (name === 'srcset') { return getAbsoluteSrcsetString(doc, value); } else if (name === 'style') { - return absoluteToStylesheet(value, getHref()); + return absoluteToStylesheet(value, getHref(doc)); } else if (tagName === 'object' && name === 'data') { return absoluteToDoc(doc, value); } @@ -501,6 +511,7 @@ function serializeNode( }); case n.TEXT_NODE: return serializeTextNode(n as Text, { + doc, maskTextClass, maskTextSelector, maskTextFn, @@ -532,6 +543,7 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { function serializeTextNode( n: Text, options: { + doc: Document; maskTextClass: string | RegExp; maskTextSelector: string | null; maskTextFn: MaskTextFn | undefined; @@ -564,7 +576,7 @@ function serializeTextNode( n, ); } - textContent = absoluteToStylesheet(textContent, getHref()); + textContent = absoluteToStylesheet(textContent, getHref(options.doc)); } if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; @@ -663,7 +675,7 @@ function serializeElementNode( (n as HTMLStyleElement).sheet as CSSStyleSheet, ); if (cssText) { - attributes._cssText = absoluteToStylesheet(cssText, getHref()); + attributes._cssText = absoluteToStylesheet(cssText, getHref(doc)); } } // form fields diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 5ccc9082ed..97094b40e8 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -47,7 +47,7 @@ function fixBrowserCompatibilityIssuesInCSS(cssText: string): string { !cssText.includes(' -webkit-background-clip: text;') ) { cssText = cssText.replace( - ' background-clip: text;', + /\sbackground-clip:\s*text;/g, ' -webkit-background-clip: text; background-clip: text;', ); } diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 77beb3be81..179cf7e8ad 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = ` " `; +exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = ` +" + \\"This + " +`; + exports[`integration tests [html file]: preload.html 1`] = ` " diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 097ff0989a..5d544ccfa7 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -203,4 +203,23 @@ ul li.specified c:hover img, ul li.specified c.\\:hover img { expect(getDuration(cachedEnd) * factor).toBeLessThan(getDuration(end)); }); }); + + it('should not incorrectly interpret escaped quotes', () => { + // the ':hover' in the below is a decoy which is not part of the selector, + // previously that part was being incorrectly consumed by the selector regex + const should_not_modify = + ".tailwind :is(.before\\:content-\\[\\'\\'\\])::before { --tw-content: \":hover\"; content: var(--tw-content); }.tailwind :is(.\\[\\&\\>li\\]\\:before\\:content-\\[\\'-\\'\\] > li)::before { color: pink; }"; + expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + should_not_modify, + ); + }); + + it('should not incorrectly interpret at rules', () => { + // the ':hover' in the below is a decoy which is not part of the selector, + const should_not_modify = + '@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=:hover");'; + expect(adaptCssForReplay(should_not_modify, cache)).toEqual( + should_not_modify, + ); + }); }); diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index e320254111..88c8b24900 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1479,29 +1479,34 @@ exports[`record integration tests can record clicks 1`] = ` \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"link\\\\n \\\\n \\", + \\"textContent\\": \\"link\\", \\"id\\": 22 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 24 - } - ], - \\"id\\": 23 - }, + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", \\"id\\": 25 } ], - \\"id\\": 21 + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 26 } ], \\"id\\": 16 @@ -15117,7 +15122,7 @@ exports[`record integration tests should record shadow DOM 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\\\n \\", \\"id\\": 39 }, { diff --git a/packages/rrweb/test/html/link.html b/packages/rrweb/test/html/link.html index 0d7b13739d..3db0817ede 100644 --- a/packages/rrweb/test/html/link.html +++ b/packages/rrweb/test/html/link.html @@ -9,6 +9,6 @@ not link - link + link diff --git a/packages/rrweb/test/html/shadow-dom.html b/packages/rrweb/test/html/shadow-dom.html index bf4c683798..fb04aea243 100644 --- a/packages/rrweb/test/html/shadow-dom.html +++ b/packages/rrweb/test/html/shadow-dom.html @@ -78,6 +78,5 @@ }); } -