diff --git a/.changeset/plugin-dependency-cleanup.md b/.changeset/plugin-dependency-cleanup.md new file mode 100644 index 0000000000..87bf084f5a --- /dev/null +++ b/.changeset/plugin-dependency-cleanup.md @@ -0,0 +1,15 @@ +--- +"rrweb": patch +"@rrweb/types": patch +"@rrweb/replay": patch +"@rrweb/rrweb-plugin-console-record": patch +"@rrweb/rrweb-plugin-console-replay": patch +"@rrweb/rrweb-plugin-sequential-id-record": patch +"@rrweb/rrweb-plugin-sequential-id-replay": patch +"@rrweb/rrweb-plugin-canvas-webrtc-record": patch +"@rrweb/rrweb-plugin-canvas-webrtc-replay": patch +"@rrweb/rrweb-plugin-network-record": patch +"@rrweb/rrweb-plugin-network-replay": patch +--- + +Clean up plugin dependency metadata for split record and replay host packages. diff --git a/docs/recipes/network.md b/docs/recipes/network.md index c7297e3364..797023c201 100644 --- a/docs/recipes/network.md +++ b/docs/recipes/network.md @@ -76,10 +76,10 @@ All options are described below: It is up to you to decide how to best replay your network events using the `onNetworkData` callback. ```js -import rrweb from 'rrweb'; +import { Replayer } from '@rrweb/replay'; import { getReplayNetworkPlugin } from '@rrweb/rrweb-plugin-network-replay'; -const replayer = new rrweb.Replayer(events, { +const replayer = new Replayer(events, { plugins: [ getReplayNetworkPlugin({ onNetworkData: ({ requests }) => { diff --git a/docs/recipes/plugin-api.md b/docs/recipes/plugin-api.md index e61be76eb0..b6fccf0464 100644 --- a/docs/recipes/plugin-api.md +++ b/docs/recipes/plugin-api.md @@ -18,18 +18,39 @@ The plugin API is designed to enable extending the functionality of rrweb withou Consistent with other functionality in rrweb, a plugin can implement record or replay or both features. ```ts -export type RecordPlugin = { +import type { RecordPlugin, ReplayPlugin } from '@rrweb/types'; +``` + +For reference, the public types exported by `@rrweb/types` are shaped as follows. Import these types rather than redeclaring them in application code. + +```ts +type RecordPlugin = { name: string; - observer: (cb: Function, options: TOptions) => listenerHandler; + observer?: ( + cb: (...args: Array) => void, + win: IWindow, + options: TOptions, + ) => listenerHandler; + eventProcessor?: (event: eventWithTime) => eventWithTime & TExtend; + getMirror?: (mirrors: { + nodeMirror: IMirror; + crossOriginIframeMirror: ICrossOriginIframeMirror; + crossOriginIframeStyleMirror: ICrossOriginIframeMirror; + }) => void; options: TOptions; }; -export type ReplayPlugin = { - handler: ( +type ReplayPlugin = { + handler?: ( event: eventWithTime, isSync: boolean, - context: { replayer: Replayer }, + context: { replayer: TReplayer }, + ) => void; + onBuild?: ( + node: Node | TNode, + context: { id: number; replayer: TReplayer }, ) => void; + getMirror?: (mirrors: { nodeMirror: TMirror }) => void; }; ``` @@ -41,10 +62,11 @@ Both record and replay plugins have a type interface. ```ts import { record } from '@rrweb/record'; +import type { RecordPlugin } from '@rrweb/types'; const exampleRecordPlugin: RecordPlugin<{ foo: string }> = { name: 'my-scope/example@1', - observer(cb, options) { + observer(cb, _win, options) { const timer = setInterval(() => { cb({ foo: options.foo, @@ -84,6 +106,7 @@ In this example, the record plugin will emit events like this: ```ts import { Replayer } from '@rrweb/replay'; +import { EventType, type ReplayPlugin } from '@rrweb/types'; const exampleReplayPlugin: ReplayPlugin = { handler(event, isSync, context) { diff --git a/docs/superpowers/plans/2026-06-27-plugin-dependencies.md b/docs/superpowers/plans/2026-06-27-plugin-dependencies.md new file mode 100644 index 0000000000..c2a570c31a --- /dev/null +++ b/docs/superpowers/plans/2026-06-27-plugin-dependencies.md @@ -0,0 +1,926 @@ +# Plugin Dependencies 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:** Clean up plugin package dependency metadata so plugins work with the split `rrweb`, `@rrweb/record`, `@rrweb/replay`, and `@rrweb/all` host entrypoints without misleading `rrweb` peer dependencies. + +**Architecture:** Move replay plugin typing to a host-neutral structural contract in `@rrweb/types`, keep existing `rrweb` exports as compatibility aliases, and make plugin packages depend on published-artifact imports instead of host peers. Runtime behavior and bundling policy stay unchanged. + +**Tech Stack:** Yarn v1 workspaces, TypeScript project references, Vite library builds, Changesets, npm pack smoke tests. + +--- + +## File Structure + +- Modify `packages/types/src/index.ts`: add the host-neutral `ReplayPlugin` contract next to `RecordPlugin`. +- Modify `packages/rrweb/src/types.ts`: import the new base replay type and preserve the concrete `rrweb` `ReplayPlugin` alias. +- Modify `packages/replay/src/index.ts`: export `ReplayPlugin` for `@rrweb/replay` users. +- Modify replay plugin sources: + - `packages/plugins/rrweb-plugin-console-replay/src/index.ts` + - `packages/plugins/rrweb-plugin-network-replay/src/index.ts` + - `packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts` + - `packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts` +- Modify canvas record source: + - `packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts` +- Modify all plugin `package.json` files under `packages/plugins/*`. +- Regenerate TypeScript project references: + - `tsconfig.json` + - `packages/plugins/*/tsconfig.json` +- Modify plugin READMEs under `packages/plugins/*/README.md`. +- Modify recipe docs: + - `docs/recipes/network.md` + - `docs/recipes/plugin-api.md` +- Create `scripts/check-plugin-host-smoke.mjs`: strict consumer smoke test for split host installs. +- Create `.changeset/plugin-dependency-cleanup.md`: patch changeset for affected published packages. + +--- + +### Task 1: Add Host-Neutral Replay Plugin Types + +**Files:** + +- Modify: `packages/types/src/index.ts` +- Modify: `packages/rrweb/src/types.ts` +- Modify: `packages/replay/src/index.ts` + +- [ ] **Step 1: Add `ReplayPlugin` to `@rrweb/types`** + +In `packages/types/src/index.ts`, insert this block immediately after `RecordPlugin`: + +```ts +export type ReplayPlugin< + TReplayer = unknown, + TNode = unknown, + TMirror = unknown, +> = { + handler?: ( + event: eventWithTime, + isSync: boolean, + context: { replayer: TReplayer }, + ) => void; + onBuild?: ( + node: Node | TNode, + context: { id: number; replayer: TReplayer }, + ) => void; + getMirror?: (mirrors: { nodeMirror: TMirror }) => void; +}; +``` + +- [ ] **Step 2: Run the focused type check** + +Run: + +```bash +yarn workspace @rrweb/types check-types +``` + +Expected: PASS. This type is self-contained and should not introduce cycles. + +- [ ] **Step 3: Change `rrweb` to use the base replay type** + +In `packages/rrweb/src/types.ts`, update the `@rrweb/types` import list to include the aliased base type: + +```ts + ReplayPlugin as BaseReplayPlugin, +``` + +Replace the existing concrete `ReplayPlugin` definition with: + +```ts +export type ReplayPlugin = BaseReplayPlugin; +``` + +Keep this existing line: + +```ts +export type { Replayer } from './replay'; +``` + +- [ ] **Step 4: Verify the concrete `rrweb` type still builds** + +Run: + +```bash +yarn workspace rrweb check-types +``` + +Expected: PASS. `import type { ReplayPlugin } from 'rrweb'` remains a supported public import. + +- [ ] **Step 5: Export `ReplayPlugin` from `@rrweb/replay`** + +In `packages/replay/src/index.ts`, change the import from `rrweb` to include the type: + +```ts +import { + Replayer, + type ReplayPlugin, + type playerConfig, + type PlayerMachineState, + type SpeedMachineState, +} from 'rrweb'; +``` + +Change the export block to: + +```ts +export { + Replayer, + type ReplayPlugin, + type playerConfig, + type PlayerMachineState, + type SpeedMachineState, +}; +``` + +- [ ] **Step 6: Verify the replay package type export** + +Run: + +```bash +yarn workspace @rrweb/replay check-types +``` + +Expected: PASS. + +- [ ] **Step 7: Commit the shared type surface** + +```bash +git add packages/types/src/index.ts packages/rrweb/src/types.ts packages/replay/src/index.ts +git commit -m "fix: add host-neutral replay plugin type" +``` + +--- + +### Task 2: Remove Plugin Type Imports From `rrweb` + +**Files:** + +- Modify: `packages/plugins/rrweb-plugin-console-replay/src/index.ts` +- Modify: `packages/plugins/rrweb-plugin-network-replay/src/index.ts` +- Modify: `packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts` +- Modify: `packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts` +- Modify: `packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts` + +- [ ] **Step 1: Update console replay imports and context type** + +In `packages/plugins/rrweb-plugin-console-replay/src/index.ts`, replace: + +```ts +import type { eventWithTime } from '@rrweb/types'; +import { EventType, IncrementalSource } from '@rrweb/types'; +import type { ReplayPlugin, Replayer } from 'rrweb'; +``` + +with: + +```ts +import type { eventWithTime, ReplayPlugin } from '@rrweb/types'; +import { EventType, IncrementalSource } from '@rrweb/types'; +``` + +Add this local structural context type after `type LogReplayConfig`: + +```ts +type ReplayerWithWarnings = { + config: { + showWarning: boolean; + }; +}; +``` + +Change the exported function type to: + +```ts +export const getReplayConsolePlugin: ( + options?: LogReplayConfig, +) => ReplayPlugin = (options) => { +``` + +Change the handler context type to: + +```ts + context: { replayer: ReplayerWithWarnings }, +``` + +- [ ] **Step 2: Update network replay to use the shared type** + +In `packages/plugins/rrweb-plugin-network-replay/src/index.ts`, replace: + +```ts +import type { eventWithTime, NetworkData } from '@rrweb/types'; +``` + +with: + +```ts +import type { eventWithTime, NetworkData, ReplayPlugin } from '@rrweb/types'; +``` + +Delete the local `type ReplayPlugin` block: + +```ts +type ReplayPlugin = { + handler?: (event: eventWithTime, isSync: boolean, context: unknown) => void; +}; +``` + +- [ ] **Step 3: Update sequential replay to use `@rrweb/types`** + +In `packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts`, replace: + +```ts +import type { ReplayPlugin } from 'rrweb'; +import type { eventWithTime } from '@rrweb/types'; +``` + +with: + +```ts +import type { eventWithTime, ReplayPlugin } from '@rrweb/types'; +``` + +- [ ] **Step 4: Update canvas replay to avoid concrete host types** + +In `packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts`, replace the first four imports: + +```ts +import type { RRNode } from 'rrdom'; +import type { Mirror } from 'rrweb-snapshot'; +import SimplePeer from 'simple-peer-light'; +import type { ReplayPlugin, Replayer } from 'rrweb'; +``` + +with: + +```ts +import type { ReplayPlugin } from '@rrweb/types'; +import SimplePeer from 'simple-peer-light'; +``` + +Add these structural types after the imports: + +```ts +type ReplayCanvasNode = Node | { nodeName: string }; + +type ReplayCanvasContext = { + id: number; + replayer: unknown; +}; + +type ReplayCanvasMirror = { + getNode(id: number): Node | null; +}; +``` + +Replace the `canvasFoundCallback` field type with: + +```ts + private canvasFoundCallback: ( + node: ReplayCanvasNode, + context: ReplayCanvasContext, + ) => void; +``` + +Replace the `mirror` field type with: + +```ts + private mirror: ReplayCanvasMirror | undefined; +``` + +Replace `initPlugin()` with: + +```ts + public initPlugin(): ReplayPlugin< + unknown, + ReplayCanvasNode, + ReplayCanvasMirror + > { + return { + onBuild: ( + node: ReplayCanvasNode, + context: ReplayCanvasContext, + ) => { + if (node.nodeName === 'CANVAS') { + this.canvasFoundCallback(node, context); + } + }, + getMirror: (options) => { + this.mirror = options.nodeMirror; + }, + }; + } +``` + +- [ ] **Step 5: Hide `simple-peer-light` from the canvas record public API** + +In `packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts`, add this structural type after `CrossOriginIframeMessageEventContent`: + +```ts +type WebRTCPeer = { + signal(signal: RTCSessionDescriptionInit): void; + send(data: string): void; + addStream(stream: MediaStream): void; + on(event: 'error', callback: (err: Error) => void): void; + on(event: 'close', callback: () => void): void; + on( + event: 'signal', + callback: (data: RTCSessionDescriptionInit) => void, + ): void; + on(event: 'connect', callback: () => void): void; + on(event: 'data', callback: (data: SimplePeer.SimplePeerData) => void): void; + on(event: 'stream', callback: (stream: MediaStream) => void): void; +}; +``` + +Replace every class-member `SimplePeer.Instance` type in that file with `WebRTCPeer`. + +Keep the runtime import: + +```ts +import SimplePeer from 'simple-peer-light'; +``` + +- [ ] **Step 6: Run plugin type checks** + +Run: + +```bash +yarn workspace @rrweb/rrweb-plugin-console-replay check-types +yarn workspace @rrweb/rrweb-plugin-network-replay check-types +yarn workspace @rrweb/rrweb-plugin-sequential-id-replay check-types +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-replay check-types +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-record check-types +``` + +Expected: all commands PASS. + +- [ ] **Step 7: Assert plugin source no longer imports types from `rrweb`** + +Run: + +```bash +rg -n "from 'rrweb'|from \"rrweb\"" packages/plugins -g '*.ts' -g '!test/**' +``` + +Expected: no matches outside package-local tests. + +- [ ] **Step 8: Commit the plugin source cleanup** + +```bash +git add packages/plugins/rrweb-plugin-console-replay/src/index.ts \ + packages/plugins/rrweb-plugin-network-replay/src/index.ts \ + packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts \ + packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts \ + packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts +git commit -m "fix: remove rrweb type imports from plugins" +``` + +--- + +### Task 3: Fix Plugin Manifests and Project References + +**Files:** + +- Modify: `packages/plugins/*/package.json` +- Modify: `tsconfig.json` +- Modify: `packages/plugins/*/tsconfig.json` + +- [ ] **Step 1: Update canvas WebRTC record manifest** + +In `packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json`, replace the current `devDependencies` and `peerDependencies` block with: + +```json + "dependencies": { + "@rrweb/types": "^2.0.1", + "simple-peer-light": "^9.10.0" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 2: Update canvas WebRTC replay manifest** + +In `packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json`, replace the current `devDependencies` and `peerDependencies` block with: + +```json + "dependencies": { + "@rrweb/types": "^2.0.1", + "simple-peer-light": "^9.10.0" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 3: Update console record manifest** + +In `packages/plugins/rrweb-plugin-console-record/package.json`, replace `peerDependencies` with `dependencies`: + +```json + "dependencies": { + "@rrweb/types": "^2.0.1", + "@rrweb/utils": "^2.0.1" + }, +``` + +Keep `rrweb` in `devDependencies` because `test/html/index.ts` imports it. + +- [ ] **Step 4: Update console replay manifest** + +In `packages/plugins/rrweb-plugin-console-replay/package.json`, replace the current `devDependencies` and `peerDependencies` block with: + +```json + "dependencies": { + "@rrweb/rrweb-plugin-console-record": "^2.0.1", + "@rrweb/types": "^2.0.1" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 5: Update network record manifest** + +In `packages/plugins/rrweb-plugin-network-record/package.json`, replace the current dependency blocks with: + +```json + "dependencies": { + "@rrweb/types": "^2.0.1", + "@rrweb/utils": "^2.0.1" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1", + "vitest": "^1.4.0" + } +``` + +- [ ] **Step 6: Update network replay manifest** + +In `packages/plugins/rrweb-plugin-network-replay/package.json`, replace the current dependency blocks with: + +```json + "dependencies": { + "@rrweb/rrweb-plugin-network-record": "^2.0.1", + "@rrweb/types": "^2.0.1" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 7: Update sequential ID record manifest** + +In `packages/plugins/rrweb-plugin-sequential-id-record/package.json`, replace the current dependency blocks with: + +```json + "dependencies": { + "@rrweb/types": "^2.0.1" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 8: Update sequential ID replay manifest** + +In `packages/plugins/rrweb-plugin-sequential-id-replay/package.json`, replace the current dependency blocks with: + +```json + "dependencies": { + "@rrweb/rrweb-plugin-sequential-id-record": "^2.0.1", + "@rrweb/types": "^2.0.1" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^6.0.1", + "vite-plugin-dts": "^3.9.1" + } +``` + +- [ ] **Step 9: Regenerate TypeScript project references** + +Run: + +```bash +yarn references:update +``` + +Expected: `tsconfig.json` and plugin `tsconfig.json` references update to reflect direct package dependencies. + +- [ ] **Step 10: Confirm no plugin manifest has host peers** + +Run: + +```bash +jq -r 'select(.peerDependencies != null) | .name + " " + (.peerDependencies | keys | join(","))' packages/plugins/*/package.json +``` + +Expected: no output. + +- [ ] **Step 11: Commit manifests and references** + +```bash +git add packages/plugins/*/package.json tsconfig.json packages/plugins/*/tsconfig.json +git commit -m "fix: declare plugin package dependencies" +``` + +--- + +### Task 4: Update Plugin Docs, Recipe Docs, and Changeset + +**Files:** + +- Modify: `packages/plugins/*/README.md` +- Modify: `docs/recipes/network.md` +- Modify: `docs/recipes/plugin-api.md` +- Create: `.changeset/plugin-dependency-cleanup.md` + +- [ ] **Step 1: Add record host compatibility text** + +Add this paragraph after the opening description in these files: + +- `packages/plugins/rrweb-plugin-console-record/README.md` +- `packages/plugins/rrweb-plugin-network-record/README.md` +- `packages/plugins/rrweb-plugin-sequential-id-record/README.md` +- `packages/plugins/rrweb-plugin-canvas-webrtc-record/README.md` + +```md +Use this plugin with one compatible recording host: `rrweb`, `@rrweb/record`, or `@rrweb/all`. +``` + +- [ ] **Step 2: Add replay host compatibility text** + +Add this paragraph after the opening description in these files: + +- `packages/plugins/rrweb-plugin-console-replay/README.md` +- `packages/plugins/rrweb-plugin-network-replay/README.md` +- `packages/plugins/rrweb-plugin-sequential-id-replay/README.md` +- `packages/plugins/rrweb-plugin-canvas-webrtc-replay/README.md` + +```md +Use this plugin with one compatible replay host: `rrweb`, `@rrweb/replay`, or `@rrweb/all`. +``` + +- [ ] **Step 3: Update the network replay recipe to use `@rrweb/replay`** + +In `docs/recipes/network.md`, replace: + +```js +import rrweb from 'rrweb'; +import { getReplayNetworkPlugin } from '@rrweb/rrweb-plugin-network-replay'; + +const replayer = new rrweb.Replayer(events, { +``` + +with: + +```js +import { Replayer } from '@rrweb/replay'; +import { getReplayNetworkPlugin } from '@rrweb/rrweb-plugin-network-replay'; + +const replayer = new Replayer(events, { +``` + +- [ ] **Step 4: Update the plugin API recipe imports** + +In `docs/recipes/plugin-api.md`, add this import before the interface examples: + +```ts +import type { RecordPlugin, ReplayPlugin } from '@rrweb/types'; +``` + +Replace the displayed replay interface with the generic host-neutral shape: + +```ts +export type ReplayPlugin< + TReplayer = unknown, + TNode = unknown, + TMirror = unknown, +> = { + handler?: ( + event: eventWithTime, + isSync: boolean, + context: { replayer: TReplayer }, + ) => void; + onBuild?: ( + node: Node | TNode, + context: { id: number; replayer: TReplayer }, + ) => void; + getMirror?: (mirrors: { nodeMirror: TMirror }) => void; +}; +``` + +- [ ] **Step 5: Add the changeset** + +Create `.changeset/plugin-dependency-cleanup.md` with this content: + +```md +--- +'rrweb': patch +'@rrweb/types': patch +'@rrweb/replay': patch +'@rrweb/rrweb-plugin-console-record': patch +'@rrweb/rrweb-plugin-console-replay': patch +'@rrweb/rrweb-plugin-sequential-id-record': patch +'@rrweb/rrweb-plugin-sequential-id-replay': patch +'@rrweb/rrweb-plugin-canvas-webrtc-record': patch +'@rrweb/rrweb-plugin-canvas-webrtc-replay': patch +'@rrweb/rrweb-plugin-network-record': patch +'@rrweb/rrweb-plugin-network-replay': patch +--- + +Clean up plugin dependency metadata for split record and replay host packages. +``` + +- [ ] **Step 6: Run Markdown formatting on touched docs** + +Run: + +```bash +yarn prettier --write \ + docs/recipes/network.md \ + docs/recipes/plugin-api.md \ + packages/plugins/rrweb-plugin-*/README.md \ + .changeset/plugin-dependency-cleanup.md +``` + +Expected: files are formatted. + +- [ ] **Step 7: Commit docs and changeset** + +```bash +git add docs/recipes/network.md docs/recipes/plugin-api.md \ + packages/plugins/*/README.md .changeset/plugin-dependency-cleanup.md +git commit -m "docs: document plugin host compatibility" +``` + +--- + +### Task 5: Add Split-Host Consumer Smoke Tests + +**Files:** + +- Create: `scripts/check-plugin-host-smoke.mjs` +- Modify: `package.json` + +- [ ] **Step 1: Create the smoke-test script** + +Create `scripts/check-plugin-host-smoke.mjs` with these acceptance criteria: + +- assert every required package has a built `dist` directory before packing; +- scan every plugin package's built `.d.ts` and `.d.cts` files and fail if a plugin declaration imports `rrweb`, `rrdom`, or `rrweb-snapshot`; +- pack `@rrweb/types`, `@rrweb/utils`, `rrdom`, `rrweb-snapshot`, `rrweb`, `@rrweb/record`, `@rrweb/replay`, and all eight plugin packages with `npm pack --json --ignore-scripts`; +- create a strict throwaway `@rrweb/record` consumer that imports `@rrweb/record` plus the console, network, sequential-id, and canvas WebRTC record plugins; +- create a strict throwaway `@rrweb/replay` consumer that imports `@rrweb/replay` plus the console, network, sequential-id, and canvas WebRTC replay plugins and their paired record plugin dependencies; +- install each consumer with `npm install --ignore-scripts` and compile it with `npx tsc --noEmit`; +- use `skipLibCheck: true` in the throwaway consumers so existing host-package declaration internals do not hide the plugin entrypoint signal; +- clean up the temporary directory in a `finally` block. + +- [ ] **Step 2: Add a package script** + +In root `package.json`, add this script after `check-types`: + +```json +"check:plugin-hosts": "node scripts/check-plugin-host-smoke.mjs", +``` + +- [ ] **Step 3: Run the script before builds to verify the guard message** + +Run: + +```bash +yarn check:plugin-hosts +``` + +Expected if any required `dist` directory is missing: + +```text +Missing .../dist. Run the required package builds before this smoke test. +``` + +Expected if previous builds already exist: the script installs both consumers and prints: + +```text +Plugin host smoke tests passed. +``` + +- [ ] **Step 4: Commit the smoke test** + +```bash +git add scripts/check-plugin-host-smoke.mjs package.json +git commit -m "test: add plugin host smoke checks" +``` + +--- + +### Task 6: Build, Inspect Artifacts, and Verify + +**Files:** + +- Inspect generated `dist` files only. +- No source files should be edited in this task unless a verification step fails. + +- [ ] **Step 1: Build shared packages and affected plugins** + +Run: + +```bash +yarn workspace @rrweb/types build +yarn workspace rrweb build +yarn workspace @rrweb/replay build +yarn workspace @rrweb/rrweb-plugin-console-record build +yarn workspace @rrweb/rrweb-plugin-console-replay build +yarn workspace @rrweb/rrweb-plugin-sequential-id-record build +yarn workspace @rrweb/rrweb-plugin-sequential-id-replay build +yarn workspace @rrweb/rrweb-plugin-network-record build +yarn workspace @rrweb/rrweb-plugin-network-replay build +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-record build +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-replay build +``` + +Expected: all commands PASS. + +- [ ] **Step 2: Assert no plugin declaration imports from `rrweb`** + +Run: + +```bash +rg -n "from 'rrweb'|from \"rrweb\"" packages/plugins/*/dist -g '*.d.ts' -g '*.d.cts' +``` + +Expected: no output. + +- [ ] **Step 3: Inspect runtime bundle policy** + +Run: + +```bash +rg -n "simple-peer-light|@rrweb/utils|@rrweb/types|rrweb-plugin-console-record" packages/plugins/*/dist -g '*.js' -g '*.cjs' +``` + +Expected: use the output to confirm runtime imports are either bundled as before or left as imports with matching `dependencies`. Do not externalize runtime imports in this PR. + +- [ ] **Step 4: Run type checks** + +Run: + +```bash +yarn workspace @rrweb/types check-types +yarn workspace rrweb check-types +yarn workspace @rrweb/replay check-types +yarn workspace @rrweb/rrweb-plugin-console-record check-types +yarn workspace @rrweb/rrweb-plugin-console-replay check-types +yarn workspace @rrweb/rrweb-plugin-sequential-id-record check-types +yarn workspace @rrweb/rrweb-plugin-sequential-id-replay check-types +yarn workspace @rrweb/rrweb-plugin-network-record check-types +yarn workspace @rrweb/rrweb-plugin-network-replay check-types +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-record check-types +yarn workspace @rrweb/rrweb-plugin-canvas-webrtc-replay check-types +``` + +Expected: all commands PASS. + +- [ ] **Step 5: Run the split-host smoke tests** + +Run: + +```bash +yarn check:plugin-hosts +``` + +Expected: + +```text +Plugin host smoke tests passed. +``` + +- [ ] **Step 6: Run focused plugin tests** + +Run: + +```bash +yarn workspace @rrweb/rrweb-plugin-console-record test +yarn workspace @rrweb/rrweb-plugin-network-record test +``` + +Expected: both commands PASS. + +- [ ] **Step 7: Return to the owning task for verification failures** + +If verification failed, stop this task and return to the task that owns the +failed file. For example, a failing plugin declaration import check belongs to +Task 2 or Task 3, and a failing smoke test belongs to Task 5. After fixing the +owning task, rerun Task 6 from Step 1. + +When verification passes, confirm the worktree state: + +```bash +git status --short +``` + +Expected: no source-file modifications remain unstaged. + +```bash +git status --short +``` + +prints no output. + +--- + +### Task 7: Final PR Preparation + +**Files:** + +- Use `git status` and PR body only. + +- [ ] **Step 1: Review the final diff** + +Run: + +```bash +git status --short +git log --oneline origin/main..HEAD +git diff --stat origin/main..HEAD +``` + +Expected: + +- commits are split by type surface, plugin source cleanup, manifests/references, docs/changeset, and smoke tests +- no unrelated files are changed + +- [ ] **Step 2: Update the PR body** + +Edit the PR body to include the three options and final validation. Use: + +```bash +gh pr edit --body-file /tmp/plugin-dependency-pr-body.md +``` + +The body must include: + +```md +## Options considered + +### Option 1: No host peer + +Pros: + +- supports all valid host entrypoints +- avoids false `rrweb` peer warnings for split-package users +- keeps manifests focused on published-artifact imports + +Cons: + +- no install-time warning when a plugin is installed without a host +- compatibility is enforced through docs, TypeScript, and smoke tests + +Chosen because npm cannot express one of `rrweb`, `@rrweb/record`, `@rrweb/replay`, or `@rrweb/all`. + +### Option 2: Optional host peers + +Pros: + +- keeps host compatibility visible in metadata +- avoids hard install failures + +Cons: + +- does not require that any compatible host is installed +- creates noisy metadata and still does not fix undeclared imports + +### Option 3: Strict host peer + +Pros: + +- gives clear feedback when there is one canonical host +- familiar plugin-package model + +Cons: + +- no longer matches rrweb's package split +- forces or warns about `rrweb` for valid split-package consumers +``` + +- [ ] **Step 3: Push the implementation branch** + +Run: + +```bash +git push +``` + +Expected: branch updates successfully. diff --git a/docs/superpowers/specs/2026-06-27-plugin-dependencies-design.md b/docs/superpowers/specs/2026-06-27-plugin-dependencies-design.md new file mode 100644 index 0000000000..472d5d4e25 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-plugin-dependencies-design.md @@ -0,0 +1,156 @@ +# Plugin Dependency Design + +## Context + +The plugin packages currently declare `rrweb` as a peer dependency. That was a reasonable shape when `rrweb` was the only obvious host entrypoint, but the package split now makes `@rrweb/record`, `@rrweb/replay`, and `@rrweb/all` first-class ways to use the same plugin APIs. + +That creates two problems: + +- A hard `rrweb` peer tells consumers that `rrweb` is required even when they use `@rrweb/record`, `@rrweb/replay`, or `@rrweb/all`. +- Some plugin manifests hide real dependencies. Examples include `simple-peer-light` in the canvas WebRTC plugins, paired record plugins imported by replay plugins, and `@rrweb/utils` imported by record plugins. + +The goal is to make plugin package metadata match the package split and the published artifacts without breaking existing public imports. + +## Goals + +- Remove misleading host package peers from plugin packages. +- Keep `rrweb`, `@rrweb/record`, `@rrweb/replay`, and `@rrweb/all` valid host entrypoints. +- Preserve existing public type imports from `rrweb`, especially `import type { ReplayPlugin } from 'rrweb'`. +- Make replay plugin typing available without forcing plugin packages to import types from the monolithic `rrweb` entrypoint. +- Declare every dependency needed by published JavaScript or published declarations. +- Document the trade-off between no host peers, optional host peers, and strict host peers in the PR description. + +## Non-Goals + +- Do not require plugin consumers to install a specific host package. +- Do not remove any existing public exports from `rrweb`. +- Do not change runtime plugin behavior. +- Do not add a broad dependency-policy checker unless it is dist-aware and small. +- Do not externalize plugin runtime imports as part of this change. + +## Proposed Approach + +Use the no-host-peer option. + +Plugin packages should not declare `rrweb` as a required peer. The host relationship is a compatibility contract documented in README and recipe docs, not a package-manager peer contract, because npm cannot express "one of `rrweb`, `@rrweb/record`, `@rrweb/replay`, or `@rrweb/all`." + +The manifests should instead declare the packages required by the plugin's published output: + +- Published JavaScript imports left in `dist` or `umd` need `dependencies`. +- Published declaration imports left in `dist/*.d.ts` or `dist/*.d.cts` need `dependencies`. +- Source-only build, test, and example imports stay in `devDependencies`. +- Host peers such as `rrweb` should be removed from plugin packages. +- Non-host peers that are actually imported, such as `@rrweb/utils` or `@rrweb/types`, should move to `dependencies` when the published artifacts need them. + +The build currently bundles many runtime imports. This change should not change that bundling policy. The implementation should verify built JavaScript separately from built declarations. + +## Type Surface + +`RecordPlugin` already lives in `@rrweb/types`. Replay plugins are less clean today: `ReplayPlugin` is exported from `rrweb`, and some replay plugins import it from `rrweb`. + +Do not move the existing concrete `ReplayPlugin` type into `@rrweb/types` as-is. It references concrete replay types such as `Replayer`, plus `RRNode` and `Mirror`, which risks dependency cycles with `rrweb`, `rrdom`, and `rrweb-snapshot`. + +Instead: + +1. Add a host-neutral replay plugin contract to `@rrweb/types`. +2. Keep `rrweb` exporting `ReplayPlugin` for backwards compatibility. +3. Export `ReplayPlugin` from `@rrweb/replay` so replay-entrypoint users have a first-class type import. +4. Update replay plugin source so emitted plugin declarations no longer import types from `rrweb`. + +The acceptance check is not just source cleanup. Built plugin declarations should not contain `from 'rrweb'`. + +## Documentation + +Update plugin documentation so package-manager peers are not the only compatibility signal. + +Record plugin READMEs should say that the plugin works with one compatible recording host: + +- `rrweb` +- `@rrweb/record` +- `@rrweb/all` + +Replay plugin READMEs should say that the plugin works with one compatible replay host: + +- `rrweb` +- `@rrweb/replay` +- `@rrweb/all` + +Recipe docs that still imply `rrweb` is the only host should be updated when they touch the affected plugins. + +The PR description must include the three options considered: + +1. No host peer. +2. Optional host peers. +3. Strict host peer. + +It should list pros and cons for each option and explain that no host peer was chosen because it matches the first-class split packages and avoids false package-manager warnings. + +## PR Option Summary + +### Option 1: No Host Peer + +Pros: + +- Supports all valid host entrypoints without false warnings. +- Avoids forcing `rrweb` into projects that intentionally use `@rrweb/record`, `@rrweb/replay`, or `@rrweb/all`. +- Lets plugin manifests focus on actual published-artifact imports. +- Works better with strict package managers once declarations no longer reference undeclared packages. + +Cons: + +- Package managers will not warn when a user installs a plugin without any compatible host package. +- Compatibility is enforced through documentation, TypeScript shape, and tests rather than peer metadata. + +### Option 2: Optional Host Peers + +Pros: + +- Makes host compatibility visible in package metadata. +- Avoids hard install failures when peers are marked optional. + +Cons: + +- Does not enforce that at least one compatible host is installed. +- Multiple optional host peers can imply every host is relevant to every plugin. +- Keeps noisy metadata without fixing real undeclared imports. + +### Option 3: Strict Host Peer + +Pros: + +- Gives clear install-time feedback when there is one canonical host. +- Matches a familiar plugin pattern. + +Cons: + +- No longer models rrweb's package split. +- Produces false warnings or forced installs for `@rrweb/record`, `@rrweb/replay`, and `@rrweb/all` users. +- Encourages accidental coupling back to the monolithic entrypoint. + +## Testing + +Focused verification should cover both package metadata and published artifacts. + +- Run type checks for changed plugin packages and shared type packages. +- Build affected plugin packages. +- Inspect built plugin declarations and assert they do not import from `rrweb`. +- Inspect built JavaScript and declarations separately to decide which packages must be dependencies. +- Run `yarn references:update` after manifest changes because project references are generated from workspace metadata. +- Add changesets for published packages whose manifests or public type surfaces change. + +Add strict-consumer smoke tests: + +- Pack and install one record plugin with `@rrweb/record` only, then compile a tiny TypeScript import. +- Pack and install one replay plugin with `@rrweb/replay` only, then compile a tiny TypeScript import. + +These smoke tests are important because Yarn v1 workspaces can mask undeclared dependencies through hoisting. + +## Risks + +The largest design risk is the replay type boundary. Moving the concrete `ReplayPlugin` type directly into `@rrweb/types` would create a poor dependency direction. The implementation should use a structural host-neutral contract instead. + +The second risk is relying on source imports rather than built artifacts. These packages publish `dist`, `umd`, and `package.json`, not source files. The implementation must inspect emitted JavaScript and declarations. + +The third risk is the canvas WebRTC plugin public type surface. `simple-peer-light` is currently undeclared, and `SimplePeer.Instance` leaks through constructor types. The implementation should either make that public type reliable through dependencies and emitted declarations or hide it behind a local structural type. + +Removing host peers also removes install-time compatibility warnings. Documentation and smoke tests need to compensate for that trade-off. diff --git a/package.json b/package.json index 3bd9945806..136b6faedd 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "test:watch": "yarn turbo run test:watch", "test:update": "yarn turbo run test:update", "check-types": "yarn turbo run check-types --continue", + "check:plugin-hosts": "node scripts/check-plugin-host-smoke.mjs", "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 --concurrency=18", diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/README.md b/packages/plugins/rrweb-plugin-canvas-webrtc-record/README.md index cc25f23a11..ffc0a28d16 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/README.md +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/README.md @@ -2,6 +2,8 @@ Plugin that live streams contents of canvas elements via webrtc +Use this plugin with one compatible recording host: `rrweb`, `@rrweb/record`, or `@rrweb/all`. + ## Example of live streaming via `yarn live-stream` https://user-images.githubusercontent.com/4106/186701616-fd71a107-5d53-423c-ba09-0395a3a0252f.mov diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json index f455862d62..65bb092a17 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/package.json @@ -47,13 +47,13 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", + "dependencies": { + "@rrweb/types": "^2.1.0", + "simple-peer-light": "^9.10.0" + }, "devDependencies": { - "rrweb": "^2.1.0", "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts index 6390468e08..835368d07d 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/src/index.ts @@ -13,7 +13,7 @@ export type CrossOriginIframeMessageEventContent = { data: | { type: 'signal'; - signal: RTCSessionDescriptionInit; + signal: WebRTCSignal; } | { type: 'who-has-canvas'; @@ -26,8 +26,39 @@ export type CrossOriginIframeMessageEventContent = { }; }; +type WebRTCSignal = + | string + | RTCSessionDescriptionInit + | { + type: 'candidate'; + candidate: RTCIceCandidateInit; + } + | { + type: 'renegotiate'; + renegotiate: true; + } + | { + type: 'transceiverRequest'; + transceiverRequest: { + kind: string; + init?: RTCRtpTransceiverInit; + }; + }; + +type WebRTCPeer = { + signal(signal: WebRTCSignal): void; + send(data: string): void; + addStream(stream: MediaStream): void; + on(event: 'error', callback: (err: Error) => void): void; + on(event: 'close', callback: () => void): void; + on(event: 'signal', callback: (data: WebRTCSignal) => void): void; + on(event: 'connect', callback: () => void): void; + on(event: 'data', callback: (data: unknown) => void): void; + on(event: 'stream', callback: (stream: MediaStream) => void): void; +}; + export class RRWebPluginCanvasWebRTCRecord { - private peer: SimplePeer.Instance | null = null; + private peer: WebRTCPeer | null = null; private mirror: IMirror | undefined; private crossOriginIframeMirror: ICrossOriginIframeMirror | undefined; private streamMap: Map = new Map(); @@ -35,16 +66,16 @@ export class RRWebPluginCanvasWebRTCRecord { private outgoingStreams = new Set(); private streamNodeMap = new Map(); private canvasWindowMap = new Map(); - private windowPeerMap = new WeakMap(); - private peerWindowMap = new WeakMap(); - private signalSendCallback: (msg: RTCSessionDescriptionInit) => void; + private windowPeerMap = new WeakMap(); + private peerWindowMap = new WeakMap(); + private signalSendCallback: (msg: WebRTCSignal) => void; constructor({ signalSendCallback, peer, }: { signalSendCallback: RRWebPluginCanvasWebRTCRecord['signalSendCallback']; - peer?: SimplePeer.Instance; + peer?: WebRTCPeer; }) { this.signalSendCallback = signalSendCallback; window.addEventListener('message', (event: MessageEvent) => @@ -63,13 +94,13 @@ export class RRWebPluginCanvasWebRTCRecord { }; } - public signalReceive(signal: RTCSessionDescriptionInit) { + public signalReceive(signal: WebRTCSignal) { if (!this.peer) this.setupPeer(); this.peer?.signal(signal); } public signalReceiveFromCrossOriginIframe( - signal: RTCSessionDescriptionInit, + signal: WebRTCSignal, source: WindowProxy, ) { const peer = this.setupPeer(source); @@ -88,8 +119,8 @@ export class RRWebPluginCanvasWebRTCRecord { this.outgoingStreams.add(stream); } - public setupPeer(source?: WindowProxy): SimplePeer.Instance { - let peer: SimplePeer.Instance; + public setupPeer(source?: WindowProxy): WebRTCPeer { + let peer: WebRTCPeer; if (!source) { if (this.peer) return this.peer; @@ -128,7 +159,7 @@ export class RRWebPluginCanvasWebRTCRecord { console.log('closing'); }); - peer.on('signal', (data: RTCSessionDescriptionInit) => { + peer.on('signal', (data: WebRTCSignal) => { if (this.inRootFrame()) { if (peer === this.peer) { // connected to replayer @@ -175,9 +206,9 @@ export class RRWebPluginCanvasWebRTCRecord { if (!this.inRootFrame()) return peer; - peer.on('data', (data: SimplePeer.SimplePeerData) => { + peer.on('data', (data: unknown) => { try { - const json = JSON.parse(data as string) as WebRTCDataChannel; + const json = JSON.parse(String(data)) as WebRTCDataChannel; this.streamNodeMap.set(json.streamId, json.nodeId); } catch (error) { console.error('Could not parse data', error); diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json b/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json index 8ffb27ccca..d934f7add6 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-record/tsconfig.json @@ -1,14 +1,18 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "references": [ { - "path": "../../rrweb" + "path": "../../types" } ] } diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/README.md b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/README.md index de21b73722..b0ac5ab42d 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/README.md +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/README.md @@ -2,6 +2,8 @@ Plugin that live streams contents of canvas elements via webrtc +Use this plugin with one compatible replay host: `rrweb`, `@rrweb/replay`, or `@rrweb/all`. + ## Example of live streaming via `yarn live-stream` https://user-images.githubusercontent.com/4106/186701616-fd71a107-5d53-423c-ba09-0395a3a0252f.mov diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json index f0945b56b6..69e1f4eace 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/package.json @@ -47,13 +47,13 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", + "dependencies": { + "@rrweb/types": "^2.1.0", + "simple-peer-light": "^9.10.0" + }, "devDependencies": { - "rrweb": "^2.1.0", "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts index 7f9bb15d87..92832ce3fd 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/src/index.ts @@ -1,18 +1,56 @@ -import type { RRNode } from 'rrdom'; -import type { Mirror } from 'rrweb-snapshot'; import SimplePeer from 'simple-peer-light'; -import type { ReplayPlugin, Replayer } from 'rrweb'; +import type { ReplayPlugin } from '@rrweb/types'; import type { WebRTCDataChannel } from './types'; +type ReplayCanvasNode = Node | { nodeName: string }; + +type ReplayCanvasContext = { + id: number; + replayer: unknown; +}; + +type ReplayCanvasMirror = { + getNode(id: number): Node | null; +}; + +type WebRTCSignal = + | string + | RTCSessionDescriptionInit + | { + type: 'candidate'; + candidate: RTCIceCandidateInit; + } + | { + type: 'renegotiate'; + renegotiate: true; + } + | { + type: 'transceiverRequest'; + transceiverRequest: { + kind: string; + init?: RTCRtpTransceiverInit; + }; + }; + +type WebRTCPeer = { + signal(signal: WebRTCSignal): void; + on(event: 'error', callback: (err: Error) => void): void; + on(event: 'close', callback: () => void): void; + on(event: 'signal', callback: (data: WebRTCSignal) => void): void; + on(event: 'connect', callback: () => void): void; + on(event: 'data', callback: (data: unknown) => void): void; + on(event: 'stream', callback: (stream: MediaStream) => void): void; +}; + // TODO: restrict callback to real nodes only, or make sure callback gets called when real node gets added to dom as well export class RRWebPluginCanvasWebRTCReplay { private canvasFoundCallback: ( - node: Node | RRNode, - context: { id: number; replayer: Replayer }, + node: ReplayCanvasNode, + context: ReplayCanvasContext, ) => void; - private signalSendCallback: (signal: RTCSessionDescriptionInit) => void; - private mirror: Mirror | undefined; + private signalSendCallback: (signal: WebRTCSignal) => void; + private mirror: ReplayCanvasMirror | undefined; constructor({ canvasFoundCallback, @@ -25,12 +63,13 @@ export class RRWebPluginCanvasWebRTCReplay { this.signalSendCallback = signalSendCallback; } - public initPlugin(): ReplayPlugin { + public initPlugin(): ReplayPlugin< + ReplayCanvasContext['replayer'], + ReplayCanvasNode, + ReplayCanvasMirror + > { return { - onBuild: ( - node: Node | RRNode, - context: { id: number; replayer: Replayer }, - ) => { + onBuild: (node: ReplayCanvasNode, context: ReplayCanvasContext) => { if (node.nodeName === 'CANVAS') { this.canvasFoundCallback(node, context); } @@ -104,11 +143,11 @@ export class RRWebPluginCanvasWebRTCReplay { } } - private peer: SimplePeer.Instance | null = null; + private peer: WebRTCPeer | null = null; private streamNodeMap = new Map(); private streams = new Set(); private runningStreams = new WeakSet(); - public signalReceive(msg: RTCSessionDescriptionInit) { + public signalReceive(msg: WebRTCSignal) { if (!this.peer) { this.peer = new SimplePeer({ initiator: false, @@ -125,7 +164,7 @@ export class RRWebPluginCanvasWebRTCReplay { console.log('closing'); }); - this.peer.on('signal', (data: RTCSessionDescriptionInit) => { + this.peer.on('signal', (data: WebRTCSignal) => { this.signalSendCallback(data); }); @@ -133,9 +172,9 @@ export class RRWebPluginCanvasWebRTCReplay { // connected! }); - this.peer.on('data', (data: SimplePeer.SimplePeerData) => { + this.peer.on('data', (data: unknown) => { try { - const json = JSON.parse(data as string) as WebRTCDataChannel; + const json = JSON.parse(String(data)) as WebRTCDataChannel; this.streamNodeMap.set(json.streamId, json.nodeId); } catch (error) { console.error('Could not parse data', error); diff --git a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json index 8ffb27ccca..d934f7add6 100644 --- a/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json +++ b/packages/plugins/rrweb-plugin-canvas-webrtc-replay/tsconfig.json @@ -1,14 +1,18 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "references": [ { - "path": "../../rrweb" + "path": "../../types" } ] } diff --git a/packages/plugins/rrweb-plugin-console-record/README.md b/packages/plugins/rrweb-plugin-console-record/README.md index 906bade47c..e62deac84e 100644 --- a/packages/plugins/rrweb-plugin-console-record/README.md +++ b/packages/plugins/rrweb-plugin-console-record/README.md @@ -3,6 +3,8 @@ Please refer to the [console recipe](../../../docs/recipes/console.md) on how to use this plugin. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible recording host: `rrweb`, `@rrweb/record`, or `@rrweb/all`. + ## Sponsors [Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site. diff --git a/packages/plugins/rrweb-plugin-console-record/package.json b/packages/plugins/rrweb-plugin-console-record/package.json index b8efb4ff2a..9e38c861f8 100644 --- a/packages/plugins/rrweb-plugin-console-record/package.json +++ b/packages/plugins/rrweb-plugin-console-record/package.json @@ -49,6 +49,10 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", + "dependencies": { + "@rrweb/types": "^2.1.0", + "@rrweb/utils": "^2.1.0" + }, "devDependencies": { "rrweb": "^2.1.0", "typescript": "^5.4.5", @@ -56,9 +60,5 @@ "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0", "puppeteer": "^20.9.0" - }, - "peerDependencies": { - "rrweb": "^2.0.1", - "@rrweb/utils": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-console-record/tsconfig.json b/packages/plugins/rrweb-plugin-console-record/tsconfig.json index 4de4923989..bdd4de60f8 100644 --- a/packages/plugins/rrweb-plugin-console-record/tsconfig.json +++ b/packages/plugins/rrweb-plugin-console-record/tsconfig.json @@ -1,16 +1,27 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts", "vitest.config.ts", "test"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts", + "vitest.config.ts", + "test" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo", - // TODO: enable me in the future // at time of writing (April 2024) there are 6 errors in this package "strict": false }, "references": [ + { + "path": "../../types" + }, + { + "path": "../../utils" + }, { "path": "../../rrweb" } diff --git a/packages/plugins/rrweb-plugin-console-replay/README.md b/packages/plugins/rrweb-plugin-console-replay/README.md index 3c82938a2b..d2ee389971 100644 --- a/packages/plugins/rrweb-plugin-console-replay/README.md +++ b/packages/plugins/rrweb-plugin-console-replay/README.md @@ -3,6 +3,8 @@ Please refer to the [console recipe](../../../docs/recipes/console.md) on how to use this plugin. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible replay host: `rrweb`, `@rrweb/replay`, or `@rrweb/all`. + ## Sponsors [Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on Github with a link to your site. diff --git a/packages/plugins/rrweb-plugin-console-replay/package.json b/packages/plugins/rrweb-plugin-console-replay/package.json index e906d0ba3b..22fedc3636 100644 --- a/packages/plugins/rrweb-plugin-console-replay/package.json +++ b/packages/plugins/rrweb-plugin-console-replay/package.json @@ -47,14 +47,13 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", - "devDependencies": { + "dependencies": { "@rrweb/rrweb-plugin-console-record": "^2.1.0", - "rrweb": "^2.1.0", + "@rrweb/types": "^2.1.0" + }, + "devDependencies": { "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-console-replay/src/index.ts b/packages/plugins/rrweb-plugin-console-replay/src/index.ts index 31f664e52e..212d1d13a0 100644 --- a/packages/plugins/rrweb-plugin-console-replay/src/index.ts +++ b/packages/plugins/rrweb-plugin-console-replay/src/index.ts @@ -3,9 +3,8 @@ import { type LogData, PLUGIN_NAME, } from '@rrweb/rrweb-plugin-console-record'; -import type { eventWithTime } from '@rrweb/types'; +import type { eventWithTime, ReplayPlugin } from '@rrweb/types'; import { EventType, IncrementalSource } from '@rrweb/types'; -import type { ReplayPlugin, Replayer } from 'rrweb'; /** * define an interface to replay log records @@ -18,6 +17,12 @@ type LogReplayConfig = { replayLogger?: ReplayLogger; }; +type ReplayerWithWarnings = { + config: { + showWarning: boolean; + }; +}; + const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; type PatchedConsoleLog = { [ORIGINAL_ATTRIBUTE_NAME]: typeof console.log; @@ -111,7 +116,7 @@ class LogReplayPlugin { export const getReplayConsolePlugin: ( options?: LogReplayConfig, -) => ReplayPlugin = (options) => { +) => ReplayPlugin = (options) => { const replayLogger = options?.replayLogger || new LogReplayPlugin(options).getConsoleLogger(); @@ -119,7 +124,7 @@ export const getReplayConsolePlugin: ( handler( event: eventWithTime, _isSync: boolean, - context: { replayer: Replayer }, + context: { replayer: ReplayerWithWarnings }, ) { let logData: LogData | null = null; if ( diff --git a/packages/plugins/rrweb-plugin-console-replay/tsconfig.json b/packages/plugins/rrweb-plugin-console-replay/tsconfig.json index 195393c9ec..13d92da939 100644 --- a/packages/plugins/rrweb-plugin-console-replay/tsconfig.json +++ b/packages/plugins/rrweb-plugin-console-replay/tsconfig.json @@ -1,7 +1,13 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts", "dist", "tsconfig.json"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts", + "dist", + "tsconfig.json" + ], "compilerOptions": { "rootDir": ".", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" @@ -11,7 +17,7 @@ "path": "../rrweb-plugin-console-record" }, { - "path": "../../rrweb" + "path": "../../types" } ] } diff --git a/packages/plugins/rrweb-plugin-network-record/README.md b/packages/plugins/rrweb-plugin-network-record/README.md index d77c0c87bd..535864264a 100644 --- a/packages/plugins/rrweb-plugin-network-record/README.md +++ b/packages/plugins/rrweb-plugin-network-record/README.md @@ -3,6 +3,8 @@ Please refer to the [network recipe](../../../docs/recipes/network.md) on how to use this plugin. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible recording host: `rrweb`, `@rrweb/record`, or `@rrweb/all`. + ## Sponsors [Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on GitHub with a link to your site. diff --git a/packages/plugins/rrweb-plugin-network-record/package.json b/packages/plugins/rrweb-plugin-network-record/package.json index 0e8fda98f0..430057df0f 100644 --- a/packages/plugins/rrweb-plugin-network-record/package.json +++ b/packages/plugins/rrweb-plugin-network-record/package.json @@ -46,18 +46,14 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", - "devDependencies": { + "dependencies": { "@rrweb/types": "^2.1.0", - "@rrweb/utils": "^2.1.0", - "rrweb": "^2.1.0", + "@rrweb/utils": "^2.1.0" + }, + "devDependencies": { "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1", "vitest": "^1.4.0" - }, - "peerDependencies": { - "rrweb": "^2.1.0", - "@rrweb/types": "^2.1.0", - "@rrweb/utils": "^2.1.0" } } diff --git a/packages/plugins/rrweb-plugin-network-replay/README.md b/packages/plugins/rrweb-plugin-network-replay/README.md index 3618e80783..520657f72c 100644 --- a/packages/plugins/rrweb-plugin-network-replay/README.md +++ b/packages/plugins/rrweb-plugin-network-replay/README.md @@ -3,6 +3,8 @@ Please refer to the [network recipe](../../../docs/recipes/network.md) on how to use this plugin. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible replay host: `rrweb`, `@rrweb/replay`, or `@rrweb/all`. + ## Sponsors [Become a sponsor](https://opencollective.com/rrweb#sponsor) and get your logo on our README on GitHub with a link to your site. diff --git a/packages/plugins/rrweb-plugin-network-replay/package.json b/packages/plugins/rrweb-plugin-network-replay/package.json index 811792dcb9..944e763ec4 100644 --- a/packages/plugins/rrweb-plugin-network-replay/package.json +++ b/packages/plugins/rrweb-plugin-network-replay/package.json @@ -44,16 +44,13 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", - "devDependencies": { + "dependencies": { "@rrweb/rrweb-plugin-network-record": "^2.1.0", - "@rrweb/types": "^2.1.0", - "rrweb": "^2.1.0", + "@rrweb/types": "^2.1.0" + }, + "devDependencies": { "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.1.0", - "@rrweb/types": "^2.1.0" } } diff --git a/packages/plugins/rrweb-plugin-network-replay/src/index.ts b/packages/plugins/rrweb-plugin-network-replay/src/index.ts index 554ac929c6..c8f14bbfad 100644 --- a/packages/plugins/rrweb-plugin-network-replay/src/index.ts +++ b/packages/plugins/rrweb-plugin-network-replay/src/index.ts @@ -1,11 +1,7 @@ -import type { eventWithTime, NetworkData } from '@rrweb/types'; +import type { eventWithTime, NetworkData, ReplayPlugin } from '@rrweb/types'; import { EventType } from '@rrweb/types'; import { PLUGIN_NAME } from '@rrweb/rrweb-plugin-network-record'; -type ReplayPlugin = { - handler?: (event: eventWithTime, isSync: boolean, context: unknown) => void; -}; - export type OnNetworkData = (data: NetworkData) => void; export type NetworkReplayOptions = { diff --git a/packages/plugins/rrweb-plugin-network-replay/tsconfig.json b/packages/plugins/rrweb-plugin-network-replay/tsconfig.json index dd9ef0fc92..0c01994217 100644 --- a/packages/plugins/rrweb-plugin-network-replay/tsconfig.json +++ b/packages/plugins/rrweb-plugin-network-replay/tsconfig.json @@ -1,17 +1,22 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts", "test"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts", + "test" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "references": [ { - "path": "../../types" + "path": "../rrweb-plugin-network-record" }, { - "path": "../rrweb-plugin-network-record" + "path": "../../types" } ] } diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/README.md b/packages/plugins/rrweb-plugin-sequential-id-record/README.md index e258a76e3a..946db9511c 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-record/README.md +++ b/packages/plugins/rrweb-plugin-sequential-id-record/README.md @@ -3,6 +3,8 @@ Use this plugin in combination with the [@rrweb/rrweb-plugin-sequential-id-replay](../rrweb-plugin-sequential-id-replay) plugin to record and replay events with a sequential id. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible recording host: `rrweb`, `@rrweb/record`, or `@rrweb/all`. + ## Installation ```bash diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/package.json b/packages/plugins/rrweb-plugin-sequential-id-record/package.json index 284abaa5b0..5d222ef78e 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-record/package.json +++ b/packages/plugins/rrweb-plugin-sequential-id-record/package.json @@ -47,13 +47,12 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", + "dependencies": { + "@rrweb/types": "^2.1.0" + }, "devDependencies": { - "rrweb": "^2.1.0", "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-sequential-id-record/tsconfig.json b/packages/plugins/rrweb-plugin-sequential-id-record/tsconfig.json index 8ffb27ccca..d934f7add6 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-record/tsconfig.json +++ b/packages/plugins/rrweb-plugin-sequential-id-record/tsconfig.json @@ -1,14 +1,18 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts" + ], "compilerOptions": { "rootDir": "src", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "references": [ { - "path": "../../rrweb" + "path": "../../types" } ] } diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/README.md b/packages/plugins/rrweb-plugin-sequential-id-replay/README.md index 53914e63fa..f128a568c1 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/README.md +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/README.md @@ -3,6 +3,8 @@ Use this plugin in combination with the [@rrweb/rrweb-plugin-sequential-id-record](../rrweb-plugin-sequential-id-record) plugin to record and replay events with a sequential id. See the [guide](../../../guide.md) for more info on rrweb. +Use this plugin with one compatible replay host: `rrweb`, `@rrweb/replay`, or `@rrweb/all`. + ## Installation ```bash diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json index 2c95a6e799..d85522a020 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/package.json +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/package.json @@ -47,14 +47,13 @@ "url": "https://github.com/rrweb-io/rrweb/issues" }, "homepage": "https://github.com/rrweb-io/rrweb#readme", - "devDependencies": { + "dependencies": { "@rrweb/rrweb-plugin-sequential-id-record": "^2.1.0", - "rrweb": "^2.1.0", + "@rrweb/types": "^2.1.0" + }, + "devDependencies": { "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1" - }, - "peerDependencies": { - "rrweb": "^2.0.1" } } diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts b/packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts index 189afb43fd..76af9e437f 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/src/index.ts @@ -1,6 +1,5 @@ import type { SequentialIdOptions } from '@rrweb/rrweb-plugin-sequential-id-record'; -import type { ReplayPlugin } from 'rrweb'; -import type { eventWithTime } from '@rrweb/types'; +import type { eventWithTime, ReplayPlugin } from '@rrweb/types'; type Options = SequentialIdOptions & { warnOnMissingId: boolean; diff --git a/packages/plugins/rrweb-plugin-sequential-id-replay/tsconfig.json b/packages/plugins/rrweb-plugin-sequential-id-replay/tsconfig.json index 5781b845a0..ed8ead7359 100644 --- a/packages/plugins/rrweb-plugin-sequential-id-replay/tsconfig.json +++ b/packages/plugins/rrweb-plugin-sequential-id-replay/tsconfig.json @@ -1,7 +1,13 @@ { "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["vite.config.ts", "dist", "tsconfig.json"], + "include": [ + "src" + ], + "exclude": [ + "vite.config.ts", + "dist", + "tsconfig.json" + ], "compilerOptions": { "rootDir": ".", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" @@ -11,7 +17,7 @@ "path": "../rrweb-plugin-sequential-id-record" }, { - "path": "../../rrweb" + "path": "../../types" } ] } diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts index e71f9ac531..e56c6647aa 100644 --- a/packages/replay/src/index.ts +++ b/packages/replay/src/index.ts @@ -2,6 +2,7 @@ import { Replayer, type playerConfig, type PlayerMachineState, + type ReplayPlugin, type SpeedMachineState, } from 'rrweb'; import 'rrweb/dist/style.css'; @@ -10,5 +11,6 @@ export { Replayer, type playerConfig, type PlayerMachineState, + type ReplayPlugin, type SpeedMachineState, }; diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 0bd68c0619..779ca56068 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -37,6 +37,7 @@ import type { styleSheetRuleCallback, viewportResizeCallback, PackFn, + ReplayPlugin as BaseReplayPlugin, UnpackFn, } from '@rrweb/types'; import type ProcessedNodeManager from './record/processed-node-manager'; @@ -161,18 +162,7 @@ export type MutationBufferParam = Pick< | 'processedNodeManager' >; -export type ReplayPlugin = { - handler?: ( - event: eventWithTime, - isSync: boolean, - context: { replayer: Replayer }, - ) => void; - onBuild?: ( - node: Node | RRNode, - context: { id: number; replayer: Replayer }, - ) => void; - getMirror?: (mirrors: { nodeMirror: Mirror }) => void; -}; +export type ReplayPlugin = BaseReplayPlugin; export type { Replayer } from './replay'; export type playerConfig = { speed: number; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7e87ec6676..59c301144c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -327,6 +327,23 @@ export type RecordPlugin = { options: TOptions; }; +export type ReplayPlugin< + TReplayer = unknown, + TNode = unknown, + TMirror = unknown, +> = { + handler?: ( + event: eventWithTime, + isSync: boolean, + context: { replayer: TReplayer }, + ) => void; + onBuild?: ( + node: Node | TNode, + context: { id: number; replayer: TReplayer }, + ) => void; + getMirror?: (mirrors: { nodeMirror: TMirror }) => void; +}; + export type hooksParam = { mutation?: mutationCallBack; mousemove?: mousemoveCallBack; diff --git a/scripts/check-plugin-host-smoke.mjs b/scripts/check-plugin-host-smoke.mjs new file mode 100755 index 0000000000..e3278f183c --- /dev/null +++ b/scripts/check-plugin-host-smoke.mjs @@ -0,0 +1,264 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tempRoot = mkdtempSync(join(tmpdir(), 'rrweb-plugin-host-smoke-')); +const packDir = join(tempRoot, 'packs'); +mkdirSync(packDir); + +const packages = { + types: 'packages/types', + utils: 'packages/utils', + rrdom: 'packages/rrdom', + rrwebSnapshot: 'packages/rrweb-snapshot', + rrweb: 'packages/rrweb', + record: 'packages/record', + replay: 'packages/replay', + canvasWebrtcRecord: 'packages/plugins/rrweb-plugin-canvas-webrtc-record', + canvasWebrtcReplay: 'packages/plugins/rrweb-plugin-canvas-webrtc-replay', + consoleRecord: 'packages/plugins/rrweb-plugin-console-record', + consoleReplay: 'packages/plugins/rrweb-plugin-console-replay', + networkRecord: 'packages/plugins/rrweb-plugin-network-record', + networkReplay: 'packages/plugins/rrweb-plugin-network-replay', + sequentialIdRecord: 'packages/plugins/rrweb-plugin-sequential-id-record', + sequentialIdReplay: 'packages/plugins/rrweb-plugin-sequential-id-replay', +}; + +const forbiddenDeclarationImport = + /(?:from\s+|import\()\s*['"](rrweb|rrdom|rrweb-snapshot)['"]/; + +function run(command, args, options = {}) { + return execFileSync(command, args, { + cwd: options.cwd || root, + encoding: 'utf8', + stdio: options.stdio || 'pipe', + }); +} + +function assertBuilt(packagePath) { + const dist = join(root, packagePath, 'dist'); + if (!existsSync(dist)) { + throw new Error( + `Missing ${dist}. Run the required package builds before this smoke test.`, + ); + } +} + +function findDeclarationFiles(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const entryPath = join(dir, entry.name); + if (entry.isDirectory()) { + return findDeclarationFiles(entryPath); + } + return entryPath.endsWith('.d.ts') || entryPath.endsWith('.d.cts') + ? [entryPath] + : []; + }); +} + +function assertPluginDeclarationsHostNeutral() { + for (const packagePath of Object.values(packages).filter((value) => + value.startsWith('packages/plugins/'), + )) { + assertBuilt(packagePath); + for (const file of findDeclarationFiles(join(root, packagePath, 'dist'))) { + const content = readFileSync(file, 'utf8'); + const match = content.match(forbiddenDeclarationImport); + if (match) { + throw new Error( + `Plugin declaration ${file} imports forbidden host package "${match[1]}".`, + ); + } + } + } +} + +function pack(packagePath) { + assertBuilt(packagePath); + const output = run('npm', [ + 'pack', + '--json', + '--ignore-scripts', + '--pack-destination', + packDir, + `./${packagePath}`, + ]); + const [packed] = JSON.parse(output); + return join(packDir, packed.filename); +} + +function packageName(packagePath) { + const manifest = JSON.parse( + readFileSync(join(root, packagePath, 'package.json'), 'utf8'), + ); + return manifest.name; +} + +function writeConsumer(name, dependencies, localPackageSpecs, source) { + const dir = join(tempRoot, name); + mkdirSync(dir); + const overrides = Object.fromEntries( + Object.entries(localPackageSpecs).filter( + ([packageName]) => !(packageName in dependencies), + ), + ); + writeFileSync( + join(dir, 'package.json'), + JSON.stringify( + { + private: true, + type: 'module', + dependencies, + overrides, + devDependencies: { + typescript: '5.4.5', + }, + }, + null, + 2, + ), + ); + writeFileSync( + join(dir, 'tsconfig.json'), + JSON.stringify( + { + compilerOptions: { + strict: true, + target: 'ES2022', + module: 'NodeNext', + moduleResolution: 'NodeNext', + lib: ['DOM', 'ES2022'], + skipLibCheck: true, + noEmit: true, + }, + include: ['index.ts'], + }, + null, + 2, + ), + ); + writeFileSync(join(dir, 'index.ts'), source); + return dir; +} + +try { + assertPluginDeclarationsHostNeutral(); + + const packed = Object.fromEntries( + Object.entries(packages).map(([name, packagePath]) => [ + name, + `file:${pack(packagePath)}`, + ]), + ); + const localPackageSpecs = Object.fromEntries( + Object.entries(packages).map(([name, packagePath]) => [ + packageName(packagePath), + packed[name], + ]), + ); + + const recordConsumer = writeConsumer( + 'record-consumer', + { + '@rrweb/record': packed.record, + '@rrweb/rrweb-plugin-canvas-webrtc-record': packed.canvasWebrtcRecord, + '@rrweb/rrweb-plugin-console-record': packed.consoleRecord, + '@rrweb/rrweb-plugin-network-record': packed.networkRecord, + '@rrweb/rrweb-plugin-sequential-id-record': packed.sequentialIdRecord, + }, + localPackageSpecs, + `import { record } from '@rrweb/record'; +import { RRWebPluginCanvasWebRTCRecord } from '@rrweb/rrweb-plugin-canvas-webrtc-record'; +import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; +import { getRecordNetworkPlugin } from '@rrweb/rrweb-plugin-network-record'; +import { getRecordSequentialIdPlugin } from '@rrweb/rrweb-plugin-sequential-id-record'; + +const canvasRecordPlugin = new RRWebPluginCanvasWebRTCRecord({ + signalSendCallback(signal: unknown) { + void signal; + }, +}).initPlugin(); + +record({ + emit(event) { + void event; + }, + plugins: [ + getRecordConsolePlugin(), + getRecordNetworkPlugin(), + getRecordSequentialIdPlugin(), + canvasRecordPlugin, + ], +}); +`, + ); + + const replayConsumer = writeConsumer( + 'replay-consumer', + { + '@rrweb/replay': packed.replay, + '@rrweb/rrweb-plugin-canvas-webrtc-replay': packed.canvasWebrtcReplay, + '@rrweb/rrweb-plugin-console-replay': packed.consoleReplay, + '@rrweb/rrweb-plugin-network-replay': packed.networkReplay, + '@rrweb/rrweb-plugin-sequential-id-replay': packed.sequentialIdReplay, + }, + localPackageSpecs, + `import { Replayer } from '@rrweb/replay'; +import { RRWebPluginCanvasWebRTCReplay } from '@rrweb/rrweb-plugin-canvas-webrtc-replay'; +import { getReplayConsolePlugin } from '@rrweb/rrweb-plugin-console-replay'; +import { getReplayNetworkPlugin } from '@rrweb/rrweb-plugin-network-replay'; +import { getReplaySequentialIdPlugin } from '@rrweb/rrweb-plugin-sequential-id-replay'; + +const canvasReplayPlugin = new RRWebPluginCanvasWebRTCReplay({ + canvasFoundCallback(node: unknown, context: unknown) { + void node; + void context; + }, + signalSendCallback(signal: unknown) { + void signal; + }, +}).initPlugin(); + +const replayer = new Replayer([], { + plugins: [ + getReplayConsolePlugin(), + getReplayNetworkPlugin({ + onNetworkData(data) { + void data; + }, + }), + getReplaySequentialIdPlugin(), + canvasReplayPlugin, + ], +}); +replayer.play(); +`, + ); + + for (const dir of [recordConsumer, replayConsumer]) { + run('npm', ['install', '--ignore-scripts'], { + cwd: dir, + stdio: 'inherit', + }); + run('npx', ['tsc', '--noEmit'], { + cwd: dir, + stdio: 'inherit', + }); + } + + console.log('Plugin host smoke tests passed.'); +} finally { + rmSync(tempRoot, { recursive: true, force: true }); +}