(
key: string,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
): T {
- const path = reference.split(':');
- const id = parseInt(path[0], 16);
+ // parseInt stops at the ':' so we only need to split when there's a path.
+ const id = parseInt(reference, 16);
+ const path =
+ reference.indexOf(':') === -1 ? EMPTY_REFERENCE_PATH : reference.split(':');
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (initializingChunk !== null && isArray(initializingChunk._children)) {
@@ -3246,10 +3252,22 @@ function resolveModule(
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
- const clientReferenceMetadata: ClientReferenceMetadata = parseModel(
- response,
- model,
- );
+ const prevHandler = initializingHandler;
+ initializingHandler = null;
+ let clientReferenceMetadata: ClientReferenceMetadata;
+ try {
+ clientReferenceMetadata = parseModel(response, model);
+ if (initializingHandler !== null) {
+ // We resolve the client reference below and have nothing to wait on,
+ // so the metadata can't reference a row that hasn't arrived.
+ throw new Error(
+ 'A client reference was blocked on a row that has not been received yet. ' +
+ 'This is a bug in React.',
+ );
+ }
+ } finally {
+ initializingHandler = prevHandler;
+ }
const clientReference = resolveClientReference<$FlowFixMe>(
response._bundlerConfig,
clientReferenceMetadata,
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
index 0c1f9add5b44..2e846579db36 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
@@ -1107,6 +1107,233 @@ describe('ReactFlightDOMEdge', () => {
expect(items[5]).toEqual(items[10]);
});
+ function clientComponent(name, chunkFilename) {
+ return clientExports(
+ function Client() {
+ return {name};
+ },
+ 'chunk-' + name,
+ chunkFilename,
+ Promise.resolve(),
+ );
+ }
+
+ async function renderClients(chunkFilenames) {
+ const Clients = chunkFilenames.map(chunkFilename =>
+ clientComponent('Client', chunkFilename),
+ );
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(
+
+ {Clients.map((Client, i) => (
+
+ ))}
+
,
+ webpackMap,
+ ),
+ );
+ const [stream1, stream2] = passThrough(stream).tee();
+ const payload = await readResult(stream1);
+ const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
+ serverConsumerManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(model),
+ );
+ expect(await readResult(ssrStream)).toBe(
+ '' + 'Client'.repeat(Clients.length) + '
',
+ );
+ return payload;
+ }
+
+ it('should dedupe strings inside client reference metadata', async () => {
+ // Bundlers repeat the same chunk in the metadata of every client reference
+ // that needs it.
+ const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
+ const shared = await renderClients(new Array(10).fill(chunk));
+ // The same length, so sharing is the only difference between the two.
+ const distinct = await renderClients(
+ Array.from(
+ {length: 10},
+ (_, i) => 'unique/hashed-chunk-' + ('' + i).padStart(16, '0') + '.js',
+ ),
+ );
+
+ // However many references there are, the chunk goes on the wire once, as a
+ // row that every import row points at.
+ expect(shared.split(chunk).length - 1).toBe(1);
+ expect(distinct.length - shared.length).toBeGreaterThan(8 * chunk.length);
+
+ // The client resolves a client reference while parsing its row, so the
+ // outlined copy has to arrive before every row that points at it. The
+ // chunk id is too short to be outlined, so it counts the rows.
+ const beforeOutlinedCopy = shared.slice(0, shared.indexOf(chunk));
+ expect(beforeOutlinedCopy).not.toContain('chunk-Client');
+ });
+
+ it('should escape strings inside client reference metadata', async () => {
+ // A leading $ has to be escaped whether the string gets outlined or not.
+ const outlinedChunk = '$shared/hashed-chunk-0f1e2d3c4b5a6978.js';
+ const inlineChunk = '$chunk.js';
+ const outlined = await renderClients(new Array(3).fill(outlinedChunk));
+ const inline = await renderClients(new Array(3).fill(inlineChunk));
+
+ expect(outlined.split('$' + outlinedChunk).length - 1).toBe(1);
+ expect(inline.split('$' + inlineChunk).length - 1).toBe(3);
+ });
+
+ it('should not dedupe import strings below the size limit', async () => {
+ // A short string costs more to reference than to repeat.
+ const shortChunk = 'abc/chunk-15.js';
+ const longChunk = 'abcd/chunk-16.js';
+ const short = await renderClients(new Array(10).fill(shortChunk));
+ const long = await renderClients(new Array(10).fill(longChunk));
+
+ expect(short.split(shortChunk).length - 1).toBe(10);
+ expect(long.split(longChunk).length - 1).toBe(1);
+ // The longer chunk is the one that produces the smaller payload.
+ expect(long.length).toBeLessThan(short.length);
+ });
+
+ it('should stop tracking new import strings once the budget is spent', async () => {
+ // Every chunk is outlined the first time it's seen, so chunks that never
+ // repeat spend budget too. 32 fillers of 1 KiB fill the 32 KiB budget.
+ const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
+ const repeats = new Array(10).fill(chunk);
+ const filler = (count, length) =>
+ Array.from({length: count}, (_, i) =>
+ ('filler/chunk-' + i + '-').padEnd(length - 3, 'x').concat('.js'),
+ );
+ const fitsInTheRest = await renderClients(filler(31, 1024).concat(repeats));
+ const findsItSpent = await renderClients(filler(32, 1024).concat(repeats));
+
+ expect(fitsInTheRest.split(chunk).length - 1).toBe(1);
+ expect(findsItSpent.split(chunk).length - 1).toBe(10);
+
+ // A string outlined before the budget is spent keeps deduping after.
+ const lastFiller = filler(1, 32768 - 31 * 1024 - chunk.length);
+ const trackedBefore = await renderClients(
+ [chunk].concat(filler(31, 1024), lastFiller, repeats),
+ );
+
+ expect(trackedBefore.split(chunk).length - 1).toBe(1);
+
+ const bigChunk = 'path/to/' + 'a'.repeat(40000) + '.js';
+ const big = await renderClients(new Array(3).fill(bigChunk));
+
+ expect(big.split(bigChunk).length - 1).toBe(3);
+ });
+
+ it('should dedupe import strings produced by toJSON', async () => {
+ const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
+ const payload = await renderClients(
+ Array.from({length: 3}, () => ({
+ toJSON() {
+ return chunk;
+ },
+ })),
+ );
+
+ expect(payload.split(chunk).length - 1).toBe(1);
+ });
+
+ it('should error on circular client reference metadata', async () => {
+ const circular = [];
+ circular.push(circular);
+ const Client = clientComponent('Client', circular);
+
+ const errors = [];
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(, webpackMap, {
+ onError(error) {
+ errors.push(error.message);
+ },
+ }),
+ );
+ await readResult(stream);
+
+ expect(errors).toEqual([
+ expect.stringContaining('Converting circular structure to JSON'),
+ ]);
+ });
+
+ it('should not dedupe strings in the model', async () => {
+ // Only import metadata is deduped. Keying a map on arbitrary model strings
+ // would hold them in memory for the rest of the request.
+ const text = 'a repeated model string well past the import threshold';
+ const model = new Array(10).fill(text);
+
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(model),
+ );
+ const [stream1, stream2] = passThrough(stream).tee();
+
+ const payload = await readResult(stream1);
+ expect(payload.split(text).length - 1).toBe(10);
+
+ const result = await ReactServerDOMClient.createFromReadableStream(
+ stream2,
+ {
+ serverConsumerManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ },
+ );
+ expect(result).toEqual(model);
+ });
+
+ // @gate __DEV__
+ it('should not dedupe import metadata on the debug channel', async () => {
+ // The debug channel is a separate transport, so a row it emits can't be
+ // referenced from the main stream and vice versa.
+ const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
+ const A = clientComponent('Client', chunk);
+ const B = clientComponent('Client', chunk);
+ const C = clientComponent('Client', chunk);
+
+ function Server({a, b, c}) {
+ return ReactServer.createElement('div', null, a, b, c);
+ }
+
+ let debugContent = '';
+ const debugChannel = {
+ writable: new WritableStream({
+ write(value) {
+ debugContent += Buffer.from(value).toString('utf8');
+ },
+ }),
+ };
+
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(
+ // We can't use JSX here because it'll use the Client React.
+ ReactServer.createElement(Server, {
+ a: ReactServer.createElement(A),
+ b: ReactServer.createElement(B),
+ c: ReactServer.createElement(C),
+ }),
+ webpackMap,
+ {debugChannel},
+ ),
+ );
+ const payload = await readResult(stream);
+
+ // The main stream dedupes as usual.
+ expect(payload.split(chunk).length - 1).toBe(1);
+
+ // The debug channel can't point at that row, so every import row it writes
+ // spells the chunk out. The chunk id is too short to be outlined, so it
+ // counts those rows.
+ expect(debugContent).toContain('chunk-Client');
+ expect(debugContent.split(chunk).length).toBe(
+ debugContent.split('chunk-Client').length,
+ );
+ });
+
it('warns if passing a this argument to bind() of a server reference', async () => {
const ServerModule = serverExports({
greet: function () {},
@@ -2370,6 +2597,66 @@ describe('ReactFlightDOMEdge', () => {
);
});
+ async function renderThroughDebugChannel(chunkFilename) {
+ const Client = clientComponent('Client', chunkFilename);
+ // The client reference shows up in the owner's props on the debug channel.
+ function Server({component}) {
+ return ReactServer.createElement(component, null);
+ }
+
+ let debugReadableStreamController;
+ const debugReadableStream = new ReadableStream({
+ start(controller) {
+ debugReadableStreamController = controller;
+ },
+ });
+
+ const stream = await serverAct(() =>
+ ReactServerDOMServer.renderToReadableStream(
+ ReactServer.createElement(Server, {component: Client}),
+ webpackMap,
+ {
+ debugChannel: {
+ writable: new WritableStream({
+ write(chunk) {
+ debugReadableStreamController.enqueue(chunk);
+ },
+ close() {
+ debugReadableStreamController.close();
+ },
+ }),
+ },
+ },
+ ),
+ );
+
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
+ serverConsumerManifest: {moduleMap: null, moduleLoading: null},
+ debugChannel: {readable: debugReadableStream},
+ });
+
+ function ClientRoot() {
+ return use(response);
+ }
+
+ const ssrStream = await serverAct(() =>
+ ReactDOMServer.renderToReadableStream(),
+ );
+ return readResult(ssrStream);
+ }
+
+ it('can resolve a client reference while debug info is still blocked', async () => {
+ const result = await renderThroughDebugChannel('path/to/chunk.js');
+
+ expect(result).toBe('Client');
+ });
+
+ it('should escape strings in import metadata on the debug channel', async () => {
+ const result = await renderThroughDebugChannel('$path/to/chunk.js');
+
+ expect(result).toBe('Client');
+ });
+
it('should properly resolve with deduped objects', async () => {
const obj = {foo: 'hi'};
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index d9177cd27645..e3893a75de3b 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -616,6 +616,9 @@ export type Request = {
writtenClientReferences: Map,
writtenServerReferences: Map, number>,
writtenObjects: WeakMap,
+ writtenImportStrings: Map,
+ // The combined length of the keys in writtenImportStrings.
+ writtenImportStringsSize: number,
temporaryReferences: void | TemporaryReferenceSet,
identifierPrefix: string,
identifierCount: number,
@@ -741,6 +744,8 @@ function RequestInstance(
this.writtenClientReferences = new Map();
this.writtenServerReferences = new Map();
this.writtenObjects = new WeakMap();
+ this.writtenImportStrings = new Map();
+ this.writtenImportStringsSize = 0;
this.temporaryReferences = temporaryReferences;
this.identifierPrefix = identifierPrefix || '';
this.identifierCount = 1;
@@ -2250,6 +2255,16 @@ let canEmitDebugInfo: boolean = false;
let serializedSize = 0;
const MAX_ROW_SIZE = 3200;
+// Bundler metadata repeats the same chunk URLs across every client reference of
+// a route, so strings in it at least this long get outlined and deduplicated
+// when they repeat. The threshold is bounded away from zero because outlining
+// something as short as an export name costs more than copying it.
+const MIN_DEDUPLICATED_IMPORT_STRING_LENGTH = 16;
+
+// Tracked strings are retained for the rest of the request, so their combined
+// length is capped.
+const MAX_DEDUPLICATED_IMPORT_STRINGS_SIZE = 32768;
+
function deferTask(request: Request, task: Task): ReactJSONValue {
// Like outlineTask but instead the item is scheduled to be serialized
// after its parent in the stream.
@@ -3172,9 +3187,15 @@ function serializeClientReference(
try {
const clientReferenceMetadata: ClientReferenceMetadata =
resolveClientReferenceMetadata(request.bundlerConfig, clientReference);
+ // Stringify before claiming a chunk id so a throw can't leave it pending.
+ const json = stringifyImportMetadata(
+ request,
+ clientReferenceMetadata,
+ false,
+ );
request.pendingChunks++;
const importId = request.nextChunkId++;
- emitImportChunk(request, importId, clientReferenceMetadata, false);
+ emitImportChunk(request, importId, json, false);
writtenClientReferences.set(clientReferenceKey, importId);
if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') {
// If we're encoding the "type" of an element, we can refer
@@ -3222,9 +3243,14 @@ function serializeDebugClientReference(
try {
const clientReferenceMetadata: ClientReferenceMetadata =
resolveClientReferenceMetadata(request.bundlerConfig, clientReference);
+ const json = stringifyImportMetadata(
+ request,
+ clientReferenceMetadata,
+ true,
+ );
request.pendingDebugChunks++;
const importId = request.nextChunkId++;
- emitImportChunk(request, importId, clientReferenceMetadata, true);
+ emitImportChunk(request, importId, json, true);
if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') {
// If we're encoding the "type" of an element, we can refer
// to that by a lazy reference instead of directly since React
@@ -3594,6 +3620,41 @@ function escapeStringValue(value: string): string {
}
}
+function serializeImportString(request: Request, value: string): string {
+ // No maximum length because import strings are short and repeat often.
+ // Deduping model strings too would need one to skip very long strings.
+ if (value.length < MIN_DEDUPLICATED_IMPORT_STRING_LENGTH) {
+ return escapeStringValue(value);
+ }
+ const writtenStrings = request.writtenImportStrings;
+ const existing = writtenStrings.get(value);
+ if (existing !== undefined) {
+ return existing;
+ }
+ const size = request.writtenImportStringsSize + value.length;
+ if (size > MAX_DEDUPLICATED_IMPORT_STRINGS_SIZE) {
+ // The map is full. Strings already outlined keep deduping; new ones are
+ // written out every time.
+ return escapeStringValue(value);
+ }
+ request.writtenImportStringsSize = size;
+ // Chunk names are almost always shared, so the first occurrence is outlined
+ // right away instead of waiting for a repeat.
+ request.pendingChunks++;
+ const outlinedId = request.nextChunkId++;
+ // $FlowFixMe[incompatible-type] stringify can return null
+ const json: string = stringify(escapeStringValue(value));
+ // The client reads import metadata synchronously, so this row has to have
+ // been written by the time the referencing row arrives. Import chunks are
+ // flushed ahead of regular ones, which regular chunks can't guarantee.
+ request.completedImportChunks.push(
+ stringToChunk(outlinedId.toString(16) + ':' + json + '\n'),
+ );
+ const ref = serializeByValueID(outlinedId);
+ writtenStrings.set(value, ref);
+ return ref;
+}
+
let modelRoot: null | ReactClientValue = false;
function renderModel(
@@ -4584,14 +4645,140 @@ function emitErrorChunk(
}
}
+// Null on the debug channel, which can't reference rows in the main stream.
+let importStringRequest: null | Request = null;
+
+function importMetadataReplacer(key: string, value: mixed): mixed {
+ if (typeof value === 'string') {
+ const request = importStringRequest;
+ if (request === null) {
+ return escapeStringValue(value);
+ }
+ return serializeImportString(request, value);
+ }
+ return value;
+}
+
+function stringifyImportMetadataWithReplacer(
+ request: Request,
+ clientReferenceMetadata: ClientReferenceMetadata,
+ debug: boolean,
+): string {
+ const prevRequest = importStringRequest;
+ importStringRequest = __DEV__ && debug ? null : request;
+ try {
+ // $FlowFixMe[incompatible-type] stringify can return null
+ return stringify(clientReferenceMetadata, importMetadataReplacer);
+ } finally {
+ importStringRequest = prevRequest;
+ }
+}
+
+// Bundler metadata is two or three levels deep. The bound is only there so a
+// cycle ends up in stringify itself, which throws its own error for it.
+const MAX_IMPORT_METADATA_DEPTH = 16;
+
+const NOT_PLAIN_IMPORT_METADATA = {};
+
+// Copies the metadata with every string replaced by its serialized form, so
+// that stringify can run without a replacer. Anything stringify would treat
+// specially (toJSON, boxed primitives, class instances) makes this give up
+// instead, because the copy would not reproduce that treatment.
+function transformImportMetadata(
+ request: Request,
+ value: mixed,
+ depth: number,
+): mixed {
+ switch (typeof value) {
+ case 'string':
+ return serializeImportString(request, value);
+ case 'number':
+ case 'boolean':
+ case 'undefined':
+ return value;
+ case 'object': {
+ if (value === null) {
+ return null;
+ }
+ if (depth > MAX_IMPORT_METADATA_DEPTH) {
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ if (typeof (value as any).toJSON === 'function') {
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ if (isArray(value)) {
+ const length = value.length;
+ const copy: Array = new Array(length);
+ for (let i = 0; i < length; i++) {
+ const element = value[i];
+ if (typeof element === 'string') {
+ copy[i] = serializeImportString(request, element);
+ continue;
+ }
+ const child = transformImportMetadata(request, element, depth + 1);
+ if (child === NOT_PLAIN_IMPORT_METADATA) {
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ copy[i] = child;
+ }
+ return copy;
+ }
+ const proto = getPrototypeOf(value);
+ if (proto !== ObjectPrototype && proto !== null) {
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ const keys = Object.keys(value);
+ const copy: {[string]: mixed} = {};
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ if (key in ObjectPrototype) {
+ // The copy inherits from Object.prototype, so assigning this key would
+ // hit an accessor like __proto__ or, if the prototype is frozen, throw.
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ const element = (value as any)[key];
+ if (typeof element === 'string') {
+ copy[key] = serializeImportString(request, element);
+ continue;
+ }
+ const child = transformImportMetadata(request, element, depth + 1);
+ if (child === NOT_PLAIN_IMPORT_METADATA) {
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+ copy[key] = child;
+ }
+ return copy;
+ }
+ default:
+ return NOT_PLAIN_IMPORT_METADATA;
+ }
+}
+
+function stringifyImportMetadata(
+ request: Request,
+ clientReferenceMetadata: ClientReferenceMetadata,
+ debug: boolean,
+): string {
+ if (!(__DEV__ && debug)) {
+ const copy = transformImportMetadata(request, clientReferenceMetadata, 0);
+ if (copy !== NOT_PLAIN_IMPORT_METADATA) {
+ // $FlowFixMe[incompatible-type] stringify can return null
+ return stringify(copy);
+ }
+ }
+ return stringifyImportMetadataWithReplacer(
+ request,
+ clientReferenceMetadata,
+ debug,
+ );
+}
+
function emitImportChunk(
request: Request,
id: number,
- clientReferenceMetadata: ClientReferenceMetadata,
+ json: string,
debug: boolean,
): void {
- // $FlowFixMe[incompatible-type] stringify can return null
- const json: string = stringify(clientReferenceMetadata);
const row = serializeRowHeader('I', id) + json + '\n';
const processedChunk = stringToChunk(row);
if (__DEV__ && debug) {
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index a1ebcc3106f5..ba39b2b6acc5 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -592,5 +592,6 @@
"604": "The server render could not complete because client rendering was requested outside a Suspense boundary. See this error's cause for additional details.",
"605": "Recoverable Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render so a downstream renderer can recover it. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.",
"606": "Expected a suspended recoverable. This is a bug in React. Please file an issue.",
- "607": "This library called use() to suspend in a previous render but did not call use() when it finished. This indicates an incorrect use of use(). Learn more: https://react.dev/warnings/conditional-use-of-use"
+ "607": "This library called use() to suspend in a previous render but did not call use() when it finished. This indicates an incorrect use of use(). Learn more: https://react.dev/warnings/conditional-use-of-use",
+ "608": "A client reference was blocked on a row that has not been received yet. This is a bug in React."
}
diff --git a/scripts/sizebot/compare-sizes.js b/scripts/sizebot/compare-sizes.js
new file mode 100644
index 000000000000..6e14a604956f
--- /dev/null
+++ b/scripts/sizebot/compare-sizes.js
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+/* eslint-disable no-for-of-loops/no-for-of-loops */
+
+// Measures every build artifact in `build` against the base revision's artifacts
+// in `base-build` and writes the raw numbers to `sizebot-results.json`.
+//
+// This runs in the `pull_request` half of CI, where the GitHub token is
+// read-only, so it never talks to the API and never renders anything
+// user-facing. `render-comment.js` turns this JSON into the pull request comment
+// from a trusted checkout. See `.github/workflows/runtime_sizebot_comment.yml`
+// for why the two halves are separate.
+
+const {promisify} = require('util');
+const glob = promisify(require('glob'));
+const gzipSize = require('gzip-size');
+const {readFileSync, statSync, writeFileSync} = require('fs');
+
+// Bump on any incompatible change to the JSON below: added required fields,
+// renamed or removed fields, or a changed meaning for an existing one. Purely
+// additive optional fields do not need a bump. The reader lives on the default
+// branch while the writer lives on the pull request branch, so the two can
+// legitimately disagree and `render-comment.js` needs to be able to tell.
+const RESULTS_VERSION = 1;
+
+const RESULTS_PATH = 'sizebot-results.json';
+const BASE_DIR = 'base-build';
+const HEAD_DIR = 'build';
+
+function measure(dir, artifactPath) {
+ const file = dir + '/' + artifactPath;
+ return {
+ size: statSync(file).size,
+ sizeGzip: gzipSize.fileSync(file),
+ };
+}
+
+function writeResults(results) {
+ writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2) + '\n');
+}
+
+(async function () {
+ let headSha;
+ let baseSha;
+ try {
+ headSha = String(readFileSync(HEAD_DIR + '/COMMIT_SHA')).trim();
+ baseSha = String(readFileSync(BASE_DIR + '/COMMIT_SHA')).trim();
+ } catch {
+ // Let the renderer explain this one. It is expected to happen whenever the
+ // build configuration changes upstream, which is not a CI failure.
+ writeResults({
+ version: RESULTS_VERSION,
+ status: 'base-artifacts-unavailable',
+ });
+ return;
+ }
+
+ // A missing size is recorded as null rather than 0, so the renderer can tell
+ // "this artifact does not exist on that side" apart from "this artifact is
+ // empty". It derives the new-file and deleted-file cases from those nulls.
+ const artifactsByPath = new Map();
+
+ const headArtifactPaths = await glob('**/*.js', {cwd: HEAD_DIR});
+ for (const artifactPath of headArtifactPaths) {
+ let base;
+ try {
+ base = measure(BASE_DIR, artifactPath);
+ } catch {
+ // There's no matching base artifact. This is a new file.
+ base = null;
+ }
+ const head = measure(HEAD_DIR, artifactPath);
+ artifactsByPath.set(artifactPath, {
+ path: artifactPath,
+ baseSize: base === null ? null : base.size,
+ baseSizeGzip: base === null ? null : base.sizeGzip,
+ headSize: head.size,
+ headSizeGzip: head.sizeGzip,
+ });
+ }
+
+ // Check for base artifacts that were deleted in the head.
+ const baseArtifactPaths = await glob('**/*.js', {cwd: BASE_DIR});
+ for (const artifactPath of baseArtifactPaths) {
+ if (!artifactsByPath.has(artifactPath)) {
+ const base = measure(BASE_DIR, artifactPath);
+ artifactsByPath.set(artifactPath, {
+ path: artifactPath,
+ baseSize: base.size,
+ baseSizeGzip: base.sizeGzip,
+ headSize: null,
+ headSizeGzip: null,
+ });
+ }
+ }
+
+ // Every artifact is reported, with no threshold filtering. The thresholds and
+ // the critical bundle list belong to the renderer, so that a pull request
+ // cannot quietly widen them to hide a regression.
+ writeResults({
+ version: RESULTS_VERSION,
+ status: 'ok',
+ baseSha,
+ headSha,
+ artifacts: Array.from(artifactsByPath.values()),
+ });
+})();
diff --git a/scripts/sizebot/pull-request-comment.js b/scripts/sizebot/pull-request-comment.js
new file mode 100644
index 000000000000..17056fef4825
--- /dev/null
+++ b/scripts/sizebot/pull-request-comment.js
@@ -0,0 +1,253 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+/* eslint-disable no-for-of-loops/no-for-of-loops */
+
+// The GitHub API half of sizebot, called from `actions/github-script` steps in
+// `.github/workflows/runtime_sizebot_comment.yml`.
+//
+// `resolve` figures out which pull request a `workflow_run` event belongs to,
+// finds any comment sizebot has already left on it, and decides whether this
+// event should write at all. `post` creates or updates the comment from the body
+// that `render-comment.js` produced in between.
+
+const {readFileSync, writeFileSync} = require('fs');
+const {
+ MARKER_PREFIX,
+ extractReport,
+ parseReportHead,
+} = require('./render-comment');
+
+const CONTEXT_PATH = 'sizebot-context.json';
+const COMMENT_PATH = 'sizebot-comment.md';
+
+const COMMENT_AUTHOR = 'github-actions[bot]';
+
+// `pulls.listFiles` stops paginating here, so a pull request larger than this
+// cannot be shown to touch only DevTools.
+const MAX_LISTABLE_FILES = 3000;
+
+const DEVTOOLS_PATH = 'packages/react-devtools';
+
+async function findExistingComment(github, context, prNumber) {
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ per_page: 100,
+ });
+ for (const comment of comments) {
+ if (
+ comment.user.login === COMMENT_AUTHOR &&
+ comment.body.startsWith(MARKER_PREFIX)
+ ) {
+ return comment;
+ }
+ }
+ return null;
+}
+
+// `workflow_run.pull_requests` is empty for runs triggered by a fork, and
+// neither `commits/{sha}/pulls` nor the search API index fork pull request head
+// commits. Looking the branch up by `owner:ref` is what actually works for both
+// fork and same-repo pull requests.
+async function findPullRequestNumber(github, context, workflowRun) {
+ if (
+ workflowRun.pull_requests != null &&
+ workflowRun.pull_requests.length > 0
+ ) {
+ return workflowRun.pull_requests[0].number;
+ }
+
+ if (workflowRun.head_repository == null) {
+ return null;
+ }
+
+ const {data: pulls} = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head: `${workflowRun.head_repository.owner.login}:${workflowRun.head_branch}`,
+ state: 'open',
+ per_page: 100,
+ });
+ if (pulls.length === 0) {
+ return null;
+ }
+ return pulls[0].number;
+}
+
+async function findPullRequest(github, context, workflowRun) {
+ const number = await findPullRequestNumber(github, context, workflowRun);
+ if (number === null) {
+ return null;
+ }
+ // Always finish with `pulls.get`. The list endpoint omits `changed_files`,
+ // which `isDevToolsOnly` needs, and that is the endpoint the fork path uses.
+ const {data} = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: number,
+ });
+ return data;
+}
+
+async function isDevToolsOnly(github, context, pullRequest) {
+ if (
+ !Number.isInteger(pullRequest.changed_files) ||
+ // `listFiles` would silently truncate, and a truncated list can look
+ // DevTools-only when it is not.
+ pullRequest.changed_files > MAX_LISTABLE_FILES
+ ) {
+ return false;
+ }
+ const files = await github.paginate(github.rest.pulls.listFiles, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pullRequest.number,
+ per_page: 100,
+ });
+ if (files.length === 0) {
+ return false;
+ }
+ return files.every(file => file.filename.includes(DEVTOOLS_PATH));
+}
+
+async function resolve({github, context, core}) {
+ const workflowRun = context.payload.workflow_run;
+ const action = context.payload.action;
+
+ const pullRequest = await findPullRequest(github, context, workflowRun);
+ if (pullRequest === null) {
+ core.info('No open pull request for this run. Nothing to comment on.');
+ core.setOutput('action', 'skip');
+ return;
+ }
+
+ // A pull request number must never come from the build artifact, which the
+ // fork controls. Confirm the one we resolved really does belong to this run.
+ const runRepo = workflowRun.head_repository?.full_name ?? null;
+ const pullRequestRepo = pullRequest.head.repo?.full_name ?? null;
+ if (runRepo === null || pullRequestRepo === null) {
+ // A deleted fork leaves us no way to check, so don't write anything.
+ core.info('Head repository is unavailable. Nothing to comment on.');
+ core.setOutput('action', 'skip');
+ return;
+ }
+ if (pullRequestRepo !== runRepo) {
+ core.setFailed(
+ `Pull request #${pullRequest.number} has head repository ` +
+ `${pullRequestRepo}, but the run came from ${runRepo}.`
+ );
+ return;
+ }
+
+ // Mirrors the sizebot job's own condition in runtime_build_and_test.yml.
+ if (pullRequest.base.ref !== 'main') {
+ core.info(
+ `Pull request #${pullRequest.number} targets ${pullRequest.base.ref}, not main.`
+ );
+ core.setOutput('action', 'skip');
+ return;
+ }
+
+ const existing = await findExistingComment(
+ github,
+ context,
+ pullRequest.number
+ );
+ const existingReportHead =
+ existing === null ? null : parseReportHead(existing.body);
+
+ // The only event we ever drop: a comment already describes the current head,
+ // and this event is about an older commit. Without this, a run cancelled by a
+ // force push reports `cancelled` after the newer run's comment has landed and
+ // replaces good numbers with a cancellation notice.
+ if (
+ existingReportHead !== null &&
+ existingReportHead === pullRequest.head.sha &&
+ workflowRun.head_sha !== pullRequest.head.sha
+ ) {
+ core.info(
+ `Comment already reports on ${pullRequest.head.sha}; this run is for ` +
+ `${workflowRun.head_sha}. Leaving it alone.`
+ );
+ core.setOutput('action', 'skip');
+ return;
+ }
+
+ const devtoolsOnly =
+ action === 'completed'
+ ? await isDevToolsOnly(github, context, pullRequest)
+ : false;
+
+ writeFileSync(
+ CONTEXT_PATH,
+ JSON.stringify(
+ {
+ action,
+ prNumber: pullRequest.number,
+ prHeadSha: pullRequest.head.sha,
+ runHeadSha: workflowRun.head_sha,
+ runUrl: workflowRun.html_url,
+ runStatus: workflowRun.status,
+ runConclusion: workflowRun.conclusion,
+ devtoolsOnly,
+ existingCommentId: existing === null ? null : existing.id,
+ existingReportHead,
+ existingReport: existing === null ? null : extractReport(existing.body),
+ commentRunUrl: `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
+ },
+ null,
+ 2
+ ) + '\n'
+ );
+
+ core.setOutput('action', 'continue');
+ // Only a completed run can have produced results to download.
+ core.setOutput('download_results', String(action === 'completed'));
+}
+
+async function post({github, context, core}) {
+ const sizebotContext = JSON.parse(readFileSync(CONTEXT_PATH, 'utf8'));
+ const body = readFileSync(COMMENT_PATH, 'utf8');
+
+ async function create() {
+ const {data} = await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: sizebotContext.prNumber,
+ body,
+ });
+ core.info(`Created ${data.html_url}`);
+ }
+
+ if (sizebotContext.existingCommentId === null) {
+ await create();
+ return;
+ }
+
+ try {
+ const {data} = await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: sizebotContext.existingCommentId,
+ body,
+ });
+ core.info(`Updated ${data.html_url}`);
+ } catch (error) {
+ if (error.status !== 404) {
+ throw error;
+ }
+ // Someone deleted the comment between resolving it and writing to it.
+ core.info('Existing comment is gone, posting a new one.');
+ await create();
+ }
+}
+
+module.exports = {post, resolve};
diff --git a/scripts/sizebot/render-comment.js b/scripts/sizebot/render-comment.js
new file mode 100644
index 000000000000..32eceef8d859
--- /dev/null
+++ b/scripts/sizebot/render-comment.js
@@ -0,0 +1,489 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+/* eslint-disable no-for-of-loops/no-for-of-loops */
+
+// Turns `sizebot-results.json` into the body of the sizebot pull request
+// comment. Runs from a checkout of the default branch, never from the pull
+// request branch, so the thresholds, the critical bundle list and the table
+// itself cannot be influenced by the pull request being measured. Everything it
+// reads out of the results file is therefore treated as untrusted input.
+//
+// Reads `sizebot-context.json` (written by the resolve step) and, when the build
+// produced one, `sizebot-results.json`. Writes `sizebot-comment.md`, plus
+// `sizebot-message.md` when the report is too large to fit in a comment and
+// `sizebot-problem.txt` when the build configuration no longer matches this
+// file's expectations.
+
+const {existsSync, readFileSync, writeFileSync} = require('fs');
+
+// Results shapes this file knows how to read. `compare-sizes.js` on the pull
+// request branch may be older or newer than this list.
+const SUPPORTED_VERSIONS = new Set([1]);
+const SUPPORTED_STATUSES = new Set(['ok', 'base-artifacts-unavailable']);
+
+const CRITICAL_THRESHOLD = 0.02;
+const SIGNIFICANCE_THRESHOLD = 0.002;
+const CRITICAL_ARTIFACT_PATHS = new Set([
+ // We always report changes to these bundles, even if the change is
+ // insignificant or non-existent.
+ 'oss-stable/react-dom/cjs/react-dom.production.js',
+ 'oss-stable/react-dom/cjs/react-dom-client.production.js',
+ 'oss-experimental/react-dom/cjs/react-dom.production.js',
+ 'oss-experimental/react-dom/cjs/react-dom-client.production.js',
+ 'facebook-www/ReactDOM-prod.classic.js',
+ 'facebook-www/ReactDOM-prod.modern.js',
+]);
+
+// GitHub comments are limited to 65536 characters.
+const MAX_COMMENT_LENGTH = 65536;
+
+// Both the notice and the report are delimited so each can be rewritten without
+// disturbing the other, and so a report can be read back out of a comment
+// verbatim when a newer build supersedes it. Relying on "everything after the
+// notice" instead would swallow the footer and append a second one every time.
+const MARKER_PREFIX = '';
+const NOTICE_END = '';
+const REPORT_START = '';
+const REPORT_END = '';
+
+const CONTEXT_PATH = 'sizebot-context.json';
+const RESULTS_PATH = 'sizebot-results.json';
+const COMMENT_PATH = 'sizebot-comment.md';
+const MESSAGE_PATH = 'sizebot-message.md';
+const PROBLEM_PATH = 'sizebot-problem.txt';
+
+// Build artifact paths end up inside markdown link text and inside a URL, so
+// anything that could break out of either is rejected rather than escaped.
+const SAFE_ARTIFACT_PATH = /^[A-Za-z0-9_@./+-]+$/;
+
+function isSafeArtifactPath(value) {
+ return (
+ typeof value === 'string' &&
+ value.length > 0 &&
+ value.length < 512 &&
+ SAFE_ARTIFACT_PATH.test(value) &&
+ !value.includes('..') &&
+ !value.startsWith('/')
+ );
+}
+
+function isSize(value) {
+ return value === null || (Number.isFinite(value) && value >= 0);
+}
+
+function isSha(value) {
+ return typeof value === 'string' && /^[0-9a-f]{7,40}$/.test(value);
+}
+
+const kilobyteFormatter = new Intl.NumberFormat('en', {
+ style: 'unit',
+ unit: 'kilobyte',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+});
+
+function kbs(bytes) {
+ // An artifact that exists on only one side has no size on the other. The
+ // report has always shown that as 0.00 kB rather than an empty cell.
+ return kilobyteFormatter.format((bytes === null ? 0 : bytes) / 1000);
+}
+
+const percentFormatter = new Intl.NumberFormat('en', {
+ style: 'percent',
+ signDisplay: 'exceptZero',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+});
+
+function ratio(baseSize, headSize) {
+ if (baseSize === null) {
+ return Infinity;
+ }
+ if (headSize === null) {
+ return -1;
+ }
+ return (headSize - baseSize) / baseSize;
+}
+
+function change(decimal) {
+ if (decimal === Infinity) {
+ return 'New file';
+ }
+ if (decimal === -1) {
+ return 'Deleted';
+ }
+ // Compare the magnitude, not the signed value. Testing `decimal < 0.0001`
+ // reported every size decrease as unchanged, which is why `signDisplay:
+ // 'exceptZero'` above never had a negative number to render.
+ if (Math.abs(decimal) < 0.0001) {
+ return '=';
+ }
+ return percentFormatter.format(decimal);
+}
+
+const header = `| Name | +/- | Base | Current | +/- gzip | Base gzip | Current gzip |
+| ---- | --- | ---- | ------- | -------- | --------- | ------------ |`;
+
+function row(result, baseSha, headSha) {
+ const diffViewUrl = `https://react-builds.vercel.app/commits/${headSha}/files/${result.path}?compare=${baseSha}`;
+ const rowArr = [
+ `| [${result.path}](${diffViewUrl})`,
+ `**${change(result.change)}**`,
+ `${kbs(result.baseSize)}`,
+ `${kbs(result.headSize)}`,
+ `${change(result.changeGzip)}`,
+ `${kbs(result.baseSizeGzip)}`,
+ `${kbs(result.headSizeGzip)}`,
+ ];
+ return rowArr.join(' | ');
+}
+
+function validateResults(raw) {
+ if (raw === null || typeof raw !== 'object') {
+ return {ok: false, reason: 'malformed'};
+ }
+ // Checked before anything else so a shape this file cannot read produces a
+ // clear message instead of a misrendered table.
+ if (!SUPPORTED_VERSIONS.has(raw.version)) {
+ return {ok: false, reason: 'unsupported-version'};
+ }
+ if (!SUPPORTED_STATUSES.has(raw.status)) {
+ return {ok: false, reason: 'malformed'};
+ }
+ if (raw.status === 'base-artifacts-unavailable') {
+ return {ok: true, results: {status: raw.status}};
+ }
+ if (!isSha(raw.baseSha) || !isSha(raw.headSha)) {
+ return {ok: false, reason: 'malformed'};
+ }
+ if (!Array.isArray(raw.artifacts)) {
+ return {ok: false, reason: 'malformed'};
+ }
+ for (const artifact of raw.artifacts) {
+ if (artifact === null || typeof artifact !== 'object') {
+ return {ok: false, reason: 'malformed'};
+ }
+ if (!isSafeArtifactPath(artifact.path)) {
+ return {ok: false, reason: 'malformed'};
+ }
+ if (
+ !isSize(artifact.baseSize) ||
+ !isSize(artifact.baseSizeGzip) ||
+ !isSize(artifact.headSize) ||
+ !isSize(artifact.headSizeGzip)
+ ) {
+ return {ok: false, reason: 'malformed'};
+ }
+ if (artifact.baseSize === null && artifact.headSize === null) {
+ return {ok: false, reason: 'malformed'};
+ }
+ }
+ return {ok: true, results: raw};
+}
+
+function renderTable(results) {
+ const {baseSha, headSha} = results;
+
+ const resultsMap = new Map();
+ for (const artifact of results.artifacts) {
+ resultsMap.set(artifact.path, {
+ ...artifact,
+ change: ratio(artifact.baseSize, artifact.headSize),
+ changeGzip: ratio(artifact.baseSizeGzip, artifact.headSizeGzip),
+ });
+ }
+
+ const sorted = Array.from(resultsMap.values());
+ sorted.sort((a, b) => b.change - a.change);
+
+ const criticalResults = [];
+ const missingCriticalPaths = [];
+ for (const artifactPath of CRITICAL_ARTIFACT_PATHS) {
+ const result = resultsMap.get(artifactPath);
+ if (result === undefined) {
+ missingCriticalPaths.push(artifactPath);
+ continue;
+ }
+ criticalResults.push(row(result, baseSha, headSha));
+ }
+
+ const significantResults = [];
+ for (const result of sorted) {
+ // If result exceeds critical threshold, add to top section.
+ if (
+ (Math.abs(result.change) > CRITICAL_THRESHOLD ||
+ // New file
+ result.change === Infinity ||
+ // Deleted file
+ result.change === -1) &&
+ // Skip critical artifacts. We added those earlier, in a fixed order.
+ !CRITICAL_ARTIFACT_PATHS.has(result.path)
+ ) {
+ criticalResults.push(row(result, baseSha, headSha));
+ }
+
+ // Do the same for results that exceed the significant threshold. These
+ // will go into the bottom, collapsed section. Intentionally including
+ // critical artifacts in this section, too.
+ if (
+ Math.abs(result.change) > SIGNIFICANCE_THRESHOLD ||
+ result.change === Infinity ||
+ result.change === -1
+ ) {
+ significantResults.push(row(result, baseSha, headSha));
+ }
+ }
+
+ const markdown = `Comparing: ${baseSha}...${headSha}
+
+## Critical size changes
+
+Includes critical production bundles, as well as any change greater than ${
+ CRITICAL_THRESHOLD * 100
+ }%:
+
+${header}
+${criticalResults.join('\n')}
+
+## Significant size changes
+
+Includes any change greater than ${SIGNIFICANCE_THRESHOLD * 100}%:
+
+${
+ significantResults.length > 0
+ ? `
+Expand to show
+
+${header}
+${significantResults.join('\n')}
+ `
+ : '(No significant changes)'
+}`;
+
+ return {markdown, missingCriticalPaths};
+}
+
+function renderCompletedReport(context) {
+ const {runConclusion, runUrl, devtoolsOnly} = context;
+
+ // The common outcome for a first-time contributor's pull request: the run is
+ // created but held until a maintainer approves it.
+ if (runConclusion === 'action_required') {
+ return {
+ markdown: `[The build for this commit](${runUrl}) needs maintainer approval before it can run, so there is no size report yet.`,
+ missingCriticalPaths: [],
+ };
+ }
+
+ if (runConclusion !== 'success') {
+ return {
+ markdown: `The build for this commit did not complete, so there is no size report. See [the workflow run](${runUrl}) for details.`,
+ missingCriticalPaths: [],
+ };
+ }
+
+ if (devtoolsOnly) {
+ return {
+ markdown:
+ 'No size report: this pull request only touches `packages/react-devtools`, which does not affect production bundle size.',
+ missingCriticalPaths: [],
+ };
+ }
+
+ if (!existsSync(RESULTS_PATH)) {
+ return {
+ markdown: `The build succeeded but produced no size results, so there is no size report. See [the workflow run](${runUrl}) for details.`,
+ missingCriticalPaths: [],
+ };
+ }
+
+ let raw;
+ try {
+ raw = JSON.parse(readFileSync(RESULTS_PATH, 'utf8'));
+ } catch {
+ raw = null;
+ }
+
+ const validated = validateResults(raw);
+ if (!validated.ok) {
+ if (validated.reason === 'unsupported-version') {
+ return {
+ markdown:
+ 'This pull request produced a size report in a format this repository no longer reads. ' +
+ 'Merge the latest changes from the `main` branch to pick up the current one.',
+ missingCriticalPaths: [],
+ };
+ }
+ return {
+ markdown: `The size results for this commit could not be read, so there is no size report. See [the workflow run](${runUrl}) for details.`,
+ missingCriticalPaths: [],
+ };
+ }
+
+ if (validated.results.status === 'base-artifacts-unavailable') {
+ return {
+ markdown:
+ "Failed to read build artifacts. It's possible a build configuration has changed upstream. " +
+ 'Try pulling the latest changes from the `main` branch.',
+ missingCriticalPaths: [],
+ };
+ }
+
+ return renderTable(validated.results);
+}
+
+function renderNotice(context, reportHead) {
+ const {action, prHeadSha, runHeadSha, runStatus, runUrl} = context;
+ const lines = [];
+
+ // One rule covers both the case where an older run's results arrive after the
+ // head moved, and the case where a new build supersedes a report already on
+ // display: the report simply is not about the pull request's current head.
+ if (reportHead !== null && reportHead !== prHeadSha) {
+ lines.push(
+ `These sizes are for ${reportHead}, which is no longer the head of this pull request.`
+ );
+ if (action === 'requested') {
+ lines.push(`A build for ${runHeadSha} is in progress.`);
+ }
+ } else if (action === 'requested' && runStatus === 'waiting') {
+ lines.push(
+ `[The build for this commit](${runUrl}) is waiting for maintainer approval before it can run.`
+ );
+ }
+
+ if (lines.length === 0) {
+ return '';
+ }
+ return lines.map(line => `> ${line}`).join('\n> \n');
+}
+
+function renderBody(context) {
+ let reportHead;
+ let report;
+ let missingCriticalPaths = [];
+
+ if (context.action === 'requested') {
+ // Only a comment that names the commit it describes holds real numbers. A
+ // previous placeholder has body text too, but carrying that forward would
+ // pin the comment to a stale run link instead of refreshing it.
+ if (
+ context.existingReportHead !== null &&
+ context.existingReport !== null
+ ) {
+ // Keep the numbers from the previous build visible. The notice below
+ // explains that they describe an older commit.
+ reportHead = context.existingReportHead;
+ report = context.existingReport;
+ } else {
+ reportHead = null;
+ report = `A size report will appear here when [the build](${context.runUrl}) finishes.`;
+ }
+ } else {
+ reportHead = context.runHeadSha;
+ const rendered = renderCompletedReport(context);
+ report = rendered.markdown;
+ missingCriticalPaths = rendered.missingCriticalPaths;
+ }
+
+ if (missingCriticalPaths.length > 0) {
+ report =
+ '> [!CAUTION]\n' +
+ '> These critical bundles are missing from the build. If that was an intentional\n' +
+ '> change to the build configuration, update `CRITICAL_ARTIFACT_PATHS` in\n' +
+ '> `scripts/sizebot/render-comment.js`:\n' +
+ missingCriticalPaths.map(p => `> - \`${p}\``).join('\n') +
+ '\n\n' +
+ report;
+ }
+
+ const notice = renderNotice(context, reportHead);
+ const footerSha = reportHead === null ? context.runHeadSha : reportHead;
+
+ function assemble(reportRegion) {
+ return `${MARKER_PREFIX} report-head=${
+ reportHead === null ? 'none' : reportHead
+ } -->
+${NOTICE_START}
+${notice === '' ? '' : `> [!WARNING]\n${notice}\n`}${NOTICE_END}
+${REPORT_START}
+${reportRegion}
+${REPORT_END}
+
+Generated by sizebot against ${footerSha}
+`;
+ }
+
+ return {
+ body: assemble(report),
+ report,
+ assemble,
+ reportHead,
+ missingCriticalPaths,
+ };
+}
+
+// Reads the report region back out of a comment, so a completed report can be
+// carried forward when a new build is requested for a newer commit.
+function extractReport(body) {
+ const start = body.indexOf(REPORT_START);
+ const end = body.indexOf(REPORT_END);
+ if (start === -1 || end === -1 || end < start) {
+ return null;
+ }
+ const report = body.slice(start + REPORT_START.length, end).trim();
+ return report === '' ? null : report;
+}
+
+function parseReportHead(body) {
+ const match =
+ //.exec(body);
+ if (match === null || match[1] === 'none') {
+ return null;
+ }
+ return match[1];
+}
+
+function main() {
+ const context = JSON.parse(readFileSync(CONTEXT_PATH, 'utf8'));
+ const {body, report, assemble, missingCriticalPaths} = renderBody(context);
+
+ let comment = body;
+ if (body.length > MAX_COMMENT_LENGTH) {
+ // The link resolves because the artifact is uploaded to this same run,
+ // before the comment is posted.
+ writeFileSync(MESSAGE_PATH, report + '\n');
+ comment = assemble(
+ `The size diff is too large to display in a single comment. [This workflow run](${context.commentRunUrl}) contains an artifact called \`sizebot-message.md\` with the full report.`
+ );
+ }
+ writeFileSync(COMMENT_PATH, comment);
+
+ if (missingCriticalPaths.length > 0) {
+ writeFileSync(
+ PROBLEM_PATH,
+ `Missing expected bundles:\n${missingCriticalPaths.join('\n')}\n`
+ );
+ }
+
+ process.stdout.write(comment);
+}
+
+module.exports = {
+ MARKER_PREFIX,
+ extractReport,
+ parseReportHead,
+ renderBody,
+};
+
+if (require.main === module) {
+ main();
+}
diff --git a/scripts/tasks/danger.js b/scripts/tasks/danger.js
deleted file mode 100644
index d5aab0be414e..000000000000
--- a/scripts/tasks/danger.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-'use strict';
-
-const path = require('path');
-const spawn = require('child_process').spawn;
-
-const extension = process.platform === 'win32' ? '.cmd' : '';
-
-// sizebot public_repo token (this is publicly visible on purpose)
-const token = 'ghp_UfuUaoow8veN3ZV1' + 'sGquTDgiVjRDmL2qLY1D';
-spawn(
- path.join('node_modules', '.bin', 'danger-ci' + extension),
- [
- '--id',
- process.env.RELEASE_CHANNEL === 'experimental' ? 'experimental' : 'stable',
- ],
- {
- // Allow colors to pass through
- stdio: 'inherit',
- env: {
- ...process.env,
- DANGER_GITHUB_API_TOKEN: token,
- },
- }
-).on('close', function (code) {
- if (code !== 0) {
- console.error('Danger failed');
- } else {
- console.log('Danger passed');
- }
-
- process.exit(code);
-});
diff --git a/yarn.lock b/yarn.lock
index e047451cd08f..ce640aa8fda9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2840,36 +2840,6 @@
resolved "https://registry.yarnpkg.com/@fluent/syntax/-/syntax-0.19.0.tgz#43f882faba6908b0f1013f6a94e009d0dfbdcb77"
integrity sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==
-"@gitbeaker/core@^21.7.0":
- version "21.7.0"
- resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-21.7.0.tgz#fcf7a12915d39f416e3f316d0a447a814179b8e5"
- integrity sha512-cw72rE7tA27wc6JJe1WqeAj9v/6w0S7XJcEji+bRNjTlUfE1zgfW0Gf1mbGUi7F37SOABGCosQLfg9Qe63aIqA==
- dependencies:
- "@gitbeaker/requester-utils" "^21.7.0"
- form-data "^3.0.0"
- li "^1.3.0"
- xcase "^2.0.1"
-
-"@gitbeaker/node@^21.3.0":
- version "21.7.0"
- resolved "https://registry.yarnpkg.com/@gitbeaker/node/-/node-21.7.0.tgz#2c19613f44ee497a8808c555abec614ebd2dfcad"
- integrity sha512-OdM3VcTKYYqboOsnbiPcO0XimXXpYK4gTjARBZ6BWc+1LQXKmqo+OH6oUbyxOoaFu9hHECafIt3WZU3NM4sZTg==
- dependencies:
- "@gitbeaker/core" "^21.7.0"
- "@gitbeaker/requester-utils" "^21.7.0"
- form-data "^3.0.0"
- got "^11.1.4"
- xcase "^2.0.1"
-
-"@gitbeaker/requester-utils@^21.7.0":
- version "21.7.0"
- resolved "https://registry.yarnpkg.com/@gitbeaker/requester-utils/-/requester-utils-21.7.0.tgz#e9a9cfaf268d2a99eb7bbdc930943240a5f88878"
- integrity sha512-eLTaVXlBnh8Qimj6QuMMA06mu/mLcJm3dy8nqhhn/Vm/D25sPrvpGwmbfFyvzj6QujPqtHvFfsCHtyZddL01qA==
- dependencies:
- form-data "^3.0.0"
- query-string "^6.12.1"
- xcase "^2.0.1"
-
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
@@ -3409,7 +3379,7 @@
node-fetch "^2.6.7"
universal-user-agent "^6.0.0"
-"@octokit/rest@^16.43.0 || ^17.11.0 || ^18.12.0", "@octokit/rest@^18.12.0":
+"@octokit/rest@^18.12.0":
version "18.12.0"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881"
integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==
@@ -5163,13 +5133,6 @@ async-each@^1.0.1:
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
-async-retry@1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0"
- integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==
- dependencies:
- retry "0.12.0"
-
async@^2.0.0, async@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
@@ -5926,11 +5889,6 @@ buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
-buffer-equal-constant-time@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
- integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
-
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
@@ -6208,7 +6166,7 @@ chalk@^1.0.0, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -6549,7 +6507,7 @@ colors@1.0.3:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
-colors@1.4.0, colors@^1.1.2:
+colors@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -6581,7 +6539,7 @@ commander@^10.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
-commander@^2.18.0, commander@^2.20.0, commander@^2.6.0, commander@^2.8.1:
+commander@^2.20.0, commander@^2.6.0, commander@^2.8.1:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -6833,11 +6791,6 @@ core-js@^3.6.4:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
-core-js@^3.8.2:
- version "3.27.2"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7"
- integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w==
-
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -7124,49 +7077,6 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
-danger@^11.2.3:
- version "11.2.3"
- resolved "https://registry.yarnpkg.com/danger/-/danger-11.2.3.tgz#2a0a6d3581478005d0f2abf5b3995a8409165067"
- integrity sha512-NNDOUDZWCi1fqEicSWbnk8lOOoqY+vwekB8twUiknEyyvDDKypWEcUolq6SNg9Kd6HMqnX80K8U8nqzmDRC1QQ==
- dependencies:
- "@gitbeaker/node" "^21.3.0"
- "@octokit/rest" "^18.12.0"
- async-retry "1.2.3"
- chalk "^2.3.0"
- commander "^2.18.0"
- core-js "^3.8.2"
- debug "^4.1.1"
- fast-json-patch "^3.0.0-1"
- get-stdin "^6.0.0"
- http-proxy-agent "^5.0.0"
- https-proxy-agent "^5.0.1"
- hyperlinker "^1.0.0"
- json5 "^2.1.0"
- jsonpointer "^5.0.0"
- jsonwebtoken "^9.0.0"
- lodash.find "^4.6.0"
- lodash.includes "^4.3.0"
- lodash.isobject "^3.0.2"
- lodash.keys "^4.0.8"
- lodash.mapvalues "^4.6.0"
- lodash.memoize "^4.1.2"
- memfs-or-file-map-to-github-branch "^1.2.1"
- micromatch "^4.0.4"
- node-cleanup "^2.1.2"
- node-fetch "^2.6.7"
- override-require "^1.1.1"
- p-limit "^2.1.0"
- parse-diff "^0.7.0"
- parse-git-config "^2.0.3"
- parse-github-url "^1.0.2"
- parse-link-header "^2.0.0"
- pinpoint "^1.1.0"
- prettyjson "^1.2.1"
- readline-sync "^1.4.9"
- regenerator-runtime "^0.13.9"
- require-from-string "^2.0.2"
- supports-hyperlinks "^1.0.1"
-
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
@@ -7665,13 +7575,6 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
-ecdsa-sig-formatter@1.0.11:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
- integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
- dependencies:
- safe-buffer "^5.0.1"
-
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -8583,13 +8486,6 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
-expand-tilde@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
- integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
- dependencies:
- homedir-polyfill "^1.0.1"
-
expect@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc"
@@ -8751,7 +8647,7 @@ fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
-fast-json-patch@3.1.1, fast-json-patch@^3.0.0-1:
+fast-json-patch@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"
integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==
@@ -8985,11 +8881,6 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
-filter-obj@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
- integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
-
finalhandler@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
@@ -9195,15 +9086,6 @@ form-data-encoder@^2.1.2:
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
-form-data@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
- integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.8"
- mime-types "^2.1.12"
-
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
@@ -9250,11 +9132,6 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-fs-exists-sync@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
- integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=
-
fs-extra@11.2.0:
version "11.2.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
@@ -9446,15 +9323,6 @@ gifsicle@^5.0.0:
execa "^5.0.0"
logalot "^2.0.0"
-git-config-path@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/git-config-path/-/git-config-path-1.0.1.tgz#6d33f7ed63db0d0e118131503bab3aca47d54664"
- integrity sha1-bTP37WPbDQ4RgTFQO6s6ykfVRmQ=
- dependencies:
- extend-shallow "^2.0.1"
- fs-exists-sync "^0.1.0"
- homedir-polyfill "^1.0.0"
-
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -9714,7 +9582,7 @@ gopd@^1.0.1:
dependencies:
get-intrinsic "^1.1.3"
-got@^11.1.4, got@^11.8.5:
+got@^11.8.5:
version "11.8.6"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a"
integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==
@@ -9873,11 +9741,6 @@ has-bigints@^1.0.1:
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
-has-flag@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
- integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=
-
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -10025,7 +9888,7 @@ hermes-parser@^0.25.1:
dependencies:
hermes-estree "0.25.1"
-homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
+homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
@@ -10214,11 +10077,6 @@ human-signals@^4.3.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==
-hyperlinker@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
- integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
-
hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
@@ -10438,7 +10296,7 @@ ini@2.0.0, ini@~2.0.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
-ini@^1.3.4, ini@^1.3.5, ini@~1.3.0, ini@~1.3.3:
+ini@^1.3.4, ini@~1.3.0, ini@~1.3.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@@ -11738,13 +11596,6 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
-json5@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
- integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
- dependencies:
- minimist "^1.2.0"
-
json5@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
@@ -11778,21 +11629,6 @@ jsonify@~0.0.0:
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
-jsonpointer@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
- integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
-
-jsonwebtoken@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d"
- integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==
- dependencies:
- jws "^3.2.2"
- lodash "^4.17.21"
- ms "^2.1.1"
- semver "^7.3.8"
-
jsx-ast-utils@^1.3.4:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
@@ -11813,23 +11649,6 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
-jwa@^1.4.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
- integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
- dependencies:
- buffer-equal-constant-time "^1.0.1"
- ecdsa-sig-formatter "1.0.11"
- safe-buffer "^5.0.1"
-
-jws@^3.2.2:
- version "3.2.3"
- resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
- integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
- dependencies:
- jwa "^1.4.2"
- safe-buffer "^5.0.1"
-
keyv@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
@@ -11948,11 +11767,6 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
-li@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b"
- integrity sha1-IsWbyu+qmo7zWc91l4TkvxBq6hs=
-
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@@ -12066,11 +11880,6 @@ lodash.difference@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
-lodash.find@^4.6.0:
- version "4.6.0"
- resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
- integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
-
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
@@ -12081,11 +11890,6 @@ lodash.hasin@4.5.2:
resolved "https://registry.yarnpkg.com/lodash.hasin/-/lodash.hasin-4.5.2.tgz#f91e352378d21ef7090b9e7687c2ca35c5b4d52a"
integrity sha1-+R41I3jSHvcJC552h8LKNcW01So=
-lodash.includes@^4.3.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
- integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
-
lodash.isempty@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
@@ -12096,31 +11900,11 @@ lodash.isnil@4.0.0:
resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c"
integrity sha1-SeKM1VkBNFjIFMVHnTxmOiG/qmw=
-lodash.isobject@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
- integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
-
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
-lodash.keys@^4.0.8:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205"
- integrity sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=
-
-lodash.mapvalues@^4.6.0:
- version "4.6.0"
- resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
- integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
-
-lodash.memoize@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
- integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -12384,13 +12168,6 @@ mem@^5.0.0:
mimic-fn "^2.1.0"
p-is-promise "^2.1.0"
-memfs-or-file-map-to-github-branch@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/memfs-or-file-map-to-github-branch/-/memfs-or-file-map-to-github-branch-1.2.1.tgz#fdb9a85408262316a9bd5567409bf89be7d72f96"
- integrity sha512-I/hQzJ2a/pCGR8fkSQ9l5Yx+FQ4e7X6blNHyWBm2ojeFLT3GVzGkTj7xnyWpdclrr7Nq4dmx3xrvu70m3ypzAQ==
- dependencies:
- "@octokit/rest" "^16.43.0 || ^17.11.0 || ^18.12.0"
-
memfs@^3.4.3:
version "3.5.1"
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.1.tgz#f0cd1e2bfaef58f6fe09bfb9c2288f07fea099ec"
@@ -12756,11 +12533,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-node-cleanup@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
- integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=
-
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
@@ -13248,11 +13020,6 @@ osenv@0.0.3:
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.0.3.tgz#cd6ad8ddb290915ad9e22765576025d411f29cb6"
integrity sha1-zWrY3bKQkVrZ4idlV2Al1BHynLY=
-override-require@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/override-require/-/override-require-1.1.1.tgz#6ae22fadeb1f850ffb0cf4c20ff7b87e5eb650df"
- integrity sha1-auIvresfhQ/7DPTCD/e4fl62UN8=
-
p-cancelable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
@@ -13326,13 +13093,6 @@ p-limit@^2.0.0, p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
-p-limit@^2.1.0:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
- integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
- dependencies:
- p-try "^2.0.0"
-
p-limit@^3.0.2, p-limit@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
@@ -13454,11 +13214,6 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
-parse-diff@^0.7.0:
- version "0.7.1"
- resolved "https://registry.yarnpkg.com/parse-diff/-/parse-diff-0.7.1.tgz#9b7a2451c3725baf2c87c831ba192d40ee2237d4"
- integrity sha512-1j3l8IKcy4yRK2W4o9EYvJLSzpAVwz4DXqCewYyx2vEwk2gcf3DBPqc8Fj4XV3K33OYJ08A8fWwyu/ykD/HUSg==
-
parse-filepath@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
@@ -13468,20 +13223,6 @@ parse-filepath@^1.0.2:
map-cache "^0.2.0"
path-root "^0.1.1"
-parse-git-config@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/parse-git-config/-/parse-git-config-2.0.3.tgz#6fb840d4a956e28b971c97b33a5deb73a6d5b6bb"
- integrity sha512-Js7ueMZOVSZ3tP8C7E3KZiHv6QQl7lnJ+OkbxoaFazzSa2KyEHqApfGbU3XboUgUnq4ZuUmskUpYKTNx01fm5A==
- dependencies:
- expand-tilde "^2.0.2"
- git-config-path "^1.0.1"
- ini "^1.3.5"
-
-parse-github-url@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/parse-github-url/-/parse-github-url-1.0.2.tgz#242d3b65cbcdda14bb50439e3242acf6971db395"
- integrity sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==
-
parse-json@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-7.1.1.tgz#68f7e6f0edf88c54ab14c00eb700b753b14e2120"
@@ -13510,13 +13251,6 @@ parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
-parse-link-header@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-2.0.0.tgz#949353e284f8aa01f2ac857a98f692b57733f6b7"
- integrity sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==
- dependencies:
- xtend "~4.0.1"
-
parse-node-version@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
@@ -13766,11 +13500,6 @@ pino@8.20.0:
sonic-boom "^3.7.0"
thread-stream "^2.0.0"
-pinpoint@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/pinpoint/-/pinpoint-1.1.0.tgz#0cf7757a6977f1bf7f6a32207b709e377388e874"
- integrity sha1-DPd1eml38b9/ajIge3CeN3OI6HQ=
-
pirates@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-3.0.2.tgz#7e6f85413fd9161ab4e12b539b06010d85954bb9"
@@ -14008,14 +13737,6 @@ pretty-format@^29.7.0:
ansi-styles "^5.0.0"
react-is "^18.0.0"
-prettyjson@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289"
- integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok=
- dependencies:
- colors "^1.1.2"
- minimist "^1.2.0"
-
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -14198,16 +13919,6 @@ query-string@^5.0.1:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
-query-string@^6.12.1:
- version "6.14.1"
- resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a"
- integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==
- dependencies:
- decode-uri-component "^0.2.0"
- filter-obj "^1.1.0"
- split-on-first "^1.0.0"
- strict-uri-encode "^2.0.0"
-
querystring-es3@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -14517,11 +14228,6 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
-readline-sync@^1.4.9:
- version "1.4.10"
- resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
- integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
-
real-require@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
@@ -14578,11 +14284,6 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
-regenerator-runtime@^0.13.9:
- version "0.13.11"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
- integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
-
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
@@ -14872,11 +14573,6 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
-retry@0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
- integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
-
retry@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
@@ -15234,7 +14930,7 @@ semver@^7.1.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667"
integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A==
-semver@^7.2.1, semver@^7.3.8:
+semver@^7.2.1:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
@@ -15700,11 +15396,6 @@ spdy@^4.0.2:
select-hose "^2.0.0"
spdy-transport "^3.0.0"
-split-on-first@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
- integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
-
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -15806,11 +15497,6 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
-strict-uri-encode@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
- integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
-
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@@ -16088,7 +15774,7 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.3.0, supports-color@^5.4.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@@ -16109,14 +15795,6 @@ supports-color@^8.0.0:
dependencies:
has-flag "^4.0.0"
-supports-hyperlinks@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7"
- integrity sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==
- dependencies:
- has-flag "^2.0.0"
- supports-color "^5.0.0"
-
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
@@ -17532,11 +17210,6 @@ ws@^7:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
-xcase@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9"
- integrity sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==
-
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"