diff --git a/.changeset/lazy-toes-confess.md b/.changeset/lazy-toes-confess.md new file mode 100644 index 0000000000..f3e159a61e --- /dev/null +++ b/.changeset/lazy-toes-confess.md @@ -0,0 +1,5 @@ +--- +'rrvideo': patch +--- + +Refactor: Improve the video quality and add a progress bar for the CLI tool diff --git a/packages/rrvideo/README.md b/packages/rrvideo/README.md index 25d988aa0d..cec3e7d6e9 100644 --- a/packages/rrvideo/README.md +++ b/packages/rrvideo/README.md @@ -4,11 +4,12 @@ rrvideo is a tool for transforming the session recorded by [rrweb](https://github.com/rrweb-io/rrweb) into a video. +![Demo Video](./demo/demo.gif) + ## Install rrvideo -1. Install [ffmpeg](https://ffmpeg.org/download.html)。 -2. Install [Node.JS](https://nodejs.org/en/download/)。 -3. Run `npm i -g rrvideo` to install the rrvideo CLI。 +1. Install [Node.JS](https://nodejs.org/en/download/)。 +2. Run `npm i -g rrvideo` to install the rrvideo CLI. ## Use rrvideo @@ -18,7 +19,7 @@ rrvideo is a tool for transforming the session recorded by [rrweb](https://githu rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE ``` -Running this command will output a `rrvideo-output.mp4` file in the current working directory. +Running this command will output a `rrvideo-output.webm` file in the current working directory. ### Config the output path diff --git a/packages/rrvideo/README.zh_CN.md b/packages/rrvideo/README.zh_CN.md index d133e87335..9a625f1fb6 100644 --- a/packages/rrvideo/README.zh_CN.md +++ b/packages/rrvideo/README.zh_CN.md @@ -2,11 +2,12 @@ rrvideo 是用于将 [rrweb](https://github.com/rrweb-io/rrweb) 录制的数据转为视频格式的工具。 +![Demo Video](./demo/demo.gif) + ## 安装 rrvideo -1. 安装 [ffmpeg](https://ffmpeg.org/download.html)。 -2. 安装 [Node.JS](https://nodejs.org/en/download/)。 -3. 执行 `npm i -g rrvideo` 以安装 rrvideo CLI。 +1. 安装 [Node.JS](https://nodejs.org/en/download/)。 +2. 执行 `npm i -g rrvideo` 以安装 rrvideo CLI。 ## 使用 rrvideo @@ -16,7 +17,7 @@ rrvideo 是用于将 [rrweb](https://github.com/rrweb-io/rrweb) 录制的数据 rrvideo --input PATH_TO_YOUR_RRWEB_EVENTS_FILE ``` -运行以上命令会在执行文件夹中生成一个 `rrvideo-output.mp4` 文件。 +运行以上命令会在执行文件夹中生成一个 `rrvideo-output.webm` 文件。 ### 指定输出路径 diff --git a/packages/rrvideo/demo/demo.gif b/packages/rrvideo/demo/demo.gif new file mode 100644 index 0000000000..510e4ebe88 Binary files /dev/null and b/packages/rrvideo/demo/demo.gif differ diff --git a/packages/rrvideo/jest.config.js b/packages/rrvideo/jest.config.js new file mode 100644 index 0000000000..631615f876 --- /dev/null +++ b/packages/rrvideo/jest.config.js @@ -0,0 +1,6 @@ +// eslint-disable-next-line tsdoc/syntax +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/packages/rrvideo/package.json b/packages/rrvideo/package.json index 793f4c3f6d..df6d5b37c0 100644 --- a/packages/rrvideo/package.json +++ b/packages/rrvideo/package.json @@ -11,18 +11,28 @@ ], "types": "build/index.d.ts", "scripts": { + "install": "playwright install", "build": "tsc", + "test": "jest", + "check-types": "tsc -noEmit", "prepublish": "yarn build" }, "author": "yanzhen@smartx.com", "license": "MIT", "devDependencies": { + "@types/fs-extra": "11.0.1", + "@types/jest": "^27.4.1", "@types/minimist": "^1.2.1", + "@types/node": "^18.15.11", + "jest": "^27.5.1", + "ts-jest": "^27.1.3", "@rrweb/types": "^2.0.0-alpha.8" }, "dependencies": { + "@open-tech-world/cli-progress-bar": "^2.0.2", + "fs-extra": "^11.1.1", "minimist": "^1.2.5", - "puppeteer": "^19.7.2", + "playwright": "^1.32.1", "rrweb-player": "^2.0.0-alpha.8" } } diff --git a/packages/rrvideo/src/cli.ts b/packages/rrvideo/src/cli.ts index 318e8fa349..600c531a23 100644 --- a/packages/rrvideo/src/cli.ts +++ b/packages/rrvideo/src/cli.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import minimist from 'minimist'; +import { ProgressBar } from '@open-tech-world/cli-progress-bar'; import type { RRwebPlayerOptions } from 'rrweb-player'; import { transformToVideo } from './index'; @@ -24,10 +25,18 @@ if (argv.config) { >; } +const pBar = new ProgressBar({ prefix: 'Transforming' }); +const onProgressUpdate = (percent: number) => { + if (percent < 1) pBar.run({ value: percent * 100, total: 100 }); + else + pBar.run({ value: 100, total: 100, prefix: 'Transformation Completed!' }); +}; + transformToVideo({ input: argv.input as string, output: argv.output as string, rrwebPlayer: config, + onProgressUpdate, }) .then((file) => { console.log(`Successfully transformed into "${file}".`); diff --git a/packages/rrvideo/src/index.ts b/packages/rrvideo/src/index.ts index 99471a8c28..0909746929 100644 --- a/packages/rrvideo/src/index.ts +++ b/packages/rrvideo/src/index.ts @@ -1,9 +1,7 @@ -import * as fs from 'fs'; +import * as fs from 'fs-extra'; import * as path from 'path'; -import { spawn } from 'child_process'; -import puppeteer from 'puppeteer'; -import type { Page, Browser } from 'puppeteer'; -import type { eventWithTime } from '@rrweb/types'; +import { chromium } from 'playwright'; +import { EventType, eventWithTime } from '@rrweb/types'; import type { RRwebPlayerOptions } from 'rrweb-player'; const rrwebScriptPath = path.resolve( @@ -13,38 +11,38 @@ const rrwebScriptPath = path.resolve( const rrwebStylePath = path.resolve(rrwebScriptPath, '../style.css'); const rrwebRaw = fs.readFileSync(rrwebScriptPath, 'utf-8'); const rrwebStyle = fs.readFileSync(rrwebStylePath, 'utf-8'); +// The max valid scale value for the scaling method which can improve the video quality. +const MaxScaleValue = 2.5; type RRvideoConfig = { input: string; output?: string; headless?: boolean; - fps?: number; - cb?: (file: string, error: null | Error) => void; - // start playback delay time - startDelayTime?: number; + // A number between 0 and 1. The higher the value, the better the quality of the video. + resolutionRatio?: number; + // A callback function that will be called when the progress of the replay is updated. + onProgressUpdate?: (percent: number) => void; rrwebPlayer?: Omit; }; const defaultConfig: Required = { input: '', - output: 'rrvideo-output.mp4', + output: 'rrvideo-output.webm', headless: true, - fps: 15, - cb: () => { + // A good trade-off value between quality and file size. + resolutionRatio: 0.8, + onProgressUpdate: () => { // }, - startDelayTime: 1000, rrwebPlayer: {}, }; -function getHtml( - events: Array, - config?: Omit, -): string { +function getHtml(events: Array, config?: RRvideoConfig): string { return ` + `; } -export class RRvideo { - private browser!: Browser; - private page!: Page; - private state: 'idle' | 'recording' | 'closed' = 'idle'; - private config = { - ...defaultConfig, +/** + * Preprocess all events to get a maximum view port size. + */ +function getMaxViewport(events: eventWithTime[]) { + let maxWidth = 0, + maxHeight = 0; + events.forEach((event) => { + if (event.type !== EventType.Meta) return; + if (event.data.width > maxWidth) maxWidth = event.data.width; + if (event.data.height > maxHeight) maxHeight = event.data.height; + }); + return { + width: maxWidth, + height: maxHeight, }; - - constructor(config: RRvideoConfig) { - this.updateConfig(config); - } - - public async transform() { - try { - this.browser = await puppeteer.launch({ - headless: this.config.headless, - }); - this.page = await this.browser.newPage(); - await this.page.goto('about:blank'); - - await this.page.exposeFunction('onReplayFinish', () => { - void this.finishRecording(); - }); - - const eventsPath = path.isAbsolute(this.config.input) - ? this.config.input - : path.resolve(process.cwd(), this.config.input); - const events = JSON.parse( - fs.readFileSync(eventsPath, 'utf-8'), - ) as eventWithTime[]; - - await this.page.setContent(getHtml(events, this.config.rrwebPlayer)); - - setTimeout(() => { - void this.startRecording().then(() => { - return this.page.evaluate('window.replayer.play();'); - }); - }, this.config.startDelayTime); - } catch (error) { - this.config.cb('', error as Error); - } - } - - public updateConfig(config: RRvideoConfig) { - if (!config.input) throw new Error('input is required'); - config.output = config.output || defaultConfig.output; - Object.assign(this.config, defaultConfig, config); - } - - private async startRecording() { - this.state = 'recording'; - let wrapperSelector = '.replayer-wrapper'; - if (this.config.rrwebPlayer.width && this.config.rrwebPlayer.height) { - wrapperSelector = '.rr-player'; - } - const wrapperEl = await this.page.$(wrapperSelector); - - if (!wrapperEl) { - throw new Error('failed to get replayer element'); - } - - // start ffmpeg - const args = [ - // fps - '-framerate', - this.config.fps.toString(), - // input - '-f', - 'image2pipe', - '-i', - '-', - // output - '-y', - this.config.output, - ]; - - const ffmpegProcess = spawn('ffmpeg', args); - ffmpegProcess.stderr.setEncoding('utf-8'); - ffmpegProcess.stderr.on('data', console.log); - - let processError: Error | null = null; - - const timer = setInterval(() => { - if (this.state === 'recording' && !processError) { - void wrapperEl - .screenshot({ - encoding: 'binary', - }) - .then((buffer) => ffmpegProcess.stdin.write(buffer)) - .catch(); - } else { - clearInterval(timer); - if (this.state === 'closed' && !processError) { - ffmpegProcess.stdin.end(); - } - } - }, 1000 / this.config.fps); - - const outputPath = path.isAbsolute(this.config.output) - ? this.config.output - : path.resolve(process.cwd(), this.config.output); - ffmpegProcess.on('close', () => { - if (processError) { - return; - } - this.config.cb(outputPath, null); - }); - ffmpegProcess.on('error', (error) => { - if (processError) { - return; - } - processError = error; - this.config.cb(outputPath, error); - }); - ffmpegProcess.stdin.on('error', (error) => { - if (processError) { - return; - } - processError = error; - this.config.cb(outputPath, error); - }); - } - - private async finishRecording() { - this.state = 'closed'; - await this.browser.close(); - } } -export function transformToVideo(config: RRvideoConfig): Promise { - return new Promise((resolve, reject) => { - const rrvideo = new RRvideo({ - ...config, - cb(file, error) { - if (error) { - return reject(error); - } - resolve(file); - }, - }); - void rrvideo.transform(); +export async function transformToVideo(options: RRvideoConfig) { + const defaultVideoDir = '__rrvideo__temp__'; + const config = { ...defaultConfig }; + if (!options.input) throw new Error('input is required'); + // If the output is not specified or undefined, use the default value. + if (!options.output) delete options.output; + Object.assign(config, options); + if (config.resolutionRatio > 1) config.resolutionRatio = 1; // The max value is 1. + + const eventsPath = path.isAbsolute(config.input) + ? config.input + : path.resolve(process.cwd(), config.input); + const outputPath = path.isAbsolute(config.output) + ? config.output + : path.resolve(process.cwd(), config.output); + const events = JSON.parse( + fs.readFileSync(eventsPath, 'utf-8'), + ) as eventWithTime[]; + + // Make the browser viewport fit the player size. + const maxViewport = getMaxViewport(events); + // Use the scaling method to improve the video quality. + const scaledViewport = { + width: Math.round( + maxViewport.width * (config.resolutionRatio ?? 1) * MaxScaleValue, + ), + height: Math.round( + maxViewport.height * (config.resolutionRatio ?? 1) * MaxScaleValue, + ), + }; + Object.assign(config.rrwebPlayer, scaledViewport); + const browser = await chromium.launch({ + headless: config.headless, }); + const context = await browser.newContext({ + viewport: scaledViewport, + recordVideo: { + dir: defaultVideoDir, + size: scaledViewport, + }, + }); + const page = await context.newPage(); + await page.goto('about:blank'); + await page.exposeFunction( + 'onReplayProgressUpdate', + (data: { payload: number }) => { + config.onProgressUpdate(data.payload); + }, + ); + + // Wait for the replay to finish + await new Promise( + (resolve) => + void page + .exposeFunction('onReplayFinish', () => resolve()) + .then(() => page.setContent(getHtml(events, config))), + ); + const videoPath = (await page.video()?.path()) || ''; + const cleanFiles = async (videoPath: string) => { + await fs.remove(videoPath); + if ((await fs.readdir(defaultVideoDir)).length === 0) { + await fs.remove(defaultVideoDir); + } + }; + await context.close(); + await Promise.all([ + fs + .move(videoPath, outputPath, { overwrite: true }) + .catch((e) => { + console.error( + "Can't create video file. Please check the output path.", + e, + ); + }) + .finally(() => void cleanFiles(videoPath)), + browser.close(), + ]); + return outputPath; } diff --git a/packages/rrvideo/test/cli.test.ts b/packages/rrvideo/test/cli.test.ts new file mode 100644 index 0000000000..29efe4815c --- /dev/null +++ b/packages/rrvideo/test/cli.test.ts @@ -0,0 +1,45 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import exampleEvents from './events/example'; + +describe('should be able to run cli', () => { + beforeAll(() => { + fs.mkdirSync(path.resolve(__dirname, './generated')); + fs.writeJsonSync( + path.resolve(__dirname, './generated/example.json'), + exampleEvents, + { + spaces: 2, + }, + ); + }); + afterAll(async () => { + await fs.remove(path.resolve(__dirname, './generated')); + }); + + it('should throw error without input path', () => { + expect(() => { + execSync('node ./build/cli.js', { stdio: 'pipe' }); + }).toThrowError(/.*please pass --input to your rrweb events file.*/); + }); + + it('should generate a video without output path', () => { + execSync('node ./build/cli.js --input ./test/generated/example.json', { + stdio: 'pipe', + }); + const outputFile = path.resolve(__dirname, '../rrvideo-output.webm'); + expect(fs.existsSync(outputFile)).toBe(true); + fs.removeSync(outputFile); + }); + + it('should generate a video with specific output path', () => { + const outputFile = path.resolve(__dirname, './generated/output.webm'); + execSync( + `node ./build/cli.js --input ./test/generated/example.json --output ${outputFile}`, + { stdio: 'pipe' }, + ); + expect(fs.existsSync(outputFile)).toBe(true); + fs.removeSync(outputFile); + }); +}); diff --git a/packages/rrvideo/test/events/example.ts b/packages/rrvideo/test/events/example.ts new file mode 100644 index 0000000000..00701fd6ca --- /dev/null +++ b/packages/rrvideo/test/events/example.ts @@ -0,0 +1,147 @@ +import { EventType, IncrementalSource } from '@rrweb/types'; +import type { eventWithTime } from '@rrweb/types'; + +const now = Date.now(); +const events: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 100, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 100, + }, + // full snapshot: + { + data: { + node: { + id: 1, + type: 0, + childNodes: [ + { id: 2, name: 'html', type: 1, publicId: '', systemId: '' }, + { + id: 3, + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + id: 4, + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [], + }, + { + id: 5, + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + }, + ], + }, + ], + }, + initialOffset: { top: 0, left: 0 }, + }, + type: EventType.FullSnapshot, + timestamp: now + 100, + }, + // mutation that adds select elements + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 5, + nextId: null, + node: { + type: 2, + tagName: 'select', + childNodes: [], + attributes: {}, + id: 26, + }, + }, + { + parentId: 26, + nextId: null, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueC' }, + childNodes: [], + id: 27, + }, + }, + { + parentId: 27, + nextId: null, + node: { type: 3, textContent: 'C', id: 28 }, + }, + { + parentId: 26, + nextId: 27, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueB', selected: true }, + childNodes: [], + id: 29, + }, + }, + { + parentId: 26, + nextId: 29, + node: { + type: 2, + tagName: 'option', + attributes: { value: 'valueA' }, + childNodes: [], + id: 30, + }, + }, + { + parentId: 30, + nextId: null, + node: { type: 3, textContent: 'A', id: 31 }, + }, + { + parentId: 29, + nextId: null, + node: { type: 3, textContent: 'B', id: 32 }, + }, + ], + }, + timestamp: now + 200, + }, + // input event + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + text: 'valueA', + isChecked: false, + id: 26, + }, + timestamp: now + 300, + }, +]; + +export default events; diff --git a/packages/rrvideo/test/tsconfig.json b/packages/rrvideo/test/tsconfig.json new file mode 100644 index 0000000000..875cb60012 --- /dev/null +++ b/packages/rrvideo/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/packages/rrvideo/tsconfig.json b/packages/rrvideo/tsconfig.json index ea6d53e2ca..95955dda4a 100644 --- a/packages/rrvideo/tsconfig.json +++ b/packages/rrvideo/tsconfig.json @@ -16,6 +16,7 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, + "exclude": ["build", "node_modules", "test"], "references": [ { "path": "../rrweb-player" diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 9a3db36a3b..e4d8f3b766 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -44,7 +44,7 @@ "@types/chai": "^4.1.4", "@types/jest": "^27.0.2", "@types/jsdom": "^20.0.0", - "@types/node": "^10.11.3", + "@types/node": "^18.15.11", "@types/puppeteer": "^1.12.4", "cross-env": "^5.2.0", "jest": "^27.2.4", diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 474b4bd198..9064f03e93 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -51,7 +51,7 @@ "@types/inquirer": "^8.2.1", "@types/jest": "^27.4.1", "@types/jest-image-snapshot": "^5.1.0", - "@types/node": "^17.0.21", + "@types/node": "^18.15.11", "@types/offscreencanvas": "^2019.6.4", "@types/puppeteer": "^5.4.4", "construct-style-sheets-polyfill": "^3.1.0", diff --git a/packages/rrweb/test/events/input.ts b/packages/rrweb/test/events/input.ts index e53f8dccb9..a1dedc41a7 100644 --- a/packages/rrweb/test/events/input.ts +++ b/packages/rrweb/test/events/input.ts @@ -74,6 +74,7 @@ const events: eventWithTime[] = [ node: { type: 2, tagName: 'select', + attributes: {}, childNodes: [], id: 26, }, diff --git a/yarn.lock b/yarn.lock index 2ecefecc30..a3e1e00c82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2517,6 +2517,24 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@open-tech-world/cli-progress-bar@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@open-tech-world/cli-progress-bar/-/cli-progress-bar-2.0.2.tgz#9a4f3470ee460224e7a38790d0d3f257fe9d6812" + integrity sha512-miNmdKNdKp7Lhy295wxnJcWFrbIAoQmypoCynlG8HTQPxsG5dhOpPL5udsZFoW99RxcLAj2JTCaEhrosTzPE3g== + dependencies: + "@open-tech-world/es-cli-styles" "^0.3.0" + "@open-tech-world/es-utils" "^0.10.0" + +"@open-tech-world/es-cli-styles@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@open-tech-world/es-cli-styles/-/es-cli-styles-0.3.0.tgz#316c3c2934ed87629366a5827bc2353fa4c061c7" + integrity sha512-0SyxBAUkUDkjt83snx1QvPDk6zJJIb/xRbY0daWGG6ONCfx5RITxc0qty2tLmyBNsCM41+V1shtHNeSrEBu8KQ== + +"@open-tech-world/es-utils@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@open-tech-world/es-utils/-/es-utils-0.10.0.tgz#97ca72c7dad63dcf9a3534b01f3d763f187bafb2" + integrity sha512-JqpxcVk3Go4RAArI3oB18fhIMeeuNSThwmSQM7ScygVv2/AHRAxkkiodtggC7QniJvLs7i5AzL9pILNvAEBYDg== + "@polka/url@^0.5.0": version "0.5.0" resolved "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz" @@ -2805,6 +2823,14 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/fs-extra@11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" + integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" @@ -2885,6 +2911,13 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/jsonfile@*": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b" + integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png== + dependencies: + "@types/node" "*" + "@types/lodash.mergewith@4.6.6": version "4.6.6" resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz#c4698f5b214a433ff35cb2c75ee6ec7f99d79f10" @@ -2917,20 +2950,15 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" integrity sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ== -"@types/node@^10.11.3": - version "10.17.60" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - "@types/node@^12.7.1": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== -"@types/node@^17.0.21": - version "17.0.21" - resolved "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz" - integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== +"@types/node@^18.15.11": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -4292,13 +4320,6 @@ chrome-launcher@0.15.0: is-wsl "^2.2.0" lighthouse-logger "^1.0.0" -chromium-bidi@0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.5.tgz#a352e755536dde609bd2c77e4b1f0906bff8784e" - integrity sha512-rkav9YzRfAshSTG3wNXF7P7yNiI29QAo1xBXElPoCoSQR5n20q3cOyVhDv6S7+GlF/CJ/emUxlQiR0xOPurkGg== - dependencies: - mitt "3.0.0" - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" @@ -4645,16 +4666,6 @@ core-util-is@^1.0.2: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz#947e174c796483ccf0a48476c24e4fefb7e1aea8" - integrity sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg== - dependencies: - import-fresh "^3.2.1" - js-yaml "^4.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz" @@ -5147,11 +5158,6 @@ devtools-protocol@0.0.1036444: resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz#a570d3cdde61527c82f9b03919847b8ac7b1c2b9" integrity sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw== -devtools-protocol@0.0.1094867: - version "0.0.1094867" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz#2ab93908e9376bd85d4e0604aa2651258f13e374" - integrity sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ== - devtools-protocol@0.0.869402: version "0.0.869402" resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz" @@ -6626,6 +6632,15 @@ fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^7.0.1, fs-extra@~7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -6873,16 +6888,6 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^9.2.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.0.tgz#be6e50d172d025c3fcf87903ae25b36b787c0bb0" - integrity sha512-EAZejC7JvnQINayvB/7BJbpZpNOJ8Lrw2OZNEvQxe0vaLn1SuwMcfV7/MNaX8L/T0wmptBFI4YMtDvSBxYDc7w== - dependencies: - fs.realpath "^1.0.0" - minimatch "^7.4.1" - minipass "^4.2.4" - path-scurry "^1.6.1" - glob@~7.2.0: version "7.2.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.2.tgz#29deb38e1ef90f132d5958abe9c3ee8e87f3c318" @@ -9406,11 +9411,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.14.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - magic-string@^0.25.7: version "0.25.7" resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" @@ -9700,13 +9700,6 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^7.4.1: - version "7.4.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.2.tgz#157e847d79ca671054253b840656720cb733f10f" - integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA== - dependencies: - brace-expansion "^2.0.1" - minimatch@~3.0.5: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -9728,12 +9721,7 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minipass@^4.0.2, minipass@^4.2.4: - version "4.2.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb" - integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q== - -mitt@3.0.0, mitt@^3.0.0: +mitt@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== @@ -10371,14 +10359,6 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.1.tgz#dab45f7bb1d3f45a0e271ab258999f4ab7e23132" - integrity sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA== - dependencies: - lru-cache "^7.14.1" - minipass "^4.0.2" - path-strip-sep@^1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/path-strip-sep/-/path-strip-sep-1.0.10.tgz" @@ -10520,6 +10500,18 @@ pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^5.0.0" +playwright-core@1.32.1: + version "1.32.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.1.tgz#5a10c32403323b07d75ea428ebeed866a80b76a1" + integrity sha512-KZYUQC10mXD2Am1rGlidaalNGYk3LU1vZqqNk0gT4XPty1jOqgup8KDP8l2CUlqoNKhXM5IfGjWgW37xvGllBA== + +playwright@^1.32.1: + version "1.32.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.32.1.tgz#c48195850740fbdbd7702f37e5a891b13259f689" + integrity sha512-GnEizysWMvoqHC3I9l8+4/ZxeLwLNdJJG76xdKGxzOcIZDcw5RSk/FKrFb5CuA+zcLpjIM2p9eR9Z4CuUDkWXg== + dependencies: + playwright-core "1.32.1" + pngjs@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz" @@ -11081,23 +11073,6 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" -puppeteer-core@19.7.5: - version "19.7.5" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.7.5.tgz#cedc8eb7862fe7a8aa2a25ed167c0f1230de72b2" - integrity sha512-EJuNha+SxPfaYFbkoWU80H3Wb1SiQH5fFyb2xdbWda0ziax5mhV63UMlqNfPeTDIWarwtR4OIcq/9VqY8HPOsg== - dependencies: - chromium-bidi "0.4.5" - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1094867" - extract-zip "2.0.1" - https-proxy-agent "5.0.1" - proxy-from-env "1.1.0" - rimraf "4.4.0" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.12.1" - puppeteer@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f" @@ -11133,17 +11108,6 @@ puppeteer@^17.1.3: unbzip2-stream "1.4.3" ws "8.8.1" -puppeteer@^19.7.2: - version "19.7.5" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.7.5.tgz#d7db0dfcc80ca2cdf8eb0100bae1ce888a841389" - integrity sha512-UqD8K+yaZa6/hwzP54AATCiHrEYGGxzQcse9cZzrtsVGd8wT0llCdYhsBp8n+zvnb1ofY0YFgI3TYZ/MiX5uXQ== - dependencies: - cosmiconfig "8.1.0" - https-proxy-agent "5.0.1" - progress "2.0.3" - proxy-from-env "1.1.0" - puppeteer-core "19.7.5" - puppeteer@^9.1.1: version "9.1.1" resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz" @@ -11647,13 +11611,6 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.0.tgz#c7a9f45bb2ec058d2e60ef9aca5167974313d605" - integrity sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ== - dependencies: - glob "^9.2.0" - rimraf@^2.6.2: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" @@ -13656,11 +13613,6 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -ws@8.12.1: - version "8.12.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" - integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== - ws@8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"