From e3b75bca726324eb4b165a682883435bf83be9e1 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 19 May 2026 12:31:14 +0200 Subject: [PATCH 1/5] docs: design record tree-shaking fix --- .../0001-record-source-boundary-resolver.md | 3 + .../plans/2026-05-19-record-tree-shaking.md | 361 ++++++++++++++++++ .../2026-05-19-record-tree-shaking-design.md | 80 ++++ 3 files changed, 444 insertions(+) create mode 100644 docs/adr/0001-record-source-boundary-resolver.md create mode 100644 docs/superpowers/plans/2026-05-19-record-tree-shaking.md create mode 100644 docs/superpowers/specs/2026-05-19-record-tree-shaking-design.md diff --git a/docs/adr/0001-record-source-boundary-resolver.md b/docs/adr/0001-record-source-boundary-resolver.md new file mode 100644 index 0000000000..210778a6f0 --- /dev/null +++ b/docs/adr/0001-record-source-boundary-resolver.md @@ -0,0 +1,3 @@ +# Use a record-local source-boundary resolver + +`@rrweb/record` needs to build without replay-only `rrweb-snapshot` code such as `postcss`, but adding public snapshot/rebuild subpath exports would expand the package API and create compatibility obligations. We will keep the public package API unchanged and add a resolver scoped to the `@rrweb/record` build that maps selected bare package imports to local source entrypoints, restoring Rollup's module visibility for tree-shaking. diff --git a/docs/superpowers/plans/2026-05-19-record-tree-shaking.md b/docs/superpowers/plans/2026-05-19-record-tree-shaking.md new file mode 100644 index 0000000000..b8705f128f --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-record-tree-shaking.md @@ -0,0 +1,361 @@ +# Record Tree-Shaking Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build `@rrweb/record` without bundling replay-only `postcss` code from `rrweb-snapshot`. + +**Architecture:** Keep the public package API unchanged. Add a resolver scoped to `packages/record/vite.config.ts` so the record build sees local source module boundaries for `rrweb`, `rrweb-snapshot`, and `rrdom`; mark `rrweb-snapshot` as side-effect-free so Rollup can drop unused rebuild modules. + +**Tech Stack:** Yarn workspaces, Turbo, Vite 6, Rollup plugin hooks, Vitest, TypeScript. + +--- + +## File Structure + +- Modify `packages/record/test/record.test.ts`: add build-output assertions for `postcss` absence and `dist/record.js` size. +- Modify `packages/rrweb-snapshot/package.json`: add `"sideEffects": false`. +- Modify `packages/record/vite.config.ts`: add an inline exact-bare-import `resolveId` plugin for record's build. +- Modify `packages/rrweb/src/entries/record.ts`: replace default-import-then-export with direct named re-export. + +## Task 1: Confirm The Baseline + +**Files:** +- Read: `packages/record/dist/record.js` +- Modify: none + +- [ ] **Step 1: Build the current record package before implementation changes** + +Run: + +```bash +yarn workspace @rrweb/record build +``` + +Expected: build succeeds and writes `packages/record/dist/record.js`. In this workspace, the measured baseline build succeeded before implementation changes. + +- [ ] **Step 2: Record the current ESM bundle size** + +Run: + +```bash +wc -c packages/record/dist/record.js +``` + +Expected: output is `397373 packages/record/dist/record.js`. This is the baseline byte count for Task 2's test constant. + +- [ ] **Step 3: Confirm the current bundle contains `postcss`** + +Run: + +```bash +rg -n "postcss" packages/record/dist +``` + +Expected: at least one match in a built JavaScript artifact. In this workspace, `postcss` appears in `packages/record/dist/record.js` and `packages/record/dist/record.umd.min.cjs` before the fix. + +## Task 2: Add Failing Bundle Regression Tests + +**Files:** +- Modify: `packages/record/test/record.test.ts` + +- [ ] **Step 1: Replace the test file with build-output checks** + +```ts +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { record } from '../src/index'; + +const distDir = path.resolve(__dirname, '../dist'); +const recordJsPath = path.join(distDir, 'record.js'); + +// Measured before the tree-shaking fix: 397373 bytes. +// The fixed ESM bundle must stay at least 200 KiB smaller. +const BASELINE_RECORD_JS_BYTES = 397373; +const MAX_RECORD_JS_BYTES = BASELINE_RECORD_JS_BYTES - 200 * 1024; + +function requireBuiltRecordBundle() { + if (!existsSync(recordJsPath)) { + throw new Error( + 'Missing packages/record/dist/record.js. Run `yarn workspace @rrweb/record build` before running this test.', + ); + } +} + +function emittedJavaScriptFiles() { + if (!existsSync(distDir)) { + throw new Error( + 'Missing packages/record/dist. Run `yarn workspace @rrweb/record build` before running this test.', + ); + } + return readdirSync(distDir) + .filter((file) => file.endsWith('.js') || file.endsWith('.cjs')) + .map((file) => path.join(distDir, file)); +} + +describe('record', () => { + it('should be a function', () => { + expect(typeof record).toBe('function'); + }); + + it('does not bundle replay-only postcss code', () => { + requireBuiltRecordBundle(); + + for (const file of emittedJavaScriptFiles()) { + expect(readFileSync(file, 'utf-8')).not.toContain('postcss'); + } + }); + + it('keeps the ESM record bundle at least 200 KiB below the baseline size', () => { + requireBuiltRecordBundle(); + + expect(statSync(recordJsPath).size).toBeLessThanOrEqual( + MAX_RECORD_JS_BYTES, + ); + }); +}); +``` + +- [ ] **Step 2: Run the test and verify it fails for the current implementation** + +Run: + +```bash +yarn workspace @rrweb/record test +``` + +Expected: the new `postcss` assertion fails, or the size assertion fails, because the implementation change has not been made yet. + +## Task 3: Mark rrweb-snapshot As Side-Effect-Free + +**Files:** +- Modify: `packages/rrweb-snapshot/package.json` + +- [ ] **Step 1: Add the package metadata** + +Insert `"sideEffects": false` after the `"files"` array. + +```json + "files": [ + "umd", + "dist", + "package.json" + ], + "sideEffects": false, + "author": "yanzhen@smartx.com", +``` + +- [ ] **Step 2: Validate JSON syntax** + +Run: + +```bash +node -e "JSON.parse(require('node:fs').readFileSync('packages/rrweb-snapshot/package.json', 'utf8')); console.log('ok')" +``` + +Expected: prints `ok`. + +## Task 4: Add The Record-Local Source Resolver + +**Files:** +- Modify: `packages/record/vite.config.ts` + +- [ ] **Step 1: Replace the config with an inline exact-import resolver** + +```ts +import path from 'path'; +import type { Plugin } from 'vite'; +import config from '../../vite.config.default'; + +const sourceEntryByPackageName = new Map([ + ['rrweb', path.resolve(__dirname, '../rrweb/src/entries/record.ts')], + [ + 'rrweb-snapshot', + path.resolve(__dirname, '../rrweb-snapshot/src/index.ts'), + ], + ['rrdom', path.resolve(__dirname, '../rrdom/src/index.ts')], +]); + +function resolveLocalSourceEntries(): Plugin { + return { + name: 'resolve-local-source-entries', + enforce: 'pre', + resolveId(source) { + return sourceEntryByPackageName.get(source) || null; + }, + }; +} + +export default config(path.resolve(__dirname, 'src/index.ts'), 'rrwebRecord', { + plugins: [resolveLocalSourceEntries()], +}); +``` + +- [ ] **Step 2: Run TypeScript checking for the record package** + +Run: + +```bash +yarn workspace @rrweb/record check-types +``` + +Expected: TypeScript check succeeds. + +## Task 5: Make The rrweb Record Entry A Direct Re-Export + +**Files:** +- Modify: `packages/rrweb/src/entries/record.ts` + +- [ ] **Step 1: Replace default-import-then-export with a direct named re-export** + +```ts +export { default as record } from '../record'; +``` + +- [ ] **Step 2: Type-check rrweb** + +Run: + +```bash +yarn workspace rrweb check-types +``` + +Expected: TypeScript check succeeds. + +## Task 6: Build And Set The Final Size Threshold + +**Files:** +- Modify: `packages/record/test/record.test.ts` + +- [ ] **Step 1: Build the fixed record package** + +Run: + +```bash +yarn workspace @rrweb/record build +``` + +Expected: build succeeds and writes `packages/record/dist/record.js`. + +- [ ] **Step 2: Measure the fixed ESM bundle size** + +Run: + +```bash +wc -c packages/record/dist/record.js +``` + +Expected: output is at least `204800` bytes smaller than the baseline from Task 1. + +- [ ] **Step 3: Update the test comment with measured before and after sizes** + +Use the exact byte count printed in Step 2 in a new comment line above the threshold constants. Keep `BASELINE_RECORD_JS_BYTES` equal to `397373`. For example, if Step 2 prints `180245 packages/record/dist/record.js`, the threshold block becomes: + +```ts +// Measured before the tree-shaking fix: 397373 bytes. +// Measured after the tree-shaking fix: 180245 bytes. +// The fixed ESM bundle must stay at least 200 KiB smaller. +const BASELINE_RECORD_JS_BYTES = 397373; +const MAX_RECORD_JS_BYTES = BASELINE_RECORD_JS_BYTES - 200 * 1024; +``` + +## Task 7: Verify The Regression Tests Pass + +**Files:** +- Test: `packages/record/test/record.test.ts` + +- [ ] **Step 1: Run the focused record tests** + +Run: + +```bash +yarn workspace @rrweb/record test +``` + +Expected: all tests in `packages/record/test/record.test.ts` pass. + +- [ ] **Step 2: Confirm `postcss` is absent from emitted record JavaScript** + +Run: + +```bash +rg -n "postcss" packages/record/dist --glob '*.{js,cjs}' +``` + +Expected: no matches and exit code `1`. + +## Task 8: Run Final Verification + +**Files:** +- Test: `packages/rrweb-snapshot/package.json` +- Test: `packages/record/vite.config.ts` +- Test: `packages/rrweb/src/entries/record.ts` +- Test: `packages/record/test/record.test.ts` + +- [ ] **Step 1: Check rrweb-snapshot types** + +Run: + +```bash +yarn workspace rrweb-snapshot check-types +``` + +Expected: succeeds. + +- [ ] **Step 2: Rebuild record** + +Run: + +```bash +yarn workspace @rrweb/record build +``` + +Expected: succeeds. + +- [ ] **Step 3: Run record tests** + +Run: + +```bash +yarn workspace @rrweb/record test +``` + +Expected: succeeds. + +- [ ] **Step 4: Review the final diff** + +Run: + +```bash +git diff -- packages/rrweb-snapshot/package.json packages/record/vite.config.ts packages/rrweb/src/entries/record.ts packages/record/test/record.test.ts +``` + +Expected: diff only contains the planned implementation and test changes. + +## Task 9: Commit The Implementation + +**Files:** +- Stage: `packages/rrweb-snapshot/package.json` +- Stage: `packages/record/vite.config.ts` +- Stage: `packages/rrweb/src/entries/record.ts` +- Stage: `packages/record/test/record.test.ts` + +- [ ] **Step 1: Stage implementation files only** + +Run: + +```bash +git add packages/rrweb-snapshot/package.json packages/record/vite.config.ts packages/rrweb/src/entries/record.ts packages/record/test/record.test.ts +``` + +Expected: staged diff excludes the pre-existing `packages/rrweb-player/.svelte-kit/ambient.d.ts` change. + +- [ ] **Step 2: Commit** + +Run: + +```bash +git commit -m "fix: tree-shake postcss from record bundle" +``` + +Expected: commit succeeds. diff --git a/docs/superpowers/specs/2026-05-19-record-tree-shaking-design.md b/docs/superpowers/specs/2026-05-19-record-tree-shaking-design.md new file mode 100644 index 0000000000..5386cf7037 --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-record-tree-shaking-design.md @@ -0,0 +1,80 @@ +# Record Tree-Shaking Design + +## Context + +`rrweb-snapshot` contains both record-time snapshot code and replay-time rebuild code. The rebuild path imports `postcss`, which is large and unnecessary for `@rrweb/record` consumers. Because `rrweb-snapshot` currently publishes a single barrel bundle, consumers that import record-only APIs can still end up with replay-only code in their output. + +The goal is to reimplement the behavior targeted by rrweb PR #1784 without copying that implementation. The implementation should use the current repository structure and keep the change low risk. + +## Goals + +- Build `@rrweb/record` without bundling `postcss`. +- Preserve the current public API for `rrweb`, `rrweb-snapshot`, `rrdom`, and `@rrweb/record`. +- Keep the implementation narrow and easy to review. +- Add tests that catch both the direct dependency regression and the bundle-size impact. + +## Non-Goals + +- Do not add new public `rrweb-snapshot` subpath exports. +- Do not split `rrweb-snapshot` into separate published snapshot and rebuild packages. +- Do not refactor snapshot/rebuild utilities unless the narrow build fix cannot pass verification without it. +- Do not inspect or copy the PR #1784 implementation. + +## Proposed Approach + +Use a source-boundary build fix for `packages/record`. + +1. Mark `rrweb-snapshot` as side-effect-free in `packages/rrweb-snapshot/package.json`. +2. Add a Vite/Rollup `resolveId` plugin to `packages/record/vite.config.ts`. +3. During the record package build, redirect bare imports of `rrweb`, `rrweb-snapshot`, and `rrdom` to their local source entry files instead of their pre-built package entrypoints. +4. Change `packages/rrweb/src/entries/record.ts` to directly re-export the named `record` export from the record module. +5. Extend `packages/record/test/record.test.ts` so it verifies the built record output does not contain `postcss` and records a significant file-size drop signal. + +## Architecture + +The public packages keep their existing import paths. The special behavior is limited to the `@rrweb/record` build config. + +The record package currently imports `record` through `rrweb`. That package import can resolve to the pre-built `rrweb` bundle, which hides source module boundaries from Rollup. The custom resolver restores those boundaries by mapping: + +- `rrweb` to `packages/rrweb/src/entries/record.ts` +- `rrweb-snapshot` to `packages/rrweb-snapshot/src/index.ts` +- `rrdom` to `packages/rrdom/src/index.ts` + +The resolver should match only exact bare imports. It should not rewrite subpath imports such as `rrweb/foo` unless a future change explicitly adds that case. + +The resolver should live inline in `packages/record/vite.config.ts`. It is a record-package build concern, not a shared monorepo helper. + +Once Rollup sees source modules, `"sideEffects": false` on `rrweb-snapshot` allows unused rebuild modules to be removed when record consumers do not use rebuild exports. This should use plain `"sideEffects": false`, not a narrower allowlist. PR #1834's `rebuild.ts` changes do not add import-time side effects; its DOM work and sandbox checks happen only when exported functions are called. + +The `rrweb` record entry should use a direct named re-export. This gives Rollup a simpler export graph than a default import followed by a re-export. + +## Testing + +Keep the existing smoke test that `record` is a function. + +Add a build-output regression test for `@rrweb/record`: + +- Inspect emitted `.js` and `.cjs` files in `packages/record/dist`. +- Assert no emitted `.js` or `.cjs` file contains the string `postcss`. +- Use `packages/record/dist/record.js` as the canonical size signal. +- Measure the current `dist/record.js` size locally before changing implementation files, then assert the fixed `dist/record.js` size is at least 200KB smaller than that baseline. + +The implementation should encode that baseline-derived threshold as a constant in the test, with a comment documenting the measured before/after sizes. The test should compare durable built artifacts, not transient visualizer output or source maps. It should not run the build itself; if `dist/record.js` is missing, it should fail with a clear message telling the developer to run `yarn workspace @rrweb/record build` first. + +## Verification + +Run focused verification after implementation: + +```sh +yarn workspace rrweb-snapshot check-types +yarn workspace @rrweb/record build +yarn workspace @rrweb/record test +``` + +## Risks + +The resolver must be scoped to the record build. Applying it globally could change other package builds unexpectedly. + +The file-size threshold must be strict enough to catch regressions but not so strict that unrelated output formatting or dependency updates cause noise. It should focus on the main JavaScript bundle and leave source maps out of the assertion. + +Marking `rrweb-snapshot` as side-effect-free assumes its module-level code does not intentionally perform observable side effects on import. That matches the intended package usage for tree-shaking and remains compatible with PR #1834's `rebuild.ts` changes, but verification should include existing snapshot and record tests. From 59217690aa4fd84a06feb9e642d4744cbadfd33e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 19 May 2026 12:56:32 +0200 Subject: [PATCH 2/5] fix: tree-shake postcss from record bundle --- packages/record/test/record.test.ts | 46 ++++++++++++++++++++++++++++ packages/record/vite.config.ts | 24 ++++++++++++++- packages/rrweb-snapshot/package.json | 1 + packages/rrweb/src/entries/record.ts | 4 +-- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/record/test/record.test.ts b/packages/record/test/record.test.ts index 1a51435cc4..1f2db863a6 100644 --- a/packages/record/test/record.test.ts +++ b/packages/record/test/record.test.ts @@ -1,8 +1,54 @@ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; import { describe, it, expect } from 'vitest'; import { record } from '../src/index'; +const distDir = path.resolve(__dirname, '../dist'); +const recordJsPath = path.join(distDir, 'record.js'); + +// Measured before the tree-shaking fix: 397373 bytes. +// Measured after the tree-shaking fix: 161287 bytes. +// The fixed ESM bundle must stay at least 200 KiB smaller. +const BASELINE_RECORD_JS_BYTES = 397373; +const MAX_RECORD_JS_BYTES = BASELINE_RECORD_JS_BYTES - 200 * 1024; + +function requireBuiltRecordBundle() { + if (!existsSync(recordJsPath)) { + throw new Error( + 'Missing packages/record/dist/record.js. Run `yarn workspace @rrweb/record build` before running this test.', + ); + } +} + +function emittedJavaScriptFiles() { + if (!existsSync(distDir)) { + throw new Error( + 'Missing packages/record/dist. Run `yarn workspace @rrweb/record build` before running this test.', + ); + } + return readdirSync(distDir) + .filter((file) => file.endsWith('.js') || file.endsWith('.cjs')) + .map((file) => path.join(distDir, file)); +} + describe('record', () => { it('should be a function', () => { expect(typeof record).toBe('function'); }); + + it('does not bundle replay-only postcss code', () => { + requireBuiltRecordBundle(); + + for (const file of emittedJavaScriptFiles()) { + expect(readFileSync(file, 'utf-8')).not.toContain('postcss'); + } + }); + + it('keeps the ESM record bundle at least 200 KiB below the baseline size', () => { + requireBuiltRecordBundle(); + + expect(statSync(recordJsPath).size).toBeLessThanOrEqual( + MAX_RECORD_JS_BYTES, + ); + }); }); diff --git a/packages/record/vite.config.ts b/packages/record/vite.config.ts index fff4031c94..f3ab67859a 100644 --- a/packages/record/vite.config.ts +++ b/packages/record/vite.config.ts @@ -1,4 +1,26 @@ import path from 'path'; +import type { Plugin } from 'vite'; import config from '../../vite.config.default'; -export default config(path.resolve(__dirname, 'src/index.ts'), 'rrwebRecord'); +const sourceEntryByPackageName = new Map([ + ['rrweb', path.resolve(__dirname, '../rrweb/src/entries/record.ts')], + [ + 'rrweb-snapshot', + path.resolve(__dirname, '../rrweb-snapshot/src/index.ts'), + ], + ['rrdom', path.resolve(__dirname, '../rrdom/src/index.ts')], +]); + +function resolveLocalSourceEntries(): Plugin { + return { + name: 'resolve-local-source-entries', + enforce: 'pre', + resolveId(source) { + return sourceEntryByPackageName.get(source) || null; + }, + }; +} + +export default config(path.resolve(__dirname, 'src/index.ts'), 'rrwebRecord', { + plugins: [resolveLocalSourceEntries()], +}); diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 39e4b35e64..8d4a082f30 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -48,6 +48,7 @@ "dist", "package.json" ], + "sideEffects": false, "author": "yanzhen@smartx.com", "license": "MIT", "bugs": { diff --git a/packages/rrweb/src/entries/record.ts b/packages/rrweb/src/entries/record.ts index 0655b7afc3..255a08aaa8 100644 --- a/packages/rrweb/src/entries/record.ts +++ b/packages/rrweb/src/entries/record.ts @@ -1,3 +1 @@ -import record from '../record'; - -export { record }; +export { default as record } from '../record'; From 5bd685d7084230275164e407e27e6ec466e4eecb Mon Sep 17 00:00:00 2001 From: Juice10 Date: Tue, 19 May 2026 11:03:02 +0000 Subject: [PATCH 3/5] Apply formatting changes --- .../plans/2026-05-19-record-tree-shaking.md | 14 ++++++++++---- packages/record/vite.config.ts | 5 +---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/plans/2026-05-19-record-tree-shaking.md b/docs/superpowers/plans/2026-05-19-record-tree-shaking.md index b8705f128f..e4b68b4ed0 100644 --- a/docs/superpowers/plans/2026-05-19-record-tree-shaking.md +++ b/docs/superpowers/plans/2026-05-19-record-tree-shaking.md @@ -20,6 +20,7 @@ ## Task 1: Confirm The Baseline **Files:** + - Read: `packages/record/dist/record.js` - Modify: none @@ -56,6 +57,7 @@ Expected: at least one match in a built JavaScript artifact. In this workspace, ## Task 2: Add Failing Bundle Regression Tests **Files:** + - Modify: `packages/record/test/record.test.ts` - [ ] **Step 1: Replace the test file with build-output checks** @@ -129,6 +131,7 @@ Expected: the new `postcss` assertion fails, or the size assertion fails, becaus ## Task 3: Mark rrweb-snapshot As Side-Effect-Free **Files:** + - Modify: `packages/rrweb-snapshot/package.json` - [ ] **Step 1: Add the package metadata** @@ -158,6 +161,7 @@ Expected: prints `ok`. ## Task 4: Add The Record-Local Source Resolver **Files:** + - Modify: `packages/record/vite.config.ts` - [ ] **Step 1: Replace the config with an inline exact-import resolver** @@ -169,10 +173,7 @@ import config from '../../vite.config.default'; const sourceEntryByPackageName = new Map([ ['rrweb', path.resolve(__dirname, '../rrweb/src/entries/record.ts')], - [ - 'rrweb-snapshot', - path.resolve(__dirname, '../rrweb-snapshot/src/index.ts'), - ], + ['rrweb-snapshot', path.resolve(__dirname, '../rrweb-snapshot/src/index.ts')], ['rrdom', path.resolve(__dirname, '../rrdom/src/index.ts')], ]); @@ -204,6 +205,7 @@ Expected: TypeScript check succeeds. ## Task 5: Make The rrweb Record Entry A Direct Re-Export **Files:** + - Modify: `packages/rrweb/src/entries/record.ts` - [ ] **Step 1: Replace default-import-then-export with a direct named re-export** @@ -225,6 +227,7 @@ Expected: TypeScript check succeeds. ## Task 6: Build And Set The Final Size Threshold **Files:** + - Modify: `packages/record/test/record.test.ts` - [ ] **Step 1: Build the fixed record package** @@ -262,6 +265,7 @@ const MAX_RECORD_JS_BYTES = BASELINE_RECORD_JS_BYTES - 200 * 1024; ## Task 7: Verify The Regression Tests Pass **Files:** + - Test: `packages/record/test/record.test.ts` - [ ] **Step 1: Run the focused record tests** @@ -287,6 +291,7 @@ Expected: no matches and exit code `1`. ## Task 8: Run Final Verification **Files:** + - Test: `packages/rrweb-snapshot/package.json` - Test: `packages/record/vite.config.ts` - Test: `packages/rrweb/src/entries/record.ts` @@ -335,6 +340,7 @@ Expected: diff only contains the planned implementation and test changes. ## Task 9: Commit The Implementation **Files:** + - Stage: `packages/rrweb-snapshot/package.json` - Stage: `packages/record/vite.config.ts` - Stage: `packages/rrweb/src/entries/record.ts` diff --git a/packages/record/vite.config.ts b/packages/record/vite.config.ts index f3ab67859a..94191afe62 100644 --- a/packages/record/vite.config.ts +++ b/packages/record/vite.config.ts @@ -4,10 +4,7 @@ import config from '../../vite.config.default'; const sourceEntryByPackageName = new Map([ ['rrweb', path.resolve(__dirname, '../rrweb/src/entries/record.ts')], - [ - 'rrweb-snapshot', - path.resolve(__dirname, '../rrweb-snapshot/src/index.ts'), - ], + ['rrweb-snapshot', path.resolve(__dirname, '../rrweb-snapshot/src/index.ts')], ['rrdom', path.resolve(__dirname, '../rrdom/src/index.ts')], ]); From 7c679a1dac560278638ed086986c964f1b3e40fd Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 19 May 2026 13:06:55 +0200 Subject: [PATCH 4/5] chore: add changeset for record tree-shaking --- .changeset/record-tree-shake-postcss.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/record-tree-shake-postcss.md diff --git a/.changeset/record-tree-shake-postcss.md b/.changeset/record-tree-shake-postcss.md new file mode 100644 index 0000000000..d05ef3c938 --- /dev/null +++ b/.changeset/record-tree-shake-postcss.md @@ -0,0 +1,7 @@ +--- +"@rrweb/record": patch +"rrweb": patch +"rrweb-snapshot": patch +--- + +Tree-shake replay-only `postcss` code from the `@rrweb/record` bundle. From c9afb6bc0bea3044a6ddcffcc0e15b35646c8ab3 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 19 May 2026 14:26:50 +0200 Subject: [PATCH 5/5] refactor: add rrweb-snapshot utility boundaries --- .../0001-record-source-boundary-resolver.md | 4 ++++ packages/rrweb-snapshot/src/index.ts | 2 ++ packages/rrweb-snapshot/src/rebuild-utils.ts | 14 ++++++++++++ packages/rrweb-snapshot/src/rebuild.ts | 2 +- packages/rrweb-snapshot/src/snapshot-utils.ts | 22 +++++++++++++++++++ packages/rrweb-snapshot/src/snapshot.ts | 2 +- packages/rrweb-snapshot/src/utils.ts | 12 ++++++++++ 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/rrweb-snapshot/src/rebuild-utils.ts create mode 100644 packages/rrweb-snapshot/src/snapshot-utils.ts diff --git a/docs/adr/0001-record-source-boundary-resolver.md b/docs/adr/0001-record-source-boundary-resolver.md index 210778a6f0..eb9b261cec 100644 --- a/docs/adr/0001-record-source-boundary-resolver.md +++ b/docs/adr/0001-record-source-boundary-resolver.md @@ -1,3 +1,7 @@ # Use a record-local source-boundary resolver `@rrweb/record` needs to build without replay-only `rrweb-snapshot` code such as `postcss`, but adding public snapshot/rebuild subpath exports would expand the package API and create compatibility obligations. We will keep the public package API unchanged and add a resolver scoped to the `@rrweb/record` build that maps selected bare package imports to local source entrypoints, restoring Rollup's module visibility for tree-shaking. + +## Follow-up Refactor + +`rrweb-snapshot` now has internal snapshot-domain and rebuild-domain utility entrypoints. These entrypoints currently re-export from the legacy shared `utils.ts` module, preserving public API compatibility while making a future split of snapshot-only, rebuild-only, and shared helpers incremental. diff --git a/packages/rrweb-snapshot/src/index.ts b/packages/rrweb-snapshot/src/index.ts index f1d4e680a4..970563a6a0 100644 --- a/packages/rrweb-snapshot/src/index.ts +++ b/packages/rrweb-snapshot/src/index.ts @@ -16,6 +16,8 @@ import rebuild, { createCache, } from './rebuild'; export * from './types'; +// Legacy broad export kept for compatibility. New internal imports should +// prefer snapshot-utils.ts / rebuild-utils.ts domain entrypoints. export * from './utils'; export { diff --git a/packages/rrweb-snapshot/src/rebuild-utils.ts b/packages/rrweb-snapshot/src/rebuild-utils.ts new file mode 100644 index 0000000000..da4a0f6704 --- /dev/null +++ b/packages/rrweb-snapshot/src/rebuild-utils.ts @@ -0,0 +1,14 @@ +/** + * Rebuild-domain utility entrypoint. + * + * Intent: + * - Keep rebuild.ts imports explicit and isolated from snapshot-only helpers. + * - Serve as a stable seam for future extraction into rebuild-only modules. + * - Preserve current public API behavior by re-exporting from legacy utils.ts. + */ +export { + isElement, + Mirror, + isNodeMetaEqual, + extractFileExtension, +} from './utils'; diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 692da2d281..44d61315c2 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -12,7 +12,7 @@ import { Mirror, isNodeMetaEqual, extractFileExtension, -} from './utils'; +} from './rebuild-utils'; import postcss from 'postcss'; const tagMap: tagMap = { diff --git a/packages/rrweb-snapshot/src/snapshot-utils.ts b/packages/rrweb-snapshot/src/snapshot-utils.ts new file mode 100644 index 0000000000..33455e212a --- /dev/null +++ b/packages/rrweb-snapshot/src/snapshot-utils.ts @@ -0,0 +1,22 @@ +/** + * Snapshot-domain utility entrypoint. + * + * Intent: + * - Keep snapshot.ts imports explicit and decoupled from rebuild concerns. + * - Serve as a stable seam for future extraction into snapshot-only modules. + * - Preserve current public API behavior by re-exporting from legacy utils.ts. + */ +export { + Mirror, + is2DCanvasBlank, + isElement, + isShadowRoot, + maskInputValue, + isNativeShadowDom, + stringifyStylesheet, + getInputType, + toLowerCase, + extractFileExtension, + absolutifyURLs, + markCssSplits, +} from './utils'; diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index b5426a1751..3d32a88a71 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -30,7 +30,7 @@ import { extractFileExtension, absolutifyURLs, markCssSplits, -} from './utils'; +} from './snapshot-utils'; import dom from '@rrweb/utils'; let _id = 1; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 418ce8230a..260ba6e47a 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -1,3 +1,15 @@ +/** + * Legacy shared utility module. + * + * This file currently contains helpers used by both snapshot and rebuild paths + * and is also part of the public API surface, re-exported from index.ts. + * + * Migration intent: + * - snapshot.ts should consume snapshot-domain helpers via snapshot-utils.ts + * - rebuild.ts should consume rebuild-domain helpers via rebuild-utils.ts + * - when safe, split internals into snapshot-only / rebuild-only / shared + * modules while keeping this module as a compatibility shim for external users + */ import type { idNodeMap, MaskInputFn,