misc: add benchmarking script for edge rendering (#40716)

This PR adds the benchmarking script I've been using for #40251 to
measure the performance improvements that we make to the Edge SSR
runtime.

This tool:
- uploads two version of the benchmarking project to Vercel, one with
the latest canary and the one with your current local changes in dist
(don't forget to build!)
- runs some tests against the published url to measure TTFB
- displays a nice chart and table

What this doesn't do (yet):

- allow you to choose which URL to compare
- allow you to change the measured metric
- run a battery of differnet test


## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`


https://user-images.githubusercontent.com/11064311/191270204-04447e20-5a40-43a9-bcda-b7eaeb3d270a.mov


## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Jimmy Lai 2022-09-27 17:57:16 +02:00 committed by GitHub
parent bb42d40fb7
commit 6be2868787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 864 additions and 15 deletions

8
bench/vercel/.env.dev Normal file
View file

@ -0,0 +1,8 @@
# The Vercel team you want to deploy the project too
VERCEL_TEST_TEAM=
# The corresponding Vercel token
VERCEL_TEST_TOKEN=
# The Vercel project you want to deploy the test project too
VERCEL_TEST_PROJECT_NAME=

5
bench/vercel/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.vercel
.next
*.tgz
yarn.lock
.env

22
bench/vercel/README.md Normal file
View file

@ -0,0 +1,22 @@
# Benchmarking Next.js on production
This script allows you to measure some performance metrics of your local build of Next.js on production by uploading your current build to Vercel with an example app and running some basic benchmarks on it.
## Requirements
- the Vercel CLI
## Setup
Rename the provided `./env.local` file to `./env` and fill in the required `VERCEL_TEST_TOKEN` and `VERCEL_TEST_TEAM` values. You can find and generate those from vercel.com.
Run `pnpm install`, `pnpm bench` and profit.
Note: if you made some changes to Next.js, make sure you compiled them by running at the root of the monorepo either `pnpm dev` or `pnpm build --force`.
## How it works
- with the Vercel CLI, we setup a project
- we `npm pack` the local Next build and add it to the repo
- we upload the repo to Vercel and let it build
- once it builds, we get the deployment url and run some tests

70
bench/vercel/bench.js Normal file
View file

@ -0,0 +1,70 @@
import { Command } from 'commander'
import console from 'console'
import chalk from 'chalk'
import PQueue from 'p-queue'
import { generateProjects, cleanupProjectFolders } from './project-utils.js'
import { printBenchmarkResults } from './chart.js'
import { genRetryableRequest } from './gen-request.js'
const program = new Command()
const queue = new PQueue({ concurrency: 25 })
const TTFB_OUTLIERS_THRESHOLD = 250
program.option('-p, --path <path>')
program.parse(process.argv)
const options = program.opts()
if (options.path) {
console.log('Running benchmark for path: ', options.path)
}
try {
const [originDeploymentURL, headDeploymentURL] = await generateProjects()
const originBenchmarkURL = `${originDeploymentURL}${options.path || ''}`
const headBenchmarkURL = `${headDeploymentURL}${options.path || ''}`
console.log(`Origin deployment URL: ${originBenchmarkURL}`)
console.log(`Head deployment URL: ${headBenchmarkURL}`)
console.log(`Running benchmark...`)
const benchResults = await runBenchmark(originBenchmarkURL)
const headBenchResults = await runBenchmark(headBenchmarkURL)
console.log(chalk.bold('Benchmark results for cold:'))
printBenchmarkResults(
{
origin: benchResults,
head: headBenchResults,
},
(r) => r.cold && r.firstByte <= TTFB_OUTLIERS_THRESHOLD && r.firstByte
)
console.log(chalk.bold('Benchmark results for hot:'))
printBenchmarkResults(
{
origin: benchResults,
head: headBenchResults,
},
(r) => !r.cold && r.firstByte <= TTFB_OUTLIERS_THRESHOLD && r.firstByte
)
} catch (err) {
console.log(chalk.red('Benchmark failed: ', err))
} finally {
await cleanupProjectFolders()
}
async function runBenchmark(url) {
return (
await Promise.all(
Array.from({ length: 500 }).map(() =>
queue.add(() => genRetryableRequest(url))
)
)
).filter(Boolean)
}

2
bench/vercel/benchmark-app/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vercel
webpack-stats-client.json

View file

@ -0,0 +1,14 @@
import * as React from 'react'
export default function Root({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
export const config = {
runtime: 'experimental-edge',
}

View file

@ -0,0 +1,14 @@
import * as React from 'react'
// if (!('hot' in Math)) Math.hot = false
export default function page() {
// const previous = Math.hot
// Math.hot = true
// return <div>{previous ? 'HOT' : 'COLD'}</div>
return <div>hello</div>
}
export const config = {
runtime: 'experimental-edge',
}

View file

@ -0,0 +1,34 @@
const { StatsWriterPlugin } = require('webpack-stats-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
experimental: {
appDir: true,
},
webpack: (config, options) => {
const { nextRuntime = 'client' } = options
if (process.env.ANALYZE) {
if (nextRuntime === 'edge')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: true,
reportFilename: options.isServer
? '../analyze/server.html'
: './analyze/client.html',
})
)
config.plugins.push(
new StatsWriterPlugin({
filename: `../webpack-stats-${nextRuntime}.json`,
stats: {
assets: true,
chunks: true,
modules: true,
},
})
)
}
return config
},
}

View file

@ -0,0 +1,14 @@
{
"name": "stats-app",
"private": true,
"license": "MIT",
"dependencies": {
"webpack-bundle-analyzer": "^4.6.1",
"webpack-stats-plugin": "^1.1.0"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}

View file

@ -0,0 +1,20 @@
if (!('hot' in Math)) Math.hot = false
export default function page({ hot }) {
return `${hot ? 'HOT' : 'COLD'}`
}
export async function getServerSideProps() {
const wasHot = Math.hot
Math.hot = true
return {
props: {
hot: wasHot,
},
}
}
export const config = {
runtime: 'experimental-edge',
}

99
bench/vercel/chart.js Normal file
View file

@ -0,0 +1,99 @@
import downsampler from 'downsample-lttb'
import asciichart from 'asciichart'
import terminalSize from 'term-size'
const CHART_WIDTH = terminalSize().columns - 15 // space for the labels
function getMetrics(data) {
const sorted = [...data].sort((a, b) => a - b)
const getPercentile = (percentile) => {
const index = Math.floor((sorted.length - 1) * percentile)
return sorted[index]
}
return {
hits: sorted.length,
confidenceInterval: round(getConfidenceInterval(sorted)),
median: getPercentile(0.5),
avg: sorted.reduce((a, b) => a + b, 0) / sorted.length,
p75: getPercentile(0.75),
p95: getPercentile(0.95),
p99: getPercentile(0.99),
p25: getPercentile(0.25),
min: sorted[0],
max: sorted[sorted.length - 1],
}
}
function round(num) {
return Math.round(num * 100) / 100
}
// thanks Copilot
function getConfidenceInterval(data) {
const n = data.length
const m = data.reduce((a, b) => a + b) / n
const s = Math.sqrt(
data.map((x) => Math.pow(x - m, 2)).reduce((a, b) => a + b) / n
)
const z = 1.96 // 95% confidence
const e = z * (s / Math.sqrt(n))
return e
}
export function downsample(data, maxPoints) {
const sortedData = [...data].sort((a, b) => a - b)
return downsampler
.processData(
// the downsampler expects a 2d array of [x, y] values, so we need to add an index
sortedData.map((p, i) => [p, i]),
maxPoints
)
.map((p) => p[0])
}
export function printBenchmarkResults({ origin, head }, metricSelector) {
const [processedOriginData, processedHeadData] = [origin, head].map(
(results) => results.map(metricSelector).filter(Boolean)
)
const [originMetrics, headMetrics] = [
processedOriginData,
processedHeadData,
].map(getMetrics)
const deltaMetrics = {
min: headMetrics.min - originMetrics.min,
max: headMetrics.max - originMetrics.max,
avg: headMetrics.avg - originMetrics.avg,
median: headMetrics.median - originMetrics.median,
p95: headMetrics.p95 - originMetrics.p95,
p99: headMetrics.p99 - originMetrics.p99,
p75: headMetrics.p75 - originMetrics.p75,
p25: headMetrics.p25 - originMetrics.p25,
}
console.table({
origin: originMetrics,
head: headMetrics,
delta: deltaMetrics,
})
const [originData, headData] = [processedOriginData, processedHeadData].map(
(data) =>
downsample(
data,
Math.min(
CHART_WIDTH,
processedOriginData.length,
processedHeadData.length
)
)
)
console.log(
asciichart.plot([originData, headData], {
height: 15,
colors: [asciichart.blue, asciichart.red],
})
)
}

View file

@ -0,0 +1,41 @@
import https from 'https'
import timer from '@szmarczak/http-timer'
// a wrapper around genAsyncRequest that will retry the request 5 times if it fails
export async function genRetryableRequest(url) {
let retries = 0
while (retries < 5) {
try {
return await genAsyncRequest(url)
} catch (err) {}
retries++
await new Promise((r) => setTimeout(r, 1000))
}
throw new Error(`Failed to fetch ${url}, too many retries`)
}
// a wrapper around http.request that is enhanced with timing information
async function genAsyncRequest(url) {
return new Promise((resolve, reject) => {
const request = https.get(url)
timer(request)
request.on('response', (response) => {
let body = ''
response.on('data', (data) => {
body += data
})
response.on('end', () => {
resolve({
...response.timings.phases,
cold: !body.includes('HOT'),
})
})
response.on('error', (err) => {
reject(err)
})
})
request.on('error', (err) => {
reject(err)
})
})
}

View file

@ -0,0 +1,52 @@
import execa from 'execa'
import fs from 'fs/promises'
import path from 'path'
export async function generatePackageJson(folder, withLocalNext = false) {
const packageJson = JSON.parse(
await fs.readFile(path.join(folder, 'package.json'))
)
const currentVersions = await getCurrentRootReactPackagesVersions()
packageJson.dependencies = packageJson.dependencies || {}
packageJson.dependencies['react'] = currentVersions.react
packageJson.dependencies['react-dom'] = currentVersions['react-dom']
if (withLocalNext) {
packageJson.dependencies.next = await packNextBuild(folder)
} else {
packageJson.dependencies.next = await getCurrentNextVersion()
}
await fs.writeFile(
path.join(folder, 'package.json'),
JSON.stringify(packageJson, null, 2)
)
}
export async function packNextBuild(folder) {
const process = await execa('npm', [
'pack',
'../../packages/next',
`--pack-destination=${folder}`,
])
return `file:./${process.stdout}`
}
async function getCurrentNextVersion() {
const packageJson = JSON.parse(
await fs.readFile('../../packages/next/package.json', 'utf8')
)
return packageJson.version
}
async function getCurrentRootReactPackagesVersions() {
const packageJson = JSON.parse(
await fs.readFile('../../package.json', 'utf8')
)
return {
react: packageJson.devDependencies['react-exp'],
'react-dom': packageJson.devDependencies['react-dom-exp'],
}
}

24
bench/vercel/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "bench-production",
"version": "1.0.0",
"description": "Scripts for benchmarking in production.",
"main": "bench.js",
"type": "module",
"scripts": {
"bench": "node bench.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@szmarczak/http-timer": "5.0.1",
"asciichart": "1.5.25",
"commander": "2.20.0",
"dotenv": "10.0.0",
"downsample-lttb": "0.0.1",
"listr2": "5.0.5",
"p-queue": "7.3.0",
"term-size": "3.0.2",
"webpack-bundle-analyzer": "^4.6.1",
"webpack-stats-plugin": "^1.1.0"
}
}

View file

@ -0,0 +1,218 @@
import { config } from 'dotenv'
import fetch from 'node-fetch'
import chalk from 'chalk'
import execa from 'execa'
import path from 'path'
import url from 'url'
import { generatePackageJson } from './generate-package-json.js'
import { Listr } from 'listr2'
config()
const TEST_PROJECT_NAME = process.env.VERCEL_TEST_PROJECT_NAME
const ORIGIN_PROJECT_NAME = TEST_PROJECT_NAME + '-origin'
const HEAD_PROJECT_NAME = TEST_PROJECT_NAME + '-head'
const TEST_TEAM_NAME = process.env.VERCEL_TEST_TEAM
const TEST_TOKEN = process.env.VERCEL_TEST_TOKEN
const VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG =
process.env.VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const appFolder = path.join(__dirname, 'benchmark-app')
const originAppFolder = path.join(__dirname, 'benchmark-app-origin')
const headAppFolder = path.join(__dirname, 'benchmark-app-head')
export async function generateProjects() {
const { originUrl, headUrl } = await new Listr(
[
{
title: 'Origin project',
task: (ctx, task) =>
task.newListr(
(parent) => [
{
title: 'Resetting project',
task: async () => {
await resetProject(ORIGIN_PROJECT_NAME)
},
},
{
title: 'copying app',
task: async () => {
await execa('cp', ['-f', '-R', appFolder, originAppFolder])
},
},
{
title: 'Set Next.js version in package.json',
task: async () => {
await generatePackageJson(originAppFolder)
},
},
{
title: 'deploying project',
task: async () => {
const url = await deployProject(
ORIGIN_PROJECT_NAME,
originAppFolder
)
ctx.originUrl = url
},
},
],
{ concurrent: false }
),
},
{
title: 'Head project',
task: (ctx, task) =>
task.newListr(
(parent) => [
{
title: 'Resetting project',
task: async () => {
await resetProject(HEAD_PROJECT_NAME)
},
},
{
title: 'copying app',
task: async () => {
await execa('cp', ['-f', '-R', appFolder, headAppFolder])
},
},
{
title: 'pack local Next.js version',
task: async () => {
await generatePackageJson(headAppFolder, true)
},
},
{
title: 'deploying project',
task: async () => {
const url = await deployProject(
HEAD_PROJECT_NAME,
headAppFolder
)
ctx.headUrl = url
},
},
],
{ concurrent: false }
),
},
],
{ concurrent: true }
).run()
return [originUrl, headUrl]
}
export async function cleanupProjectFolders() {
await Promise.all([
execa('rm', ['-rf', originAppFolder]),
execa('rm', ['-rf', headAppFolder]),
])
}
async function resetProject(projectName) {
const deleteRes = await fetch(
`https://vercel.com/api/v8/projects/${encodeURIComponent(
projectName
)}?teamId=${TEST_TEAM_NAME}`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${TEST_TOKEN}`,
},
}
)
if (!deleteRes.ok && deleteRes.status !== 404) {
throw new Error(
`Failed to delete project got status ${
deleteRes.status
}, ${await deleteRes.text()}`
)
}
const createRes = await fetch(
`https://vercel.com/api/v8/projects?teamId=${TEST_TEAM_NAME}`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
Authorization: `Bearer ${TEST_TOKEN}`,
},
body: JSON.stringify({
framework: 'nextjs',
name: projectName,
}),
}
)
if (!createRes.ok) {
throw new Error(
`Failed to create project got status ${
createRes.status
}, ${await createRes.text()}`
)
}
}
export async function deployProject(projectName, appFolder) {
try {
const vercelFlags = ['--scope', TEST_TEAM_NAME]
const vercelEnv = { ...process.env, TOKEN: TEST_TOKEN }
// link the project
const linkRes = await execa(
'vercel',
['link', '-p', projectName, '--confirm', ...vercelFlags],
{
cwd: appFolder,
env: vercelEnv,
}
)
if (linkRes.exitCode !== 0) {
throw new Error(
`Failed to link project ${linkRes.stdout} ${linkRes.stderr} (${linkRes.exitCode})`
)
}
const deployRes = await execa(
'vercel',
[
'deploy',
'--build-env',
'NEXT_PRIVATE_TEST_MODE=1',
'--build-env',
'NEXT_TELEMETRY_DISABLED=1',
...(VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG
? [
'--build-env',
`VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG=${VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG}`,
]
: []),
'--force',
...vercelFlags,
],
{
cwd: appFolder,
env: vercelEnv,
}
)
if (deployRes.exitCode !== 0) {
throw new Error(
`Failed to deploy project ${linkRes.stdout} ${linkRes.stderr} (${linkRes.exitCode})`
)
}
return deployRes.stdout
} catch (err) {
console.log(chalk.red('Deployment failed: ', err))
throw err
}
}

View file

@ -325,6 +325,30 @@ importers:
webpack: 5.74.0_@swc+core@1.2.203
webpack-bundle-analyzer: 4.3.0
bench/vercel:
specifiers:
'@szmarczak/http-timer': 5.0.1
asciichart: 1.5.25
commander: 2.20.0
dotenv: 10.0.0
downsample-lttb: 0.0.1
listr2: 5.0.5
p-queue: 7.3.0
term-size: 3.0.2
webpack-bundle-analyzer: ^4.6.1
webpack-stats-plugin: ^1.1.0
dependencies:
'@szmarczak/http-timer': 5.0.1
asciichart: 1.5.25
commander: 2.20.0
dotenv: 10.0.0
downsample-lttb: 0.0.1
listr2: 5.0.5
p-queue: 7.3.0
term-size: 3.0.2
webpack-bundle-analyzer: 4.6.1
webpack-stats-plugin: 1.1.0
packages/create-next-app:
specifiers:
'@types/async-retry': 1.4.2
@ -6689,6 +6713,16 @@ packages:
defer-to-connect: 2.0.0
dev: true
/@szmarczak/http-timer/5.0.1:
resolution:
{
integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==,
}
engines: { node: '>=14.16' }
dependencies:
defer-to-connect: 2.0.1
dev: false
/@taskr/clear/1.1.0:
resolution:
{
@ -8300,7 +8334,6 @@ packages:
}
engines: { node: '>=0.4.0' }
hasBin: true
dev: true
/add-stream/1.0.0:
resolution:
@ -8371,7 +8404,6 @@ packages:
dependencies:
clean-stack: 2.2.0
indent-string: 4.0.0
dev: true
/ajv-keywords/3.5.2_ajv@6.12.6:
resolution:
@ -8756,7 +8788,10 @@ packages:
dev: true
/array-flatten/1.1.1:
resolution: { integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= }
resolution:
{
integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==,
}
dev: true
/array-ify/1.0.0:
@ -8910,6 +8945,13 @@ packages:
}
dev: true
/asciichart/1.5.25:
resolution:
{
integrity: sha512-PNxzXIPPOtWq8T7bgzBtk9cI2lgS4SJZthUHEiQ1aoIc3lNzGfUvIvo9LiAnq26TACo9t1/4qP6KTGAUbzX9Xg==,
}
dev: false
/asn1.js/4.10.1:
resolution:
{
@ -10599,7 +10641,6 @@ packages:
integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==,
}
engines: { node: '>=6' }
dev: true
/cli-boxes/2.2.1:
resolution:
@ -10674,6 +10715,17 @@ packages:
string-width: 1.0.2
dev: true
/cli-truncate/2.1.0:
resolution:
{
integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==,
}
engines: { node: '>=8' }
dependencies:
slice-ansi: 3.0.0
string-width: 4.2.3
dev: false
/cli-width/2.2.0:
resolution:
{
@ -10881,6 +10933,13 @@ packages:
}
dev: true
/colorette/2.0.19:
resolution:
{
integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==,
}
dev: false
/colors/1.4.0:
resolution:
{
@ -10921,7 +10980,6 @@ packages:
{
integrity: sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==,
}
dev: true
/commander/2.20.3:
resolution:
@ -10951,7 +11009,6 @@ packages:
integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==,
}
engines: { node: '>= 10' }
dev: true
/comment-json/3.0.3:
resolution:
@ -11297,7 +11354,10 @@ packages:
safe-buffer: 5.1.2
/cookie-signature/1.0.6:
resolution: { integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw= }
resolution:
{
integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==,
}
dev: true
/cookie/0.4.0:
@ -12400,6 +12460,14 @@ packages:
engines: { node: '>=10' }
dev: true
/defer-to-connect/2.0.1:
resolution:
{
integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==,
}
engines: { node: '>=10' }
dev: false
/define-lazy-prop/2.0.0:
resolution:
{
@ -12953,7 +13021,13 @@ packages:
integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==,
}
engines: { node: '>=10' }
dev: true
/downsample-lttb/0.0.1:
resolution:
{
integrity: sha512-Olebo5gyh44OAXTd2BKdcbN5VaZOIKFzoeo9JUFwxDlGt6Sd8fUo6SKaLcafy8aP2UrsKmWDpsscsFEghMjeZA==,
}
dev: false
/duplexer/0.1.1:
resolution:
@ -13973,7 +14047,6 @@ packages:
{
integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==,
}
dev: true
/events/3.3.0:
resolution:
@ -14842,7 +14915,10 @@ packages:
dev: true
/forwarded/0.1.2:
resolution: { integrity: sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= }
resolution:
{
integrity: sha512-Ua9xNhH0b8pwE3yRbFfXJvfdWF0UHNCdeyb2sbi9Ul/M+r3PTdrz7Cv4SCfZRMjmzEM9PhraqfZFbGTIg3OMyA==,
}
engines: { node: '>= 0.6' }
dev: true
@ -19311,6 +19387,28 @@ packages:
- zenObservable
dev: true
/listr2/5.0.5:
resolution:
{
integrity: sha512-DpBel6fczu7oQKTXMekeprc0o3XDgGMkD7JNYyX+X0xbwK+xgrx9dcyKoXKqpLSUvAWfmoePS7kavniOcq3r4w==,
}
engines: { node: ^14.13.1 || >=16.0.0 }
peerDependencies:
enquirer: '>= 2.3.0 < 3'
peerDependenciesMeta:
enquirer:
optional: true
dependencies:
cli-truncate: 2.1.0
colorette: 2.0.19
log-update: 4.0.0
p-map: 4.0.0
rfdc: 1.3.0
rxjs: 7.5.7
through: 2.3.8
wrap-ansi: 7.0.0
dev: false
/load-json-file/1.1.0:
resolution: { integrity: sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= }
engines: { node: '>=0.10.0' }
@ -19667,6 +19765,19 @@ packages:
wrap-ansi: 3.0.1
dev: true
/log-update/4.0.0:
resolution:
{
integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==,
}
engines: { node: '>=10' }
dependencies:
ansi-escapes: 4.3.0
cli-cursor: 3.1.0
slice-ansi: 4.0.0
wrap-ansi: 6.2.0
dev: false
/long/4.0.0:
resolution:
{
@ -20168,7 +20279,10 @@ packages:
dev: true
/merge-descriptors/1.0.1:
resolution: { integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= }
resolution:
{
integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==,
}
dev: true
/merge-stream/2.0.0:
@ -21985,7 +22099,6 @@ packages:
engines: { node: '>=10' }
dependencies:
aggregate-error: 3.0.1
dev: true
/p-pipe/3.1.0:
resolution:
@ -22006,6 +22119,17 @@ packages:
p-timeout: 3.2.0
dev: true
/p-queue/7.3.0:
resolution:
{
integrity: sha512-5fP+yVQ0qp0rEfZoDTlP2c3RYBgxvRsw30qO+VtPPc95lyvSG+x6USSh1TuLB4n96IO6I8/oXQGsTgtna4q2nQ==,
}
engines: { node: '>=12' }
dependencies:
eventemitter3: 4.0.7
p-timeout: 5.1.0
dev: false
/p-reduce/2.1.0:
resolution:
{
@ -22044,6 +22168,14 @@ packages:
p-finally: 1.0.0
dev: true
/p-timeout/5.1.0:
resolution:
{
integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==,
}
engines: { node: '>=12' }
dev: false
/p-try/1.0.0:
resolution: { integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= }
engines: { node: '>=4' }
@ -25611,6 +25743,13 @@ packages:
engines: { node: '>=4' }
dev: true
/rfdc/1.3.0:
resolution:
{
integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==,
}
dev: false
/rgb-regex/1.0.1:
resolution:
{
@ -25810,6 +25949,15 @@ packages:
tslib: 2.4.0
dev: true
/rxjs/7.5.7:
resolution:
{
integrity: sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==,
}
dependencies:
tslib: 2.4.0
dev: false
/sade/1.7.4:
resolution:
{
@ -26333,6 +26481,18 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
/slice-ansi/3.0.0:
resolution:
{
integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==,
}
engines: { node: '>=8' }
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
dev: false
/slice-ansi/4.0.0:
resolution:
{
@ -27516,6 +27676,14 @@ packages:
engines: { node: '>=8' }
dev: true
/term-size/3.0.2:
resolution:
{
integrity: sha512-YfE8KwjrumviCxmeOS1r1hAwqUcd/AnhrG/Pol/Gry91EyUCS+jQH0qFUZOUkpGQ5rXsKMK6S6kjne53ytYS/w==,
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dev: false
/terminal-link/2.1.1:
resolution:
{
@ -29010,7 +29178,10 @@ packages:
dev: true
/utils-merge/1.0.1:
resolution: { integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= }
resolution:
{
integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==,
}
engines: { node: '>= 0.4.0' }
dev: true
@ -29326,6 +29497,28 @@ packages:
- bufferutil
- utf-8-validate
/webpack-bundle-analyzer/4.6.1:
resolution:
{
integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==,
}
engines: { node: '>= 10.13.0' }
hasBin: true
dependencies:
acorn: 8.8.0
acorn-walk: 8.0.0
chalk: 4.1.2
commander: 7.2.0
gzip-size: 6.0.0
lodash: 4.17.21
opener: 1.5.2
sirv: 1.0.10
ws: 7.5.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/webpack-sources/1.4.3:
resolution:
{
@ -29344,6 +29537,13 @@ packages:
engines: { node: '>=10.13.0' }
dev: true
/webpack-stats-plugin/1.1.0:
resolution:
{
integrity: sha512-D0meHk1WYryUbuCnWJuomJFAYvqs0rxv/JFu1XJT1YYpczdgnP1/vz+u/5Z31jrTxT6dJSxCg+TuKTgjhoZS6g==,
}
dev: false
/webpack/5.74.0:
resolution:
{
@ -29614,6 +29814,18 @@ packages:
strip-ansi: 5.2.0
dev: true
/wrap-ansi/6.2.0:
resolution:
{
integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==,
}
engines: { node: '>=8' }
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: false
/wrap-ansi/7.0.0:
resolution:
{
@ -29624,7 +29836,6 @@ packages:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: true
/wrappy/1.0.2:
resolution:

View file

@ -1,2 +1,3 @@
packages:
- 'packages/*'
- 'bench/vercel/'