From 85fb95cdffdd95f2f908ee71974cae06b1c866e1 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Fri, 16 Aug 2024 12:53:52 -0400 Subject: [PATCH 001/191] [flow] Eliminate a few React.Element type that will be synced to react-native (#30719) ## Summary Flow will eventually remove the specific `React.Element` type. For most of the code, it can be replaced with `React.MixedElement` or `React.Node`. When specific react elements are required, it needs to be replaced with either `React$Element` which will trigger a `internal-type` lint error that can be disabled project-wide, or use `ExactReactElement_DEPRECATED`. Fortunately in this case, this one can be replaced with just `React.MixedElement`. ## How did you test this change? `flow` --- .../react-native-renderer/src/ReactNativeRenderer.js | 4 ++-- .../react-native-renderer/src/ReactNativeTypes.js | 11 ++++++++--- packages/react/index.js | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 983079fd0eeb3..4ec5ab2c58529 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -8,7 +8,7 @@ */ import type {ReactPortal, ReactNodeList} from 'shared/ReactTypes'; -import type {ElementRef, Element, ElementType} from 'react'; +import type {ElementRef, ElementType, MixedElement} from 'react'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {RenderRootOptions} from './ReactNativeTypes'; @@ -117,7 +117,7 @@ function nativeOnCaughtError( } function render( - element: Element, + element: MixedElement, containerTag: number, callback: ?() => void, options: ?RenderRootOptions, diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 917e3988c7b38..210366842383e 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -9,7 +9,12 @@ * @flow strict */ -import type {ElementRef, ElementType, Element, AbstractComponent} from 'react'; +import type { + ElementRef, + ElementType, + MixedElement, + AbstractComponent, +} from 'react'; export type MeasureOnSuccessCallback = ( x: number, @@ -221,7 +226,7 @@ export type ReactNativeType = { eventType: string, ): void, render( - element: Element, + element: MixedElement, containerTag: number, callback: ?() => void, options: ?RenderRootOptions, @@ -256,7 +261,7 @@ export type ReactFabricType = { eventType: string, ): void, render( - element: Element, + element: MixedElement, containerTag: number, callback: ?() => void, concurrentRoot: ?boolean, diff --git a/packages/react/index.js b/packages/react/index.js index bad2c40e6c3dd..19f256fd73b5b 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -15,6 +15,7 @@ export type AbstractComponent< > = React$AbstractComponent; export type ElementType = React$ElementType; export type Element<+C> = React$Element; +export type MixedElement = React$Element; export type Key = React$Key; export type Ref = React$Ref; export type Node = React$Node; From 5030e08575c295ef352c5ae928e2366cc4765d32 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 002/191] [compiler] Exclude refs and ref values from having mutable ranges Summary: Refs, as stable values that the rules of react around mutability do not apply to, currently are treated as having mutable ranges, and through aliasing, this can extend the mutable range for other values and disrupt good memoization for those values. This PR excludes refs and their .current values from having mutable ranges. Note that this is unsafe if ref access is allowed in render: if a mutable value is assigned to ref.current and then ref.current is mutated later, we won't realize that the original mutable value's range extends. ghstack-source-id: e8f36ac25e2c9aadb0bf13bd8142e4593ee9f984 Pull Request resolved: https://github.com/facebook/react/pull/30713 --- .../src/HIR/HIR.ts | 4 ++ .../src/Inference/AnalyseFunctions.ts | 5 +- .../src/Inference/InferMutableLifetimes.ts | 10 +++- .../Inference/InferMutableRangesForAlias.ts | 15 ++++-- .../src/Inference/InferReferenceEffects.ts | 13 ++--- .../Validation/ValidateNoRefAccesInRender.ts | 4 +- .../capture-ref-for-later-mutation.expect.md | 29 +++++----- ...ified-later-preserve-memoization.expect.md | 6 ++- ...ref-modified-later-preserve-memoization.js | 2 +- ...a-function-preserve-memoization.expect.md} | 8 ++- ...ater-via-function-preserve-memoization.js} | 2 +- .../capture-ref-for-later-mutation.expect.md | 42 +++++---------- ....maybe-mutable-ref-not-preserved.expect.md | 35 ------------ .../maybe-mutable-ref-not-preserved.expect.md | 53 +++++++++++++++++++ ....ts => maybe-mutable-ref-not-preserved.ts} | 0 .../repro-ref-mutable-range.expect.md | 34 +++++++----- ...operty-dont-preserve-memoization.expect.md | 27 ++++++---- 17 files changed, 159 insertions(+), 130 deletions(-) rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md => error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md} (68%) rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js => error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js} (88%) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/{error.maybe-mutable-ref-not-preserved.ts => maybe-mutable-ref-not-preserved.ts} (100%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index e61665ce4c6bb..0810130102b0e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1591,6 +1591,10 @@ export function isUseStateType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState'; } +export function isRefOrRefValue(id: Identifier): boolean { + return isUseRefType(id) || isRefValueType(id); +} + export function isSetStateType(id: Identifier): boolean { return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState'; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index b18e19606ce0d..fbb24ea492c0f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -14,8 +14,7 @@ import { LoweredFunction, Place, ReactiveScopeDependency, - isRefValueType, - isUseRefType, + isRefOrRefValue, makeInstructionId, } from '../HIR'; import {deadCodeElimination} from '../Optimization'; @@ -139,7 +138,7 @@ function infer( name = dep.identifier.name; } - if (isUseRefType(dep.identifier) || isRefValueType(dep.identifier)) { + if (isRefOrRefValue(dep.identifier)) { /* * TODO: this is a hack to ensure we treat functions which reference refs * as having a capture and therefore being considered mutable. this ensures diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts index 2ce1aebbf8577..459baf4e287cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts @@ -11,6 +11,7 @@ import { Identifier, InstructionId, InstructionKind, + isRefOrRefValue, makeInstructionId, Place, } from '../HIR/HIR'; @@ -66,7 +67,9 @@ import {assertExhaustive} from '../Utils/utils'; */ function infer(place: Place, instrId: InstructionId): void { - place.identifier.mutableRange.end = makeInstructionId(instrId + 1); + if (!isRefOrRefValue(place.identifier)) { + place.identifier.mutableRange.end = makeInstructionId(instrId + 1); + } } function inferPlace( @@ -171,7 +174,10 @@ export function inferMutableLifetimes( const declaration = contextVariableDeclarationInstructions.get( instr.value.lvalue.place.identifier, ); - if (declaration != null) { + if ( + declaration != null && + !isRefOrRefValue(instr.value.lvalue.place.identifier) + ) { const range = instr.value.lvalue.place.identifier.mutableRange; if (range.start === 0) { range.start = declaration; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts index 975acf6fbf55a..a7e8b5c1f7a80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {HIRFunction, Identifier, InstructionId} from '../HIR/HIR'; +import { + HIRFunction, + Identifier, + InstructionId, + isRefOrRefValue, +} from '../HIR/HIR'; import DisjointSet from '../Utils/DisjointSet'; export function inferMutableRangesForAlias( @@ -19,7 +24,8 @@ export function inferMutableRangesForAlias( * mutated. */ const mutatingIdentifiers = [...aliasSet].filter( - id => id.mutableRange.end - id.mutableRange.start > 1, + id => + id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id), ); if (mutatingIdentifiers.length > 0) { @@ -36,7 +42,10 @@ export function inferMutableRangesForAlias( * last mutation. */ for (const alias of aliasSet) { - if (alias.mutableRange.end < lastMutatingInstructionId) { + if ( + alias.mutableRange.end < lastMutatingInstructionId && + !isRefOrRefValue(alias) + ) { alias.mutableRange.end = lastMutatingInstructionId as InstructionId; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index 356bc8af08bfd..4cce942c18154 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -30,8 +30,7 @@ import { isArrayType, isMutableEffect, isObjectType, - isRefValueType, - isUseRefType, + isRefOrRefValue, } from '../HIR/HIR'; import {FunctionSignature} from '../HIR/ObjectShape'; import { @@ -523,10 +522,7 @@ class InferenceState { break; } case Effect.Mutate: { - if ( - isRefValueType(place.identifier) || - isUseRefType(place.identifier) - ) { + if (isRefOrRefValue(place.identifier)) { // no-op: refs are validate via ValidateNoRefAccessInRender } else if (valueKind.kind === ValueKind.Context) { functionEffect = { @@ -567,10 +563,7 @@ class InferenceState { break; } case Effect.Store: { - if ( - isRefValueType(place.identifier) || - isUseRefType(place.identifier) - ) { + if (isRefOrRefValue(place.identifier)) { // no-op: refs are validate via ValidateNoRefAccessInRender } else if (valueKind.kind === ValueKind.Context) { functionEffect = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index b4fb2a618ada4..a93664f418f8e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -11,6 +11,7 @@ import { IdentifierId, Place, SourceLocation, + isRefOrRefValue, isRefValueType, isUseRefType, } from '../HIR'; @@ -231,8 +232,7 @@ function validateNoRefAccess( loc: SourceLocation, ): void { if ( - isRefValueType(operand.identifier) || - isUseRefType(operand.identifier) || + isRefOrRefValue(operand.identifier) || refAccessingFunctions.has(operand.identifier.id) ) { errors.push({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md index 41748867b35d5..b7371108d5867 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md @@ -36,27 +36,26 @@ import { useRef } from "react"; import { addOne } from "shared-runtime"; function useKeyCommand() { - const $ = _c(2); + const $ = _c(1); const currentPosition = useRef(0); - const handleKey = (direction) => () => { - const position = currentPosition.current; - const nextPosition = direction === "left" ? addOne(position) : position; - currentPosition.current = nextPosition; - }; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const handleKey = (direction) => () => { + const position = currentPosition.current; + const nextPosition = direction === "left" ? addOne(position) : position; + currentPosition.current = nextPosition; + }; - const moveLeft = { handler: handleKey("left") }; + const moveLeft = { handler: handleKey("left") }; - const t0 = handleKey("right"); - let t1; - if ($[0] !== t0) { - t1 = { handler: t0 }; + const moveRight = { handler: handleKey("right") }; + + t0 = [moveLeft, moveRight]; $[0] = t0; - $[1] = t1; } else { - t1 = $[1]; + t0 = $[0]; } - const moveRight = t1; - return [moveLeft, moveRight]; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md index 5eeb8c6b0ab89..c79ceca9efcee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @enablePreserveExistingMemoizationGuarantees +// @enablePreserveExistingMemoizationGuarantees @validateRefAccessDuringRender import {useCallback, useRef} from 'react'; function Component(props) { @@ -42,7 +42,9 @@ export const FIXTURE_ENTRYPOINT = { > 10 | ref.current.inner = event.target.value; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 11 | }); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (7:11) + | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $44:TObject (7:11) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange 14 | ref.current.inner = null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.js index b209be7586992..cb259b457e3e3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.js @@ -1,4 +1,4 @@ -// @enablePreserveExistingMemoizationGuarantees +// @enablePreserveExistingMemoizationGuarantees @validateRefAccessDuringRender import {useCallback, useRef} from 'react'; function Component(props) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md similarity index 68% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md index ad21b4ba8c5e9..045ac8c3d61cb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @enablePreserveExistingMemoizationGuarantees +// @enablePreserveExistingMemoizationGuarantees @validateRefAccessDuringRender import {useCallback, useRef} from 'react'; function Component(props) { @@ -45,7 +45,11 @@ export const FIXTURE_ENTRYPOINT = { > 10 | ref.current.inner = event.target.value; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 11 | }); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (7:11) + | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $53:TObject (7:11) + +InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $77[20:22]:TObject accesses a ref (17:17) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17) 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange 14 | const reset = () => { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js similarity index 88% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js index 8e71ed63269d8..dec828c2d0d19 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.js @@ -1,4 +1,4 @@ -// @enablePreserveExistingMemoizationGuarantees +// @enablePreserveExistingMemoizationGuarantees @validateRefAccessDuringRender import {useCallback, useRef} from 'react'; function Component(props) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md index 52cea4b047c05..54184be0f3f38 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md @@ -37,42 +37,26 @@ import { useRef } from "react"; import { addOne } from "shared-runtime"; function useKeyCommand() { - const $ = _c(6); + const $ = _c(1); const currentPosition = useRef(0); - const handleKey = (direction) => () => { - const position = currentPosition.current; - const nextPosition = direction === "left" ? addOne(position) : position; - currentPosition.current = nextPosition; - }; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = { handler: handleKey("left") }; + const handleKey = (direction) => () => { + const position = currentPosition.current; + const nextPosition = direction === "left" ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + + const moveLeft = { handler: handleKey("left") }; + + const moveRight = { handler: handleKey("right") }; + + t0 = [moveLeft, moveRight]; $[0] = t0; } else { t0 = $[0]; } - const moveLeft = t0; - - const t1 = handleKey("right"); - let t2; - if ($[1] !== t1) { - t2 = { handler: t1 }; - $[1] = t1; - $[2] = t2; - } else { - t2 = $[2]; - } - const moveRight = t2; - let t3; - if ($[3] !== moveLeft || $[4] !== moveRight) { - t3 = [moveLeft, moveRight]; - $[3] = moveLeft; - $[4] = moveRight; - $[5] = t3; - } else { - t3 = $[5]; - } - return t3; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md deleted file mode 100644 index 61fe8b8deb7ee..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md +++ /dev/null @@ -1,35 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees:true - -import {useRef, useMemo} from 'react'; -import {makeArray} from 'shared-runtime'; - -function useFoo() { - const r = useRef(); - return useMemo(() => makeArray(r), []); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [], -}; - -``` - - -## Error - -``` - 6 | function useFoo() { - 7 | const r = useRef(); -> 8 | return useMemo(() => makeArray(r), []); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (8:8) - 9 | } - 10 | - 11 | export const FIXTURE_ENTRYPOINT = { -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md new file mode 100644 index 0000000000000..61ab6bafaecd1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees:true + +import {useRef, useMemo} from 'react'; +import {makeArray} from 'shared-runtime'; + +function useFoo() { + const r = useRef(); + return useMemo(() => makeArray(r), []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees:true + +import { useRef, useMemo } from "react"; +import { makeArray } from "shared-runtime"; + +function useFoo() { + const $ = _c(1); + const r = useRef(); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = makeArray(r); + $[0] = t1; + } else { + t1 = $[0]; + } + t0 = t1; + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) [{}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md index 96fd656ca3003..8b656b8e23b27 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md @@ -31,7 +31,7 @@ import { c as _c } from "react/compiler-runtime"; import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime"; function Foo(props, ref) { - const $ = _c(5); + const $ = _c(7); let value; let t0; if ($[0] !== ref) { @@ -45,19 +45,6 @@ function Foo(props, ref) { } mutate(value); - if (CONST_TRUE) { - const t1 = identity(ref); - let t2; - if ($[3] !== t1) { - t2 = ; - $[3] = t1; - $[4] = t2; - } else { - t2 = $[4]; - } - t0 = t2; - break bb0; - } } $[0] = ref; $[1] = value; @@ -69,6 +56,25 @@ function Foo(props, ref) { if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; } + if (CONST_TRUE) { + let t1; + if ($[3] !== ref) { + t1 = identity(ref); + $[3] = ref; + $[4] = t1; + } else { + t1 = $[4]; + } + let t2; + if ($[5] !== t1) { + t2 = ; + $[5] = t1; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; + } return value; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md index e13d5a82c39b9..1efca9f0066be 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md @@ -42,21 +42,26 @@ function Component(props) { t0 = $[0]; } const ref = useRef(t0); - - const onChange = (event) => { - ref.current.inner = event.target.value; - }; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = (event) => { + ref.current.inner = event.target.value; + }; + $[1] = t1; + } else { + t1 = $[1]; + } + const onChange = t1; ref.current.inner = null; - let t1; - if ($[1] !== onChange) { - t1 = ; - $[1] = onChange; - $[2] = t1; + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = ; + $[2] = t2; } else { - t1 = $[2]; + t2 = $[2]; } - return t1; + return t2; } export const FIXTURE_ENTRYPOINT = { From 7468ac530e73992f28169ac69e18395a75edfc47 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 003/191] [compiler] Fixture to show ref-in-render enforcement issue with useCallback Test Plan: Documents that useCallback calls interfere with it being ok for refs to escape as part of functions into jsx ghstack-source-id: a5df427981ca32406fb2325e583b64bbe26b1cdd Pull Request resolved: https://github.com/facebook/react/pull/30714 --- .../error.return-ref-callback.expect.md | 37 +++++++++++++++++ .../compiler/error.return-ref-callback.js | 16 ++++++++ .../error.useCallback-ref-in-render.expect.md | 41 +++++++++++++++++++ .../error.useCallback-ref-in-render.js | 16 ++++++++ 4 files changed, 110 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md new file mode 100644 index 0000000000000..ae42ba1ee5422 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +component Foo() { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + + +## Error + +``` + 8 | }; + 9 | +> 10 | return s; + | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $25:TObject (10:10) + 11 | } + 12 | + 13 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js new file mode 100644 index 0000000000000..3a650cdeed8e8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js @@ -0,0 +1,16 @@ +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +component Foo() { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md new file mode 100644 index 0000000000000..ad1c0e7c3fe3a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +component Foo() { + const ref = useRef(); + + const s = useCallback(() => { + return ref.current; + }); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + + +## Error + +``` + 4 | const ref = useRef(); + 5 | +> 6 | const s = useCallback(() => { + | ^^^^^^^ +> 7 | return ref.current; + | ^^^^^^^^^^^^^^^^^^^^^^^ +> 8 | }); + | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at read $27:TObject (6:8) + 9 | + 10 | return ; + 11 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js new file mode 100644 index 0000000000000..3fc086164d607 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js @@ -0,0 +1,16 @@ +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +component Foo() { + const ref = useRef(); + + const s = useCallback(() => { + return ref.current; + }); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; From 8a531601115927400aa04d26a5f1800d159e1e7e Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 004/191] [compiler] Don't error on ref-in-render on StartMemoize Test Plan: Fixes the previous issue: ref enforcement ignores memoization marker instructions ghstack-source-id: f35d6a611c5e740e9ea354ec80c3d7cdb3c0d658 Pull Request resolved: https://github.com/facebook/react/pull/30715 --- .../Validation/ValidateNoRefAccesInRender.ts | 3 + ...ified-later-preserve-memoization.expect.md | 20 ++--- ...ia-function-preserve-memoization.expect.md | 24 ++---- .../error.useCallback-ref-in-render.expect.md | 41 ---------- .../useCallback-ref-in-render.expect.md | 76 +++++++++++++++++++ ...render.js => useCallback-ref-in-render.js} | 7 +- 6 files changed, 97 insertions(+), 74 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.useCallback-ref-in-render.js => useCallback-ref-in-render.js} (70%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index a93664f418f8e..a855d9f8b414e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -185,6 +185,9 @@ function validateNoRefAccessInRenderImpl( } break; } + case 'StartMemoize': + case 'FinishMemoize': + break; default: { for (const operand of eachInstructionValueOperand(instr.value)) { validateNoRefValueAccess(errors, refAccessingFunctions, operand); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md index c79ceca9efcee..bff8f0e01f582 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md @@ -31,23 +31,13 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 5 | const ref = useRef({inner: null}); - 6 | -> 7 | const onChange = useCallback(event => { - | ^^^^^^^^^^ -> 8 | // The ref should still be mutable here even though function deps are frozen in - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 9 | // @enablePreserveExistingMemoizationGuarantees mode - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 10 | ref.current.inner = event.target.value; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 11 | }); - | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $44:TObject (7:11) - -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange - 14 | ref.current.inner = null; +> 14 | ref.current.inner = null; + | ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) + 15 | + 16 | return ; + 17 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md index 045ac8c3d61cb..9014de67d4478 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md @@ -34,25 +34,15 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 5 | const ref = useRef({inner: null}); - 6 | -> 7 | const onChange = useCallback(event => { - | ^^^^^^^^^^ -> 8 | // The ref should still be mutable here even though function deps are frozen in - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 9 | // @enablePreserveExistingMemoizationGuarantees mode - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 10 | ref.current.inner = event.target.value; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 11 | }); - | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $53:TObject (7:11) - -InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $77[20:22]:TObject accesses a ref (17:17) + 15 | ref.current.inner = null; + 16 | }; +> 17 | reset(); + | ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $77[20:22]:TObject accesses a ref (17:17) InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17) - 12 | - 13 | // The ref is modified later, extending its range and preventing memoization of onChange - 14 | const reset = () => { + 18 | + 19 | return ; + 20 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md deleted file mode 100644 index ad1c0e7c3fe3a..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.expect.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Input - -```javascript -// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees - -component Foo() { - const ref = useRef(); - - const s = useCallback(() => { - return ref.current; - }); - - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [], -}; - -``` - - -## Error - -``` - 4 | const ref = useRef(); - 5 | -> 6 | const s = useCallback(() => { - | ^^^^^^^ -> 7 | return ref.current; - | ^^^^^^^^^^^^^^^^^^^^^^^ -> 8 | }); - | ^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at read $27:TObject (6:8) - 9 | - 10 | return ; - 11 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.expect.md new file mode 100644 index 0000000000000..e1427437ce86d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.expect.md @@ -0,0 +1,76 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees +import {useCallback, useRef} from 'react'; + +component Foo() { + const ref = useRef(); + + const s = useCallback(() => { + return ref.current; + }); + + return ; +} + +component A(r: mixed) { + return
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useCallback, useRef } from "react"; + +function Foo() { + const $ = _c(2); + const ref = useRef(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => ref.current; + $[0] = t0; + } else { + t0 = $[0]; + } + const s = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +function A(t0) { + const $ = _c(1); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
; + $[0] = t1; + } else { + t1 = $[0]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +### Eval output +(kind: ok)
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.js similarity index 70% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.js index 3fc086164d607..9fefa503203e2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-ref-in-render.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-ref-in-render.js @@ -1,4 +1,5 @@ // @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees +import {useCallback, useRef} from 'react'; component Foo() { const ref = useRef(); @@ -7,7 +8,11 @@ component Foo() { return ref.current; }); - return
; + return ; +} + +component A(r: mixed) { + return
; } export const FIXTURE_ENTRYPOINT = { From 1016174af520fc89bafab7189405fce8ff3a9bb5 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 005/191] [compiler] Reposition ref-in-render errors to the read location of .current Summary: Since we want to make ref-in-render errors enabled by default, we should position those errors at the location of the read. Not only will this be a better experience, but it also aligns the behavior of Forget and Flow. This PR also cleans up the resulting error messages to not emit implementation details about place values. ghstack-source-id: 1d1131706867a6fc88efddd631c4d16d2181e592 Pull Request resolved: https://github.com/facebook/react/pull/30723 --- .../Validation/ValidateNoRefAccesInRender.ts | 73 +++++++++++++++---- ...invalid-access-ref-during-render.expect.md | 7 +- ...tating-refs-in-render-transitive.expect.md | 2 +- ...d-ref-prop-in-render-destructure.expect.md | 7 +- ...ref-prop-in-render-property-load.expect.md | 7 +- ...error.invalid-ref-value-as-props.expect.md | 2 +- ...d-set-and-read-ref-during-render.expect.md | 2 +- ...ef-nested-property-during-render.expect.md | 4 +- .../error.return-ref-callback.expect.md | 2 +- ...ified-later-preserve-memoization.expect.md | 2 +- ...ia-function-preserve-memoization.expect.md | 2 +- 11 files changed, 79 insertions(+), 31 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index a855d9f8b414e..4da6b23a1a9ed 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -15,7 +15,6 @@ import { isRefValueType, isUseRefType, } from '../HIR'; -import {printPlace} from '../HIR/PrintHIR'; import { eachInstructionValueOperand, eachTerminalOperand, @@ -53,6 +52,7 @@ function validateNoRefAccessInRenderImpl( refAccessingFunctions: Set, ): Result { const errors = new CompilerError(); + const lookupLocations: Map = new Map(); for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { switch (instr.value.kind) { @@ -64,10 +64,12 @@ function validateNoRefAccessInRenderImpl( severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: `Cannot access ref value at ${printPlace( - operand, - )}`, + loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, suggestions: null, }); } @@ -75,12 +77,24 @@ function validateNoRefAccessInRenderImpl( break; } case 'PropertyLoad': { + if ( + isRefValueType(instr.lvalue.identifier) && + instr.value.property === 'current' + ) { + lookupLocations.set(instr.lvalue.identifier.id, instr.loc); + } break; } case 'LoadLocal': { if (refAccessingFunctions.has(instr.value.place.identifier.id)) { refAccessingFunctions.add(instr.lvalue.identifier.id); } + if (isRefValueType(instr.lvalue.identifier)) { + const loc = lookupLocations.get(instr.value.place.identifier.id); + if (loc !== undefined) { + lookupLocations.set(instr.lvalue.identifier.id, loc); + } + } break; } case 'StoreLocal': { @@ -88,6 +102,13 @@ function validateNoRefAccessInRenderImpl( refAccessingFunctions.add(instr.value.lvalue.place.identifier.id); refAccessingFunctions.add(instr.lvalue.identifier.id); } + if (isRefValueType(instr.value.lvalue.place.identifier)) { + const loc = lookupLocations.get(instr.value.value.identifier.id); + if (loc !== undefined) { + lookupLocations.set(instr.value.lvalue.place.identifier.id, loc); + lookupLocations.set(instr.lvalue.identifier.id, loc); + } + } break; } case 'ObjectMethod': @@ -140,7 +161,11 @@ function validateNoRefAccessInRenderImpl( reason: 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', loc: callee.loc, - description: `Function ${printPlace(callee)} accesses a ref`, + description: + callee.identifier.name !== null && + callee.identifier.name.kind === 'named' + ? `Function \`${callee.identifier.name.value}\` accesses a ref` + : null, suggestions: null, }); } @@ -149,7 +174,7 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, operand, - operand.loc, + lookupLocations.get(operand.identifier.id) ?? operand.loc, ); } } @@ -162,7 +187,7 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, operand, - operand.loc, + lookupLocations.get(operand.identifier.id) ?? operand.loc, ); } break; @@ -175,13 +200,18 @@ function validateNoRefAccessInRenderImpl( errors, refAccessingFunctions, instr.value.object, - instr.loc, + lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc, ); for (const operand of eachInstructionValueOperand(instr.value)) { if (operand === instr.value.object) { continue; } - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); } break; } @@ -190,14 +220,24 @@ function validateNoRefAccessInRenderImpl( break; default: { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); } break; } } } for (const operand of eachTerminalOperand(block.terminal)) { - validateNoRefValueAccess(errors, refAccessingFunctions, operand); + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); } } @@ -211,6 +251,7 @@ function validateNoRefAccessInRenderImpl( function validateNoRefValueAccess( errors: CompilerError, refAccessingFunctions: Set, + lookupLocations: Map, operand: Place, ): void { if ( @@ -221,8 +262,12 @@ function validateNoRefValueAccess( severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: `Cannot access ref value at ${printPlace(operand)}`, + loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, suggestions: null, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md index c5e8b68c04002..02748366456fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md @@ -15,10 +15,11 @@ function Component(props) { ## Error ``` + 2 | function Component(props) { 3 | const ref = useRef(null); - 4 | const value = ref.current; -> 5 | return value; - | ^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $22:TObject (5:5) +> 4 | const value = ref.current; + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) + 5 | return value; 6 | } 7 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md index 0d4cb1318911b..f23ff6f3c8c57 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md @@ -24,7 +24,7 @@ function Component() { 7 | }; 8 | const changeRef = setRef; > 9 | changeRef(); - | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $39[11:13]:TObject accesses a ref (9:9) + | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) 10 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md index d8e8174d2cc62..100dafaf38a88 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md @@ -14,10 +14,11 @@ function Component({ref}) { ## Error ``` + 1 | // @validateRefAccessDuringRender @compilationMode(infer) 2 | function Component({ref}) { - 3 | const value = ref.current; -> 4 | return
{value}
; - | ^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at read $17:TObject (4:4) +> 3 | const value = ref.current; + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3) + 4 | return
{value}
; 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md index e374900a95ad4..4d8c4c735c70e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md @@ -14,10 +14,11 @@ function Component(props) { ## Error ``` + 1 | // @validateRefAccessDuringRender @compilationMode(infer) 2 | function Component(props) { - 3 | const value = props.ref.current; -> 4 | return
{value}
; - | ^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at read $15:TObject (4:4) +> 3 | const value = props.ref.current; + | ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3) + 4 | return
{value}
; 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md index 96e2ca9a1ef28..f86f6b7a39145 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md @@ -17,7 +17,7 @@ function Component(props) { 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | return ; - | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $19:TObject (4:4) + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md index 5db1568427835..d38adb5589db3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md @@ -20,7 +20,7 @@ function Component(props) { > 4 | ref.current = props.value; | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $24:TObject (5:5) +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) 5 | return ref.current; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md index 290b8539e7b14..527adfc6e9fc5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md @@ -18,9 +18,9 @@ function Component(props) { 2 | function Component(props) { 3 | const ref = useRef({inner: null}); > 4 | ref.current.inner = props.value; - | ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $30:TObject (5:5) +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) 5 | return ref.current.inner; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md index ae42ba1ee5422..98f2a86714034 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md @@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = { 8 | }; 9 | > 10 | return s; - | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value at freeze $25:TObject (10:10) + | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10) 11 | } 12 | 13 | export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md index bff8f0e01f582..7a8f271bd2879 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md @@ -34,7 +34,7 @@ export const FIXTURE_ENTRYPOINT = { 12 | 13 | // The ref is modified later, extending its range and preventing memoization of onChange > 14 | ref.current.inner = null; - | ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) 15 | 16 | return ; 17 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md index 9014de67d4478..f3ab9816218f9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md @@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = { 15 | ref.current.inner = null; 16 | }; > 17 | reset(); - | ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $77[20:22]:TObject accesses a ref (17:17) + | ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17) InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17) 18 | From 21a95239e15cf62e4bf3922af998383b844df2e3 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 006/191] [compiler] Allow functions containing refs to be returned Summary: We previously were excessively strict about preventing functions that access refs from being returned--doing so is potentially valid for hooks, because the return value may only be used in an event or effect. ghstack-source-id: cfa8bb1b54e8eb365f2de50d051bd09e09162d7b Pull Request resolved: https://github.com/facebook/react/pull/30724 --- .../Validation/ValidateNoRefAccesInRender.ts | 53 +++++++++++------- .../error.return-ref-callback.expect.md | 37 ------------- .../compiler/return-ref-callback.expect.md | 55 +++++++++++++++++++ ...ref-callback.js => return-ref-callback.js} | 2 + 4 files changed, 90 insertions(+), 57 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.return-ref-callback.js => return-ref-callback.js} (89%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index 4da6b23a1a9ed..df6241a73f448 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -59,20 +59,7 @@ function validateNoRefAccessInRenderImpl( case 'JsxExpression': case 'JsxFragment': { for (const operand of eachInstructionValueOperand(instr.value)) { - if (isRefValueType(operand.identifier)) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); - } + validateNoDirectRefValueAccess(errors, operand, lookupLocations); } break; } @@ -232,12 +219,17 @@ function validateNoRefAccessInRenderImpl( } } for (const operand of eachTerminalOperand(block.terminal)) { - validateNoRefValueAccess( - errors, - refAccessingFunctions, - lookupLocations, - operand, - ); + if (block.terminal.kind !== 'return') { + validateNoRefValueAccess( + errors, + refAccessingFunctions, + lookupLocations, + operand, + ); + } else { + // Allow functions containing refs to be returned, but not direct ref values + validateNoDirectRefValueAccess(errors, operand, lookupLocations); + } } } @@ -297,3 +289,24 @@ function validateNoRefAccess( }); } } + +function validateNoDirectRefValueAccess( + errors: CompilerError, + operand: Place, + lookupLocations: Map, +): void { + if (isRefValueType(operand.identifier)) { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: + 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', + loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + description: + operand.identifier.name !== null && + operand.identifier.name.kind === 'named' + ? `Cannot access ref value \`${operand.identifier.name.value}\`` + : null, + suggestions: null, + }); + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md deleted file mode 100644 index 98f2a86714034..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.expect.md +++ /dev/null @@ -1,37 +0,0 @@ - -## Input - -```javascript -// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees - -component Foo() { - const ref = useRef(); - - const s = () => { - return ref.current; - }; - - return s; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [], -}; - -``` - - -## Error - -``` - 8 | }; - 9 | -> 10 | return s; - | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10) - 11 | } - 12 | - 13 | export const FIXTURE_ENTRYPOINT = { -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md new file mode 100644 index 0000000000000..ed1dfa39ea55c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +import {useRef} from 'react'; + +component Foo() { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { useRef } from "react"; + +function Foo() { + const $ = _c(1); + const ref = useRef(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => ref.current; + $[0] = t0; + } else { + t0 = $[0]; + } + const s = t0; + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js similarity index 89% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js index 3a650cdeed8e8..f1a45ebc4ff3e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback.js @@ -1,5 +1,7 @@ // @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees +import {useRef} from 'react'; + component Foo() { const ref = useRef(); From 5edbe29dbe945d821021a1152b267f5a86efc55b Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Fri, 16 Aug 2024 13:27:13 -0400 Subject: [PATCH 007/191] [compiler] Make ref enforcement on by default Summary: The change earlier in this stack makes it less safe to have ref enforcement disabled. This diff enables it by default. ghstack-source-id: d3ab5f1b28b7aed0f0d6d69547bb638a1e326b66 Pull Request resolved: https://github.com/facebook/react/pull/30716 --- .../src/HIR/Environment.ts | 2 +- .../capture-ref-for-later-mutation.expect.md | 69 -------------- ...r.capture-ref-for-later-mutation.expect.md | 50 +++++++++++ ... error.capture-ref-for-later-mutation.tsx} | 0 .../error.repro-ref-mutable-range.expect.md | 40 +++++++++ ....tsx => error.repro-ref-mutable-range.tsx} | 0 ...operty-dont-preserve-memoization.expect.md | 42 +++++++++ ...ted-property-dont-preserve-memoization.js} | 0 .../capture-ref-for-later-mutation.expect.md | 70 --------------- ...r.capture-ref-for-later-mutation.expect.md | 51 +++++++++++ ... error.capture-ref-for-later-mutation.tsx} | 0 ....maybe-mutable-ref-not-preserved.expect.md | 35 ++++++++ ... error.maybe-mutable-ref-not-preserved.ts} | 0 .../error.useMemo-with-refs.flow.expect.md | 31 +++++++ ...low.js => error.useMemo-with-refs.flow.js} | 0 .../maybe-mutable-ref-not-preserved.expect.md | 53 ----------- .../useMemo-with-refs.flow.expect.md | 54 ----------- .../repro-ref-mutable-range.expect.md | 89 ------------------- ...operty-dont-preserve-memoization.expect.md | 75 ---------------- .../src/__tests__/parseConfigPragma-test.ts | 6 +- .../__tests__/ReactCompilerRule-test.ts | 20 ++++- .../src/rules/ReactCompilerRule.ts | 5 +- 22 files changed, 275 insertions(+), 417 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{capture-ref-for-later-mutation.tsx => error.capture-ref-for-later-mutation.tsx} (100%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{repro-ref-mutable-range.tsx => error.repro-ref-mutable-range.tsx} (100%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{useCallback-set-ref-nested-property-dont-preserve-memoization.js => error.useCallback-set-ref-nested-property-dont-preserve-memoization.js} (100%) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/{capture-ref-for-later-mutation.tsx => error.capture-ref-for-later-mutation.tsx} (100%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/{maybe-mutable-ref-not-preserved.ts => error.maybe-mutable-ref-not-preserved.ts} (100%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/{useMemo-with-refs.flow.js => error.useMemo-with-refs.flow.js} (100%) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 7eef0a7cc24b9..ca03b8a7b1e39 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -223,7 +223,7 @@ const EnvironmentConfigSchema = z.object({ validateHooksUsage: z.boolean().default(true), // Validate that ref values (`ref.current`) are not accessed during render. - validateRefAccessDuringRender: z.boolean().default(false), + validateRefAccessDuringRender: z.boolean().default(true), /* * Validates that setState is not unconditionally called during render, as it can lead to diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md deleted file mode 100644 index b7371108d5867..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md +++ /dev/null @@ -1,69 +0,0 @@ - -## Input - -```javascript -import {useRef} from 'react'; -import {addOne} from 'shared-runtime'; - -function useKeyCommand() { - const currentPosition = useRef(0); - const handleKey = direction => () => { - const position = currentPosition.current; - const nextPosition = direction === 'left' ? addOne(position) : position; - currentPosition.current = nextPosition; - }; - const moveLeft = { - handler: handleKey('left'), - }; - const moveRight = { - handler: handleKey('right'), - }; - return [moveLeft, moveRight]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useKeyCommand, - params: [], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { useRef } from "react"; -import { addOne } from "shared-runtime"; - -function useKeyCommand() { - const $ = _c(1); - const currentPosition = useRef(0); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const handleKey = (direction) => () => { - const position = currentPosition.current; - const nextPosition = direction === "left" ? addOne(position) : position; - currentPosition.current = nextPosition; - }; - - const moveLeft = { handler: handleKey("left") }; - - const moveRight = { handler: handleKey("right") }; - - t0 = [moveLeft, moveRight]; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useKeyCommand, - params: [], -}; - -``` - -### Eval output -(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md new file mode 100644 index 0000000000000..52350036257d0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left'), + }; + const moveRight = { + handler: handleKey('right'), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + + +## Error + +``` + 10 | }; + 11 | const moveLeft = { +> 12 | handler: handleKey('left'), + | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) + +InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) + 13 | }; + 14 | const moveRight = { + 15 | handler: handleKey('right'), +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.tsx similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md new file mode 100644 index 0000000000000..1e5fda2a35fb0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime'; + +function Foo(props, ref) { + const value = {}; + if (CONST_TRUE) { + mutate(value); + return ; + } + mutate(value); + if (CONST_TRUE) { + return ; + } + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}, {current: 'fake-ref-object'}], +}; + +``` + + +## Error + +``` + 9 | mutate(value); + 10 | if (CONST_TRUE) { +> 11 | return ; + | ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (11:11) + 12 | } + 13 | return value; + 14 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.tsx similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md new file mode 100644 index 0000000000000..dbc3060a26f43 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @enablePreserveExistingMemoizationGuarantees:false +import {useCallback, useRef} from 'react'; + +function Component(props) { + const ref = useRef({inner: null}); + + const onChange = useCallback(event => { + // The ref should still be mutable here even though function deps are frozen in + // @enablePreserveExistingMemoizationGuarantees mode + ref.current.inner = event.target.value; + }); + + ref.current.inner = null; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + + +## Error + +``` + 11 | }); + 12 | +> 13 | ref.current.inner = null; + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13) + 14 | + 15 | return ; + 16 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md deleted file mode 100644 index 54184be0f3f38..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md +++ /dev/null @@ -1,70 +0,0 @@ - -## Input - -```javascript -// @enableReactiveScopesInHIR:false -import {useRef} from 'react'; -import {addOne} from 'shared-runtime'; - -function useKeyCommand() { - const currentPosition = useRef(0); - const handleKey = direction => () => { - const position = currentPosition.current; - const nextPosition = direction === 'left' ? addOne(position) : position; - currentPosition.current = nextPosition; - }; - const moveLeft = { - handler: handleKey('left'), - }; - const moveRight = { - handler: handleKey('right'), - }; - return [moveLeft, moveRight]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useKeyCommand, - params: [], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enableReactiveScopesInHIR:false -import { useRef } from "react"; -import { addOne } from "shared-runtime"; - -function useKeyCommand() { - const $ = _c(1); - const currentPosition = useRef(0); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const handleKey = (direction) => () => { - const position = currentPosition.current; - const nextPosition = direction === "left" ? addOne(position) : position; - currentPosition.current = nextPosition; - }; - - const moveLeft = { handler: handleKey("left") }; - - const moveRight = { handler: handleKey("right") }; - - t0 = [moveLeft, moveRight]; - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useKeyCommand, - params: [], -}; - -``` - -### Eval output -(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.expect.md new file mode 100644 index 0000000000000..f0b0e6f3a8679 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +// @enableReactiveScopesInHIR:false +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left'), + }; + const moveRight = { + handler: handleKey('right'), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + + +## Error + +``` + 11 | }; + 12 | const moveLeft = { +> 13 | handler: handleKey('left'), + | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13) + +InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (16:16) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (16:16) + 14 | }; + 15 | const moveRight = { + 16 | handler: handleKey('right'), +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.tsx similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/error.capture-ref-for-later-mutation.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md new file mode 100644 index 0000000000000..1ac3884f92f25 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees:true + +import {useRef, useMemo} from 'react'; +import {makeArray} from 'shared-runtime'; + +function useFoo() { + const r = useRef(); + return useMemo(() => makeArray(r), []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + + +## Error + +``` + 6 | function useFoo() { + 7 | const r = useRef(); +> 8 | return useMemo(() => makeArray(r), []); + | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8) + 9 | } + 10 | + 11 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md new file mode 100644 index 0000000000000..de37e0d3d4391 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +// @flow @validatePreserveExistingMemoizationGuarantees +import {identity} from 'shared-runtime'; + +component Component(disableLocalRef, ref) { + const localRef = useFooRef(); + const mergedRef = useMemo(() => { + return disableLocalRef ? ref : identity(ref, localRef); + }, [disableLocalRef, ref, localRef]); + return
; +} + +``` + + +## Error + +``` + 5 | const localRef = useFooRef(); + 6 | const mergedRef = useMemo(() => { +> 7 | return disableLocalRef ? ref : identity(ref, localRef); + | ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7) + 8 | }, [disableLocalRef, ref, localRef]); + 9 | return
; + 10 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md deleted file mode 100644 index 61ab6bafaecd1..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-mutable-ref-not-preserved.expect.md +++ /dev/null @@ -1,53 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees:true - -import {useRef, useMemo} from 'react'; -import {makeArray} from 'shared-runtime'; - -function useFoo() { - const r = useRef(); - return useMemo(() => makeArray(r), []); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees:true - -import { useRef, useMemo } from "react"; -import { makeArray } from "shared-runtime"; - -function useFoo() { - const $ = _c(1); - const r = useRef(); - let t0; - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = makeArray(r); - $[0] = t1; - } else { - t1 = $[0]; - } - t0 = t1; - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [], -}; - -``` - -### Eval output -(kind: ok) [{}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md deleted file mode 100644 index 1fc118ac59ba4..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-refs.flow.expect.md +++ /dev/null @@ -1,54 +0,0 @@ - -## Input - -```javascript -// @flow @validatePreserveExistingMemoizationGuarantees -import {identity} from 'shared-runtime'; - -component Component(disableLocalRef, ref) { - const localRef = useFooRef(); - const mergedRef = useMemo(() => { - return disableLocalRef ? ref : identity(ref, localRef); - }, [disableLocalRef, ref, localRef]); - return
; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { identity } from "shared-runtime"; - -const Component = React.forwardRef(Component_withRef); -function Component_withRef(t0, ref) { - const $ = _c(6); - const { disableLocalRef } = t0; - const localRef = useFooRef(); - let t1; - let t2; - if ($[0] !== disableLocalRef || $[1] !== ref || $[2] !== localRef) { - t2 = disableLocalRef ? ref : identity(ref, localRef); - $[0] = disableLocalRef; - $[1] = ref; - $[2] = localRef; - $[3] = t2; - } else { - t2 = $[3]; - } - t1 = t2; - const mergedRef = t1; - let t3; - if ($[4] !== mergedRef) { - t3 =
; - $[4] = mergedRef; - $[5] = t3; - } else { - t3 = $[5]; - } - return t3; -} - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md deleted file mode 100644 index 8b656b8e23b27..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md +++ /dev/null @@ -1,89 +0,0 @@ - -## Input - -```javascript -import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime'; - -function Foo(props, ref) { - const value = {}; - if (CONST_TRUE) { - mutate(value); - return ; - } - mutate(value); - if (CONST_TRUE) { - return ; - } - return value; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{}, {current: 'fake-ref-object'}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime"; - -function Foo(props, ref) { - const $ = _c(7); - let value; - let t0; - if ($[0] !== ref) { - t0 = Symbol.for("react.early_return_sentinel"); - bb0: { - value = {}; - if (CONST_TRUE) { - mutate(value); - t0 = ; - break bb0; - } - - mutate(value); - } - $[0] = ref; - $[1] = value; - $[2] = t0; - } else { - value = $[1]; - t0 = $[2]; - } - if (t0 !== Symbol.for("react.early_return_sentinel")) { - return t0; - } - if (CONST_TRUE) { - let t1; - if ($[3] !== ref) { - t1 = identity(ref); - $[3] = ref; - $[4] = t1; - } else { - t1 = $[4]; - } - let t2; - if ($[5] !== t1) { - t2 = ; - $[5] = t1; - $[6] = t2; - } else { - t2 = $[6]; - } - return t2; - } - return value; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{}, { current: "fake-ref-object" }], -}; - -``` - -### Eval output -(kind: ok)
{"ref":{"current":"fake-ref-object"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md deleted file mode 100644 index 1efca9f0066be..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md +++ /dev/null @@ -1,75 +0,0 @@ - -## Input - -```javascript -// @enablePreserveExistingMemoizationGuarantees:false -import {useCallback, useRef} from 'react'; - -function Component(props) { - const ref = useRef({inner: null}); - - const onChange = useCallback(event => { - // The ref should still be mutable here even though function deps are frozen in - // @enablePreserveExistingMemoizationGuarantees mode - ref.current.inner = event.target.value; - }); - - ref.current.inner = null; - - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false -import { useCallback, useRef } from "react"; - -function Component(props) { - const $ = _c(3); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = { inner: null }; - $[0] = t0; - } else { - t0 = $[0]; - } - const ref = useRef(t0); - let t1; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = (event) => { - ref.current.inner = event.target.value; - }; - $[1] = t1; - } else { - t1 = $[1]; - } - const onChange = t1; - - ref.current.inner = null; - let t2; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = ; - $[2] = t2; - } else { - t2 = $[2]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{}], -}; - -``` - -### Eval output -(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts index 8150a523263c7..706563b33b457 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts @@ -14,16 +14,16 @@ describe('parseConfigPragma()', () => { // Validate defaults first to make sure that the parser is getting the value from the pragma, // and not just missing it and getting the default value expect(defaultConfig.enableUseTypeAnnotations).toBe(false); - expect(defaultConfig.validateRefAccessDuringRender).toBe(false); + expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false); expect(defaultConfig.validateNoSetStateInRender).toBe(true); const config = parseConfigPragma( - '@enableUseTypeAnnotations @validateRefAccessDuringRender:true @validateNoSetStateInRender:false', + '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false', ); expect(config).toEqual({ ...defaultConfig, enableUseTypeAnnotations: true, - validateRefAccessDuringRender: true, + validateNoSetStateInPassiveEffects: true, validateNoSetStateInRender: false, }); }); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts index c87bcadbe98bc..c1aba9f4c8405 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts @@ -93,12 +93,12 @@ const tests: CompilerTestCases = { `, }, { - // TODO(gsn): Move this to invalid test suite, when we turn on - // validateRefAccessDuringRender validation + // Don't report the issue if Flow already has name: '[InvalidInput] Ref access during render', code: normalizeIndent` function Component(props) { const ref = useRef(null); + // $FlowFixMe[react-rule-unsafe-ref] const value = ref.current; return value; } @@ -106,6 +106,22 @@ const tests: CompilerTestCases = { }, ], invalid: [ + { + name: '[InvalidInput] Ref access during render', + code: normalizeIndent` + function Component(props) { + const ref = useRef(null); + const value = ref.current; + return value; + } + `, + errors: [ + { + message: + 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', + }, + ], + }, { name: 'Reportable levels can be configured', options: [{reportableLevels: new Set([ErrorSeverity.Todo])}], diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 2fe34a0b7ff2b..7b6525842c4d2 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -179,7 +179,10 @@ const rule: Rule.RuleModule = { if (!isReportableDiagnostic(detail)) { return; } - if (hasFlowSuppression(detail.loc, 'react-rule-hook')) { + if ( + hasFlowSuppression(detail.loc, 'react-rule-hook') || + hasFlowSuppression(detail.loc, 'react-rule-unsafe-ref') + ) { // If Flow already caught this error, we don't need to report it again. return; } From 1eaccd8285f0bd40407705a9356391a171adf3b1 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 16 Aug 2024 21:08:20 +0200 Subject: [PATCH 008/191] [Fax] Make `react-markup` publishable via scripts (#30722) --- ReactVersions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReactVersions.js b/ReactVersions.js index 736d6c8f54717..731f5f336270e 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -52,7 +52,7 @@ const stablePackages = { // These packages do not exist in the @canary or @latest channel, only // @experimental. We don't use semver, just the commit sha, so this is just a // list of package names instead of a map. -const experimentalPackages = []; +const experimentalPackages = ['react-markup']; module.exports = { ReactVersion, From 7b41cdc093c7a28a089e2c402cbe98cac68de509 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 16 Aug 2024 14:21:57 -0700 Subject: [PATCH 009/191] [Flight][Static] Implement halting a prerender behind enableHalt (#30705) enableHalt turns on a mode for flight prerenders where aborts are treated like infinitely stalled outcomes while still completing the prerender. For regular tasks we simply serialize the slot as a promise that never settles. For ReadableStream, Blob, and Async Iterators we just never advance the serialization so they remain unfinished when consumed on the client. When enableHalt is turned on aborts of prerenders will halt rather than error. The abort reason is forwarded to the upstream produces of the aforementioned async iterators, blobs, and ReadableStreams. In the future if we expose a signal that you can consume from within a render to cancel additional work the abort reason will also be forwarded there --- .../src/server/ReactFlightDOMServerNode.js | 17 +- .../src/server/ReactFlightDOMServerBrowser.js | 17 +- .../src/server/ReactFlightDOMServerEdge.js | 17 +- .../src/server/ReactFlightDOMServerNode.js | 17 +- .../src/__tests__/ReactFlightDOM-test.js | 134 +++++++ .../__tests__/ReactFlightDOMBrowser-test.js | 115 +++++- .../src/__tests__/ReactFlightDOMEdge-test.js | 341 +++++++++++++----- .../src/__tests__/ReactFlightDOMNode-test.js | 133 +++++++ .../src/server/ReactFlightDOMServerBrowser.js | 17 +- .../src/server/ReactFlightDOMServerEdge.js | 17 +- .../src/server/ReactFlightDOMServerNode.js | 17 +- .../react-server/src/ReactFlightServer.js | 124 +++++-- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 19 files changed, 855 insertions(+), 120 deletions(-) diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index bb65ef4b659a7..1434d17015a54 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -187,10 +190,20 @@ function prerenderToNodeStream( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index ef980764942d7..56c3d5b71f432 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -146,10 +149,20 @@ function prerender( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index ef980764942d7..56c3d5b71f432 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -146,10 +149,20 @@ function prerender( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index e484d4b7e77d5..f9b0c163b2154 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -189,10 +192,20 @@ function prerenderToNodeStream( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index faaf8aef01b0d..6a0ce0152b704 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2722,4 +2722,138 @@ describe('ReactFlightDOM', () => { await readInto(container, fizzReadable); expect(getMeaningfulChildren(container)).toEqual(
hello world
); }); + + // @gate enableHalt + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ + + +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + resolveGreeting(); + const {prelude} = await pendingResult; + + const preludeWeb = Readable.toWeb(prelude); + const response = ReactServerDOMClient.createFromReadableStream(preludeWeb); + + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + function ClientApp() { + return use(response); + } + + const errors = []; + let abortFizz; + await serverAct(async () => { + const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( + React.createElement(ClientApp), + { + onError(error) { + errors.push(error); + }, + }, + ); + pipe(fizzWritable); + abortFizz = abort; + }); + + await serverAct(() => { + abortFizz('boom'); + }); + + expect(errors).toEqual(['boom']); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(getMeaningfulChildren(container)).toEqual(
loading...
); + }); + + // @gate enableHalt + it('will leave async iterables in an incomplete state when halting', async () => { + let resolve; + const wait = new Promise(r => (resolve = r)); + const errors = []; + + const multiShotIterable = { + async *[Symbol.asyncIterator]() { + yield {hello: 'A'}; + await wait; + yield {hi: 'B'}; + return 'C'; + }, + }; + + const controller = new AbortController(); + const {pendingResult} = await serverAct(() => { + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + { + multiShotIterable, + }, + {}, + { + onError(x) { + errors.push(x); + }, + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + await serverAct(() => resolve()); + + const {prelude} = await pendingResult; + + const result = await ReactServerDOMClient.createFromReadableStream( + Readable.toWeb(prelude), + ); + + const iterator = result.multiShotIterable[Symbol.asyncIterator](); + + expect(await iterator.next()).toEqual({ + value: {hello: 'A'}, + done: false, + }); + + const race = Promise.race([ + iterator.next(), + new Promise(r => setTimeout(() => r('timeout'), 10)), + ]); + + await 1; + jest.advanceTimersByTime('100'); + expect(await race).toBe('timeout'); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index db8edf7ad6831..7bbfea1484bed 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -29,6 +29,7 @@ let ReactDOM; let ReactDOMClient; let ReactDOMFizzServer; let ReactServerDOMServer; +let ReactServerDOMStaticServer; let ReactServerDOMClient; let Suspense; let use; @@ -60,7 +61,13 @@ describe('ReactFlightDOMBrowser', () => { serverExports = WebpackMock.serverExports; webpackMap = WebpackMock.webpackMap; webpackServerMap = WebpackMock.webpackServerMap; - ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMServer = require('react-server-dom-webpack/server'); + if (__EXPERIMENTAL__) { + jest.mock('react-server-dom-webpack/static', () => + require('react-server-dom-webpack/static.browser'), + ); + ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); + } __unmockReact(); jest.resetModules(); @@ -2332,4 +2339,110 @@ describe('ReactFlightDOMBrowser', () => { expect(error.digest).toBe('aborted'); expect(errors).toEqual([reason]); }); + + // @gate experimental + it('can prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + ), + }; + }); + + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream( + passThrough(prelude), + ); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('
hello world
'); + }); + + // @gate enableHalt + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ + + +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream( + passThrough(prelude), + ); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe('
loading...
'); + }); }); 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 ffef621e9761b..b38c33dc7761b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -23,9 +23,9 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') { // Patch for Edge environments for global scope global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage; -// Don't wait before processing work on the server. -// TODO: we can replace this with FlightServer.act(). -global.setTimeout = cb => cb(); +const { + patchMessageChannel, +} = require('../../../../scripts/jest/patchMessageChannel'); let serverExports; let clientExports; @@ -36,8 +36,11 @@ let React; let ReactServer; let ReactDOMServer; let ReactServerDOMServer; +let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; +let ReactServerScheduler; +let reactServerAct; function normalizeCodeLocInfo(str) { return ( @@ -52,6 +55,10 @@ describe('ReactFlightDOMEdge', () => { beforeEach(() => { jest.resetModules(); + ReactServerScheduler = require('scheduler'); + patchMessageChannel(ReactServerScheduler); + reactServerAct = require('internal-test-utils').act; + // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => @@ -68,6 +75,12 @@ describe('ReactFlightDOMEdge', () => { ReactServer = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server'); + if (__EXPERIMENTAL__) { + jest.mock('react-server-dom-webpack/static', () => + require('react-server-dom-webpack/static.edge'), + ); + ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); + } jest.resetModules(); __unmockReact(); @@ -81,6 +94,17 @@ describe('ReactFlightDOMEdge', () => { use = React.use; }); + async function serverAct(callback) { + let maybePromise; + await reactServerAct(() => { + maybePromise = callback(); + if (maybePromise && typeof maybePromise.catch === 'function') { + maybePromise.catch(() => {}); + } + }); + return maybePromise; + } + function passThrough(stream) { // Simulate more realistic network by splitting up and rejoining some chunks. // This lets us test that we don't accidentally rely on particular bounds of the chunks. @@ -174,9 +198,8 @@ describe('ReactFlightDOMEdge', () => { return ; } - const stream = ReactServerDOMServer.renderToReadableStream( - , - webpackMap, + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), ); const response = ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { @@ -189,8 +212,8 @@ describe('ReactFlightDOMEdge', () => { return use(response); } - const ssrStream = await ReactDOMServer.renderToReadableStream( - , + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream(), ); const result = await readResult(ssrStream); expect(result).toEqual('Client Component'); @@ -200,10 +223,12 @@ describe('ReactFlightDOMEdge', () => { const testString = '"\n\t'.repeat(500) + '🙃'; const testString2 = 'hello'.repeat(400); - const stream = ReactServerDOMServer.renderToReadableStream({ - text: testString, - text2: testString2, - }); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream({ + text: testString, + text2: testString2, + }), + ); const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); @@ -234,7 +259,9 @@ describe('ReactFlightDOMEdge', () => { with: {many: 'properties in it'}, }; const props = {root:
{new Array(30).fill(obj)}
}; - const stream = ReactServerDOMServer.renderToReadableStream(props); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(props), + ); const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); @@ -302,7 +329,9 @@ describe('ReactFlightDOMEdge', () => { ); const resolvedChildren = new Array(30).fill(str); - const stream = ReactServerDOMServer.renderToReadableStream(children); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(children), + ); const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); @@ -318,7 +347,9 @@ describe('ReactFlightDOMEdge', () => { }); // Use the SSR render to resolve any lazy elements - const ssrStream = await ReactDOMServer.renderToReadableStream(model); + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream(model), + ); // Should still match the result when parsed const result = await readResult(ssrStream); expect(result).toEqual(resolvedChildren.join('')); @@ -370,22 +401,28 @@ describe('ReactFlightDOMEdge', () => { const resolvedChildren = new Array(30).fill( '
this is a long return value
', ); - const stream = ReactServerDOMServer.renderToReadableStream(children); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(children), + ); const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); expect(serializedContent.length).toBeLessThan(__DEV__ ? 605 : 400); expect(timesRendered).toBeLessThan(5); - const model = await ReactServerDOMClient.createFromReadableStream(stream2, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); + const model = await serverAct(() => + ReactServerDOMClient.createFromReadableStream(stream2, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }), + ); // Use the SSR render to resolve any lazy elements - const ssrStream = await ReactDOMServer.renderToReadableStream(model); + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream(model), + ); // Should still match the result when parsed const result = await readResult(ssrStream); expect(result).toEqual(resolvedChildren.join('')); @@ -398,8 +435,10 @@ describe('ReactFlightDOMEdge', () => { } return
Fin
; } - const stream = ReactServerDOMServer.renderToReadableStream( - , + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + , + ), ); const serializedContent = await readResult(stream); const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0; @@ -426,8 +465,8 @@ describe('ReactFlightDOMEdge', () => { new BigUint64Array(buffer, 0), new DataView(buffer, 3), ]; - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(buffers), + const stream = await serverAct(() => + passThrough(ReactServerDOMServer.renderToReadableStream(buffers)), ); const result = await ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { @@ -446,8 +485,8 @@ describe('ReactFlightDOMEdge', () => { const blob = new Blob([bytes, bytes], { type: 'application/x-test', }); - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(blob), + const stream = await serverAct(() => + passThrough(ReactServerDOMServer.renderToReadableStream(blob)), ); const result = await ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { @@ -476,8 +515,8 @@ describe('ReactFlightDOMEdge', () => { expect(formData.get('file') instanceof File).toBe(true); expect(formData.get('file').name).toBe('filename.test'); - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(formData), + const stream = await serverAct(() => + passThrough(ReactServerDOMServer.renderToReadableStream(formData)), ); const result = await ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { @@ -507,8 +546,8 @@ describe('ReactFlightDOMEdge', () => { const map = new Map(); map.set('value', awaitedValue); - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(map, webpackMap), + const stream = await serverAct(() => + passThrough(ReactServerDOMServer.renderToReadableStream(map, webpackMap)), ); // Parsing the root blocks because the module hasn't loaded yet @@ -549,16 +588,18 @@ describe('ReactFlightDOMEdge', () => { }, }); - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream(s, webpackMap), + const stream = await serverAct(() => + passThrough(ReactServerDOMServer.renderToReadableStream(s, webpackMap)), ); - const result = await ReactServerDOMClient.createFromReadableStream(stream, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); + const result = await serverAct(() => + ReactServerDOMClient.createFromReadableStream(stream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }), + ); const reader = result.getReader(); @@ -589,20 +630,24 @@ describe('ReactFlightDOMEdge', () => { }, }; - const stream = passThrough( - ReactServerDOMServer.renderToReadableStream( - multiShotIterable, - webpackMap, + const stream = await serverAct(() => + passThrough( + ReactServerDOMServer.renderToReadableStream( + multiShotIterable, + webpackMap, + ), ), ); // Parsing the root blocks because the module hasn't loaded yet - const result = await ReactServerDOMClient.createFromReadableStream(stream, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); + const result = await serverAct(() => + ReactServerDOMClient.createFromReadableStream(stream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }), + ); const iterator = result[Symbol.asyncIterator](); @@ -635,9 +680,11 @@ describe('ReactFlightDOMEdge', () => { }, }; - const stream = ReactServerDOMServer.renderToReadableStream({ - iterable, - }); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream({ + iterable, + }), + ); const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); @@ -728,7 +775,9 @@ describe('ReactFlightDOMEdge', () => { }, }); - const stream = ReactServerDOMServer.renderToReadableStream(s, {}); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(s, {}), + ); const [stream1, stream2] = passThrough(stream).tee(); @@ -785,7 +834,9 @@ describe('ReactFlightDOMEdge', () => { }, }); - const stream = ReactServerDOMServer.renderToReadableStream(s, {}); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(s, {}), + ); const [stream1, stream2] = passThrough(stream).tee(); @@ -841,23 +892,21 @@ describe('ReactFlightDOMEdge', () => { greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}), }; - const stream = ReactServerDOMServer.renderToReadableStream( - model, - webpackMap, + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(model, webpackMap), ); - const rootModel = await ReactServerDOMClient.createFromReadableStream( - stream, - { + const rootModel = await serverAct(() => + ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { moduleMap: null, moduleLoading: null, }, - }, + }), ); - const ssrStream = await ReactDOMServer.renderToReadableStream( - rootModel.greeting, + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream(rootModel.greeting), ); const result = await readResult(ssrStream); expect(result).toEqual('Hello, Seb'); @@ -916,13 +965,15 @@ describe('ReactFlightDOMEdge', () => { return ReactServer.createElement('span', null, 'hi'); } - const stream = ReactServerDOMServer.renderToReadableStream( - ReactServer.createElement( - 'div', - null, - ReactServer.createElement(Foo, null), + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + webpackMap, ), - webpackMap, ); await readResult(stream); @@ -943,35 +994,31 @@ describe('ReactFlightDOMEdge', () => { root: ReactServer.createElement(Erroring), }; - const stream = ReactServerDOMServer.renderToReadableStream( - model, - webpackMap, - { + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(model, webpackMap, { onError() {}, - }, + }), ); - const rootModel = await ReactServerDOMClient.createFromReadableStream( - stream, - { + const rootModel = await serverAct(() => + ReactServerDOMClient.createFromReadableStream(stream, { ssrManifest: { moduleMap: null, moduleLoading: null, }, - }, + }), ); const errors = []; - const result = ReactDOMServer.renderToReadableStream( -
{rootModel.root}
, - { + const result = serverAct(() => + ReactDOMServer.renderToReadableStream(
{rootModel.root}
, { onError(error, {componentStack}) { errors.push({ error, componentStack: normalizeCodeLocInfo(componentStack), }); }, - }, + }), ); const theError = new Error('my error'); @@ -1000,4 +1047,130 @@ describe('ReactFlightDOMEdge', () => { }, ]); }); + + // @gate experimental + it('can prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + ), + }; + }); + + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(prelude, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); + // Use the SSR render to resolve any lazy elements + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream( + React.createElement(ClientRoot, {response}), + ), + ); + // Should still match the result when parsed + const result = await readResult(ssrStream); + expect(result).toBe('
hello world
'); + }); + + // @gate enableHalt + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ + + +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(prelude, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); + const fizzController = new AbortController(); + const errors = []; + const ssrStream = await serverAct(() => + ReactDOMServer.renderToReadableStream( + React.createElement(ClientRoot, {response}), + { + signal: fizzController.signal, + onError(error) { + errors.push(error); + }, + }, + ), + ); + fizzController.abort('boom'); + expect(errors).toEqual(['boom']); + // Should still match the result when parsed + const result = await readResult(ssrStream); + const div = document.createElement('div'); + div.innerHTML = result; + expect(div.textContent).toBe('loading...'); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 2de34cc1c493f..fbe32d2f1f697 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -20,7 +20,9 @@ let webpackModules; let webpackModuleLoading; let React; let ReactDOMServer; +let ReactServer; let ReactServerDOMServer; +let ReactServerDOMStaticServer; let ReactServerDOMClient; let Stream; let use; @@ -45,7 +47,14 @@ describe('ReactFlightDOMNode', () => { jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node'), ); + ReactServer = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server'); + if (__EXPERIMENTAL__) { + jest.mock('react-server-dom-webpack/static', () => + require('react-server-dom-webpack/static.node'), + ); + ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); + } const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; @@ -378,4 +387,128 @@ describe('ReactFlightDOMNode', () => { expect(error.digest).toBe('aborted'); expect(errors).toEqual([reason]); }); + + // @gate experimental + it('can prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + , + webpackMap, + ), + }; + }); + + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromNodeStream(prelude, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); + // Use the SSR render to resolve any lazy elements + const ssrStream = await serverAct(() => + ReactDOMServer.renderToPipeableStream( + React.createElement(ClientRoot, {response}), + ), + ); + // Should still match the result when parsed + const result = await readResult(ssrStream); + expect(result).toBe('
hello world
'); + }); + + // @gate enableHalt + it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( +
+ + + +
+ ); + } + + async function Greeting() { + await greetingPromise; + return 'hello world'; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + resolveGreeting(); + const {prelude} = await pendingResult; + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromNodeStream(prelude, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); + const errors = []; + const ssrStream = await serverAct(() => + ReactDOMServer.renderToPipeableStream( + React.createElement(ClientRoot, {response}), + { + onError(error) { + errors.push(error); + }, + }, + ), + ); + ssrStream.abort('boom'); + expect(errors).toEqual(['boom']); + // Should still match the result when parsed + const result = await readResult(ssrStream); + const div = document.createElement('div'); + div.innerHTML = result; + expect(div.textContent).toBe('loading...'); + }); }); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index a4e0c3bef693b..95e7f770428a3 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -146,10 +149,20 @@ function prerender( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index a4e0c3bef693b..95e7f770428a3 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -12,12 +12,15 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -146,10 +149,20 @@ function prerender( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 1506259476703..1d8d6ea9ef743 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -20,12 +20,15 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; +import {enableHalt} from 'shared/ReactFeatureFlags'; + import { createRequest, startWork, startFlowing, stopFlowing, abort, + halt, } from 'react-server/src/ReactFlightServer'; import { @@ -189,10 +192,20 @@ function prerenderToNodeStream( if (options && options.signal) { const signal = options.signal; if (signal.aborted) { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } } else { const listener = () => { - abort(request, (signal: any).reason); + const reason = (signal: any).reason; + if (enableHalt) { + halt(request, reason); + } else { + abort(request, reason); + } signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index d8ba106d37e72..db0ee7b3cebad 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -16,6 +16,7 @@ import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences import { enableBinaryFlight, enablePostpone, + enableHalt, enableTaint, enableRefAsProp, enableServerComponentLogs, @@ -611,11 +612,15 @@ function serializeThenable( default: { if (request.status === ABORTING) { // We can no longer accept any resolved values - newTask.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, newTask.id, model); request.abortableTasks.delete(newTask); + newTask.status = ABORTED; + if (enableHalt && request.fatalError === haltSymbol) { + emitModelChunk(request, newTask.id, reusableInfinitePromiseModel); + } else { + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, newTask.id, model); + } return newTask.id; } if (typeof thenable.status === 'string') { @@ -748,23 +753,32 @@ function serializeReadableStream( } aborted = true; request.abortListeners.delete(error); - if ( + + let cancelWith: mixed; + if (enableHalt && request.fatalError === haltSymbol) { + cancelWith = reason; + } else if ( enablePostpone && typeof reason === 'object' && reason !== null && (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { + cancelWith = reason; const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, streamTask); emitPostponeChunk(request, streamTask.id, postponeInstance); + enqueueFlush(request); } else { + cancelWith = reason; const digest = logRecoverableError(request, reason, streamTask); emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); } - enqueueFlush(request); + // $FlowFixMe should be able to pass mixed - reader.cancel(reason).then(error, error); + reader.cancel(cancelWith).then(error, error); } + request.abortListeners.add(error); reader.read().then(progress, error); return serializeByValueID(streamTask.id); @@ -866,24 +880,30 @@ function serializeAsyncIterable( } aborted = true; request.abortListeners.delete(error); - if ( + let throwWith: mixed; + if (enableHalt && request.fatalError === haltSymbol) { + throwWith = reason; + } else if ( enablePostpone && typeof reason === 'object' && reason !== null && (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { + throwWith = reason; const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, streamTask); emitPostponeChunk(request, streamTask.id, postponeInstance); + enqueueFlush(request); } else { + throwWith = reason; const digest = logRecoverableError(request, reason, streamTask); emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); } - enqueueFlush(request); if (typeof (iterator: any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe should be able to pass mixed - iterator.throw(reason).then(error, error); + iterator.throw(throwWith).then(error, error); } } request.abortListeners.add(error); @@ -1798,6 +1818,7 @@ function serializeLazyID(id: number): string { function serializeInfinitePromise(): string { return '$@'; } +const reusableInfinitePromiseModel = stringify(serializeInfinitePromise()); function serializePromiseID(id: number): string { return '$@' + id.toString(16); @@ -2066,12 +2087,18 @@ function serializeBlob(request: Request, blob: Blob): string { } aborted = true; request.abortListeners.delete(error); - const digest = logRecoverableError(request, reason, newTask); - emitErrorChunk(request, newTask.id, digest, reason); - request.abortableTasks.delete(newTask); - enqueueFlush(request); + let cancelWith: mixed; + if (enableHalt && request.fatalError === haltSymbol) { + cancelWith = reason; + } else { + cancelWith = reason; + const digest = logRecoverableError(request, reason, newTask); + emitErrorChunk(request, newTask.id, digest, reason); + request.abortableTasks.delete(newTask); + enqueueFlush(request); + } // $FlowFixMe should be able to pass mixed - reader.cancel(reason).then(error, error); + reader.cancel(cancelWith).then(error, error); } request.abortListeners.add(error); @@ -2149,6 +2176,9 @@ function renderModel( if (typeof x.then === 'function') { if (request.status === ABORTING) { task.status = ABORTED; + if (enableHalt && request.fatalError === haltSymbol) { + return serializeInfinitePromise(); + } const errorId: number = (request.fatalError: any); if (wasReactNode) { return serializeLazyID(errorId); @@ -2202,6 +2232,9 @@ function renderModel( if (request.status === ABORTING) { task.status = ABORTED; + if (enableHalt && request.fatalError === haltSymbol) { + return serializeInfinitePromise(); + } const errorId: number = (request.fatalError: any); if (wasReactNode) { return serializeLazyID(errorId); @@ -3691,9 +3724,13 @@ function retryTask(request: Request, task: Task): void { if (request.status === ABORTING) { request.abortableTasks.delete(task); task.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); + if (enableHalt && request.fatalError === haltSymbol) { + emitModelChunk(request, task.id, reusableInfinitePromiseModel); + } else { + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, task.id, model); + } return; } // Something suspended again, let's pick it back up later. @@ -3715,9 +3752,13 @@ function retryTask(request: Request, task: Task): void { if (request.status === ABORTING) { request.abortableTasks.delete(task); task.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); + if (enableHalt && request.fatalError === haltSymbol) { + emitModelChunk(request, task.id, reusableInfinitePromiseModel); + } else { + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, task.id, model); + } return; } @@ -3795,6 +3836,15 @@ function abortTask(task: Task, request: Request, errorId: number): void { request.completedErrorChunks.push(processedChunk); } +function haltTask(task: Task, request: Request): void { + if (task.status === RENDERING) { + // This task will be aborted by the render + return; + } + task.status = ABORTED; + emitModelChunk(request, task.id, reusableInfinitePromiseModel); +} + function flushCompletedChunks( request: Request, destination: Destination, @@ -4012,3 +4062,35 @@ export function abort(request: Request, reason: mixed): void { fatalError(request, error); } } + +const haltSymbol = Symbol('halt'); + +// This is called to stop rendering without erroring. All unfinished work is represented Promises +// that never resolve. +export function halt(request: Request, reason: mixed): void { + try { + if (request.status === OPEN) { + request.status = ABORTING; + } + request.fatalError = haltSymbol; + const abortableTasks = request.abortableTasks; + // We have tasks to abort. We'll emit one error row and then emit a reference + // to that row from every row that's still remaining. + if (abortableTasks.size > 0) { + request.pendingChunks++; + abortableTasks.forEach(task => haltTask(task, request)); + abortableTasks.clear(); + } + const abortListeners = request.abortListeners; + if (abortListeners.size > 0) { + abortListeners.forEach(callback => callback(reason)); + abortListeners.clear(); + } + if (request.destination !== null) { + flushCompletedChunks(request, request.destination); + } + } catch (error) { + logRecoverableError(request, error, null); + fatalError(request, error); + } +} diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index b0286405a6cca..c5351d6d92631 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -87,6 +87,8 @@ export const enableTaint = __EXPERIMENTAL__; export const enablePostpone = __EXPERIMENTAL__; +export const enableHalt = __EXPERIMENTAL__; + /** * Switches the Fabric API from doing layout in commit work instead of complete work. */ diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 4eda27d16cfcb..3618aa70e7d67 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -58,6 +58,7 @@ export const enableFilterEmptyStringAttributesDOM = true; export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = true; +export const enableHalt = false; export const enableInfiniteRenderLoopDetection = true; export const enableContextProfiling = false; export const enableLegacyCache = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 2a4421f41da0a..2aae8bd3d1c65 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -49,6 +49,7 @@ export const enableFilterEmptyStringAttributesDOM = true; export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; +export const enableHalt = false; export const enableInfiniteRenderLoopDetection = true; export const enableLazyContextPropagation = false; export const enableContextProfiling = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 8778bf6558cb4..c44e7014fc444 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -25,6 +25,7 @@ export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = false; export const enableTaint = true; export const enablePostpone = false; +export const enableHalt = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; export const disableIEWorkarounds = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 3a8a0c1d44cec..bc7ddf85acc03 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -40,6 +40,7 @@ export const enableFilterEmptyStringAttributesDOM = true; export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; +export const enableHalt = false; export const enableInfiniteRenderLoopDetection = true; export const enableLazyContextPropagation = false; export const enableContextProfiling = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index eb801d7bac4b6..57f60c24aef45 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -25,6 +25,7 @@ export const enableFlightReadableStream = true; export const enableAsyncIterableChildren = false; export const enableTaint = true; export const enablePostpone = false; +export const enableHalt = false; export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; export const disableIEWorkarounds = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 95cd1e5a6ebe6..465fa58590bcc 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -78,6 +78,8 @@ export const enableTaint = false; export const enablePostpone = false; +export const enableHalt = false; + export const enableContextProfiling = true; // TODO: www currently relies on this feature. It's disabled in open source. From e9a869fbb59a634fb9a39c9480ffa34970f35858 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Fri, 16 Aug 2024 17:39:50 -0400 Subject: [PATCH 010/191] [compiler] Run compiler pipeline on 'use no forget' This PR updates the babel plugin to continue the compilation pipeline as normal on components/hooks that have been opted out using a directive. Instead, we no longer emit the compiled function when the directive is present. Previously, we would skip over the entire pipeline. By continuing to enter the pipeline, we'll be able to detect if there are unused directives. The end result is: - (no change) 'use forget' will always opt into compilation - (new) 'use no forget' will opt out of compilation but continue to log errors without throwing them. This means that a Program containing multiple functions (some of which are opted out) will continue to compile correctly ghstack-source-id: 5bd85df2f81350cb2c1998a8761b8ed3fec32a40 Pull Request resolved: https://github.com/facebook/react/pull/30720 --- .../src/Entrypoint/Options.ts | 6 + .../src/Entrypoint/Program.ts | 146 ++++++++++-------- .../use-no-forget-with-no-errors.expect.md | 35 +++++ .../compiler/use-no-forget-with-no-errors.js | 10 ++ .../babel-plugin-react-compiler/src/index.ts | 1 + 5 files changed, 136 insertions(+), 62 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 722c62461d813..e97ececc2a137 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -165,6 +165,12 @@ export type LoggerEvent = fnLoc: t.SourceLocation | null; detail: Omit, 'suggestions'>; } + | { + kind: 'CompileSkip'; + fnLoc: t.SourceLocation | null; + reason: string; + loc: t.SourceLocation | null; + } | { kind: 'CompileSuccess'; fnLoc: t.SourceLocation | null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 499a4d124ea67..99ec1e04a65e4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -43,34 +43,23 @@ export type CompilerPass = { comments: Array; code: string | null; }; +const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']); +export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']); function findDirectiveEnablingMemoization( directives: Array, -): t.Directive | null { - for (const directive of directives) { - const directiveValue = directive.value.value; - if (directiveValue === 'use forget' || directiveValue === 'use memo') { - return directive; - } - } - return null; +): Array { + return directives.filter(directive => + OPT_IN_DIRECTIVES.has(directive.value.value), + ); } function findDirectiveDisablingMemoization( directives: Array, - options: PluginOptions, -): t.Directive | null { - for (const directive of directives) { - const directiveValue = directive.value.value; - if ( - (directiveValue === 'use no forget' || - directiveValue === 'use no memo') && - !options.ignoreUseNoForget - ) { - return directive; - } - } - return null; +): Array { + return directives.filter(directive => + OPT_OUT_DIRECTIVES.has(directive.value.value), + ); } function isCriticalError(err: unknown): boolean { @@ -102,7 +91,7 @@ export type CompileResult = { compiledFn: CodegenFunction; }; -function handleError( +function logError( err: unknown, pass: CompilerPass, fnLoc: t.SourceLocation | null, @@ -131,6 +120,13 @@ function handleError( }); } } +} +function handleError( + err: unknown, + pass: CompilerPass, + fnLoc: t.SourceLocation | null, +): void { + logError(err, pass, fnLoc); if ( pass.opts.panicThreshold === 'all_errors' || (pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) || @@ -393,6 +389,17 @@ export function compileProgram( fn: BabelFn, fnType: ReactFunctionType, ): null | CodegenFunction => { + let optInDirectives: Array = []; + let optOutDirectives: Array = []; + if (fn.node.body.type === 'BlockStatement') { + optInDirectives = findDirectiveEnablingMemoization( + fn.node.body.directives, + ); + optOutDirectives = findDirectiveDisablingMemoization( + fn.node.body.directives, + ); + } + if (lintError != null) { /** * Note that Babel does not attach comment nodes to nodes; they are dangling off of the @@ -404,7 +411,11 @@ export function compileProgram( fn, ); if (suppressionsInFunction.length > 0) { - handleError(lintError, pass, fn.node.loc ?? null); + if (optOutDirectives.length > 0) { + logError(lintError, pass, fn.node.loc ?? null); + } else { + handleError(lintError, pass, fn.node.loc ?? null); + } } } @@ -430,11 +441,50 @@ export function compileProgram( prunedMemoValues: compiledFn.prunedMemoValues, }); } catch (err) { + /** + * If an opt out directive is present, log only instead of throwing and don't mark as + * containing a critical error. + */ + if (fn.node.body.type === 'BlockStatement') { + if (optOutDirectives.length > 0) { + logError(err, pass, fn.node.loc ?? null); + return null; + } + } hasCriticalError ||= isCriticalError(err); handleError(err, pass, fn.node.loc ?? null); return null; } + /** + * Always compile functions with opt in directives. + */ + if (optInDirectives.length > 0) { + return compiledFn; + } else if (pass.opts.compilationMode === 'annotation') { + /** + * No opt-in directive in annotation mode, so don't insert the compiled function. + */ + return null; + } + + /** + * Otherwise if 'use no forget/memo' is present, we still run the code through the compiler + * for validation but we don't mutate the babel AST. This allows us to flag if there is an + * unused 'use no forget/memo' directive. + */ + if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) { + for (const directive of optOutDirectives) { + pass.opts.logger?.logEvent(pass.filename, { + kind: 'CompileSkip', + fnLoc: fn.node.body.loc ?? null, + reason: `Skipped due to '${directive.value.value}' directive.`, + loc: directive.loc ?? null, + }); + } + return null; + } + if (!pass.opts.noEmit && !hasCriticalError) { return compiledFn; } @@ -481,6 +531,16 @@ export function compileProgram( }); } + /** + * Do not modify source if there is a module scope level opt out directive. + */ + const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization( + program.node.directives, + ); + if (moduleScopeOptOutDirectives.length > 0) { + return; + } + if (pass.opts.gating != null) { const error = checkFunctionReferencedBeforeDeclarationAtTopLevel( program, @@ -596,24 +656,6 @@ function shouldSkipCompilation( } } - // Top level "use no forget", skip this file entirely - const useNoForget = findDirectiveDisablingMemoization( - program.node.directives, - pass.opts, - ); - if (useNoForget != null) { - pass.opts.logger?.logEvent(pass.filename, { - kind: 'CompileError', - fnLoc: null, - detail: { - severity: ErrorSeverity.Todo, - reason: 'Skipped due to "use no forget" directive.', - loc: useNoForget.loc ?? null, - suggestions: null, - }, - }); - return true; - } const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime'; if (hasMemoCacheFunctionImport(program, moduleName)) { return true; @@ -631,28 +673,8 @@ function getReactFunctionType( ): ReactFunctionType | null { const hookPattern = environment.hookPattern; if (fn.node.body.type === 'BlockStatement') { - // Opt-outs disable compilation regardless of mode - const useNoForget = findDirectiveDisablingMemoization( - fn.node.body.directives, - pass.opts, - ); - if (useNoForget != null) { - pass.opts.logger?.logEvent(pass.filename, { - kind: 'CompileError', - fnLoc: fn.node.body.loc ?? null, - detail: { - severity: ErrorSeverity.Todo, - reason: 'Skipped due to "use no forget" directive.', - loc: useNoForget.loc ?? null, - suggestions: null, - }, - }); - return null; - } - // Otherwise opt-ins enable compilation regardless of mode - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) { + if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0) return getComponentOrHookLike(fn, hookPattern) ?? 'Other'; - } } // Component and hook declarations are known components/hooks diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.expect.md new file mode 100644 index 0000000000000..20acbe0153135 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +function Component() { + 'use no forget'; + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +## Code + +```javascript +function Component() { + "use no forget"; + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
Hello World
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.js new file mode 100644 index 0000000000000..934487160d55c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-with-no-errors.js @@ -0,0 +1,10 @@ +function Component() { + 'use no forget'; + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index f038246a4f1ee..aac65331a0ff2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -18,6 +18,7 @@ export { compileProgram, parsePluginOptions, run, + OPT_OUT_DIRECTIVES, type CompilerPipelineValue, type PluginOptions, } from './Entrypoint'; From 34edf3b68471e87d4a92f98a10f7c6c727c948f8 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Fri, 16 Aug 2024 17:39:51 -0400 Subject: [PATCH 011/191] [compiler] Surface unused opt out directives in eslint This PR updates the eslint plugin to report unused opt out directives. One of the downsides of the opt out directive is that it opts the component/hook out of compilation forever, even if the underlying issue was fixed in product code or fixed in the compiler. ghstack-source-id: 81deb5c11b7c57f07f6ab13266066cd73b2f3729 Pull Request resolved: https://github.com/facebook/react/pull/30721 --- .../__tests__/ReactCompilerRule-test.ts | 59 +++++++++++++++++++ .../src/rules/ReactCompilerRule.ts | 51 +++++++++++++++- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts index c1aba9f4c8405..71be6b6622eb5 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts @@ -215,6 +215,65 @@ const tests: CompilerTestCases = { }, ], }, + { + name: "'use no forget' does not disable eslint rule", + code: normalizeIndent` + let count = 0; + function Component() { + 'use no forget'; + count = count + 1; + return
Hello world {count}
+ } + `, + errors: [ + { + message: + 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', + }, + ], + }, + { + name: "Unused 'use no forget' directive is reported when no errors are present on components", + code: normalizeIndent` + function Component() { + 'use no forget'; + return
Hello world
+ } + `, + errors: [ + { + message: "Unused 'use no forget' directive", + suggestions: [ + { + output: + // yuck + '\nfunction Component() {\n \n return
Hello world
\n}\n', + }, + ], + }, + ], + }, + { + name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks", + code: normalizeIndent` + function notacomponent() { + 'use no forget'; + return 1 + 1; + } + `, + errors: [ + { + message: "Unused 'use no forget' directive", + suggestions: [ + { + output: + // yuck + '\nfunction notacomponent() {\n \n return 1 + 1;\n}\n', + }, + ], + }, + ], + }, ], }; diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 7b6525842c4d2..0a0956ebe1db0 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -15,10 +15,12 @@ import BabelPluginReactCompiler, { ErrorSeverity, parsePluginOptions, validateEnvironmentConfig, + OPT_OUT_DIRECTIVES, type PluginOptions, } from 'babel-plugin-react-compiler/src'; import {Logger} from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Rule} from 'eslint'; +import {Statement} from 'estree'; import * as HermesParser from 'hermes-parser'; type CompilerErrorDetailWithLoc = Omit & { @@ -146,6 +148,7 @@ const rule: Rule.RuleModule = { userOpts['__unstable_donotuse_reportAllBailouts']; } + let shouldReportUnusedOptOutDirective = true; const options: PluginOptions = { ...parsePluginOptions(userOpts), ...COMPILER_OPTIONS, @@ -155,6 +158,7 @@ const rule: Rule.RuleModule = { logEvent: (filename, event): void => { userLogger?.logEvent(filename, event); if (event.kind === 'CompileError') { + shouldReportUnusedOptOutDirective = false; const detail = event.detail; const suggest = makeSuggestions(detail); if (__unstable_donotuse_reportAllBailouts && event.fnLoc != null) { @@ -272,7 +276,52 @@ const rule: Rule.RuleModule = { /* errors handled by injected logger */ } } - return {}; + + function reportUnusedOptOutDirective(stmt: Statement) { + if ( + stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'Literal' && + typeof stmt.expression.value === 'string' && + OPT_OUT_DIRECTIVES.has(stmt.expression.value) && + stmt.loc != null + ) { + context.report({ + message: `Unused '${stmt.expression.value}' directive`, + loc: stmt.loc, + suggest: [ + { + desc: 'Remove the directive', + fix(fixer) { + return fixer.remove(stmt); + }, + }, + ], + }); + } + } + if (shouldReportUnusedOptOutDirective) { + return { + FunctionDeclaration(fnDecl) { + for (const stmt of fnDecl.body.body) { + reportUnusedOptOutDirective(stmt); + } + }, + ArrowFunctionExpression(fnExpr) { + if (fnExpr.body.type === 'BlockStatement') { + for (const stmt of fnExpr.body.body) { + reportUnusedOptOutDirective(stmt); + } + } + }, + FunctionExpression(fnExpr) { + for (const stmt of fnExpr.body.body) { + reportUnusedOptOutDirective(stmt); + } + }, + }; + } else { + return {}; + } }, }; From a58276cbc3a70ba99572eeb9c2f7b4a54ca44b1e Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Fri, 16 Aug 2024 17:39:51 -0400 Subject: [PATCH 012/191] [playground] Allow (Arrow)FunctionExpressions This was a pet peeve where our playground could only compile top level FunctionDeclarations. Just synthesize a fake identifier if it doesn't have one. ghstack-source-id: 882483c79ceebf382b69e37aed1f293efff9c5a7 Pull Request resolved: https://github.com/facebook/react/pull/30729 --- .../components/Editor/EditorImpl.tsx | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index ebac65dc4b9f2..7b1214b4600c3 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -66,14 +66,14 @@ function parseFunctions( source: string, language: 'flow' | 'typescript', ): Array< - NodePath< - t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression - > + | NodePath + | NodePath + | NodePath > { const items: Array< - NodePath< - t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression - > + | NodePath + | NodePath + | NodePath > = []; try { const ast = parseInput(source, language); @@ -155,22 +155,33 @@ function isHookName(s: string): boolean { return /^use[A-Z0-9]/.test(s); } -function getReactFunctionType( - id: NodePath, -): ReactFunctionType { - if (id && id.node && id.isIdentifier()) { - if (isHookName(id.node.name)) { +function getReactFunctionType(id: t.Identifier | null): ReactFunctionType { + if (id != null) { + if (isHookName(id.name)) { return 'Hook'; } const isPascalCaseNameSpace = /^[A-Z].*/; - if (isPascalCaseNameSpace.test(id.node.name)) { + if (isPascalCaseNameSpace.test(id.name)) { return 'Component'; } } return 'Other'; } +function getFunctionIdentifier( + fn: + | NodePath + | NodePath + | NodePath, +): t.Identifier | null { + if (fn.isArrowFunctionExpression()) { + return null; + } + const id = fn.get('id'); + return Array.isArray(id) === false && id.isIdentifier() ? id.node : null; +} + function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { const results = new Map(); const error = new CompilerError(); @@ -188,27 +199,21 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { } else { language = 'typescript'; } + let count = 0; + const withIdentifier = (id: t.Identifier | null): t.Identifier => { + if (id != null && id.name != null) { + return id; + } else { + return t.identifier(`anonymous_${count++}`); + } + }; try { // Extract the first line to quickly check for custom test directives const pragma = source.substring(0, source.indexOf('\n')); const config = parseConfigPragma(pragma); for (const fn of parseFunctions(source, language)) { - if (!fn.isFunctionDeclaration()) { - error.pushErrorDetail( - new CompilerErrorDetail({ - reason: `Unexpected function type ${fn.node.type}`, - description: - 'Playground only supports parsing function declarations', - severity: ErrorSeverity.Todo, - loc: fn.node.loc ?? null, - suggestions: null, - }), - ); - continue; - } - - const id = fn.get('id'); + const id = withIdentifier(getFunctionIdentifier(fn)); for (const result of run( fn, { @@ -221,7 +226,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { null, null, )) { - const fnName = fn.node.id?.name ?? null; + const fnName = id.name; switch (result.kind) { case 'ast': { upsert({ @@ -230,7 +235,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { name: result.name, value: { type: 'FunctionDeclaration', - id: result.value.id, + id, async: result.value.async, generator: result.value.generator, body: result.value.body, From 13ddf1084b4304a60059e3b96fc3c039d23e9432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 16 Aug 2024 19:52:11 -0400 Subject: [PATCH 013/191] [DevTools] Find owners from the parent path that matches the Fiber or ReactComponentInfo (#30717) This enables finding Server Components on the owner path. Server Components aren't stateful so there's not actually one specific owner that it necessarily matches. So it can't be a global look up. E.g. the same Server Component can be rendered in two places or even nested inside each other. Therefore we need to find an appropriate instance using a heuristic. We can do that by traversing the parent path since the owner is likely also a parent. Not always but almost always. To simplify things we can also do the same for Fibers. That brings us one step closer to being able to get rid of the global fiberToFiberInstance map since we can just use the shadow tree to find this information. This does mean that we can't find owners that aren't parents which is usually ok. However, there is a test case that's interesting where you have a React ART tree inside a DOM tree. In that case the owners actually span multiple renderers and roots so the owner is not on the parent stack. Usually this is fine since you'd just care about the owners within React ART but ideally we'd support this. However, I think that really the fix to this is that the React ART tree itself should actually show up inside the DOM tree in DevTools and in the virtual shadow tree because that's conceptually where it belongs. That would then solve this particular issue. We'd just need some way to associate the root with a DOM parent when it gets mounted. --- .../src/__tests__/inspectedElement-test.js | 43 ++-- .../src/backend/fiber/renderer.js | 208 ++++++++++++------ 2 files changed, 159 insertions(+), 92 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index c77013aba3e71..843c6dff9c08f 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -2893,26 +2893,29 @@ describe('InspectedElement', () => { `); const inspectedElement = await inspectElementAtIndex(4); - expect(inspectedElement.owners).toMatchInlineSnapshot(` - [ - { - "compiledWithForget": false, - "displayName": "Child", - "hocDisplayNames": null, - "id": 8, - "key": null, - "type": 5, - }, - { - "compiledWithForget": false, - "displayName": "App", - "hocDisplayNames": null, - "id": 7, - "key": null, - "type": 5, - }, - ] - `); + // TODO: Ideally this should match the owners of the Group but those are + // part of a different parent tree. Ideally the Group would be parent of + // that parent tree though which would fix this issue. + // + // [ + // { + // "compiledWithForget": false, + // "displayName": "Child", + // "hocDisplayNames": null, + // "id": 8, + // "key": null, + // "type": 5, + // }, + // { + // "compiledWithForget": false, + // "displayName": "App", + // "hocDisplayNames": null, + // "id": 7, + // "key": null, + // "type": 5, + // }, + // ] + expect(inspectedElement.owners).toMatchInlineSnapshot(`[]`); }); describe('error boundary', () => { diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index a48b22f647548..62eacdf5dbe9a 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2143,29 +2143,18 @@ export function attach( const {key} = fiber; const displayName = getDisplayNameForFiber(fiber); const elementType = getElementTypeForFiber(fiber); - const debugOwner = fiber._debugOwner; - - // Ideally we should call getFiberIDThrows() for _debugOwner, - // since owners are almost always higher in the tree (and so have already been processed), - // but in some (rare) instances reported in open source, a descendant mounts before an owner. - // Since this is a DEV only field it's probably okay to also just lazily generate and ID here if needed. - // See https://github.com/facebook/react/issues/21445 - let ownerID: number; - if (debugOwner != null) { - if (typeof debugOwner.tag === 'number') { - const ownerFiberInstance = getFiberInstanceUnsafe((debugOwner: any)); - if (ownerFiberInstance !== null) { - ownerID = ownerFiberInstance.id; - } else { - ownerID = 0; - } - } else { - // TODO: Track Server Component Owners. - ownerID = 0; - } - } else { - ownerID = 0; - } + + // Finding the owner instance might require traversing the whole parent path which + // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we + // need it but we have some synchronous operations in the front end like Alt+Left + // which selects the owner immediately. Typically most owners are only a few parents + // away so maybe it's not so bad. + const debugOwner = getUnfilteredOwner(fiber); + const ownerInstance = findNearestOwnerInstance( + parentInstance, + debugOwner, + ); + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; const parentID = parentInstance ? parentInstance.id : 0; const displayNameStringID = getStringID(displayName); @@ -2231,11 +2220,15 @@ export function attach( displayName = env + '(' + displayName + ')'; } const elementType = ElementTypeVirtual; - // TODO: Support Virtual Owners. To do this we need to find a matching - // virtual instance which is not a super cheap parent traversal and so - // we should ideally only do that lazily. We should maybe change the - // frontend to get it lazily. - const ownerID: number = 0; + + // Finding the owner instance might require traversing the whole parent path which + // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we + // need it but we have some synchronous operations in the front end like Alt+Left + // which selects the owner immediately. Typically most owners are only a few parents + // away so maybe it's not so bad. + const debugOwner = getUnfilteredOwner(componentInfo); + const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner); + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; const parentID = parentInstance ? parentInstance.id : 0; const displayNameStringID = getStringID(displayName); @@ -3354,11 +3347,19 @@ export function attach( } function getUpdatersList(root: any): Array | null { - return root.memoizedUpdaters != null - ? Array.from(root.memoizedUpdaters) - .filter(fiber => getFiberIDUnsafe(fiber) !== null) - .map(fiberToSerializedElement) - : null; + const updaters = root.memoizedUpdaters; + if (updaters == null) { + return null; + } + const result = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const updater of updaters) { + const inst = getFiberInstanceUnsafe(updater); + if (inst !== null) { + result.push(instanceToSerializedElement(inst)); + } + } + return result; } function handleCommitFiberUnmount(fiber: any) { @@ -3923,13 +3924,26 @@ export function attach( } } - function fiberToSerializedElement(fiber: Fiber): SerializedElement { - return { - displayName: getDisplayNameForFiber(fiber) || 'Anonymous', - id: getFiberIDThrows(fiber), - key: fiber.key, - type: getElementTypeForFiber(fiber), - }; + function instanceToSerializedElement( + instance: DevToolsInstance, + ): SerializedElement { + if (instance.kind === FIBER_INSTANCE) { + const fiber = instance.data; + return { + displayName: getDisplayNameForFiber(fiber) || 'Anonymous', + id: instance.id, + key: fiber.key, + type: getElementTypeForFiber(fiber), + }; + } else { + const componentInfo = instance.data; + return { + displayName: componentInfo.name || 'Anonymous', + id: instance.id, + key: componentInfo.key == null ? null : componentInfo.key, + type: ElementTypeVirtual, + }; + } } function getOwnersList(id: number): Array | null { @@ -3938,33 +3952,97 @@ export function attach( console.warn(`Could not find DevToolsInstance with id "${id}"`); return null; } - if (devtoolsInstance.kind !== FIBER_INSTANCE) { - // TODO: Handle VirtualInstance. - return null; + const self = instanceToSerializedElement(devtoolsInstance); + const owners = getOwnersListFromInstance(devtoolsInstance); + // This is particular API is prefixed with the current instance too for some reason. + if (owners === null) { + return [self]; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); - if (fiber == null) { + owners.unshift(self); + owners.reverse(); + return owners; + } + + function getOwnersListFromInstance( + instance: DevToolsInstance, + ): Array | null { + let owner = getUnfilteredOwner(instance.data); + if (owner === null) { return null; } + const owners: Array = []; + let parentInstance: null | DevToolsInstance = instance.parent; + while (parentInstance !== null && owner !== null) { + const ownerInstance = findNearestOwnerInstance(parentInstance, owner); + if (ownerInstance !== null) { + owners.push(instanceToSerializedElement(ownerInstance)); + // Get the next owner and keep searching from the previous match. + owner = getUnfilteredOwner(owner); + parentInstance = ownerInstance.parent; + } else { + break; + } + } + return owners; + } - const owners: Array = [fiberToSerializedElement(fiber)]; - - let owner = fiber._debugOwner; - while (owner != null) { + function getUnfilteredOwner( + owner: ReactComponentInfo | Fiber | null | void, + ): ReactComponentInfo | Fiber | null { + if (owner == null) { + return null; + } + if (typeof owner.tag === 'number') { + const ownerFiber: Fiber = (owner: any); // Refined + owner = ownerFiber._debugOwner; + } else { + const ownerInfo: ReactComponentInfo = (owner: any); // Refined + owner = ownerInfo.owner; + } + while (owner) { if (typeof owner.tag === 'number') { const ownerFiber: Fiber = (owner: any); // Refined if (!shouldFilterFiber(ownerFiber)) { - owners.unshift(fiberToSerializedElement(ownerFiber)); + return ownerFiber; } owner = ownerFiber._debugOwner; } else { - // TODO: Track Server Component Owners. - break; + const ownerInfo: ReactComponentInfo = (owner: any); // Refined + if (!shouldFilterVirtual(ownerInfo)) { + return ownerInfo; + } + owner = ownerInfo.owner; } } + return null; + } - return owners; + function findNearestOwnerInstance( + parentInstance: null | DevToolsInstance, + owner: void | null | ReactComponentInfo | Fiber, + ): null | DevToolsInstance { + if (owner == null) { + return null; + } + // Search the parent path for any instance that matches this kind of owner. + while (parentInstance !== null) { + if ( + parentInstance.data === owner || + // Typically both owner and instance.data would refer to the current version of a Fiber + // but it is possible for memoization to ignore the owner on the JSX. Then the new Fiber + // isn't propagated down as the new owner. In that case we might match the alternate + // instead. This is a bit hacky but the fastest check since type casting owner to a Fiber + // needs a duck type check anyway. + parentInstance.data === (owner: any).alternate + ) { + return parentInstance; + } + parentInstance = parentInstance.parent; + } + // It is technically possible to create an element and render it in a different parent + // but this is a weird edge case and it is worth not having to scan the tree or keep + // a register for every fiber/component info. + return null; } // Fast path props lookup for React Native style editor. @@ -4047,7 +4125,6 @@ export function attach( } const { - _debugOwner: debugOwner, stateNode, key, memoizedProps, @@ -4174,21 +4251,8 @@ export function attach( context = {value: context}; } - let owners: null | Array = null; - let owner = debugOwner; - while (owner != null) { - if (typeof owner.tag === 'number') { - const ownerFiber: Fiber = (owner: any); // Refined - if (owners === null) { - owners = []; - } - owners.push(fiberToSerializedElement(ownerFiber)); - owner = ownerFiber._debugOwner; - } else { - // TODO: Track Server Component Owners. - break; - } - } + const owners: null | Array = + getOwnersListFromInstance(fiberInstance); const isTimedOutSuspense = tag === SuspenseComponent && memoizedState !== null; @@ -4352,8 +4416,8 @@ export function attach( displayName = env + '(' + displayName + ')'; } - // TODO: Support Virtual Owners. - const owners: null | Array = null; + const owners: null | Array = + getOwnersListFromInstance(virtualInstance); let rootType = null; let targetErrorBoundaryID = null; From 177b2419b2d8a3d14c3f3304bb7e300985d6f377 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 16 Aug 2024 16:45:03 -0700 Subject: [PATCH 014/191] [compiler] Validate environment config while parsing plugin opts Addresses a todo from a while back. We now validate environment options when parsing the plugin options, which means we can stop re-parsing/validating in later phases. ghstack-source-id: b19806e843e1254716705b33dcf86afb7223f6c7 Pull Request resolved: https://github.com/facebook/react/pull/30726 --- .../src/Entrypoint/Options.ts | 26 +++++++++++++++---- .../src/Entrypoint/Program.ts | 17 +----------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index e97ececc2a137..e966497256511 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -7,8 +7,12 @@ import * as t from '@babel/types'; import {z} from 'zod'; -import {CompilerErrorDetailOptions} from '../CompilerError'; -import {ExternalFunction, PartialEnvironmentConfig} from '../HIR/Environment'; +import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; +import { + EnvironmentConfig, + ExternalFunction, + parseEnvironmentConfig, +} from '../HIR/Environment'; import {hasOwnProperty} from '../Utils/utils'; const PanicThresholdOptionsSchema = z.enum([ @@ -32,7 +36,7 @@ const PanicThresholdOptionsSchema = z.enum([ export type PanicThresholdOptions = z.infer; export type PluginOptions = { - environment: PartialEnvironmentConfig | null; + environment: EnvironmentConfig; logger: Logger | null; @@ -194,7 +198,7 @@ export type Logger = { export const defaultOptions: PluginOptions = { compilationMode: 'infer', panicThreshold: 'none', - environment: {}, + environment: parseEnvironmentConfig({}).unwrap(), logger: null, gating: null, noEmit: false, @@ -218,7 +222,19 @@ export function parsePluginOptions(obj: unknown): PluginOptions { // normalize string configs to be case insensitive value = value.toLowerCase(); } - if (isCompilerFlag(key)) { + if (key === 'environment') { + const environmentResult = parseEnvironmentConfig(value); + if (environmentResult.isErr()) { + CompilerError.throwInvalidConfig({ + reason: + 'Error in validating environment config. This is an advanced setting and not meant to be used directly', + description: environmentResult.unwrapErr().toString(), + suggestions: null, + loc: null, + }); + } + parsedOptions[key] = environmentResult.unwrap(); + } else if (isCompilerFlag(key)) { parsedOptions[key] = value; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 99ec1e04a65e4..979e9f88d1b57 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -16,7 +16,6 @@ import { EnvironmentConfig, ExternalFunction, ReactFunctionType, - parseEnvironmentConfig, tryParseExternalFunction, } from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; @@ -292,21 +291,7 @@ export function compileProgram( return; } - /* - * TODO(lauren): Remove pass.opts.environment nullcheck once PluginOptions - * is validated - */ - const environmentResult = parseEnvironmentConfig(pass.opts.environment ?? {}); - if (environmentResult.isErr()) { - CompilerError.throwInvalidConfig({ - reason: - 'Error in validating environment config. This is an advanced setting and not meant to be used directly', - description: environmentResult.unwrapErr().toString(), - suggestions: null, - loc: null, - }); - } - const environment = environmentResult.unwrap(); + const environment = pass.opts.environment; const restrictedImportsErr = validateRestrictedImports(program, environment); if (restrictedImportsErr) { handleError(restrictedImportsErr, pass, null); From 7954db9398b9afa962167577a6c6940be3856c39 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 16 Aug 2024 18:29:18 -0700 Subject: [PATCH 015/191] [Fizz] handle throwing after abort during render (#30730) It is possible to throw after aborting during a render and we were not properly tracking this. We use an AbortSigil to mark whether a rendering task needs to abort but the throw interrupts that and we end up handling an error on the error pathway instead. This change reworks the abort-while-rendering support to be robust to throws after calling abort --- .../src/__tests__/ReactDOMFizzServer-test.js | 42 +++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 22 +++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index e9abb25eb1768..5d70a0c71ea4d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -8377,6 +8377,48 @@ describe('ReactDOMFizzServer', () => { ); }); + it('can support throwing after aborting during a render', async () => { + function App() { + return ( +
+ loading...

}> + +
+
+ ); + } + + function ComponentThatAborts() { + abortRef.current('boom'); + throw new Error('bam'); + } + + const abortRef = {current: null}; + let finished = false; + const errors = []; + await act(() => { + const {pipe, abort} = renderToPipeableStream(, { + onError(err) { + errors.push(err); + }, + }); + abortRef.current = abort; + writable.on('finish', () => { + finished = true; + }); + pipe(writable); + }); + + expect(errors).toEqual(['boom']); + + expect(finished).toBe(true); + expect(getVisibleChildren(container)).toEqual( +
+

loading...

+
, + ); + }); + it('should warn for using generators as children props', async () => { function* getChildren() { yield

Hello

; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index a6c77f8bafb7f..986e8673a5a2a 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -652,8 +652,6 @@ export function resumeRequest( return request; } -const AbortSigil = {}; - let currentRequest: null | Request = null; export function resolveRequest(): null | Request { @@ -1173,7 +1171,7 @@ function renderSuspenseBoundary( ); boundarySegment.status = COMPLETED; } catch (thrownValue: mixed) { - if (thrownValue === AbortSigil) { + if (request.status === ABORTING) { boundarySegment.status = ABORTED; } else { boundarySegment.status = ERRORED; @@ -1246,7 +1244,7 @@ function renderSuspenseBoundary( } catch (thrownValue: mixed) { newBoundary.status = CLIENT_RENDERED; let error: mixed; - if (thrownValue === AbortSigil) { + if (request.status === ABORTING) { contentRootSegment.status = ABORTED; error = request.fatalError; } else { @@ -1601,7 +1599,8 @@ function finishClassComponent( nextChildren = instance.render(); } if (request.status === ABORTING) { - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } if (__DEV__) { @@ -1757,7 +1756,8 @@ function renderFunctionComponent( legacyContext, ); if (request.status === ABORTING) { - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } const hasId = checkDidRenderIdHook(); @@ -2076,7 +2076,8 @@ function renderLazyComponent( Component = init(payload); } if (request.status === ABORTING) { - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } const resolvedProps = resolveDefaultPropsOnNonClassComponent( Component, @@ -2655,7 +2656,8 @@ function retryNode(request: Request, task: Task): void { resolvedNode = init(payload); } if (request.status === ABORTING) { - throw AbortSigil; + // eslint-disable-next-line no-throw-literal + throw null; } // Now we render the resolved node renderNodeDestructive(request, task, resolvedNode, childIndex); @@ -4127,7 +4129,7 @@ function retryRenderTask( // (unstable) API for suspending. This implementation detail can change // later, once we deprecate the old API in favor of `use`. getSuspendedThenable() - : thrownValue === AbortSigil + : request.status === ABORTING ? request.fatalError : thrownValue; @@ -4250,7 +4252,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { erroredReplay( request, task.blockedBoundary, - x === AbortSigil ? request.fatalError : x, + request.status === ABORTING ? request.fatalError : x, errorInfo, task.replay.nodes, task.replay.slots, From 6ebfd5b0829c3e7a977ef4d9a0bd96436c681251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sun, 18 Aug 2024 12:31:45 -0400 Subject: [PATCH 016/191] [Flight] Source Map Server Actions to their Server Location (#30741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses a similar technique to what we use to generate fake stack frames for server components. This generates an eval:ed wrapper function around the Server Reference proxy we create on the client. This wrapper function gets the original `name` of the action on the server and I also add a source map if `findSourceMapURL` is defined that points back to the source of the server function. For `"use server"` on the server, there's no new API. It just uses the callsite of `registerServerReference()` on the Server. We can infer the function name from the actual function on the server and we already have the `findSourceMapURL` on the client receiving it. For `"use server"` imported from the client, there's two new options added to `createServerReference()` (in addition to the optional [`encodeFormAction`](#27563)). These are only used in DEV mode. The [`findSourceMapURL`](#29708) option is the same one added in #29708. We need to pass this these references aren't created in the context of any specific request but globally. The other weird thing about this case is that this is actually a case where the compiled environment is the client so any source maps are the same as for the client layer, so the environment name here is just `"Client"`. ```diff createServerReference( id: string, callServer: CallServerCallback, encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only + functionName?: string, // DEV-only ) ``` The key is that we use the location of the `registerServerReference()`/`createServerReference()` call as the location of the function. A compiler can either emit those at the same locations as the original functions or use source maps to have those segments refer to the original location of the function (or in the case of a re-export the original location of the re-export is also a fine approximate). The compiled output must call these directly without a wrapper function because the wrapper adds a stack frame. I decided against complicated and fragile dev-only options to skip n number of frames that would just end up in prod code. The implementation just skips one frame - our own. Otherwise it'll just point all source mapping to the wrapper. We don't have a `"use server"` imported from the client implementation in the reference implementation/fixture so it's a bit tricky to test that. In the case of CJS on the server, we just use a runtime instead of compiler so it's tricky to source map those appropriately. We can implement it for ESM on the server which is the main thing we're testing in the fixture. It's easier in a real implementation where all the compilation is just one pass. It's a little tricky since we have to parse and append to other source maps but I'd like to do that as a follow up. Or maybe that's just an exercise for the reader. You can right click an action and click "Go to Definition". Screenshot 2024-08-17 at 6 04 27 PM For now they simply don't point to the right place but you can still jump to the right file in the fixture: Screenshot 2024-08-17 at 5 58 40 PM In Firefox/Safari given that the location doesn't exist in the source map yet, the browser refuses to open the file. Where as Chrome does nearest (last) line. --- .../react-client/src/ReactFlightClient.js | 39 ++- .../src/ReactFlightReplyClient.js | 248 +++++++++++++++++- .../src/ReactFlightESMReferences.js | 33 ++- .../ReactFlightServerConfigESMBundler.js | 7 + .../src/ReactFlightTurbopackReferences.js | 36 ++- ...ReactFlightServerConfigTurbopackBundler.js | 7 + .../src/ReactFlightWebpackReferences.js | 36 ++- .../__tests__/ReactFlightDOMBrowser-test.js | 12 + .../ReactFlightServerConfigWebpackBundler.js | 7 + .../react-server/src/ReactFlightServer.js | 42 ++- .../ReactFlightServerConfigBundlerCustom.js | 1 + .../forks/ReactFlightServerConfig.markup.js | 7 + 12 files changed, 418 insertions(+), 57 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 564f4e859871f..c386d503bfe6b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -13,6 +13,7 @@ import type { ReactComponentInfo, ReactAsyncInfo, ReactStackTrace, + ReactCallSite, } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; @@ -59,7 +60,7 @@ import { bindToConsole, } from './ReactFlightClientConfig'; -import {registerServerReference} from './ReactFlightReplyClient'; +import {createBoundServerReference} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; @@ -1001,30 +1002,20 @@ function waitForReference( function createServerReferenceProxy, T>( response: Response, - metaData: {id: any, bound: null | Thenable>}, + metaData: { + id: any, + bound: null | Thenable>, + name?: string, // DEV-only + env?: string, // DEV-only + location?: ReactCallSite, // DEV-only + }, ): (...A) => Promise { - const callServer = response._callServer; - const proxy = function (): Promise { - // $FlowFixMe[method-unbinding] - const args = Array.prototype.slice.call(arguments); - const p = metaData.bound; - if (!p) { - return callServer(metaData.id, args); - } - if (p.status === INITIALIZED) { - const bound = p.value; - return callServer(metaData.id, bound.concat(args)); - } - // Since this is a fake Promise whose .then doesn't chain, we have to wrap it. - // TODO: Remove the wrapper once that's fixed. - return ((Promise.resolve(p): any): Promise>).then( - function (bound) { - return callServer(metaData.id, bound.concat(args)); - }, - ); - }; - registerServerReference(proxy, metaData, response._encodeFormAction); - return proxy; + return createBoundServerReference( + metaData, + response._callServer, + response._encodeFormAction, + __DEV__ ? response._debugFindSourceMapURL : undefined, + ); } function getOutlinedModel( diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index c4033a999d96d..233f51844e2a3 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -13,6 +13,7 @@ import type { FulfilledThenable, RejectedThenable, ReactCustomFormAction, + ReactCallSite, } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; @@ -1023,7 +1024,99 @@ function isSignatureEqual( } } -export function registerServerReference( +let fakeServerFunctionIdx = 0; + +function createFakeServerFunction, T>( + name: string, + filename: string, + sourceMap: null | string, + line: number, + col: number, + environmentName: string, + innerFunction: (...A) => Promise, +): (...A) => Promise { + // This creates a fake copy of a Server Module. It represents the Server Action on the server. + // We use an eval so we can source map it to the original location. + + const comment = + '/* This module is a proxy to a Server Action. Turn on Source Maps to see the server source. */'; + + if (!name) { + // An eval:ed function with no name gets the name "eval". We give it something more descriptive. + name = ''; + } + const encodedName = JSON.stringify(name); + // We generate code where both the beginning of the function and its parenthesis is at the line + // and column of the server executed code. We use a method form since that lets us name it + // anything we want and because the beginning of the function and its parenthesis is the same + // column. Because Chrome inspects the location of the parenthesis and Firefox inspects the + // location of the beginning of the function. By not using a function expression we avoid the + // ambiguity. + let code; + if (line <= 1) { + const minSize = encodedName.length + 7; + code = + 's=>({' + + encodedName + + ' '.repeat(col < minSize ? 0 : col - minSize) + + ':' + + '(...args) => s(...args)' + + '})\n' + + comment; + } else { + code = + comment + + '\n'.repeat(line - 2) + + 'server=>({' + + encodedName + + ':\n' + + ' '.repeat(col < 1 ? 0 : col - 1) + + // The function body can get printed so we make it look nice. + // This "calls the server with the arguments". + '(...args) => server(...args)' + + '})'; + } + + if (filename.startsWith('/')) { + // If the filename starts with `/` we assume that it is a file system file + // rather than relative to the current host. Since on the server fully qualified + // stack traces use the file path. + // TODO: What does this look like on Windows? + filename = 'file://' + filename; + } + + if (sourceMap) { + // We use the prefix rsc://React/ to separate these from other files listed in + // the Chrome DevTools. We need a "host name" and not just a protocol because + // otherwise the group name becomes the root folder. Ideally we don't want to + // show these at all but there's two reasons to assign a fake URL. + // 1) A printed stack trace string needs a unique URL to be able to source map it. + // 2) If source maps are disabled or fails, you should at least be able to tell + // which file it was. + code += + '\n//# sourceURL=rsc://React/' + + encodeURIComponent(environmentName) + + '/' + + filename + + '?s' + // We add an extra s here to distinguish from the fake stack frames + fakeServerFunctionIdx++; + code += '\n//# sourceMappingURL=' + sourceMap; + } else if (filename) { + code += '\n//# sourceURL=' + filename; + } + + try { + // Eval a factory and then call it to create a closure over the inner function. + // eslint-disable-next-line no-eval + return (0, eval)(code)(innerFunction)[name]; + } catch (x) { + // If eval fails, such as if in an environment that doesn't support it, + // we fallback to just returning the inner function. + return innerFunction; + } +} + +function registerServerReference( proxy: any, reference: {id: ServerReferenceId, bound: null | Thenable>}, encodeFormAction: void | EncodeFormActionCallback, @@ -1098,16 +1191,163 @@ function bind(this: Function): Function { return newFn; } +export type FindSourceMapURLCallback = ( + fileName: string, + environmentName: string, +) => null | string; + +export function createBoundServerReference, T>( + metaData: { + id: ServerReferenceId, + bound: null | Thenable>, + name?: string, // DEV-only + env?: string, // DEV-only + location?: ReactCallSite, // DEV-only + }, + callServer: CallServerCallback, + encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only +): (...A) => Promise { + const id = metaData.id; + const bound = metaData.bound; + let action = function (): Promise { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments); + const p = bound; + if (!p) { + return callServer(id, args); + } + if (p.status === 'fulfilled') { + const boundArgs = p.value; + return callServer(id, boundArgs.concat(args)); + } + // Since this is a fake Promise whose .then doesn't chain, we have to wrap it. + // TODO: Remove the wrapper once that's fixed. + return ((Promise.resolve(p): any): Promise>).then( + function (boundArgs) { + return callServer(id, boundArgs.concat(args)); + }, + ); + }; + if (__DEV__) { + const location = metaData.location; + if (location) { + const functionName = metaData.name || ''; + const [, filename, line, col] = location; + const env = metaData.env || 'Server'; + const sourceMap = + findSourceMapURL == null ? null : findSourceMapURL(filename, env); + action = createFakeServerFunction( + functionName, + filename, + sourceMap, + line, + col, + env, + action, + ); + } + } + registerServerReference(action, {id, bound}, encodeFormAction); + return action; +} + +// This matches either of these V8 formats. +// at name (filename:0:0) +// at filename:0:0 +// at async filename:0:0 +const v8FrameRegExp = + /^ {3} at (?:(.+) \((.+):(\d+):(\d+)\)|(?:async )?(.+):(\d+):(\d+))$/; +// This matches either of these JSC/SpiderMonkey formats. +// name@filename:0:0 +// filename:0:0 +const jscSpiderMonkeyFrameRegExp = /(?:(.*)@)?(.*):(\d+):(\d+)/; + +function parseStackLocation(error: Error): null | ReactCallSite { + // This parsing is special in that we know that the calling function will always + // be a module that initializes the server action. We also need this part to work + // cross-browser so not worth a Config. It's DEV only so not super code size + // sensitive but also a non-essential feature. + let stack = error.stack; + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + const endOfFirst = stack.indexOf('\n'); + let secondFrame; + if (endOfFirst !== -1) { + // Skip the first frame. + const endOfSecond = stack.indexOf('\n', endOfFirst + 1); + if (endOfSecond === -1) { + secondFrame = stack.slice(endOfFirst + 1); + } else { + secondFrame = stack.slice(endOfFirst + 1, endOfSecond); + } + } else { + secondFrame = stack; + } + + let parsed = v8FrameRegExp.exec(secondFrame); + if (!parsed) { + parsed = jscSpiderMonkeyFrameRegExp.exec(secondFrame); + if (!parsed) { + return null; + } + } + + let name = parsed[1] || ''; + if (name === '') { + name = ''; + } + let filename = parsed[2] || parsed[5] || ''; + if (filename === '') { + filename = ''; + } + const line = +(parsed[3] || parsed[6]); + const col = +(parsed[4] || parsed[7]); + + return [name, filename, line, col]; +} + export function createServerReference, T>( id: ServerReferenceId, callServer: CallServerCallback, encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, // DEV-only + functionName?: string, ): (...A) => Promise { - const proxy = function (): Promise { + let action = function (): Promise { // $FlowFixMe[method-unbinding] const args = Array.prototype.slice.call(arguments); return callServer(id, args); }; - registerServerReference(proxy, {id, bound: null}, encodeFormAction); - return proxy; + if (__DEV__) { + // Let's see if we can find a source map for the file which contained the + // server action. We extract it from the runtime so that it's resilient to + // multiple passes of compilation as long as we can find the final source map. + const location = parseStackLocation(new Error('react-stack-top-frame')); + if (location !== null) { + const [, filename, line, col] = location; + // While the environment that the Server Reference points to can be + // in any environment, what matters here is where the compiled source + // is from and that's in the currently executing environment. We hard + // code that as the value "Client" in case the findSourceMapURL helper + // needs it. + const env = 'Client'; + const sourceMap = + findSourceMapURL == null ? null : findSourceMapURL(filename, env); + action = createFakeServerFunction( + functionName || '', + filename, + sourceMap, + line, + col, + env, + action, + ); + } + } + registerServerReference(action, {id, bound: null}, encodeFormAction); + return action; } diff --git a/packages/react-server-dom-esm/src/ReactFlightESMReferences.js b/packages/react-server-dom-esm/src/ReactFlightESMReferences.js index cd3dd349157a5..86a25c7c8608a 100644 --- a/packages/react-server-dom-esm/src/ReactFlightESMReferences.js +++ b/packages/react-server-dom-esm/src/ReactFlightESMReferences.js @@ -13,6 +13,7 @@ export type ServerReference = T & { $$typeof: symbol, $$id: string, $$bound: null | Array, + $$location?: Error, }; // eslint-disable-next-line no-unused-vars @@ -68,10 +69,30 @@ export function registerServerReference( id: string, exportName: string, ): ServerReference { - return Object.defineProperties((reference: any), { - $$typeof: {value: SERVER_REFERENCE_TAG}, - $$id: {value: id + '#' + exportName, configurable: true}, - $$bound: {value: null, configurable: true}, - bind: {value: bind, configurable: true}, - }); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }, + ); } diff --git a/packages/react-server-dom-esm/src/server/ReactFlightServerConfigESMBundler.js b/packages/react-server-dom-esm/src/server/ReactFlightServerConfigESMBundler.js index cba5223bcf7af..5e789518b40cb 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightServerConfigESMBundler.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightServerConfigESMBundler.js @@ -70,3 +70,10 @@ export function getServerReferenceBoundArguments( ): null | Array { return serverReference.$$bound; } + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js index ecf6a35dfa6ef..613a7e7dc862c 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js @@ -13,6 +13,7 @@ export type ServerReference = T & { $$typeof: symbol, $$id: string, $$bound: null | Array, + $$location?: Error, }; // eslint-disable-next-line no-unused-vars @@ -81,15 +82,32 @@ export function registerServerReference( id: string, exportName: null | string, ): ServerReference { - return Object.defineProperties((reference: any), { - $$typeof: {value: SERVER_REFERENCE_TAG}, - $$id: { - value: exportName === null ? id : id + '#' + exportName, - configurable: true, - }, - $$bound: {value: null, configurable: true}, - bind: {value: bind, configurable: true}, - }); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: exportName === null ? id : id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }, + ); } const PROMISE_PROTOTYPE = Promise.prototype; diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js index 2ebc5d3f10421..d8224aff341dc 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js @@ -91,3 +91,10 @@ export function getServerReferenceBoundArguments( ): null | Array { return serverReference.$$bound; } + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js index 6d14f412063c1..025a7368213f8 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js @@ -13,6 +13,7 @@ export type ServerReference = T & { $$typeof: symbol, $$id: string, $$bound: null | Array, + $$location?: Error, }; // eslint-disable-next-line no-unused-vars @@ -89,15 +90,32 @@ export function registerServerReference( id: string, exportName: null | string, ): ServerReference { - return Object.defineProperties((reference: any), { - $$typeof: {value: SERVER_REFERENCE_TAG}, - $$id: { - value: exportName === null ? id : id + '#' + exportName, - configurable: true, - }, - $$bound: {value: null, configurable: true}, - bind: {value: bind, configurable: true}, - }); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: exportName === null ? id : id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }, + ); } const PROMISE_PROTOTYPE = Promise.prototype; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 7bbfea1484bed..969f9e125e8c5 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -1393,9 +1393,21 @@ describe('ReactFlightDOMBrowser', () => { const body = await ReactServerDOMClient.encodeReply(args); return callServer(ref, body); }, + undefined, + undefined, + 'upper', ), }; + expect(ServerModuleBImportedOnClient.upper.name).toBe( + __DEV__ ? 'upper' : 'action', + ); + if (__DEV__) { + expect(ServerModuleBImportedOnClient.upper.toString()).toBe( + '(...args) => server(...args)', + ); + } + function Client({action}) { // Client side pass a Server Reference into an action. actionProxy = text => action(ServerModuleBImportedOnClient.upper, text); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js index a0872b61fa475..d29516ff946ea 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js @@ -91,3 +91,10 @@ export function getServerReferenceBoundArguments( ): null | Array { return serverReference.$$bound; } + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index db0ee7b3cebad..bfb12c31c7ffc 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -64,6 +64,7 @@ import type { ReactComponentInfo, ReactAsyncInfo, ReactStackTrace, + ReactCallSite, } from 'shared/ReactTypes'; import type {ReactElement} from 'shared/ReactElementType'; import type {LazyComponent} from 'react/src/ReactLazy'; @@ -72,6 +73,7 @@ import { resolveClientReferenceMetadata, getServerReferenceId, getServerReferenceBoundArguments, + getServerReferenceLocation, getClientReferenceKey, isClientReference, isServerReference, @@ -1955,17 +1957,47 @@ function serializeServerReference( return serializeServerReferenceID(existingId); } - const bound: null | Array = getServerReferenceBoundArguments( + const boundArgs: null | Array = getServerReferenceBoundArguments( request.bundlerConfig, serverReference, ); + const bound = boundArgs === null ? null : Promise.resolve(boundArgs); + const id = getServerReferenceId(request.bundlerConfig, serverReference); + + let location: null | ReactCallSite = null; + if (__DEV__) { + const error = getServerReferenceLocation( + request.bundlerConfig, + serverReference, + ); + if (error) { + const frames = parseStackTrace(error, 1); + if (frames.length > 0) { + location = frames[0]; + } + } + } + const serverReferenceMetadata: { id: ServerReferenceId, bound: null | Promise>, - } = { - id: getServerReferenceId(request.bundlerConfig, serverReference), - bound: bound ? Promise.resolve(bound) : null, - }; + name?: string, // DEV-only + env?: string, // DEV-only + location?: ReactCallSite, // DEV-only + } = + __DEV__ && location !== null + ? { + id, + bound, + name: + typeof serverReference === 'function' ? serverReference.name : '', + env: (0, request.environmentName)(), + location, + } + : { + id, + bound, + }; const metadataId = outlineModel(request, serverReferenceMetadata); writtenServerReferences.set(serverReference, metadataId); return serializeServerReferenceID(metadataId); diff --git a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js index 00578a4da2459..ac3e17c630174 100644 --- a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js +++ b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js @@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata = export const getServerReferenceId = $$$config.getServerReferenceId; export const getServerReferenceBoundArguments = $$$config.getServerReferenceBoundArguments; +export const getServerReferenceLocation = $$$config.getServerReferenceLocation; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.markup.js b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js index 12feb37ac5697..7c879de39f271 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.markup.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js @@ -90,3 +90,10 @@ export function getServerReferenceBoundArguments( 'Use a fixed URL for any forms instead.', ); } + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void { + return undefined; +} From d2413bf377e7f73661b0700aeb95d07fb2911efc Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 16 Aug 2024 17:05:29 -0700 Subject: [PATCH 017/191] [compiler] Validate against JSX in try statements Per comments on the new validation pass, this disallows creating JSX (expression/fragment) within a try statement. Developers sometimes use this pattern thinking that they can catch errors during the rendering of the element, without realizing that rendering is lazy. The validation allows us to teach developers about the error boundary pattern. ghstack-source-id: 0bc722aeaed426ddd40e075c008f0ff2576e0c33 Pull Request resolved: https://github.com/facebook/react/pull/30725 --- .../src/Entrypoint/Pipeline.ts | 5 ++ .../src/HIR/Environment.ts | 6 ++ .../Validation/ValidateNoJSXInTryStatement.ts | 52 +++++++++++++++++ ...in-catch-in-outer-try-with-catch.expect.md | 38 +++++++++++++ ...id-jsx-in-catch-in-outer-try-with-catch.js | 17 ++++++ ...or.invalid-jsx-in-try-with-catch.expect.md | 31 ++++++++++ .../error.invalid-jsx-in-try-with-catch.js | 10 ++++ ...-catch-in-outer-try-with-finally.expect.md | 56 +++++++++++++++++++ ...-jsx-in-catch-in-outer-try-with-finally.js | 17 ++++++ ...-invalid-jsx-in-try-with-finally.expect.md | 39 +++++++++++++ ...or.todo-invalid-jsx-in-try-with-finally.js | 10 ++++ 11 files changed, 281 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 590aba2fdc0c4..8307e8817b4f9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -105,6 +105,7 @@ import {outlineFunctions} from '../Optimization/OutlineFunctions'; import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {lowerContextAccess} from '../Optimization/LowerContextAccess'; import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; +import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -249,6 +250,10 @@ function* runWithEnvironment( validateNoSetStateInPassiveEffects(hir); } + if (env.config.validateNoJSXInTryStatements) { + validateNoJSXInTryStatement(hir); + } + inferReactivePlaces(hir); yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir}); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index ca03b8a7b1e39..a5614ac244a14 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -237,6 +237,12 @@ const EnvironmentConfigSchema = z.object({ */ validateNoSetStateInPassiveEffects: z.boolean().default(false), + /** + * Validates against creating JSX within a try block and recommends using an error boundary + * instead. + */ + validateNoJSXInTryStatements: z.boolean().default(false), + /** * Validates that the dependencies of all effect hooks are memoized. This helps ensure * that Forget does not introduce infinite renders caused by a dependency changing, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts new file mode 100644 index 0000000000000..b92a89d764301 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts @@ -0,0 +1,52 @@ +/** + * 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. + */ + +import {CompilerError, ErrorSeverity} from '..'; +import {BlockId, HIRFunction} from '../HIR'; +import {retainWhere} from '../Utils/utils'; + +/** + * Developers may not be aware of error boundaries and lazy evaluation of JSX, leading them + * to use patterns such as `let el; try { el = } catch { ... }` to attempt to + * catch rendering errors. Such code will fail to catch errors in rendering, but developers + * may not realize this right away. + * + * This validation pass validates against this pattern: specifically, it errors for JSX + * created within a try block. JSX is allowed within a catch statement, unless that catch + * is itself nested inside an outer try. + */ +export function validateNoJSXInTryStatement(fn: HIRFunction): void { + const activeTryBlocks: Array = []; + const errors = new CompilerError(); + for (const [, block] of fn.body.blocks) { + retainWhere(activeTryBlocks, id => id !== block.id); + + if (activeTryBlocks.length !== 0) { + for (const instr of block.instructions) { + const {value} = instr; + switch (value.kind) { + case 'JsxExpression': + case 'JsxFragment': { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`, + loc: value.loc, + }); + break; + } + } + } + } + + if (block.terminal.kind === 'try') { + activeTryBlocks.push(block.terminal.handler); + } + } + if (errors.hasErrors()) { + throw errors; + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.expect.md new file mode 100644 index 0000000000000..40cebff89a75e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @validateNoJSXInTryStatements +import {identity} from 'shared-runtime'; + +function Component(props) { + let el; + try { + let value; + try { + value = identity(props.foo); + } catch { + el =
; + } + } catch { + return null; + } + return el; +} + +``` + + +## Error + +``` + 9 | value = identity(props.foo); + 10 | } catch { +> 11 | el =
; + | ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) (11:11) + 12 | } + 13 | } catch { + 14 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.js new file mode 100644 index 0000000000000..0935a1a63cd83 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-catch-in-outer-try-with-catch.js @@ -0,0 +1,17 @@ +// @validateNoJSXInTryStatements +import {identity} from 'shared-runtime'; + +function Component(props) { + let el; + try { + let value; + try { + value = identity(props.foo); + } catch { + el =
; + } + } catch { + return null; + } + return el; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.expect.md new file mode 100644 index 0000000000000..ee1f5335ef624 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +// @validateNoJSXInTryStatements +function Component(props) { + let el; + try { + el =
; + } catch { + return null; + } + return el; +} + +``` + + +## Error + +``` + 3 | let el; + 4 | try { +> 5 | el =
; + | ^^^^^^^ InvalidReact: Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) (5:5) + 6 | } catch { + 7 | return null; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.js new file mode 100644 index 0000000000000..3e7747c875b3c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-in-try-with-catch.js @@ -0,0 +1,10 @@ +// @validateNoJSXInTryStatements +function Component(props) { + let el; + try { + el =
; + } catch { + return null; + } + return el; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.expect.md new file mode 100644 index 0000000000000..a7ea7b7739c6a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @validateNoJSXInTryStatements +import {identity} from 'shared-runtime'; + +function Component(props) { + let el; + try { + let value; + try { + value = identity(props.foo); + } catch { + el =
; + } + } finally { + console.log(el); + } + return el; +} + +``` + + +## Error + +``` + 4 | function Component(props) { + 5 | let el; +> 6 | try { + | ^^^^^ +> 7 | let value; + | ^^^^^^^^^^^^^^ +> 8 | try { + | ^^^^^^^^^^^^^^ +> 9 | value = identity(props.foo); + | ^^^^^^^^^^^^^^ +> 10 | } catch { + | ^^^^^^^^^^^^^^ +> 11 | el =
; + | ^^^^^^^^^^^^^^ +> 12 | } + | ^^^^^^^^^^^^^^ +> 13 | } finally { + | ^^^^^^^^^^^^^^ +> 14 | console.log(el); + | ^^^^^^^^^^^^^^ +> 15 | } + | ^^^^ Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (6:15) + 16 | return el; + 17 | } + 18 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js new file mode 100644 index 0000000000000..9db091a2fb7ed --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js @@ -0,0 +1,17 @@ +// @validateNoJSXInTryStatements +import {identity} from 'shared-runtime'; + +function Component(props) { + let el; + try { + let value; + try { + value = identity(props.foo); + } catch { + el =
; + } + } finally { + console.log(el); + } + return el; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.expect.md new file mode 100644 index 0000000000000..a6a85d4519bcb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @validateNoJSXInTryStatements +function Component(props) { + let el; + try { + el =
; + } finally { + console.log(el); + } + return el; +} + +``` + + +## Error + +``` + 2 | function Component(props) { + 3 | let el; +> 4 | try { + | ^^^^^ +> 5 | el =
; + | ^^^^^^^^^^^^^^^^^ +> 6 | } finally { + | ^^^^^^^^^^^^^^^^^ +> 7 | console.log(el); + | ^^^^^^^^^^^^^^^^^ +> 8 | } + | ^^^^ Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (4:8) + 9 | return el; + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.js new file mode 100644 index 0000000000000..f0a17391c0eef --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.js @@ -0,0 +1,10 @@ +// @validateNoJSXInTryStatements +function Component(props) { + let el; + try { + el =
; + } finally { + console.log(el); + } + return el; +} From 9d082b550086e6be5f54872d518efa14303491db Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 19 Aug 2024 11:24:41 -0700 Subject: [PATCH 018/191] [Flight] model halted references explicitly (#30731) using infinitely suspending promises isn't right because this will parse as a promise which is only appropriate if the value we're halting at is a promise. Instead we need to have a special marker type that says this reference will never resolve. Additionally flight client needs to not error any halted references when the stream closes because they will otherwise appear as an error addresses: https://github.com/facebook/react/pull/30705#discussion_r1720479974 --- .../react-client/src/ReactFlightClient.js | 23 ++++ .../src/__tests__/ReactFlightDOM-test.js | 101 ++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 49 ++++----- 3 files changed, 149 insertions(+), 24 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index c386d503bfe6b..68d4ac2cd936c 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -46,6 +46,7 @@ import { enableRefAsProp, enableFlightReadableStream, enableOwnerStacks, + enableHalt, } from 'shared/ReactFeatureFlags'; import { @@ -1986,6 +1987,20 @@ function resolvePostponeDev( } } +function resolveBlocked(response: Response, id: number): void { + const chunks = response._chunks; + const chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, createBlockedChunk(response)); + } else if (chunk.status === PENDING) { + // This chunk as contructed via other means but it is actually a blocked chunk + // so we update it here. We check the status because it might have been aborted + // before we attempted to resolve it. + const blockedChunk: BlockedChunk = (chunk: any); + blockedChunk.status = BLOCKED; + } +} + function resolveHint( response: Response, code: Code, @@ -2612,6 +2627,13 @@ function processFullStringRow( } } // Fallthrough + case 35 /* "#" */: { + if (enableHalt) { + resolveBlocked(response, id); + return; + } + } + // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { // We assume anything else is JSON. resolveModel(response, id, row); @@ -2668,6 +2690,7 @@ export function processBinaryChunk( i++; } else if ( (resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ || + resolvedRowTag === 35 /* "#" */ || resolvedRowTag === 114 /* "r" */ || resolvedRowTag === 120 /* "x" */ ) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 6a0ce0152b704..97fce8a8ea11d 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2856,4 +2856,105 @@ describe('ReactFlightDOM', () => { jest.advanceTimersByTime('100'); expect(await race).toBe('timeout'); }); + + // @gate enableHalt + it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => { + const controller = new AbortController(); + function ComponentThatAborts() { + controller.abort(); + return null; + } + + async function Greeting() { + await 1; + return 'hello world'; + } + + async function Farewell() { + return 'goodbye world'; + } + + async function Wrapper() { + return ( + + + + ); + } + + function App() { + return ( +
+ + + + + + + +
+ ); + } + + const errors = []; + const {pendingResult} = await serverAct(() => { + return { + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream( + , + {}, + { + onError(x) { + errors.push(x); + }, + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + + const {prelude} = await pendingResult; + expect(errors).toEqual([]); + + const response = ReactServerDOMClient.createFromReadableStream( + Readable.toWeb(prelude), + ); + + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + function ClientApp() { + return use(response); + } + let abortFizz; + await serverAct(async () => { + const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( + React.createElement(ClientApp), + { + onError(error, errorInfo) { + errors.push(error); + }, + }, + ); + pipe(fizzWritable); + abortFizz = abort; + }); + + await serverAct(() => { + abortFizz('boom'); + }); + + // one error per boundary + expect(errors).toEqual(['boom', 'boom', 'boom']); + + const container = document.createElement('div'); + await readInto(container, fizzReadable); + expect(getMeaningfulChildren(container)).toEqual( +
+ {'loading...'} + {'loading too...'} + {'loading three...'} +
, + ); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index bfb12c31c7ffc..c9fe9e3434bb7 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -617,7 +617,7 @@ function serializeThenable( request.abortableTasks.delete(newTask); newTask.status = ABORTED; if (enableHalt && request.fatalError === haltSymbol) { - emitModelChunk(request, newTask.id, reusableInfinitePromiseModel); + emitBlockedChunk(request, newTask.id); } else { const errorId: number = (request.fatalError: any); const model = stringify(serializeByValueID(errorId)); @@ -1820,7 +1820,6 @@ function serializeLazyID(id: number): string { function serializeInfinitePromise(): string { return '$@'; } -const reusableInfinitePromiseModel = stringify(serializeInfinitePromise()); function serializePromiseID(id: number): string { return '$@' + id.toString(16); @@ -2208,9 +2207,6 @@ function renderModel( if (typeof x.then === 'function') { if (request.status === ABORTING) { task.status = ABORTED; - if (enableHalt && request.fatalError === haltSymbol) { - return serializeInfinitePromise(); - } const errorId: number = (request.fatalError: any); if (wasReactNode) { return serializeLazyID(errorId); @@ -2264,9 +2260,6 @@ function renderModel( if (request.status === ABORTING) { task.status = ABORTED; - if (enableHalt && request.fatalError === haltSymbol) { - return serializeInfinitePromise(); - } const errorId: number = (request.fatalError: any); if (wasReactNode) { return serializeLazyID(errorId); @@ -3008,6 +3001,12 @@ function emitPostponeChunk( request.completedErrorChunks.push(processedChunk); } +function emitBlockedChunk(request: Request, id: number): void { + const row = serializeRowHeader('#', id) + '\n'; + const processedChunk = stringToChunk(row); + request.completedErrorChunks.push(processedChunk); +} + function emitErrorChunk( request: Request, id: number, @@ -3757,7 +3756,7 @@ function retryTask(request: Request, task: Task): void { request.abortableTasks.delete(task); task.status = ABORTED; if (enableHalt && request.fatalError === haltSymbol) { - emitModelChunk(request, task.id, reusableInfinitePromiseModel); + emitBlockedChunk(request, task.id); } else { const errorId: number = (request.fatalError: any); const model = stringify(serializeByValueID(errorId)); @@ -3785,7 +3784,7 @@ function retryTask(request: Request, task: Task): void { request.abortableTasks.delete(task); task.status = ABORTED; if (enableHalt && request.fatalError === haltSymbol) { - emitModelChunk(request, task.id, reusableInfinitePromiseModel); + emitBlockedChunk(request, task.id); } else { const errorId: number = (request.fatalError: any); const model = stringify(serializeByValueID(errorId)); @@ -3830,6 +3829,7 @@ function performWork(request: Request): void { currentRequest = request; prepareToUseHooksForRequest(request); + const hadAbortableTasks = request.abortableTasks.size > 0; try { const pingedTasks = request.pingedTasks; request.pingedTasks = []; @@ -3840,10 +3840,11 @@ function performWork(request: Request): void { if (request.destination !== null) { flushCompletedChunks(request, request.destination); } - if (request.abortableTasks.size === 0) { - // we're done rendering - const onAllReady = request.onAllReady; - onAllReady(); + if (hadAbortableTasks && request.abortableTasks.size === 0) { + // We can ping after completing but if this happens there already + // wouldn't be any abortable tasks. So we only call allReady after + // the work which actually completed the last pending task + allReady(request); } } catch (error) { logRecoverableError(request, error, null); @@ -3868,15 +3869,6 @@ function abortTask(task: Task, request: Request, errorId: number): void { request.completedErrorChunks.push(processedChunk); } -function haltTask(task: Task, request: Request): void { - if (task.status === RENDERING) { - // This task will be aborted by the render - return; - } - task.status = ABORTED; - emitModelChunk(request, task.id, reusableInfinitePromiseModel); -} - function flushCompletedChunks( request: Request, destination: Destination, @@ -4055,6 +4047,7 @@ export function abort(request: Request, reason: mixed): void { } abortableTasks.forEach(task => abortTask(task, request, errorId)); abortableTasks.clear(); + allReady(request); } const abortListeners = request.abortListeners; if (abortListeners.size > 0) { @@ -4110,8 +4103,11 @@ export function halt(request: Request, reason: mixed): void { // to that row from every row that's still remaining. if (abortableTasks.size > 0) { request.pendingChunks++; - abortableTasks.forEach(task => haltTask(task, request)); + const errorId = request.nextChunkId++; + emitBlockedChunk(request, errorId); + abortableTasks.forEach(task => abortTask(task, request, errorId)); abortableTasks.clear(); + allReady(request); } const abortListeners = request.abortListeners; if (abortListeners.size > 0) { @@ -4126,3 +4122,8 @@ export function halt(request: Request, reason: mixed): void { fatalError(request, error); } } + +function allReady(request: Request) { + const onAllReady = request.onAllReady; + onAllReady(); +} From 591adfa40d900e9af6d9250f1ae58d72366e7957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 19 Aug 2024 14:51:22 -0400 Subject: [PATCH 019/191] [Flight] Rename Chunk constructor to ReactPromise (#30747) When printing these in DevTools they show up as the name of the constructor so then you pass a Promise to the client it logs as "Chunk" which is confusing. Ideally we'd probably just name this Promise but 1) there's a slight difference in the .then method atm 2) it's a bit tricky to name a variable and get it from the global in the same scope. Closure compiler doesn't let us just name a function because it removes it and just uses the variable name. --- .../react-client/src/ReactFlightClient.js | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 68d4ac2cd936c..6cb9dee4d15de 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -191,7 +191,12 @@ type SomeChunk = | ErroredChunk; // $FlowFixMe[missing-this-annot] -function Chunk(status: any, value: any, reason: any, response: Response) { +function ReactPromise( + status: any, + value: any, + reason: any, + response: Response, +) { this.status = status; this.value = value; this.reason = reason; @@ -201,9 +206,9 @@ function Chunk(status: any, value: any, reason: any, response: Response) { } } // We subclass Promise.prototype so that we get other methods like .catch -Chunk.prototype = (Object.create(Promise.prototype): any); +ReactPromise.prototype = (Object.create(Promise.prototype): any); // TODO: This doesn't return a new Promise chain unlike the real .then -Chunk.prototype.then = function ( +ReactPromise.prototype.then = function ( this: SomeChunk, resolve: (value: T) => mixed, reject?: (reason: mixed) => mixed, @@ -304,12 +309,12 @@ export function getRoot(response: Response): Thenable { function createPendingChunk(response: Response): PendingChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(PENDING, null, null, response); + return new ReactPromise(PENDING, null, null, response); } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(BLOCKED, null, null, response); + return new ReactPromise(BLOCKED, null, null, response); } function createErrorChunk( @@ -317,7 +322,7 @@ function createErrorChunk( error: Error | Postpone, ): ErroredChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(ERRORED, null, error, response); + return new ReactPromise(ERRORED, null, error, response); } function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { @@ -391,7 +396,7 @@ function createResolvedModelChunk( value: UninitializedModel, ): ResolvedModelChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODEL, value, null, response); + return new ReactPromise(RESOLVED_MODEL, value, null, response); } function createResolvedModuleChunk( @@ -399,7 +404,7 @@ function createResolvedModuleChunk( value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODULE, value, null, response); + return new ReactPromise(RESOLVED_MODULE, value, null, response); } function createInitializedTextChunk( @@ -407,7 +412,7 @@ function createInitializedTextChunk( value: string, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null, response); } function createInitializedBufferChunk( @@ -415,7 +420,7 @@ function createInitializedBufferChunk( value: $ArrayBufferView | ArrayBuffer, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null, response); } function createInitializedIteratorResultChunk( @@ -424,7 +429,12 @@ function createInitializedIteratorResultChunk( done: boolean, ): InitializedChunk> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, {done: done, value: value}, null, response); + return new ReactPromise( + INITIALIZED, + {done: done, value: value}, + null, + response, + ); } function createInitializedStreamChunk< @@ -437,7 +447,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, value, controller, response); + return new ReactPromise(INITIALIZED, value, controller, response); } function createResolvedIteratorResultChunk( @@ -449,7 +459,7 @@ function createResolvedIteratorResultChunk( const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODEL, iteratorResultJSON, null, response); + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response); } function resolveIteratorResultChunk( @@ -1761,7 +1771,7 @@ function startAsyncIterable( if (nextReadIndex === buffer.length) { if (closed) { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk( + return new ReactPromise( INITIALIZED, {done: true, value: undefined}, null, From 52c9c43735d0d5ebb9cd5e2a47c174cb5a5a1713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 19 Aug 2024 15:02:41 -0400 Subject: [PATCH 020/191] [Flight] Emit Infinite Promise as a Halted Row (#30746) Stacked on #30731. When logging a Promise we emit it as an infinite promise instead of blocking the replay on it. This models that as a halted row instead. No need for this special case. I unflag the receiving side since now it's used to replace a feature that's already unflagged so it's used. --- packages/react-client/src/ReactFlightClient.js | 11 ++--------- .../react-client/src/__tests__/ReactFlight-test.js | 12 +++++++++++- packages/react-server/src/ReactFlightServer.js | 9 ++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 6cb9dee4d15de..fbeba4f6c65e2 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -46,7 +46,6 @@ import { enableRefAsProp, enableFlightReadableStream, enableOwnerStacks, - enableHalt, } from 'shared/ReactFeatureFlags'; import { @@ -1194,10 +1193,6 @@ function parseModelString( } case '@': { // Promise - if (value.length === 2) { - // Infinite promise that never resolves. - return new Promise(() => {}); - } const id = parseInt(value.slice(2), 16); const chunk = getChunk(response, id); return chunk; @@ -2638,10 +2633,8 @@ function processFullStringRow( } // Fallthrough case 35 /* "#" */: { - if (enableHalt) { - resolveBlocked(response, id); - return; - } + resolveBlocked(response, id); + return; } // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index c3ab6a46805f4..ef457631e3a8c 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2952,8 +2952,14 @@ describe('ReactFlight', () => { function foo() { return 'hello'; } + function ServerComponent() { - console.log('hi', {prop: 123, fn: foo, map: new Map([['foo', foo]])}); + console.log('hi', { + prop: 123, + fn: foo, + map: new Map([['foo', foo]]), + promise: new Promise(() => {}), + }); throw new Error('err'); } @@ -3018,6 +3024,10 @@ describe('ReactFlight', () => { expect(loggedFn2).not.toBe(foo); expect(loggedFn2.toString()).toBe(foo.toString()); + const promise = mockConsoleLog.mock.calls[0][1].promise; + expect(promise).toBeInstanceOf(Promise); + expect(promise.status).toBe('blocked'); + expect(ownerStacks).toEqual(['\n in App (at **)']); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index c9fe9e3434bb7..3680cc715adba 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1817,10 +1817,6 @@ function serializeLazyID(id: number): string { return '$L' + id.toString(16); } -function serializeInfinitePromise(): string { - return '$@'; -} - function serializePromiseID(id: number): string { return '$@' + id.toString(16); } @@ -3273,7 +3269,10 @@ function renderConsoleValue( } // If it hasn't already resolved (and been instrumented) we just encode an infinite // promise that will never resolve. - return serializeInfinitePromise(); + request.pendingChunks++; + const blockedId = request.nextChunkId++; + emitBlockedChunk(request, blockedId); + return serializePromiseID(blockedId); } if (existingReference !== undefined) { From 0fa9476b9b9b7e284fb6ebe7e1c46a6a6ae85f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 19 Aug 2024 16:34:38 -0400 Subject: [PATCH 021/191] [Flight] Revert Emit Infinite Promise as a Halted Row (#30746) (#30748) This reverts commit 52c9c43735d0d5ebb9cd5e2a47c174cb5a5a1713. Just kidding. We realized we probably don't want to do the halted row thing after all. --- packages/react-client/src/ReactFlightClient.js | 11 +++++++++-- .../react-client/src/__tests__/ReactFlight-test.js | 1 - packages/react-server/src/ReactFlightServer.js | 9 +++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index fbeba4f6c65e2..6cb9dee4d15de 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -46,6 +46,7 @@ import { enableRefAsProp, enableFlightReadableStream, enableOwnerStacks, + enableHalt, } from 'shared/ReactFeatureFlags'; import { @@ -1193,6 +1194,10 @@ function parseModelString( } case '@': { // Promise + if (value.length === 2) { + // Infinite promise that never resolves. + return new Promise(() => {}); + } const id = parseInt(value.slice(2), 16); const chunk = getChunk(response, id); return chunk; @@ -2633,8 +2638,10 @@ function processFullStringRow( } // Fallthrough case 35 /* "#" */: { - resolveBlocked(response, id); - return; + if (enableHalt) { + resolveBlocked(response, id); + return; + } } // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index ef457631e3a8c..0c3b8798dc8c3 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -3026,7 +3026,6 @@ describe('ReactFlight', () => { const promise = mockConsoleLog.mock.calls[0][1].promise; expect(promise).toBeInstanceOf(Promise); - expect(promise.status).toBe('blocked'); expect(ownerStacks).toEqual(['\n in App (at **)']); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3680cc715adba..c9fe9e3434bb7 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1817,6 +1817,10 @@ function serializeLazyID(id: number): string { return '$L' + id.toString(16); } +function serializeInfinitePromise(): string { + return '$@'; +} + function serializePromiseID(id: number): string { return '$@' + id.toString(16); } @@ -3269,10 +3273,7 @@ function renderConsoleValue( } // If it hasn't already resolved (and been instrumented) we just encode an infinite // promise that will never resolve. - request.pendingChunks++; - const blockedId = request.nextChunkId++; - emitBlockedChunk(request, blockedId); - return serializePromiseID(blockedId); + return serializeInfinitePromise(); } if (existingReference !== undefined) { From a960b92cb93e7d006e5e8de850f9b8b51f655c90 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 19 Aug 2024 19:34:20 -0700 Subject: [PATCH 022/191] [Flight] model halting as never delivered chunks (#30740) stacked on: #30731 We've refined the model of halting a prerender. Now when you abort during a prerender we simply omit the rows that would complete the flight render. This is analagous to prerendering in Fizz where you must resume the prerender to actually result in errors propagating in the postponed holes. We don't have a resume yet for flight and it's not entirely clear how that will work however the key insight here is that deciding whether the never resolving rows are an error or not should really be done on the consuming side rather than in the producer. This PR also reintroduces the logs for the abort error/postpone when prerendering which will give you some indication that something wasn't finished when the prerender was aborted. --- .../react-client/src/ReactFlightClient.js | 22 -- .../src/server/ReactFlightDOMServerNode.js | 22 +- .../src/server/ReactFlightDOMServerBrowser.js | 22 +- .../src/server/ReactFlightDOMServerEdge.js | 22 +- .../src/server/ReactFlightDOMServerNode.js | 22 +- .../src/__tests__/ReactFlightDOM-test.js | 24 +- .../__tests__/ReactFlightDOMBrowser-test.js | 9 +- .../src/__tests__/ReactFlightDOMEdge-test.js | 16 +- .../src/__tests__/ReactFlightDOMNode-test.js | 15 +- .../src/server/ReactFlightDOMServerBrowser.js | 22 +- .../src/server/ReactFlightDOMServerEdge.js | 22 +- .../src/server/ReactFlightDOMServerNode.js | 22 +- .../react-server/src/ReactFlightServer.js | 296 ++++++++++-------- 13 files changed, 253 insertions(+), 283 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 6cb9dee4d15de..072df8108fccd 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -46,7 +46,6 @@ import { enableRefAsProp, enableFlightReadableStream, enableOwnerStacks, - enableHalt, } from 'shared/ReactFeatureFlags'; import { @@ -1997,20 +1996,6 @@ function resolvePostponeDev( } } -function resolveBlocked(response: Response, id: number): void { - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createBlockedChunk(response)); - } else if (chunk.status === PENDING) { - // This chunk as contructed via other means but it is actually a blocked chunk - // so we update it here. We check the status because it might have been aborted - // before we attempted to resolve it. - const blockedChunk: BlockedChunk = (chunk: any); - blockedChunk.status = BLOCKED; - } -} - function resolveHint( response: Response, code: Code, @@ -2637,13 +2622,6 @@ function processFullStringRow( } } // Fallthrough - case 35 /* "#" */: { - if (enableHalt) { - resolveBlocked(response, id); - return; - } - } - // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { // We assume anything else is JSON. resolveModel(response, id, row); diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index 1434d17015a54..a9c978b493012 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -175,35 +173,27 @@ function prerenderToNodeStream( resolve({prelude: readable}); } - const request = createRequest( + const request = createPrerenderRequest( model, moduleBasePath, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 56c3d5b71f432..11dbe1a7c1358 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -134,35 +132,27 @@ function prerender( ); resolve({prelude: stream}); } - const request = createRequest( + const request = createPrerenderRequest( model, turbopackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 56c3d5b71f432..11dbe1a7c1358 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -134,35 +132,27 @@ function prerender( ); resolve({prelude: stream}); } - const request = createRequest( + const request = createPrerenderRequest( model, turbopackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index f9b0c163b2154..9f25004ea4b67 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -177,35 +175,27 @@ function prerenderToNodeStream( resolve({prelude: readable}); } - const request = createRequest( + const request = createPrerenderRequest( model, turbopackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 97fce8a8ea11d..aae9cf48c4285 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2724,7 +2724,7 @@ describe('ReactFlightDOM', () => { }); // @gate enableHalt - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + it('does not propagate abort reasons errors when aborting a prerender', async () => { let resolveGreeting; const greetingPromise = new Promise(resolve => { resolveGreeting = resolve; @@ -2746,6 +2746,7 @@ describe('ReactFlightDOM', () => { } const controller = new AbortController(); + const errors = []; const {pendingResult} = await serverAct(async () => { // destructure trick to avoid the act scope from awaiting the returned value return { @@ -2754,15 +2755,20 @@ describe('ReactFlightDOM', () => { webpackMap, { signal: controller.signal, + onError(err) { + errors.push(err); + }, }, ), }; }); - controller.abort(); + controller.abort('boom'); resolveGreeting(); const {prelude} = await pendingResult; + expect(errors).toEqual(['boom']); + const preludeWeb = Readable.toWeb(prelude); const response = ReactServerDOMClient.createFromReadableStream(preludeWeb); @@ -2772,7 +2778,7 @@ describe('ReactFlightDOM', () => { return use(response); } - const errors = []; + errors.length = 0; let abortFizz; await serverAct(async () => { const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( @@ -2788,10 +2794,10 @@ describe('ReactFlightDOM', () => { }); await serverAct(() => { - abortFizz('boom'); + abortFizz('bam'); }); - expect(errors).toEqual(['boom']); + expect(errors).toEqual(['bam']); const container = document.createElement('div'); await readInto(container, fizzReadable); @@ -2861,7 +2867,7 @@ describe('ReactFlightDOM', () => { it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => { const controller = new AbortController(); function ComponentThatAborts() { - controller.abort(); + controller.abort('boom'); return null; } @@ -2912,11 +2918,8 @@ describe('ReactFlightDOM', () => { }; }); - controller.abort(); - const {prelude} = await pendingResult; - expect(errors).toEqual([]); - + expect(errors).toEqual(['boom']); const response = ReactServerDOMClient.createFromReadableStream( Readable.toWeb(prelude), ); @@ -2926,6 +2929,7 @@ describe('ReactFlightDOM', () => { function ClientApp() { return use(response); } + errors.length = 0; let abortFizz; await serverAct(async () => { const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream( diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 969f9e125e8c5..a4c5df377be57 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2402,7 +2402,7 @@ describe('ReactFlightDOMBrowser', () => { }); // @gate enableHalt - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + it('does not propagate abort reasons errors when aborting a prerender', async () => { let resolveGreeting; const greetingPromise = new Promise(resolve => { resolveGreeting = resolve; @@ -2424,6 +2424,7 @@ describe('ReactFlightDOMBrowser', () => { } const controller = new AbortController(); + const errors = []; const {pendingResult} = await serverAct(async () => { // destructure trick to avoid the act scope from awaiting the returned value return { @@ -2432,14 +2433,18 @@ describe('ReactFlightDOMBrowser', () => { webpackMap, { signal: controller.signal, + onError(err) { + errors.push(err); + }, }, ), }; }); - controller.abort(); + controller.abort('boom'); resolveGreeting(); const {prelude} = await pendingResult; + expect(errors).toEqual(['boom']); function ClientRoot({response}) { return use(response); 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 b38c33dc7761b..1c146014dcefa 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -1103,7 +1103,7 @@ describe('ReactFlightDOMEdge', () => { }); // @gate enableHalt - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + it('does not propagate abort reasons errors when aborting a prerender', async () => { let resolveGreeting; const greetingPromise = new Promise(resolve => { resolveGreeting = resolve; @@ -1125,6 +1125,7 @@ describe('ReactFlightDOMEdge', () => { } const controller = new AbortController(); + const errors = []; const {pendingResult} = await serverAct(async () => { // destructure trick to avoid the act scope from awaiting the returned value return { @@ -1133,15 +1134,20 @@ describe('ReactFlightDOMEdge', () => { webpackMap, { signal: controller.signal, + onError(err) { + errors.push(err); + }, }, ), }; }); - controller.abort(); + controller.abort('boom'); resolveGreeting(); const {prelude} = await pendingResult; + expect(errors).toEqual(['boom']); + function ClientRoot({response}) { return use(response); } @@ -1153,7 +1159,7 @@ describe('ReactFlightDOMEdge', () => { }, }); const fizzController = new AbortController(); - const errors = []; + errors.length = 0; const ssrStream = await serverAct(() => ReactDOMServer.renderToReadableStream( React.createElement(ClientRoot, {response}), @@ -1165,8 +1171,8 @@ describe('ReactFlightDOMEdge', () => { }, ), ); - fizzController.abort('boom'); - expect(errors).toEqual(['boom']); + fizzController.abort('bam'); + expect(errors).toEqual(['bam']); // Should still match the result when parsed const result = await readResult(ssrStream); const div = document.createElement('div'); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index fbe32d2f1f697..620da74ff1db4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -443,7 +443,7 @@ describe('ReactFlightDOMNode', () => { }); // @gate enableHalt - it('serializes unfinished tasks with infinite promises when aborting a prerender', async () => { + it('does not propagate abort reasons errors when aborting a prerender', async () => { let resolveGreeting; const greetingPromise = new Promise(resolve => { resolveGreeting = resolve; @@ -465,6 +465,7 @@ describe('ReactFlightDOMNode', () => { } const controller = new AbortController(); + const errors = []; const {pendingResult} = await serverAct(async () => { // destructure trick to avoid the act scope from awaiting the returned value return { @@ -473,14 +474,18 @@ describe('ReactFlightDOMNode', () => { webpackMap, { signal: controller.signal, + onError(err) { + errors.push(err); + }, }, ), }; }); - controller.abort(); + controller.abort('boom'); resolveGreeting(); const {prelude} = await pendingResult; + expect(errors).toEqual(['boom']); function ClientRoot({response}) { return use(response); @@ -492,7 +497,7 @@ describe('ReactFlightDOMNode', () => { moduleLoading: null, }, }); - const errors = []; + errors.length = 0; const ssrStream = await serverAct(() => ReactDOMServer.renderToPipeableStream( React.createElement(ClientRoot, {response}), @@ -503,8 +508,8 @@ describe('ReactFlightDOMNode', () => { }, ), ); - ssrStream.abort('boom'); - expect(errors).toEqual(['boom']); + ssrStream.abort('bam'); + expect(errors).toEqual(['bam']); // Should still match the result when parsed const result = await readResult(ssrStream); const div = document.createElement('div'); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index 95e7f770428a3..7954417b95a25 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -134,35 +132,27 @@ function prerender( ); resolve({prelude: stream}); } - const request = createRequest( + const request = createPrerenderRequest( model, webpackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 95e7f770428a3..7954417b95a25 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -12,15 +12,13 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -134,35 +132,27 @@ function prerender( ); resolve({prelude: stream}); } - const request = createRequest( + const request = createPrerenderRequest( model, webpackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 1d8d6ea9ef743..f459e04914b6e 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -20,15 +20,13 @@ import type {Thenable} from 'shared/ReactTypes'; import {Readable} from 'stream'; -import {enableHalt} from 'shared/ReactFeatureFlags'; - import { createRequest, + createPrerenderRequest, startWork, startFlowing, stopFlowing, abort, - halt, } from 'react-server/src/ReactFlightServer'; import { @@ -177,35 +175,27 @@ function prerenderToNodeStream( resolve({prelude: readable}); } - const request = createRequest( + const request = createPrerenderRequest( model, webpackMap, + onAllReady, + onFatalError, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.onPostpone : undefined, options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - onAllReady, - onFatalError, ); if (options && options.signal) { const signal = options.signal; if (signal.aborted) { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); } else { const listener = () => { const reason = (signal: any).reason; - if (enableHalt) { - halt(request, reason); - } else { - abort(request, reason); - } + abort(request, reason); signal.removeEventListener('abort', listener); }; signal.addEventListener('abort', listener); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index c9fe9e3434bb7..9616d4b972911 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -353,7 +353,8 @@ type Task = { interface Reference {} export type Request = { - status: 0 | 1 | 2 | 3, + status: 10 | 11 | 12 | 13, + type: 20 | 21, flushScheduled: boolean, fatalError: mixed, destination: null | Destination, @@ -425,13 +426,17 @@ function defaultPostponeHandler(reason: string) { // Noop } -const OPEN = 0; -const ABORTING = 1; -const CLOSING = 2; -const CLOSED = 3; +const OPEN = 10; +const ABORTING = 11; +const CLOSING = 12; +const CLOSED = 13; + +const RENDER = 20; +const PRERENDER = 21; function RequestInstance( this: $FlowFixMe, + type: 20 | 21, model: ReactClientValue, bundlerConfig: ClientManifest, onError: void | ((error: mixed) => ?string), @@ -440,8 +445,8 @@ function RequestInstance( temporaryReferences: void | TemporaryReferenceSet, environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only - onAllReady: void | (() => void), - onFatalError: void | ((error: mixed) => void), + onAllReady: () => void, + onFatalError: (error: mixed) => void, ) { if ( ReactSharedInternals.A !== null && @@ -466,6 +471,7 @@ function RequestInstance( TaintRegistryPendingRequests.add(cleanupQueue); } const hints = createHints(); + this.type = type; this.status = OPEN; this.flushScheduled = false; this.fatalError = null; @@ -493,8 +499,8 @@ function RequestInstance( this.onError = onError === undefined ? defaultErrorHandler : onError; this.onPostpone = onPostpone === undefined ? defaultPostponeHandler : onPostpone; - this.onAllReady = onAllReady === undefined ? noop : onAllReady; - this.onFatalError = onFatalError === undefined ? noop : onFatalError; + this.onAllReady = onAllReady; + this.onFatalError = onFatalError; if (__DEV__) { this.environmentName = @@ -522,7 +528,7 @@ function RequestInstance( pingedTasks.push(rootTask); } -function noop(): void {} +function noop() {} export function createRequest( model: ReactClientValue, @@ -533,11 +539,38 @@ export function createRequest( temporaryReferences: void | TemporaryReferenceSet, environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only - onAllReady: void | (() => void), - onFatalError: void | (() => void), ): Request { // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors return new RequestInstance( + RENDER, + model, + bundlerConfig, + onError, + identifierPrefix, + onPostpone, + temporaryReferences, + environmentName, + filterStackFrame, + noop, + noop, + ); +} + +export function createPrerenderRequest( + model: ReactClientValue, + bundlerConfig: ClientManifest, + onAllReady: () => void, + onFatalError: () => void, + onError: void | ((error: mixed) => ?string), + identifierPrefix?: string, + onPostpone: void | ((reason: string) => void), + temporaryReferences: void | TemporaryReferenceSet, + environmentName: void | string | (() => string), // DEV-only + filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only +): Request { + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors + return new RequestInstance( + PRERENDER, model, bundlerConfig, onError, @@ -616,13 +649,9 @@ function serializeThenable( // We can no longer accept any resolved values request.abortableTasks.delete(newTask); newTask.status = ABORTED; - if (enableHalt && request.fatalError === haltSymbol) { - emitBlockedChunk(request, newTask.id); - } else { - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, newTask.id, model); - } + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, newTask.id, model); return newTask.id; } if (typeof thenable.status === 'string') { @@ -732,7 +761,7 @@ function serializeReadableStream( } if (entry.done) { - request.abortListeners.delete(error); + request.abortListeners.delete(abortStream); const endStreamRow = streamTask.id.toString(16) + ':C\n'; request.completedRegularChunks.push(stringToChunk(endStreamRow)); enqueueFlush(request); @@ -754,34 +783,49 @@ function serializeReadableStream( return; } aborted = true; - request.abortListeners.delete(error); + request.abortListeners.delete(abortStream); + const digest = logRecoverableError(request, reason, streamTask); + emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); - let cancelWith: mixed; - if (enableHalt && request.fatalError === haltSymbol) { - cancelWith = reason; - } else if ( + // $FlowFixMe should be able to pass mixed + reader.cancel(reason).then(error, error); + } + function abortStream(reason: mixed) { + if (aborted) { + return; + } + aborted = true; + request.abortListeners.delete(abortStream); + if ( enablePostpone && typeof reason === 'object' && reason !== null && (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { - cancelWith = reason; const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, streamTask); - emitPostponeChunk(request, streamTask.id, postponeInstance); - enqueueFlush(request); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitPostponeChunk(request, streamTask.id, postponeInstance); + enqueueFlush(request); + } } else { - cancelWith = reason; const digest = logRecoverableError(request, reason, streamTask); - emitErrorChunk(request, streamTask.id, digest, reason); - enqueueFlush(request); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); + } } // $FlowFixMe should be able to pass mixed - reader.cancel(cancelWith).then(error, error); + reader.cancel(reason).then(error, error); } - request.abortListeners.add(error); + request.abortListeners.add(abortStream); reader.read().then(progress, error); return serializeByValueID(streamTask.id); } @@ -837,7 +881,7 @@ function serializeAsyncIterable( } if (entry.done) { - request.abortListeners.delete(error); + request.abortListeners.delete(abortIterable); let endStreamRow; if (entry.value === undefined) { endStreamRow = streamTask.id.toString(16) + ':C\n'; @@ -881,34 +925,52 @@ function serializeAsyncIterable( return; } aborted = true; - request.abortListeners.delete(error); - let throwWith: mixed; - if (enableHalt && request.fatalError === haltSymbol) { - throwWith = reason; - } else if ( + request.abortListeners.delete(abortIterable); + const digest = logRecoverableError(request, reason, streamTask); + emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + function abortIterable(reason: mixed) { + if (aborted) { + return; + } + aborted = true; + request.abortListeners.delete(abortIterable); + if ( enablePostpone && typeof reason === 'object' && reason !== null && (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { - throwWith = reason; const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, streamTask); - emitPostponeChunk(request, streamTask.id, postponeInstance); - enqueueFlush(request); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitPostponeChunk(request, streamTask.id, postponeInstance); + enqueueFlush(request); + } } else { - throwWith = reason; const digest = logRecoverableError(request, reason, streamTask); - emitErrorChunk(request, streamTask.id, digest, reason); - enqueueFlush(request); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitErrorChunk(request, streamTask.id, digest, reason); + enqueueFlush(request); + } } if (typeof (iterator: any).throw === 'function') { // The iterator protocol doesn't necessarily include this but a generator do. // $FlowFixMe should be able to pass mixed - iterator.throw(throwWith).then(error, error); + iterator.throw(reason).then(error, error); } } - request.abortListeners.add(error); + request.abortListeners.add(abortIterable); if (__DEV__) { callIteratorInDEV(iterator, progress, error); } else { @@ -2101,7 +2163,7 @@ function serializeBlob(request: Request, blob: Blob): string { return; } if (entry.done) { - request.abortListeners.delete(error); + request.abortListeners.delete(abortBlob); aborted = true; pingTask(request, newTask); return; @@ -2111,28 +2173,52 @@ function serializeBlob(request: Request, blob: Blob): string { // $FlowFixMe[incompatible-call] return reader.read().then(progress).catch(error); } - function error(reason: mixed) { if (aborted) { return; } aborted = true; - request.abortListeners.delete(error); - let cancelWith: mixed; - if (enableHalt && request.fatalError === haltSymbol) { - cancelWith = reason; + request.abortListeners.delete(abortBlob); + const digest = logRecoverableError(request, reason, newTask); + emitErrorChunk(request, newTask.id, digest, reason); + enqueueFlush(request); + // $FlowFixMe should be able to pass mixed + reader.cancel(reason).then(error, error); + } + function abortBlob(reason: mixed) { + if (aborted) { + return; + } + aborted = true; + request.abortListeners.delete(abortBlob); + if ( + enablePostpone && + typeof reason === 'object' && + reason !== null && + (reason: any).$$typeof === REACT_POSTPONE_TYPE + ) { + const postponeInstance: Postpone = (reason: any); + logPostpone(request, postponeInstance.message, newTask); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitPostponeChunk(request, newTask.id, postponeInstance); + enqueueFlush(request); + } } else { - cancelWith = reason; const digest = logRecoverableError(request, reason, newTask); - emitErrorChunk(request, newTask.id, digest, reason); - request.abortableTasks.delete(newTask); - enqueueFlush(request); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + emitErrorChunk(request, newTask.id, digest, reason); + enqueueFlush(request); + } } // $FlowFixMe should be able to pass mixed - reader.cancel(cancelWith).then(error, error); + reader.cancel(reason).then(error, error); } - request.abortListeners.add(error); + request.abortListeners.add(abortBlob); // $FlowFixMe[incompatible-call] reader.read().then(progress).catch(error); @@ -3001,12 +3087,6 @@ function emitPostponeChunk( request.completedErrorChunks.push(processedChunk); } -function emitBlockedChunk(request: Request, id: number): void { - const row = serializeRowHeader('#', id) + '\n'; - const processedChunk = stringToChunk(row); - request.completedErrorChunks.push(processedChunk); -} - function emitErrorChunk( request: Request, id: number, @@ -3755,13 +3835,9 @@ function retryTask(request: Request, task: Task): void { if (request.status === ABORTING) { request.abortableTasks.delete(task); task.status = ABORTED; - if (enableHalt && request.fatalError === haltSymbol) { - emitBlockedChunk(request, task.id); - } else { - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); - } + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, task.id, model); return; } // Something suspended again, let's pick it back up later. @@ -3783,13 +3859,9 @@ function retryTask(request: Request, task: Task): void { if (request.status === ABORTING) { request.abortableTasks.delete(task); task.status = ABORTED; - if (enableHalt && request.fatalError === haltSymbol) { - emitBlockedChunk(request, task.id); - } else { - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); - } + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, task.id, model); return; } @@ -3844,7 +3916,8 @@ function performWork(request: Request): void { // We can ping after completing but if this happens there already // wouldn't be any abortable tasks. So we only call allReady after // the work which actually completed the last pending task - allReady(request); + const onAllReady = request.onAllReady; + onAllReady(); } } catch (error) { logRecoverableError(request, error, null); @@ -4007,17 +4080,17 @@ export function stopFlowing(request: Request): void { request.destination = null; } -// This is called to early terminate a request. It creates an error at all pending tasks. export function abort(request: Request, reason: mixed): void { try { if (request.status === OPEN) { request.status = ABORTING; } const abortableTasks = request.abortableTasks; - // We have tasks to abort. We'll emit one error row and then emit a reference - // to that row from every row that's still remaining. if (abortableTasks.size > 0) { - request.pendingChunks++; + // We have tasks to abort. We'll emit one error row and then emit a reference + // to that row from every row that's still remaining if we are rendering. If we + // are prerendering (and halt semantics are enabled) we will refer to an error row + // but not actually emit it so the reciever can at that point rather than error. const errorId = request.nextChunkId++; request.fatalError = errorId; if ( @@ -4028,7 +4101,11 @@ export function abort(request: Request, reason: mixed): void { ) { const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, null); - emitPostponeChunk(request, errorId, postponeInstance); + if (!enableHalt || request.type === PRERENDER) { + // When prerendering with halt semantics we omit the referred to postpone. + request.pendingChunks++; + emitPostponeChunk(request, errorId, postponeInstance); + } } else { const error = reason === undefined @@ -4043,11 +4120,16 @@ export function abort(request: Request, reason: mixed): void { ) : reason; const digest = logRecoverableError(request, error, null); - emitErrorChunk(request, errorId, digest, error); + if (!enableHalt || request.type === RENDER) { + // When prerendering with halt semantics we omit the referred to error. + request.pendingChunks++; + emitErrorChunk(request, errorId, digest, error); + } } abortableTasks.forEach(task => abortTask(task, request, errorId)); abortableTasks.clear(); - allReady(request); + const onAllReady = request.onAllReady; + onAllReady(); } const abortListeners = request.abortListeners; if (abortListeners.size > 0) { @@ -4087,43 +4169,3 @@ export function abort(request: Request, reason: mixed): void { fatalError(request, error); } } - -const haltSymbol = Symbol('halt'); - -// This is called to stop rendering without erroring. All unfinished work is represented Promises -// that never resolve. -export function halt(request: Request, reason: mixed): void { - try { - if (request.status === OPEN) { - request.status = ABORTING; - } - request.fatalError = haltSymbol; - const abortableTasks = request.abortableTasks; - // We have tasks to abort. We'll emit one error row and then emit a reference - // to that row from every row that's still remaining. - if (abortableTasks.size > 0) { - request.pendingChunks++; - const errorId = request.nextChunkId++; - emitBlockedChunk(request, errorId); - abortableTasks.forEach(task => abortTask(task, request, errorId)); - abortableTasks.clear(); - allReady(request); - } - const abortListeners = request.abortListeners; - if (abortListeners.size > 0) { - abortListeners.forEach(callback => callback(reason)); - abortListeners.clear(); - } - if (request.destination !== null) { - flushCompletedChunks(request, request.destination); - } - } catch (error) { - logRecoverableError(request, error, null); - fatalError(request, error); - } -} - -function allReady(request: Request) { - const onAllReady = request.onAllReady; - onAllReady(); -} From 5997072f691024e0e5afd78c002c0871b1cbd6a6 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Tue, 20 Aug 2024 11:05:49 -0400 Subject: [PATCH 023/191] [flow] Remove CI_MAX_WORKERS option Noticed this from #30707. This was vestigial from from circleci and now that we're on GH actions I think we should be able to remove this option altogether. ghstack-source-id: 78e8b0243b1e1484ffaad820987ae3679a7374bf Pull Request resolved: https://github.com/facebook/react/pull/30753 --- scripts/flow/config/flowconfig | 1 - scripts/flow/createFlowConfigs.js | 5 ----- 2 files changed, 6 deletions(-) diff --git a/scripts/flow/config/flowconfig b/scripts/flow/config/flowconfig index b2032e0dadcbf..46e7b6e969690 100644 --- a/scripts/flow/config/flowconfig +++ b/scripts/flow/config/flowconfig @@ -33,7 +33,6 @@ untyped-type-import=error [options] -%CI_MAX_WORKERS% munge_underscores=false # Substituted by createFlowConfig.js: diff --git a/scripts/flow/createFlowConfigs.js b/scripts/flow/createFlowConfigs.js index c1e8474160247..7ff68cce5b03a 100644 --- a/scripts/flow/createFlowConfigs.js +++ b/scripts/flow/createFlowConfigs.js @@ -107,11 +107,6 @@ function writeConfig( }); const config = configTemplate - .replace( - '%CI_MAX_WORKERS%\n', - // On CI, we seem to need to limit workers. - process.env.CI ? 'server.max_workers=4\n' : '', - ) .replace('%REACT_RENDERER_FLOW_OPTIONS%', moduleMappings.trim()) .replace('%REACT_RENDERER_FLOW_IGNORES%', ignoredPaths.join('\n')) .replace('%FLOW_VERSION%', flowVersion); From 2505bf9b3400c6a00381e86d30b495935f5339df Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 20 Aug 2024 09:49:41 -0700 Subject: [PATCH 024/191] [Fizz] track postpones when aborting boundaries with a postpone (#30751) When aborting with a postpone value boundaries are put into client rendered mode even during prerenders. This doesn't follow the postpoen semantics of the rest of fizz where during a prerender a postpone is tracked and it will leave holes in tracked postpone state that can be resumed. This change updates this behavior to match the postpones semantics between aborts and imperative postpones. --- .../src/__tests__/ReactDOMFizzServer-test.js | 1 - packages/react-server/src/ReactFizzServer.js | 14 +++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 5d70a0c71ea4d..7d8707bcd3f22 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -7727,7 +7727,6 @@ describe('ReactDOMFizzServer', () => { const prerendered = await pendingPrerender; - expect(prerendered.postponed).toBe(null); expect(errors).toEqual([]); expect(postpones).toEqual(['manufactured', 'manufactured']); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 986e8673a5a2a..17c278f2b0b52 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -3857,7 +3857,6 @@ function abortTask(task: Task, request: Request, error: mixed): void { } else { boundary.pendingTasks--; if (boundary.status !== CLIENT_RENDERED) { - boundary.status = CLIENT_RENDERED; // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which // boundary the message is referring to const errorInfo = getThrownInfo(task.componentStack); @@ -3870,11 +3869,24 @@ function abortTask(task: Task, request: Request, error: mixed): void { ) { const postponeInstance: Postpone = (error: any); logPostpone(request, postponeInstance.message, errorInfo, null); + if (request.trackedPostpones !== null && segment !== null) { + trackPostpone(request, request.trackedPostpones, task, segment); + finishedTask(request, task.blockedBoundary, segment); + + // If this boundary was still pending then we haven't already cancelled its fallbacks. + // We'll need to abort the fallbacks, which will also error that parent boundary. + boundary.fallbackAbortableTasks.forEach(fallbackTask => + abortTask(fallbackTask, request, error), + ); + boundary.fallbackAbortableTasks.clear(); + return; + } // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { errorDigest = logRecoverableError(request, error, errorInfo, null); } + boundary.status = CLIENT_RENDERED; encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true); untrackBoundary(request, boundary); From 92d26c8e93a88ca41338d3509b4324ad19a89c1e Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 20 Aug 2024 10:22:39 -0700 Subject: [PATCH 025/191] [Flight] When halting omit any reference rather than refer to a shared missing chunk (#30750) When aborting a prerender we should leave references unfulfilled, not share a common unfullfilled reference. functionally today this doesn't matter because we don't have resuming but the semantic is that the row was not available when the abort happened and in a resume the row should fill in. But by pointing each task to a common unfulfilled chunk we lose the ability for these references to resolves to distinct values on resume. --- .../src/__tests__/ReactFlightDOM-test.js | 30 +++- .../__tests__/ReactFlightDOMBrowser-test.js | 20 ++- .../src/__tests__/ReactFlightDOMEdge-test.js | 11 +- .../src/__tests__/ReactFlightDOMNode-test.js | 11 +- .../react-server/src/ReactFlightServer.js | 139 ++++++++++++------ 5 files changed, 153 insertions(+), 58 deletions(-) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index aae9cf48c4285..41fc0bfd41088 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2797,7 +2797,16 @@ describe('ReactFlightDOM', () => { abortFizz('bam'); }); - expect(errors).toEqual(['bam']); + if (__DEV__) { + expect(errors).toEqual([new Error('Connection closed.')]); + } else { + // This is likely a bug. In Dev we get a connection closed error + // because the debug info creates a chunk that has a pending status + // and when the stream finishes we error if any chunks are still pending. + // In production there is no debug info so the missing chunk is never instantiated + // because nothing triggers model evaluation before the stream completes + expect(errors).toEqual(['bam']); + } const container = document.createElement('div'); await readInto(container, fizzReadable); @@ -2919,10 +2928,11 @@ describe('ReactFlightDOM', () => { }); const {prelude} = await pendingResult; + expect(errors).toEqual(['boom']); - const response = ReactServerDOMClient.createFromReadableStream( - Readable.toWeb(prelude), - ); + + const preludeWeb = Readable.toWeb(prelude); + const response = ReactServerDOMClient.createFromReadableStream(preludeWeb); const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); @@ -2949,7 +2959,17 @@ describe('ReactFlightDOM', () => { }); // one error per boundary - expect(errors).toEqual(['boom', 'boom', 'boom']); + if (__DEV__) { + const err = new Error('Connection closed.'); + expect(errors).toEqual([err, err, err]); + } else { + // This is likely a bug. In Dev we get a connection closed error + // because the debug info creates a chunk that has a pending status + // and when the stream finishes we error if any chunks are still pending. + // In production there is no debug info so the missing chunk is never instantiated + // because nothing triggers model evaluation before the stream completes + expect(errors).toEqual(['boom', 'boom', 'boom']); + } const container = document.createElement('div'); await readInto(container, fizzReadable); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index a4c5df377be57..fa1e65862564e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2454,12 +2454,28 @@ describe('ReactFlightDOMBrowser', () => { passThrough(prelude), ); const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + errors.length = 0; + const root = ReactDOMClient.createRoot(container, { + onUncaughtError(err) { + errors.push(err); + }, + }); await act(() => { root.render(); }); - expect(container.innerHTML).toBe('
loading...
'); + if (__DEV__) { + expect(errors).toEqual([new Error('Connection closed.')]); + expect(container.innerHTML).toBe(''); + } else { + // This is likely a bug. In Dev we get a connection closed error + // because the debug info creates a chunk that has a pending status + // and when the stream finishes we error if any chunks are still pending. + // In production there is no debug info so the missing chunk is never instantiated + // because nothing triggers model evaluation before the stream completes + expect(errors).toEqual([]); + expect(container.innerHTML).toBe('
loading...
'); + } }); }); 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 1c146014dcefa..0cb3897aea443 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -1172,7 +1172,16 @@ describe('ReactFlightDOMEdge', () => { ), ); fizzController.abort('bam'); - expect(errors).toEqual(['bam']); + if (__DEV__) { + expect(errors).toEqual([new Error('Connection closed.')]); + } else { + // This is likely a bug. In Dev we get a connection closed error + // because the debug info creates a chunk that has a pending status + // and when the stream finishes we error if any chunks are still pending. + // In production there is no debug info so the missing chunk is never instantiated + // because nothing triggers model evaluation before the stream completes + expect(errors).toEqual(['bam']); + } // Should still match the result when parsed const result = await readResult(ssrStream); const div = document.createElement('div'); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 620da74ff1db4..f2dca4a45c7fa 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -509,7 +509,16 @@ describe('ReactFlightDOMNode', () => { ), ); ssrStream.abort('bam'); - expect(errors).toEqual(['bam']); + if (__DEV__) { + expect(errors).toEqual([new Error('Connection closed.')]); + } else { + // This is likely a bug. In Dev we get a connection closed error + // because the debug info creates a chunk that has a pending status + // and when the stream finishes we error if any chunks are still pending. + // In production there is no debug info so the missing chunk is never instantiated + // because nothing triggers model evaluation before the stream completes + expect(errors).toEqual(['bam']); + } // Should still match the result when parsed const result = await readResult(ssrStream); const div = document.createElement('div'); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 9616d4b972911..efad2aa59ca81 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -649,9 +649,13 @@ function serializeThenable( // We can no longer accept any resolved values request.abortableTasks.delete(newTask); newTask.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, newTask.id, model); + if (enableHalt && request.type === PRERENDER) { + request.pendingChunks--; + } else { + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, newTask.id, model); + } return newTask.id; } if (typeof thenable.status === 'string') { @@ -1633,6 +1637,24 @@ function outlineTask(request: Request, task: Task): ReactJSONValue { return serializeLazyID(newTask.id); } +function outlineHaltedTask( + request: Request, + task: Task, + allowLazy: boolean, +): ReactJSONValue { + // In the future if we track task state for resuming we'll maybe need to + // construnct an actual task here but since we're never going to retry it + // we just claim the id and serialize it according to the proper convention + const taskId = request.nextChunkId++; + if (allowLazy) { + // We're halting in a position that can handle a lazy reference + return serializeLazyID(taskId); + } else { + // We're halting in a position that needs a value reference + return serializeByValueID(taskId); + } +} + function renderElement( request: Request, task: Task, @@ -2278,6 +2300,20 @@ function renderModel( ((model: any).$$typeof === REACT_ELEMENT_TYPE || (model: any).$$typeof === REACT_LAZY_TYPE); + if (request.status === ABORTING) { + task.status = ABORTED; + if (enableHalt && request.type === PRERENDER) { + // This will create a new task and refer to it in this slot + // the new task won't be retried because we are aborting + return outlineHaltedTask(request, task, wasReactNode); + } + const errorId = (request.fatalError: any); + if (wasReactNode) { + return serializeLazyID(errorId); + } + return serializeByValueID(errorId); + } + const x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical @@ -2291,14 +2327,6 @@ function renderModel( if (typeof x === 'object' && x !== null) { // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { - if (request.status === ABORTING) { - task.status = ABORTED; - const errorId: number = (request.fatalError: any); - if (wasReactNode) { - return serializeLazyID(errorId); - } - return serializeByValueID(errorId); - } // Something suspended, we'll need to create a new task and resolve it later. const newTask = createTask( request, @@ -2344,15 +2372,6 @@ function renderModel( } } - if (request.status === ABORTING) { - task.status = ABORTED; - const errorId: number = (request.fatalError: any); - if (wasReactNode) { - return serializeLazyID(errorId); - } - return serializeByValueID(errorId); - } - // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.keyPath = prevKeyPath; @@ -3820,6 +3839,22 @@ function retryTask(request: Request, task: Task): void { request.abortableTasks.delete(task); task.status = COMPLETED; } catch (thrownValue) { + if (request.status === ABORTING) { + request.abortableTasks.delete(task); + task.status = ABORTED; + if (enableHalt && request.type === PRERENDER) { + // When aborting a prerener with halt semantics we don't emit + // anything into the slot for a task that aborts, it remains unresolved + request.pendingChunks--; + } else { + // Otherwise we emit an error chunk into the task slot. + const errorId: number = (request.fatalError: any); + const model = stringify(serializeByValueID(errorId)); + emitModelChunk(request, task.id, model); + } + return; + } + const x = thrownValue === SuspenseException ? // This is a special type of exception used for Suspense. For historical @@ -3832,14 +3867,6 @@ function retryTask(request: Request, task: Task): void { if (typeof x === 'object' && x !== null) { // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { - if (request.status === ABORTING) { - request.abortableTasks.delete(task); - task.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); - return; - } // Something suspended again, let's pick it back up later. task.status = PENDING; task.thenableState = getThenableStateAfterSuspending(); @@ -3856,15 +3883,6 @@ function retryTask(request: Request, task: Task): void { } } - if (request.status === ABORTING) { - request.abortableTasks.delete(task); - task.status = ABORTED; - const errorId: number = (request.fatalError: any); - const model = stringify(serializeByValueID(errorId)); - emitModelChunk(request, task.id, model); - return; - } - request.abortableTasks.delete(task); task.status = ERRORED; const digest = logRecoverableError(request, x, task); @@ -3942,6 +3960,17 @@ function abortTask(task: Task, request: Request, errorId: number): void { request.completedErrorChunks.push(processedChunk); } +function haltTask(task: Task, request: Request): void { + if (task.status === RENDERING) { + // this task will be halted by the render + return; + } + task.status = ABORTED; + // We don't actually emit anything for this task id because we are intentionally + // leaving the reference unfulfilled. + request.pendingChunks--; +} + function flushCompletedChunks( request: Request, destination: Destination, @@ -4087,12 +4116,6 @@ export function abort(request: Request, reason: mixed): void { } const abortableTasks = request.abortableTasks; if (abortableTasks.size > 0) { - // We have tasks to abort. We'll emit one error row and then emit a reference - // to that row from every row that's still remaining if we are rendering. If we - // are prerendering (and halt semantics are enabled) we will refer to an error row - // but not actually emit it so the reciever can at that point rather than error. - const errorId = request.nextChunkId++; - request.fatalError = errorId; if ( enablePostpone && typeof reason === 'object' && @@ -4101,10 +4124,20 @@ export function abort(request: Request, reason: mixed): void { ) { const postponeInstance: Postpone = (reason: any); logPostpone(request, postponeInstance.message, null); - if (!enableHalt || request.type === PRERENDER) { - // When prerendering with halt semantics we omit the referred to postpone. + if (enableHalt && request.type === PRERENDER) { + // When prerendering with halt semantics we simply halt the task + // and leave the reference unfulfilled. + abortableTasks.forEach(task => haltTask(task, request)); + abortableTasks.clear(); + } else { + // When rendering we produce a shared postpone chunk and then + // fulfill each task with a reference to that chunk. + const errorId = request.nextChunkId++; + request.fatalError = errorId; request.pendingChunks++; emitPostponeChunk(request, errorId, postponeInstance); + abortableTasks.forEach(task => abortTask(task, request, errorId)); + abortableTasks.clear(); } } else { const error = @@ -4120,14 +4153,22 @@ export function abort(request: Request, reason: mixed): void { ) : reason; const digest = logRecoverableError(request, error, null); - if (!enableHalt || request.type === RENDER) { - // When prerendering with halt semantics we omit the referred to error. + if (enableHalt && request.type === PRERENDER) { + // When prerendering with halt semantics we simply halt the task + // and leave the reference unfulfilled. + abortableTasks.forEach(task => haltTask(task, request)); + abortableTasks.clear(); + } else { + // When rendering we produce a shared error chunk and then + // fulfill each task with a reference to that chunk. + const errorId = request.nextChunkId++; + request.fatalError = errorId; request.pendingChunks++; emitErrorChunk(request, errorId, digest, error); + abortableTasks.forEach(task => abortTask(task, request, errorId)); + abortableTasks.clear(); } } - abortableTasks.forEach(task => abortTask(task, request, errorId)); - abortableTasks.clear(); const onAllReady = request.onAllReady; onAllReady(); } From 4c2dfb3126f87fc270ad8a07d6180744d25cc585 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 20 Aug 2024 22:12:23 +0200 Subject: [PATCH 026/191] Ensure `react-dom/client` is built in Codesandbox preview builds (#30757) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4507b9b8e6c65..2bcbda538c964 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "publish-prereleases": "echo 'This command has been deprecated. Please refer to https://github.com/facebook/react/tree/main/scripts/release#trigger-an-automated-prerelease'", "download-build": "node ./scripts/release/download-experimental-build.js", "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)", - "download-build-in-codesandbox-ci": "yarn build --type=node react/index react-dom/index react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime", + "download-build-in-codesandbox-ci": "yarn build --type=node react/index react-dom/index react-dom/client react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime", "check-release-dependencies": "node ./scripts/release/check-release-dependencies", "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js", "flags": "node ./scripts/flags/flags.js" From 85180b8cf84274795986c8f2c8473f8816db8b7b Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 20 Aug 2024 13:30:51 -0700 Subject: [PATCH 027/191] [Fizz][Static] when aborting a prerender halt unfinished boundaries instead of erroring (#30732) When we introduced prerendering for flight we modeled an abort of a flight prerender as having unfinished rows. This is similar to how postpone was already implemented when you postponed from "within" a prerender using React.unstable_postpone. However when aborting with a postponed instance every boundary would be eagerly marked for client rendering which is more akin to prerendering and then resuming with an aborted signal. The insight with the flight work was that it's not so much the postpone that describes the intended semantics but the abort combined with a prerender. So like in flight when you abort a prerender and enableHalt is enabled boundaries and the shell won't error for any reason. Fizz will still call onPostpone and onError according to the abort reason but the consuemr of the prerender should expect to resume it before trying to use it. --- .../src/__tests__/ReactDOMFizzServer-test.js | 106 ++++++++++++++++++ .../src/__tests__/ReactDOMFizzStatic-test.js | 52 +++++++++ .../ReactDOMFizzStaticBrowser-test.js | 101 +++++++++++++++-- .../__tests__/ReactDOMFizzStaticNode-test.js | 98 ++++++++++++++-- packages/react-server/src/ReactFizzServer.js | 88 ++++++++++++++- 5 files changed, 424 insertions(+), 21 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 7d8707bcd3f22..5a763ffe949ab 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -7746,6 +7746,112 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableHalt + it('can resume a prerender that was aborted', async () => { + const promise = new Promise(r => {}); + + let prerendering = true; + + function Wait() { + if (prerendering) { + return React.use(promise); + } else { + return 'Hello'; + } + } + + function App() { + return ( +
+ +

+ + + + + +

+

+ + + + + +

+
+
+ ); + } + + const controller = new AbortController(); + const signal = controller.signal; + + const errors = []; + function onError(error) { + errors.push(error); + } + let pendingPrerender; + await act(() => { + pendingPrerender = ReactDOMFizzStatic.prerenderToNodeStream(, { + signal, + onError, + }); + }); + controller.abort('boom'); + + const prerendered = await pendingPrerender; + + expect(errors).toEqual(['boom', 'boom']); + + const preludeWritable = new Stream.PassThrough(); + preludeWritable.setEncoding('utf8'); + preludeWritable.on('data', chunk => { + writable.write(chunk); + }); + + await act(() => { + prerendered.prelude.pipe(preludeWritable); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

+ Loading again... +

+

+ Loading again too... +

+
, + ); + + prerendering = false; + + errors.length = 0; + const resumed = await ReactDOMFizzServer.resumeToPipeableStream( + , + JSON.parse(JSON.stringify(prerendered.postponed)), + { + onError, + }, + ); + + await act(() => { + resumed.pipe(writable); + }); + + expect(errors).toEqual([]); + expect(getVisibleChildren(container)).toEqual( +
+

+ Hello +

+

+ Hello +

+
, + ); + }); + // @gate enablePostpone it('does not call onError when you abort with a postpone instance during resume', async () => { let prerendering = true; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js index 1f3bfa7b3308d..03db4e3f5ed8f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js @@ -454,4 +454,56 @@ describe('ReactDOMFizzStatic', () => { }); expect(getVisibleChildren(container)).toEqual(undefined); }); + + // @gate enableHalt + it('will halt a prerender when aborting with an error during a render', async () => { + const controller = new AbortController(); + function App() { + controller.abort('sync'); + return
hello world
; + } + + const errors = []; + const result = await ReactDOMFizzStatic.prerenderToNodeStream(, { + signal: controller.signal, + onError(error) { + errors.push(error); + }, + }); + await act(async () => { + result.prelude.pipe(writable); + }); + expect(errors).toEqual(['sync']); + expect(getVisibleChildren(container)).toEqual(undefined); + }); + + // @gate enableHalt + it('will halt a prerender when aborting with an error in a microtask', async () => { + const errors = []; + + const controller = new AbortController(); + function App() { + React.use( + new Promise(() => { + Promise.resolve().then(() => { + controller.abort('async'); + }); + }), + ); + return
hello world
; + } + + errors.length = 0; + const result = await ReactDOMFizzStatic.prerenderToNodeStream(, { + signal: controller.signal, + onError(error) { + errors.push(error); + }, + }); + await act(async () => { + result.prelude.pipe(writable); + }); + expect(errors).toEqual(['async']); + expect(getVisibleChildren(container)).toEqual(undefined); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 7a3db48b016e3..357ef1dcb478e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -307,7 +307,8 @@ describe('ReactDOMFizzStaticBrowser', () => { }); // @gate experimental - it('should reject if aborting before the shell is complete', async () => { + // @gate !enableHalt + it('should reject if aborting before the shell is complete and enableHalt is disabled', async () => { const errors = []; const controller = new AbortController(); const promise = serverAct(() => @@ -339,6 +340,42 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(errors).toEqual(['aborted for reasons']); }); + // @gate enableHalt + it('should resolve an empty prelude if aborting before the shell is complete', async () => { + const errors = []; + const controller = new AbortController(); + const promise = serverAct(() => + ReactDOMFizzStatic.prerender( +
+ +
, + { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }, + ), + ); + + await jest.runAllTimers(); + + const theReason = new Error('aborted for reasons'); + controller.abort(theReason); + + let rejected = false; + let prelude; + try { + ({prelude} = await promise); + } catch (error) { + rejected = true; + } + expect(rejected).toBe(false); + expect(errors).toEqual(['aborted for reasons']); + const content = await readContent(prelude); + expect(content).toBe(''); + }); + // @gate experimental it('should be able to abort before something suspends', async () => { const errors = []; @@ -365,18 +402,26 @@ describe('ReactDOMFizzStaticBrowser', () => { ), ); - let caughtError = null; - try { - await streamPromise; - } catch (error) { - caughtError = error; + if (gate(flags => flags.enableHalt)) { + const {prelude} = await streamPromise; + const content = await readContent(prelude); + expect(errors).toEqual(['The operation was aborted.']); + expect(content).toBe(''); + } else { + let caughtError = null; + try { + await streamPromise; + } catch (error) { + caughtError = error; + } + expect(caughtError.message).toBe('The operation was aborted.'); + expect(errors).toEqual(['The operation was aborted.']); } - expect(caughtError.message).toBe('The operation was aborted.'); - expect(errors).toEqual(['The operation was aborted.']); }); // @gate experimental - it('should reject if passing an already aborted signal', async () => { + // @gate !enableHalt + it('should reject if passing an already aborted signal and enableHalt is disabled', async () => { const errors = []; const controller = new AbortController(); const theReason = new Error('aborted for reasons'); @@ -410,6 +455,44 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(errors).toEqual(['aborted for reasons']); }); + // @gate enableHalt + it('should resolve an empty prelude if passing an already aborted signal', async () => { + const errors = []; + const controller = new AbortController(); + const theReason = new Error('aborted for reasons'); + controller.abort(theReason); + + const promise = serverAct(() => + ReactDOMFizzStatic.prerender( +
+ Loading
}> + + +
, + { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }, + ), + ); + + // Technically we could still continue rendering the shell but currently the + // semantics mean that we also abort any pending CPU work. + let didThrow = false; + let prelude; + try { + ({prelude} = await promise); + } catch (error) { + didThrow = true; + } + expect(didThrow).toBe(false); + expect(errors).toEqual(['aborted for reasons']); + const content = await readContent(prelude); + expect(content).toBe(''); + }); + // @gate experimental it('supports custom abort reasons with a string', async () => { const promise = new Promise(r => {}); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js index ade755bdffea1..12ac4de34d684 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js @@ -212,7 +212,8 @@ describe('ReactDOMFizzStaticNode', () => { }); // @gate experimental - it('should reject if aborting before the shell is complete', async () => { + // @gate !enableHalt + it('should reject if aborting before the shell is complete and enableHalt is disabled', async () => { const errors = []; const controller = new AbortController(); const promise = ReactDOMFizzStatic.prerenderToNodeStream( @@ -242,6 +243,40 @@ describe('ReactDOMFizzStaticNode', () => { expect(errors).toEqual(['aborted for reasons']); }); + // @gate enableHalt + it('should resolve an empty shell if aborting before the shell is complete', async () => { + const errors = []; + const controller = new AbortController(); + const promise = ReactDOMFizzStatic.prerenderToNodeStream( +
+ +
, + { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }, + ); + + await jest.runAllTimers(); + + const theReason = new Error('aborted for reasons'); + controller.abort(theReason); + + let didThrow = false; + let prelude; + try { + ({prelude} = await promise); + } catch (error) { + didThrow = true; + } + expect(didThrow).toBe(false); + expect(errors).toEqual(['aborted for reasons']); + const content = await readContent(prelude); + expect(content).toBe(''); + }); + // @gate experimental it('should be able to abort before something suspends', async () => { const errors = []; @@ -266,18 +301,26 @@ describe('ReactDOMFizzStaticNode', () => { }, ); - let caughtError = null; - try { - await streamPromise; - } catch (error) { - caughtError = error; + if (gate(flags => flags.enableHalt)) { + const {prelude} = await streamPromise; + const content = await readContent(prelude); + expect(errors).toEqual(['This operation was aborted']); + expect(content).toBe(''); + } else { + let caughtError = null; + try { + await streamPromise; + } catch (error) { + caughtError = error; + } + expect(caughtError.message).toBe('This operation was aborted'); + expect(errors).toEqual(['This operation was aborted']); } - expect(caughtError.message).toBe('This operation was aborted'); - expect(errors).toEqual(['This operation was aborted']); }); // @gate experimental - it('should reject if passing an already aborted signal', async () => { + // @gate !enableHalt + it('should reject if passing an already aborted signal and enableHalt is disabled', async () => { const errors = []; const controller = new AbortController(); const theReason = new Error('aborted for reasons'); @@ -309,6 +352,43 @@ describe('ReactDOMFizzStaticNode', () => { expect(errors).toEqual(['aborted for reasons']); }); + // @gate enableHalt + it('should resolve with an empty prelude if passing an already aborted signal', async () => { + const errors = []; + const controller = new AbortController(); + const theReason = new Error('aborted for reasons'); + controller.abort(theReason); + + const promise = ReactDOMFizzStatic.prerenderToNodeStream( +
+ Loading
}> + + +
, + { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }, + ); + + // Technically we could still continue rendering the shell but currently the + // semantics mean that we also abort any pending CPU work. + + let didThrow = false; + let prelude; + try { + ({prelude} = await promise); + } catch (error) { + didThrow = true; + } + expect(didThrow).toBe(false); + expect(errors).toEqual(['aborted for reasons']); + const content = await readContent(prelude); + expect(content).toBe(''); + }); + // @gate experimental it('supports custom abort reasons with a string', async () => { const promise = new Promise(r => {}); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 17c278f2b0b52..daea492db5b8e 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -157,6 +157,7 @@ import { enableSuspenseAvoidThisFallbackFizz, enableCache, enablePostpone, + enableHalt, enableRenderableContext, enableRefAsProp, disableDefaultPropsExceptForClasses, @@ -3625,6 +3626,9 @@ function erroredTask( ) { // Report the error to a global handler. let errorDigest; + // We don't handle halts here because we only halt when prerendering and + // when prerendering we should be finishing tasks not erroring them when + // they halt or postpone if ( enablePostpone && typeof error === 'object' && @@ -3812,6 +3816,17 @@ function abortTask(task: Task, request: Request, error: mixed): void { logRecoverableError(request, fatal, errorInfo, null); fatalError(request, fatal, errorInfo, null); } + } else if ( + enableHalt && + request.trackedPostpones !== null && + segment !== null + ) { + const trackedPostpones = request.trackedPostpones; + // We are aborting a prerender and must treat the shell as halted + // We log the error but we still resolve the prerender + logRecoverableError(request, error, errorInfo, null); + trackPostpone(request, trackedPostpones, task, segment); + finishedTask(request, null, segment); } else { logRecoverableError(request, error, errorInfo, null); fatalError(request, error, errorInfo, null); @@ -3856,10 +3871,40 @@ function abortTask(task: Task, request: Request, error: mixed): void { } } else { boundary.pendingTasks--; + // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which + // boundary the message is referring to + const errorInfo = getThrownInfo(task.componentStack); + const trackedPostpones = request.trackedPostpones; if (boundary.status !== CLIENT_RENDERED) { - // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which - // boundary the message is referring to - const errorInfo = getThrownInfo(task.componentStack); + if (enableHalt) { + if (trackedPostpones !== null && segment !== null) { + // We are aborting a prerender + if ( + enablePostpone && + typeof error === 'object' && + error !== null && + error.$$typeof === REACT_POSTPONE_TYPE + ) { + const postponeInstance: Postpone = (error: any); + logPostpone(request, postponeInstance.message, errorInfo, null); + } else { + // We are aborting a prerender and must halt this boundary. + // We treat this like other postpones during prerendering + logRecoverableError(request, error, errorInfo, null); + } + trackPostpone(request, trackedPostpones, task, segment); + // If this boundary was still pending then we haven't already cancelled its fallbacks. + // We'll need to abort the fallbacks, which will also error that parent boundary. + boundary.fallbackAbortableTasks.forEach(fallbackTask => + abortTask(fallbackTask, request, error), + ); + boundary.fallbackAbortableTasks.clear(); + return finishedTask(request, boundary, segment); + } + } + boundary.status = CLIENT_RENDERED; + // We are aborting a render or resume which should put boundaries + // into an explicitly client rendered state let errorDigest; if ( enablePostpone && @@ -4145,6 +4190,43 @@ function retryRenderTask( ? request.fatalError : thrownValue; + if ( + enableHalt && + request.status === ABORTING && + request.trackedPostpones !== null + ) { + // We are aborting a prerender and need to halt this task. + const trackedPostpones = request.trackedPostpones; + const thrownInfo = getThrownInfo(task.componentStack); + task.abortSet.delete(task); + + if ( + enablePostpone && + typeof x === 'object' && + x !== null && + x.$$typeof === REACT_POSTPONE_TYPE + ) { + const postponeInstance: Postpone = (x: any); + logPostpone( + request, + postponeInstance.message, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); + } else { + logRecoverableError( + request, + x, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); + } + + trackPostpone(request, trackedPostpones, task, segment); + finishedTask(request, task.blockedBoundary, segment); + return; + } + if (typeof x === 'object' && x !== null) { // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { From e831c232787400474673051d63df4aaf6c01bdeb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 20 Aug 2024 16:40:01 -0400 Subject: [PATCH 028/191] Test infra: Support gate('enableFeatureFlag') (#30760) Shortcut for the common case where only a single flag is checked. Same as `gate(flags => flags.enableFeatureFlag)`. Normally I don't care about these types of conveniences but I'm about to add a lot more inline flag checks these all over our tests and it gets noisy. This helps a bit. --- .../transform-test-gate-pragma-test.js | 4 ++++ scripts/jest/setupTests.js | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/babel/__tests__/transform-test-gate-pragma-test.js b/scripts/babel/__tests__/transform-test-gate-pragma-test.js index f4abf54bec0e2..6250b5b4ded16 100644 --- a/scripts/babel/__tests__/transform-test-gate-pragma-test.js +++ b/scripts/babel/__tests__/transform-test-gate-pragma-test.js @@ -221,4 +221,8 @@ describe('dynamic gate method', () => { it('returns same conditions as pragma', () => { expect(gate(ctx => ctx.experimental && ctx.__DEV__)).toBe(true); }); + + it('converts string conditions to accessor function', () => { + expect(gate('experimental')).toBe(gate(flags => flags.experimental)); + }); }); diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 722df83f19533..d339c86433a41 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -205,8 +205,17 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { } }; + const coerceGateConditionToFunction = gateFnOrString => { + return typeof gateFnOrString === 'string' + ? // `gate('foo')` is treated as equivalent to `gate(flags => flags.foo)` + flags => flags[gateFnOrString] + : // Assume this is already a function + gateFnOrString; + }; + const gatedErrorMessage = 'Gated test was expected to fail, but it passed.'; - global._test_gate = (gateFn, testName, callback, timeoutMS) => { + global._test_gate = (gateFnOrString, testName, callback, timeoutMS) => { + const gateFn = coerceGateConditionToFunction(gateFnOrString); let shouldPass; try { const flags = getTestFlags(); @@ -230,7 +239,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { expectTestToFail(callback, error, timeoutMS)); } }; - global._test_gate_focus = (gateFn, testName, callback, timeoutMS) => { + global._test_gate_focus = (gateFnOrString, testName, callback, timeoutMS) => { + const gateFn = coerceGateConditionToFunction(gateFnOrString); let shouldPass; try { const flags = getTestFlags(); @@ -259,8 +269,9 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { }; // Dynamic version of @gate pragma - global.gate = fn => { + global.gate = gateFnOrString => { + const gateFn = coerceGateConditionToFunction(gateFnOrString); const flags = getTestFlags(); - return fn(flags); + return gateFn(flags); }; } From dc32c7f35ed6699e302dc7dbae17804555c669c6 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 20 Aug 2024 21:43:21 -0700 Subject: [PATCH 029/191] [Flight] use microtask for scheduling during prerenders (#30768) In https://github.com/facebook/react/pull/29491 I updated the work scheduler for Flight to use microtasks to perform work when something pings. This is useful but it does have some downsides in terms of our ability to do task prioritization. Additionally the initial work is not instantiated using a microtask which is inconsistent with how pings work. In this change I update the scheduling logic to use microtasks consistently for prerenders and use regular tasks for renders both for the initial work and pings. --- .../ReactInternalTestUtils.js | 2 +- packages/internal-test-utils/internalAct.js | 87 +++++++++++++++++++ .../src/__tests__/ReactFlightDOMEdge-test.js | 9 +- .../react-server/src/ReactFlightServer.js | 24 ++++- 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/packages/internal-test-utils/ReactInternalTestUtils.js b/packages/internal-test-utils/ReactInternalTestUtils.js index 4d2fa37890850..317a07262c5ad 100644 --- a/packages/internal-test-utils/ReactInternalTestUtils.js +++ b/packages/internal-test-utils/ReactInternalTestUtils.js @@ -16,7 +16,7 @@ import { clearErrors, createLogAssertion, } from './consoleMock'; -export {act} from './internalAct'; +export {act, serverAct} from './internalAct'; const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock'); import {thrownErrors, actingUpdatesScopeDepth} from './internalAct'; diff --git a/packages/internal-test-utils/internalAct.js b/packages/internal-test-utils/internalAct.js index 22bb92c24fc26..66fa324984507 100644 --- a/packages/internal-test-utils/internalAct.js +++ b/packages/internal-test-utils/internalAct.js @@ -192,3 +192,90 @@ export async function act(scope: () => Thenable): Thenable { } } } + +export async function serverAct(scope: () => Thenable): Thenable { + // We require every `act` call to assert console logs + // with one of the assertion helpers. Fails if not empty. + assertConsoleLogsCleared(); + + // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object + if (!jest.isMockFunction(setTimeout)) { + throw Error( + "This version of `act` requires Jest's timer mocks " + + '(i.e. jest.useFakeTimers).', + ); + } + + // Create the error object before doing any async work, to get a better + // stack trace. + const error = new Error(); + Error.captureStackTrace(error, act); + + // Call the provided scope function after an async gap. This is an extra + // precaution to ensure that our tests do not accidentally rely on the act + // scope adding work to the queue synchronously. We don't do this in the + // public version of `act`, though we maybe should in the future. + await waitForMicrotasks(); + + const errorHandlerNode = function (err: mixed) { + thrownErrors.push(err); + }; + // We track errors that were logged globally as if they occurred in this scope and then rethrow them. + if (typeof process === 'object') { + // Node environment + process.on('uncaughtException', errorHandlerNode); + } else if ( + typeof window === 'object' && + typeof window.addEventListener === 'function' + ) { + throw new Error('serverAct is not supported in JSDOM environments'); + } + + try { + const result = await scope(); + + do { + // Wait until end of current task/microtask. + await waitForMicrotasks(); + + // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object + if (jest.isEnvironmentTornDown()) { + error.message = + 'The Jest environment was torn down before `act` completed. This ' + + 'probably means you forgot to `await` an `act` call.'; + throw error; + } + + // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object + const j = jest; + if (j.getTimerCount() > 0) { + // There's a pending timer. Flush it now. We only do this in order to + // force Suspense fallbacks to display; the fact that it's a timer + // is an implementation detail. If there are other timers scheduled, + // those will also fire now, too, which is not ideal. (The public + // version of `act` doesn't do this.) For this reason, we should try + // to avoid using timers in our internal tests. + j.runOnlyPendingTimers(); + // If a committing a fallback triggers another update, it might not + // get scheduled until a microtask. So wait one more time. + await waitForMicrotasks(); + } else { + break; + } + } while (true); + + if (thrownErrors.length > 0) { + // Rethrow any errors logged by the global error handling. + const thrownError = aggregateErrors(thrownErrors); + thrownErrors.length = 0; + throw thrownError; + } + + return result; + } finally { + if (typeof process === 'object') { + // Node environment + process.off('uncaughtException', errorHandlerNode); + } + } +} 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 0cb3897aea443..27dbcc067e91a 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -23,10 +23,6 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') { // Patch for Edge environments for global scope global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage; -const { - patchMessageChannel, -} = require('../../../../scripts/jest/patchMessageChannel'); - let serverExports; let clientExports; let webpackMap; @@ -39,7 +35,6 @@ let ReactServerDOMServer; let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; -let ReactServerScheduler; let reactServerAct; function normalizeCodeLocInfo(str) { @@ -55,9 +50,7 @@ describe('ReactFlightDOMEdge', () => { beforeEach(() => { jest.resetModules(); - ReactServerScheduler = require('scheduler'); - patchMessageChannel(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + reactServerAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index efad2aa59ca81..df811a8c7fa99 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1794,7 +1794,11 @@ function pingTask(request: Request, task: Task): void { pingedTasks.push(task); if (pingedTasks.length === 1) { request.flushScheduled = request.destination !== null; - scheduleMicrotask(() => performWork(request)); + if (request.type === PRERENDER) { + scheduleMicrotask(() => performWork(request)); + } else { + scheduleWork(() => performWork(request)); + } } } @@ -4056,10 +4060,20 @@ function flushCompletedChunks( export function startWork(request: Request): void { request.flushScheduled = request.destination !== null; - if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request, performWork, request)); + if (request.type === PRERENDER) { + if (supportsRequestStorage) { + scheduleMicrotask(() => { + requestStorage.run(request, performWork, request); + }); + } else { + scheduleMicrotask(() => performWork(request)); + } } else { - scheduleWork(() => performWork(request)); + if (supportsRequestStorage) { + scheduleWork(() => requestStorage.run(request, performWork, request)); + } else { + scheduleWork(() => performWork(request)); + } } } @@ -4073,6 +4087,8 @@ function enqueueFlush(request: Request): void { request.destination !== null ) { request.flushScheduled = true; + // Unlike startWork and pingTask we intetionally use scheduleWork + // here even during prerenders to allow as much batching as possible scheduleWork(() => { request.flushScheduled = false; const destination = request.destination; From dd9117e3134f24d1aa39e405a95ab54188a017dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 21 Aug 2024 09:52:17 -0400 Subject: [PATCH 030/191] [Flight] Source Map Actions in Reference Node Loader Transforms (#30755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up to #30741. This is just for the reference Webpack implementation. If there is a source map associated with a Node ESM loader, we generate new source map entries for every `registerServerReference` call. To avoid messing too much with it, this doesn't rewrite the original mappings. It just reads them while finding each of the exports in the original mappings. We need to read all since whatever we append at the end is relative. Then we just generate new appended entries at the end. For the location I picked the location of the local name identifier. Since that's the name of the function and that gives us a source map name index. It means it jumps to the name rather than the beginning of the function declaration. It could be made more clever like finding a local function definition if it is reexported. We could also point to the line/column of the function declaration rather than the identifier but point to the name index of the identifier name. Now jumping to definition works in the fixture. Screenshot 2024-08-20 at 2 49 07 PM Unfortunately this technique doesn't seem to work in Firefox nor Safari. They don't apply the source map for jumping to the definition. --- fixtures/flight/package.json | 1 + fixtures/flight/yarn.lock | 2166 +++++++++++------ .../react-server-dom-webpack/package.json | 3 +- .../src/ReactFlightWebpackNodeLoader.js | 403 ++- scripts/rollup/modules.js | 3 + yarn.lock | 12 +- 6 files changed, 1780 insertions(+), 808 deletions(-) diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index a0505629baacf..f9b8a752d4f83 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -49,6 +49,7 @@ "react-dev-utils": "^12.0.1", "react-dom": "experimental", "react-refresh": "^0.11.0", + "react-server-dom-webpack": "experimental", "resolve": "^1.20.0", "resolve-url-loader": "^4.0.0", "sass-loader": "^12.3.0", diff --git a/fixtures/flight/yarn.lock b/fixtures/flight/yarn.lock index 927f680a6ca10..36e16f18a9984 100644 --- a/fixtures/flight/yarn.lock +++ b/fixtures/flight/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -2379,6 +2384,23 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2620,7 +2642,7 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": +"@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== @@ -2629,36 +2651,52 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== "@jridgewell/trace-mapping@^0.3.17": version "0.3.18" @@ -2668,6 +2706,22 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -2689,6 +2743,11 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@playwright/test@^1.41.2": version "1.41.2" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.2.tgz#bd9db40177f8fd442e16e14e0389d23751cdfc54" @@ -2960,10 +3019,10 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/graceful-fs@^4.1.2": version "4.1.4" @@ -3092,125 +3151,125 @@ dependencies: "@types/yargs-parser" "*" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -3231,11 +3290,6 @@ abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - accepts@~1.3.5: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3252,31 +3306,34 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== -acorn-node@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== +acorn-loose@^8.3.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" + acorn "^8.11.0" -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^7.0.0, acorn@^7.1.1: +acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1: +acorn@^8.11.0, acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +acorn@^8.2.4, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== @@ -3321,7 +3378,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv-keywords@^5.0.0, ajv-keywords@^5.1.0: +ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== @@ -3338,7 +3395,7 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0: +ajv@^8.0.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -3381,15 +3438,6 @@ ansi-html@^0.0.9: resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3418,6 +3466,16 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" @@ -3450,11 +3508,46 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.reduce@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz#6aadc2f995af29cb887eb866d981dc85ab6f7dc7" + integrity sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -3476,6 +3569,13 @@ autoprefixer@^10.4.8: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + babel-jest@^27.4.2, babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -3686,7 +3786,14 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2, braces@~3.0.2: +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -3706,17 +3813,6 @@ browserslist@^4.0.0: electron-to-chromium "^1.3.73" node-releases "^1.0.0-alpha.12" -browserslist@^4.14.5: - version "4.15.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.15.0.tgz#3d48bbca6a3f378e86102ffd017d9a03f122bdb0" - integrity sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ== - dependencies: - caniuse-lite "^1.0.30001164" - colorette "^1.2.1" - electron-to-chromium "^1.3.612" - escalade "^3.1.1" - node-releases "^1.1.67" - browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3, browserslist@^4.21.3: version "4.21.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" @@ -3727,6 +3823,16 @@ browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^ node-releases "^2.0.6" update-browserslist-db "^1.0.5" +browserslist@^4.21.10, browserslist@^4.21.4: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + browserslist@^4.21.5: version "4.21.9" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" @@ -3772,6 +3878,17 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.0" +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3813,7 +3930,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000888, caniuse-lite@^1.0.30001164, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000888, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373: version "1.0.30001457" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz" integrity sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA== @@ -3823,6 +3940,11 @@ caniuse-lite@^1.0.30001503: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz#10a343e49d31cbbfdae298ef73cb0a9f46670dc5" integrity sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A== +caniuse-lite@^1.0.30001646: + version "1.0.30001651" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138" + integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" @@ -3917,6 +4039,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3951,7 +4082,7 @@ color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" -color-name@^1.1.4, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -3960,11 +4091,6 @@ colord@^2.9.1: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - colorette@^2.0.10: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -3981,6 +4107,11 @@ commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -4092,7 +4223,7 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -4307,6 +4438,33 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@^2.16.1: version "2.29.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.1.tgz#9667c2615525e552b5135a3116b95b1961456e60" @@ -4358,6 +4516,15 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -4369,10 +4536,14 @@ define-properties@^1.1.2, define-properties@^1.1.3: dependencies: object-keys "^1.0.12" -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ== +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" delayed-stream@~1.0.0: version "1.0.0" @@ -4401,15 +4572,6 @@ detect-port-alt@^1.1.6: address "^1.0.1" debug "^2.6.0" -detective@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" - integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== - dependencies: - acorn-node "^1.8.2" - defined "^1.0.0" - minimist "^1.2.6" - didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -4531,15 +4693,15 @@ duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -electron-to-chromium@^1.3.612: - version "1.3.617" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.617.tgz#4192fa4db846c6ad51fffe3a06e71727e9699a74" - integrity sha512-yHXyI0fHnU0oLxdu21otLYpW3qwkbo8EBTpqeS9w14fwNjFy65SG6unrS3Gg+wX1JKWlAFCcNt13fG0nsCo/1A== - electron-to-chromium@^1.3.73: version "1.3.73" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.73.tgz#aa67787067d58cc3920089368b3b8d6fe0fc12f6" @@ -4554,6 +4716,11 @@ electron-to-chromium@^1.4.431: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz#761c34300603b9f1234f0b6155870d3002435db6" integrity sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw== +electron-to-chromium@^1.5.4: + version "1.5.12" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz#ee31756eaa2e06f2aa606f170b7ad06dd402b4e4" + integrity sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA== + emittery@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" @@ -4568,15 +4735,20 @@ emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -enhanced-resolve@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" - integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== +enhanced-resolve@^5.17.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -4618,22 +4790,97 @@ es-abstract@^1.12.0: string.prototype.trimleft "^2.1.0" string.prototype.trimright "^2.1.0" -es-abstract@^1.5.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" +es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" dependencies: @@ -4641,11 +4888,25 @@ es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -4754,7 +5015,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -4765,6 +5026,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4839,6 +5111,21 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.2" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340" @@ -4909,6 +5196,26 @@ function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -4928,14 +5235,16 @@ get-intrinsic@^1.0.0: has "^1.0.3" has-symbols "^1.0.1" -get-intrinsic@^1.0.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" - integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-package-type@^0.1.0: version "0.1.0" @@ -4947,6 +5256,15 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -4966,6 +5284,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@^10.3.10: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.1.1, glob@^7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -5007,6 +5337,14 @@ globals@^11.1.0: version "11.8.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globby@^11.0.4: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -5019,6 +5357,13 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -5027,6 +5372,11 @@ graceful-fs@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -5048,6 +5398,11 @@ harmony-reflect@^1.4.6: version "1.6.1" resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -5057,6 +5412,18 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -5066,17 +5433,31 @@ has-symbols@^1.0.1: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== -has-symbols@^1.0.3: +has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" dependencies: function-bind "^1.1.1" +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -5255,10 +5636,34 @@ ini@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -5266,30 +5671,36 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-callable@^1.1.3, is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" -is-core-module@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.11.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" - integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== dependencies: - has "^1.0.3" + hasown "^2.0.2" -is-core-module@^2.9.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" - integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== dependencies: - has "^1.0.3" + is-typed-array "^1.1.13" is-date-object@^1.0.1: version "1.0.1" @@ -5330,6 +5741,18 @@ is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -5346,27 +5769,70 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-root@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + is-symbol@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" dependencies: has-symbols "^1.0.0" +is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -5374,6 +5840,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5425,6 +5896,15 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" @@ -5914,7 +6394,12 @@ jest@^27.4.3: import-local "^3.0.2" jest-cli "^27.5.1" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6024,11 +6509,21 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6: +lilconfig@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== +lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -6083,10 +6578,6 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -6109,12 +6600,6 @@ lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -6122,6 +6607,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -6129,13 +6619,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -6148,11 +6631,12 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: - tmpl "1.0.x" + tmpl "1.0.5" mdn-data@2.0.14: version "2.0.14" @@ -6191,6 +6675,14 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.5: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -6219,20 +6711,34 @@ mini-css-extract-plugin@^2.4.5: dependencies: schema-utils "^4.0.0" -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.5: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mkdirp@~0.5.1: version "0.5.1" @@ -6253,11 +6759,25 @@ ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6267,7 +6787,7 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.2: +neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -6290,16 +6810,16 @@ node-releases@^1.0.0-alpha.12: dependencies: semver "^5.3.0" -node-releases@^1.1.67: - version "1.1.67" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" - integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== - node-releases@^2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + node-releases@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" @@ -6321,13 +6841,6 @@ nodemon@^2.0.19: touch "^3.1.0" undefsafe "^2.0.5" -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== - dependencies: - abbrev "1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6366,20 +6879,25 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-inspect@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" -object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== - object-keys@^1.0.11, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -6397,12 +6915,28 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz#2f1fe0606ec1a7658154ccd4f728504f69667923" + integrity sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A== + dependencies: + array.prototype.reduce "^1.0.6" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + gopd "^1.0.1" + safe-array-concat "^1.1.2" object.values@^1.1.0: version "1.1.0" @@ -6502,6 +7036,11 @@ p-try@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -6567,15 +7106,19 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -6591,6 +7134,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -6606,6 +7154,11 @@ pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -6639,6 +7192,11 @@ playwright@1.41.2: optionalDependencies: fsevents "2.3.2" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" @@ -6804,10 +7362,10 @@ postcss-image-set-function@^4.0.7: dependencies: postcss-value-parser "^4.2.0" -postcss-import@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" - integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== dependencies: postcss-value-parser "^4.0.0" read-cache "^1.0.0" @@ -6818,10 +7376,10 @@ postcss-initial@^4.0.1: resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== -postcss-js@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" - integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== dependencies: camelcase-css "^2.0.1" @@ -6833,13 +7391,13 @@ postcss-lab-function@^4.2.1: "@csstools/postcss-progressive-custom-properties" "^1.1.0" postcss-value-parser "^4.2.0" -postcss-load-config@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" - integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== dependencies: - lilconfig "^2.0.5" - yaml "^1.10.2" + lilconfig "^3.0.0" + yaml "^2.3.4" postcss-loader@^6.2.1: version "6.2.1" @@ -6938,12 +7496,12 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" - integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== dependencies: - postcss-selector-parser "^6.0.6" + postcss-selector-parser "^6.1.1" postcss-nesting@^10.1.10: version "10.1.10" @@ -7146,7 +7704,7 @@ postcss-selector-not@^6.0.1: dependencies: postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== @@ -7154,6 +7712,14 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selecto cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^6.0.2: version "6.0.4" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" @@ -7197,7 +7763,7 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.7: +postcss@^8.3.5, postcss@^8.4.4, postcss@^8.4.7: version "8.4.16" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== @@ -7206,6 +7772,15 @@ postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.23: + version "8.4.41" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681" + integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -7282,10 +7857,15 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== randombytes@^2.1.0: version "2.1.0" @@ -7339,12 +7919,11 @@ react-dev-utils@^12.0.1: text-table "^0.2.0" react-dom@experimental: - version "0.0.0-experimental-6ff1733e6-20230225" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-6ff1733e6-20230225.tgz#47c1e80f21230e6c650ba9e43d15fccd616441be" - integrity sha512-1vGCQDhmSOwBIb8QbTaSjBUysebPhl7WCcGuT6dW+HdlxFDvClL0M47K1e8kZWiuCkMUDjJaTlC4J32PgznbFw== + version "0.0.0-experimental-6ebfd5b0-20240818" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-6ebfd5b0-20240818.tgz#8a0b45fc4d54d45442e5194edee0114a8132bc82" + integrity sha512-G+RipTMyLYgSz4lST+8RFzP/Zdl8JaW0iWq5Yk9nG/rkdT7riWQrrMG9ZpRAxpRbBbaZ8WIgZqX5JFvRcJjyDQ== dependencies: - loose-envify "^1.1.0" - scheduler "0.0.0-experimental-6ff1733e6-20230225" + scheduler "0.0.0-experimental-6ebfd5b0-20240818" react-error-overlay@^6.0.11: version "6.0.11" @@ -7366,12 +7945,19 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== -react@experimental: - version "0.0.0-experimental-6ff1733e6-20230225" - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-6ff1733e6-20230225.tgz#24bca9d60c6b3597f389e8bcd07e7b853dddc917" - integrity sha512-5syUtEwwbWzDHN84xNu9C9cciLW9sY4c19fG27EU5ApS1s+i7fFtS+KtyPMHU9S4eK0u65uQU3hWMqFvqzLHhw== +react-server-dom-webpack@experimental: + version "0.0.0-experimental-6ebfd5b0-20240818" + resolved "https://registry.yarnpkg.com/react-server-dom-webpack/-/react-server-dom-webpack-0.0.0-experimental-6ebfd5b0-20240818.tgz#56df6a7a406a033897f9bdd33b649e6e956adcef" + integrity sha512-kDPLVKaSKwDpuxGKxWS4w0VEY1O0IUJVksfA39H7nENjmFCuHybZ6rvi+hlXgJcwV3TvVeISLSmf27yvZWUriQ== dependencies: - loose-envify "^1.1.0" + acorn-loose "^8.3.0" + neo-async "^2.6.1" + webpack-sources "^3.2.3" + +react@experimental: + version "0.0.0-experimental-6ebfd5b0-20240818" + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-6ebfd5b0-20240818.tgz#80ed47abae164ace0006ef5e5931383ddb8964ae" + integrity sha512-Wkw/YNSnolRlb4q2IF738W9zDLATEDe0TLnFZJBFgb3bXLQomxChEqFG1Z2upuh0nhdnlJylCN2Q8ammQsbQLg== read-cache@^1.0.0: version "1.0.0" @@ -7388,11 +7974,11 @@ readdirp@~3.6.0: picomatch "^2.2.1" recursive-readdir@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== dependencies: - minimatch "3.0.4" + minimatch "^3.0.5" redent@^3.0.0: version "3.0.0" @@ -7402,88 +7988,65 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -regenerate-unicode-properties@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" - integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== dependencies: regenerate "^1.4.2" -regenerate-unicode-properties@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" +regenerate-unicode-properties@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" + integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: 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.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -regenerator-transform@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" - integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== - dependencies: - "@babel/runtime" "^7.8.4" - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== +regenerator-transform@^0.15.0, regenerator-transform@^0.15.1: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== dependencies: "@babel/runtime" "^7.8.4" regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.0.tgz#4bb61461b1a19b8b913f3960364bb57887f920ee" + integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg== -regexpu-core@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.1.0" - regjsgen "^0.5.0" - regjsparser "^0.6.0" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.1.0" + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" -regexpu-core@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d" - integrity sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA== +regexpu-core@^4.6.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" + integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.0.1" - regjsgen "^0.6.0" - regjsparser "^0.8.2" + regenerate-unicode-properties "^9.0.0" + regjsgen "^0.5.2" + regjsparser "^0.7.0" unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.0.0" -regexpu-core@^5.3.1: +regexpu-core@^5.1.0, regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== @@ -7495,25 +8058,15 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" -regjsgen@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - -regjsgen@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" - integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== - -regjsparser@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" - dependencies: - jsesc "~0.5.0" +regjsgen@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== -regjsparser@^0.8.2: - version "0.8.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" - integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== +regjsparser@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" + integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== dependencies: jsesc "~0.5.0" @@ -7527,7 +8080,7 @@ regjsparser@^0.9.1: relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== renderkid@^3.0.0: version "3.0.0" @@ -7543,12 +8096,18 @@ renderkid@^3.0.0: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -7559,6 +8118,7 @@ resolve-cwd@^3.0.0: resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" @@ -7577,42 +8137,19 @@ resolve-url-loader@^4.0.0: source-map "0.6.1" resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== - -resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^1.14.2: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== - dependencies: - is-core-module "^2.1.0" - path-parse "^1.0.6" + version "1.1.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== -resolve@^1.19.0: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== +resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.3.2: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.11.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.3.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - dependencies: - path-parse "^1.0.5" - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -7626,24 +8163,52 @@ rimraf@^3.0.0: glob "^7.1.3" run-parallel@^1.1.9: - version "1.1.10" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" - integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" rxjs@^7.0.0: - version "7.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" - integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" -safe-buffer@5.1.2, safe-buffer@^5.1.0, safe-buffer@~5.1.1: +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.1.2, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sanitize.css@*: version "13.0.0" @@ -7661,6 +8226,7 @@ sass-loader@^12.3.0: sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== saxes@^5.0.1: version "5.0.1" @@ -7669,12 +8235,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@0.0.0-experimental-6ff1733e6-20230225: - version "0.0.0-experimental-6ff1733e6-20230225" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-6ff1733e6-20230225.tgz#9de04947f82afa784de799aa09aaad92ee67cb19" - integrity sha512-YDyRM4ohU6NmzsbiJ/UUdRB4ulz27n+3Su8LZ8cENrQDuu2us3sPN+y8CMGeqaNYJlS0GQTHNcLa2LZIK9BP5Q== - dependencies: - loose-envify "^1.1.0" +scheduler@0.0.0-experimental-6ebfd5b0-20240818: + version "0.0.0-experimental-6ebfd5b0-20240818" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-6ebfd5b0-20240818.tgz#583c91937f8fdde51726cf1735cdb323eb86adf2" + integrity sha512-/a4bME9pxUZOLPL8ktBU4JtqYXPk2lRslukGB93gWygXbSoNpdVfVFoSyiUlLk1bDXN0iV5NaKv3PWHOhmOn+w== schema-utils@2.7.0: version "2.7.0" @@ -7694,7 +8258,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -7703,17 +8267,7 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.8.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" - -schema-utils@^4.2.0: +schema-utils@^4.0.0, schema-utils@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== @@ -7728,39 +8282,49 @@ semver@7.0.0, semver@~7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.3.0, semver@^5.4.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" - -semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^5.3.0, semver@^5.4.1, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@^7.3.2, semver@^7.3.5: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: - lru-cache "^6.0.0" + randombytes "^2.1.0" -semver@^7.3.5: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - lru-cache "^6.0.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" -serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: - randombytes "^2.1.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" setprototypeof@1.2.0: version "1.2.0" @@ -7780,40 +8344,38 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" - integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" -signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-update-notifier@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" - integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== + version "1.1.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" + integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== dependencies: semver "~7.0.0" -sisteransi@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" - -sisteransi@^1.0.5: +sisteransi@^1.0.3, sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== @@ -7821,6 +8383,7 @@ sisteransi@^1.0.5: slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^4.0.0: version "4.0.0" @@ -7832,28 +8395,21 @@ source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.1, source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== source-map-loader@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" - integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.2.tgz#af23192f9b344daa729f6772933194cc5fa54fee" + integrity sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg== dependencies: abab "^2.0.5" iconv-lite "^0.6.3" source-map-js "^1.0.1" -source-map-support@^0.5.6: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.20: +source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -7864,10 +8420,12 @@ source-map-support@~0.5.20: source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== source-map@^0.7.3: version "0.7.4" @@ -7875,22 +8433,24 @@ source-map@^0.7.3: integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg== + version "0.0.2" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" - integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" @@ -7910,9 +8470,9 @@ streamsearch@^1.1.0: integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== string-length@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" - integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" strip-ansi "^6.0.0" @@ -7925,24 +8485,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -string-width@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^5.2.0" - -string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7951,34 +8494,62 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.3, string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimleft@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + version "2.1.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.3.tgz#dee305118117d0a1843c1fc0d38d5d0754d83c60" + integrity sha512-699Ibssmj/awVzvdNk4g83/Iu8U9vDohzmA/ly2BrQWGhamuY4Tlvs5XKmKliDt3ky6SKbE1bzPhASKCFlx9Sg== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - function-bind "^1.1.1" + string.prototype.trimstart "^1.0.3" string.prototype.trimright@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + version "2.1.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.3.tgz#dc16a21d7456cbc8b2c54d47fe01f06d9efe94eb" + integrity sha512-hoOq56oRFnnfDuXNy2lGHiwT77MehHv9d0zGfRZ8QdC+4zjrkFB9vd5i/zYTd/ymFBd4YxtbdgHt3U6ksGeuBw== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - function-bind "^1.1.1" - -strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - dependencies: - ansi-regex "^4.1.0" + string.prototype.trimend "^1.0.3" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +string.prototype.trimstart@^1.0.3, string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - ansi-regex "^5.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7986,9 +8557,9 @@ strip-ansi@^6.0.1: ansi-regex "^5.0.1" strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: ansi-regex "^6.0.1" @@ -8015,21 +8586,35 @@ strip-json-comments@^3.1.1: integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" - integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + version "3.3.4" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" + integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== stylehacks@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" - integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q== + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" postcss-selector-parser "^6.0.4" +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" @@ -8048,9 +8633,9 @@ supports-color@^8.0.0, supports-color@^8.1.0: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" - integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -8068,6 +8653,7 @@ svg-parser@^2.0.2: svgo@^1.2.2: version "1.3.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== dependencies: chalk "^2.4.1" coa "^2.0.2" @@ -8102,36 +8688,37 @@ symbol-tree@^3.2.4: integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tailwindcss@^3.0.2: - version "3.1.8" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.8.tgz#4f8520550d67a835d32f2f4021580f9fddb7b741" - integrity sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g== + version "3.4.10" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.10.tgz#70442d9aeb78758d1f911af29af8255ecdb8ffef" + integrity sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w== dependencies: + "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" chokidar "^3.5.3" - color-name "^1.1.4" - detective "^5.2.1" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.11" + fast-glob "^3.3.0" glob-parent "^6.0.2" is-glob "^4.0.3" - lilconfig "^2.0.6" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" normalize-path "^3.0.0" object-hash "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.14" - postcss-import "^14.1.0" - postcss-js "^4.0.0" - postcss-load-config "^3.1.4" - postcss-nested "5.0.6" - postcss-selector-parser "^6.0.10" - postcss-value-parser "^4.2.0" - quick-lru "^5.1.1" - resolve "^1.22.1" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" tapable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c" + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" @@ -8146,24 +8733,24 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: - version "5.3.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.5.tgz#f7d82286031f915a4f8fb81af4bd35d2e3c011bc" - integrity sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA== +terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.14" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - terser "^5.14.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" -terser@^5.10.0, terser@^5.14.1: - version "5.14.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" - integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== +terser@^5.10.0, terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -8179,19 +8766,36 @@ test-exclude@^6.0.0: text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" throat@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" - integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== + version "6.0.2" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe" + integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -8206,27 +8810,19 @@ toidentifier@1.0.1: integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== tough-cookie@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" - integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" - universalify "^0.1.2" - -tr46@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" - integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== - dependencies: - punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" tr46@^2.1.0: version "2.1.0" @@ -8240,23 +8836,25 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -tslib@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.0.3, tslib@^2.1.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" @@ -8273,6 +8871,7 @@ type-fest@^0.11.0: type-fest@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== type-is@~1.6.18: version "1.6.18" @@ -8282,6 +8881,50 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -8289,34 +8932,33 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== undici@^5.20.0: - version "5.20.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" - integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== dependencies: - busboy "^1.6.0" - -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + "@fastify/busboy" "^2.0.0" unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - unicode-match-property-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" @@ -8325,66 +8967,48 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" - -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== - -unicode-match-property-value-ecmascript@^2.1.0: +unicode-match-property-value-ecmascript@^2.0.0, unicode-match-property-value-ecmascript@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== -unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - unicode-property-aliases-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" - integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== -universalify@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -update-browserslist-db@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" - integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== +update-browserslist-db@^1.0.11, update-browserslist-db@^1.0.5, update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" @@ -8393,20 +9017,33 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== util.promisify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== v8-to-istanbul@^8.1.0: version "8.1.1" @@ -8437,15 +9074,16 @@ w3c-xmlserializer@^2.0.0: xml-name-validator "^3.0.0" walker@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: - makeerror "1.0.x" + makeerror "1.0.12" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -8472,9 +9110,9 @@ webpack-dev-middleware@^5.3.4: schema-utils "^4.0.0" webpack-hot-middleware@^2.25.3: - version "2.25.3" - resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz#be343ce2848022cfd854dd82820cd730998c6794" - integrity sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA== + version "2.26.1" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz#87214f1e3f9f3acab9271fef9e6ed7b637d719c0" + integrity sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A== dependencies: ansi-html-community "0.0.8" html-entities "^2.1.0" @@ -8502,55 +9140,48 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.74.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" - integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== + version "5.93.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5" + integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.17.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^8.0.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" - integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^2.0.2" - webidl-conversions "^6.1.0" - -whatwg-url@^8.5.0: +whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== @@ -8559,9 +9190,32 @@ whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" @@ -8575,8 +9229,9 @@ which@^2.0.1: wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8585,9 +9240,19 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3" @@ -8600,24 +9265,20 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.6: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -8628,27 +9289,22 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0, yaml@^1.7.2: - version "1.10.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" - integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== - -yaml@^1.10.2: +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" + integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: +yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -8667,17 +9323,17 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.3.1: - version "17.5.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" - integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^21.0.0" + yargs-parser "^21.1.1" yocto-queue@^0.1.0: version "0.1.0" diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index 7a1fe29d4d4a9..f0c33a7441162 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -106,6 +106,7 @@ }, "dependencies": { "acorn-loose": "^8.3.0", - "neo-async": "^2.6.1" + "neo-async": "^2.6.1", + "webpack-sources": "^3.2.0" } } diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index 5dab530965bc9..9799acc3a07b2 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -9,6 +9,9 @@ import * as acorn from 'acorn-loose'; +import readMappings from 'webpack-sources/lib/helpers/readMappings.js'; +import createMappingsSerializer from 'webpack-sources/lib/helpers/createMappingsSerializer.js'; + type ResolveContext = { conditions: Array, parentURL: string | void, @@ -95,45 +98,102 @@ export async function getSource( return defaultGetSource(url, context, defaultGetSource); } -function addLocalExportedNames(names: Map, node: any) { +type ExportedEntry = { + localName: string, + exportedName: string, + type: null | string, + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, + originalLine: number, + originalColumn: number, + originalSource: number, + nameIndex: number, +}; + +function addExportedEntry( + exportedEntries: Array, + localNames: Set, + localName: string, + exportedName: string, + type: null | 'function', + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, +) { + if (localNames.has(localName)) { + // If the same local name is exported more than once, we only need one of the names. + return; + } + exportedEntries.push({ + localName, + exportedName, + type, + loc, + originalLine: -1, + originalColumn: -1, + originalSource: -1, + nameIndex: -1, + }); +} + +function addLocalExportedNames( + exportedEntries: Array, + localNames: Set, + node: any, +) { switch (node.type) { case 'Identifier': - names.set(node.name, node.name); + addExportedEntry( + exportedEntries, + localNames, + node.name, + node.name, + null, + node.loc, + ); return; case 'ObjectPattern': for (let i = 0; i < node.properties.length; i++) - addLocalExportedNames(names, node.properties[i]); + addLocalExportedNames(exportedEntries, localNames, node.properties[i]); return; case 'ArrayPattern': for (let i = 0; i < node.elements.length; i++) { const element = node.elements[i]; - if (element) addLocalExportedNames(names, element); + if (element) + addLocalExportedNames(exportedEntries, localNames, element); } return; case 'Property': - addLocalExportedNames(names, node.value); + addLocalExportedNames(exportedEntries, localNames, node.value); return; case 'AssignmentPattern': - addLocalExportedNames(names, node.left); + addLocalExportedNames(exportedEntries, localNames, node.left); return; case 'RestElement': - addLocalExportedNames(names, node.argument); + addLocalExportedNames(exportedEntries, localNames, node.argument); return; case 'ParenthesizedExpression': - addLocalExportedNames(names, node.expression); + addLocalExportedNames(exportedEntries, localNames, node.expression); return; } } function transformServerModule( source: string, - body: any, + program: any, url: string, + sourceMap: any, loader: LoadFunction, ): string { - // If the same local name is exported more than once, we only need one of the names. - const localNames: Map = new Map(); - const localTypes: Map = new Map(); + const body = program.body; + + // This entry list needs to be in source location order. + const exportedEntries: Array = []; + // Dedupe set. + const localNames: Set = new Set(); for (let i = 0; i < body.length; i++) { const node = body[i]; @@ -143,11 +203,24 @@ function transformServerModule( break; case 'ExportDefaultDeclaration': if (node.declaration.type === 'Identifier') { - localNames.set(node.declaration.name, 'default'); + addExportedEntry( + exportedEntries, + localNames, + node.declaration.name, + 'default', + null, + node.declaration.loc, + ); } else if (node.declaration.type === 'FunctionDeclaration') { if (node.declaration.id) { - localNames.set(node.declaration.id.name, 'default'); - localTypes.set(node.declaration.id.name, 'function'); + addExportedEntry( + exportedEntries, + localNames, + node.declaration.id.name, + 'default', + 'function', + node.declaration.id.loc, + ); } else { // TODO: This needs to be rewritten inline because it doesn't have a local name. } @@ -158,41 +231,230 @@ function transformServerModule( if (node.declaration.type === 'VariableDeclaration') { const declarations = node.declaration.declarations; for (let j = 0; j < declarations.length; j++) { - addLocalExportedNames(localNames, declarations[j].id); + addLocalExportedNames( + exportedEntries, + localNames, + declarations[j].id, + ); } } else { const name = node.declaration.id.name; - localNames.set(name, name); - if (node.declaration.type === 'FunctionDeclaration') { - localTypes.set(name, 'function'); - } + addExportedEntry( + exportedEntries, + localNames, + name, + name, + + node.declaration.type === 'FunctionDeclaration' + ? 'function' + : null, + node.declaration.id.loc, + ); } } if (node.specifiers) { const specifiers = node.specifiers; for (let j = 0; j < specifiers.length; j++) { const specifier = specifiers[j]; - localNames.set(specifier.local.name, specifier.exported.name); + addExportedEntry( + exportedEntries, + localNames, + specifier.local.name, + specifier.exported.name, + null, + specifier.local.loc, + ); } } continue; } } - if (localNames.size === 0) { - return source; - } - let newSrc = source + '\n\n;'; - newSrc += - 'import {registerServerReference} from "react-server-dom-webpack/server";\n'; - localNames.forEach(function (exported, local) { - if (localTypes.get(local) !== 'function') { - // We first check if the export is a function and if so annotate it. - newSrc += 'if (typeof ' + local + ' === "function") '; + + let mappings = + sourceMap && typeof sourceMap.mappings === 'string' + ? sourceMap.mappings + : ''; + let newSrc = source; + + if (exportedEntries.length > 0) { + let lastSourceIndex = 0; + let lastOriginalLine = 0; + let lastOriginalColumn = 0; + let lastNameIndex = 0; + let sourceLineCount = 0; + let lastMappedLine = 0; + + if (sourceMap) { + // We iterate source mapping entries and our matched exports in parallel to source map + // them to their original location. + let nextEntryIdx = 0; + let nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + let nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + readMappings( + mappings, + ( + generatedLine: number, + generatedColumn: number, + sourceIndex: number, + originalLine: number, + originalColumn: number, + nameIndex: number, + ) => { + if ( + generatedLine > nextEntryLine || + (generatedLine === nextEntryLine && + generatedColumn > nextEntryColumn) + ) { + // We're past the entry which means that the best match we have is the previous entry. + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } else { + // Skip if we didn't have any mappings on the exported line. + } + nextEntryIdx++; + if (nextEntryIdx < exportedEntries.length) { + nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + } else { + nextEntryLine = -1; + nextEntryColumn = -1; + } + } + lastMappedLine = generatedLine; + if (sourceIndex > -1) { + lastSourceIndex = sourceIndex; + } + if (originalLine > -1) { + lastOriginalLine = originalLine; + } + if (originalColumn > -1) { + lastOriginalColumn = originalColumn; + } + if (nameIndex > -1) { + lastNameIndex = nameIndex; + } + }, + ); + if (nextEntryIdx < exportedEntries.length) { + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } + } + + for ( + let lastIdx = mappings.length - 1; + lastIdx >= 0 && mappings[lastIdx] === ';'; + lastIdx-- + ) { + // If the last mapped lines don't contain any segments, we don't get a callback from readMappings + // so we need to pad the number of mapped lines, with one for each empty line. + lastMappedLine++; + } + + sourceLineCount = program.loc.end.line; + if (sourceLineCount < lastMappedLine) { + throw new Error( + 'The source map has more mappings than there are lines.', + ); + } + // If the original source string had more lines than there are mappings in the source map. + // Add some extra padding of unmapped lines so that any lines that we add line up. + for ( + let extraLines = sourceLineCount - lastMappedLine; + extraLines > 0; + extraLines-- + ) { + mappings += ';'; + } + } else { + // If a file doesn't have a source map then we generate a blank source map that just + // contains the original content and segments pointing to the original lines. + sourceLineCount = 1; + let idx = -1; + while ((idx = source.indexOf('\n', idx + 1)) !== -1) { + sourceLineCount++; + } + mappings = 'AAAA' + ';AACA'.repeat(sourceLineCount - 1); + sourceMap = { + version: 3, + sources: [url], + sourcesContent: [source], + mappings: mappings, + sourceRoot: '', + }; + lastSourceIndex = 0; + lastOriginalLine = sourceLineCount; + lastOriginalColumn = 0; + lastNameIndex = -1; + lastMappedLine = sourceLineCount; + + for (let i = 0; i < exportedEntries.length; i++) { + // Point each entry to original location. + const entry = exportedEntries[i]; + entry.originalSource = 0; + entry.originalLine = entry.loc.start.line; + // We use column zero since we do the short-hand line-only source maps above. + entry.originalColumn = 0; // entry.loc.start.column; + } } - newSrc += 'registerServerReference(' + local + ','; - newSrc += JSON.stringify(url) + ','; - newSrc += JSON.stringify(exported) + ');\n'; - }); + + newSrc += '\n\n;'; + newSrc += + 'import {registerServerReference} from "react-server-dom-webpack/server";\n'; + if (mappings) { + mappings += ';;'; + } + + const createMapping = createMappingsSerializer(); + + // Create an empty mapping pointing to where we last left off to reset the counters. + let generatedLine = 1; + createMapping( + generatedLine, + 0, + lastSourceIndex, + lastOriginalLine, + lastOriginalColumn, + lastNameIndex, + ); + for (let i = 0; i < exportedEntries.length; i++) { + const entry = exportedEntries[i]; + generatedLine++; + if (entry.type !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + entry.localName + ' === "function") '; + } + newSrc += 'registerServerReference(' + entry.localName + ','; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(entry.exportedName) + ');\n'; + + mappings += createMapping( + generatedLine, + 0, + entry.originalSource, + entry.originalLine, + entry.originalColumn, + entry.nameIndex, + ); + } + } + + if (sourceMap) { + // Override with an new mappings and serialize an inline source map. + sourceMap.mappings = mappings; + newSrc += + '//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + + Buffer.from(JSON.stringify(sourceMap)).toString('base64'); + } + return newSrc; } @@ -307,10 +569,13 @@ async function parseExportNamesInto( } async function transformClientModule( - body: any, + program: any, url: string, + sourceMap: any, loader: LoadFunction, ): Promise { + const body = program.body; + const names: Array = []; await parseExportNamesInto(body, names, url, loader); @@ -351,6 +616,9 @@ async function transformClientModule( newSrc += JSON.stringify(url) + ','; newSrc += JSON.stringify(name) + ');\n'; } + + // TODO: Generate source maps for Client Reference functions so they can point to their + // original locations. return newSrc; } @@ -391,12 +659,36 @@ async function transformModuleIfNeeded( return source; } - let body; + let sourceMappingURL = null; + let sourceMappingStart = 0; + let sourceMappingEnd = 0; + let sourceMappingLines = 0; + + let program; try { - body = acorn.parse(source, { + program = acorn.parse(source, { ecmaVersion: '2024', sourceType: 'module', - }).body; + locations: true, + onComment( + block: boolean, + text: string, + start: number, + end: number, + startLoc: {line: number, column: number}, + endLoc: {line: number, column: number}, + ) { + if ( + text.startsWith('# sourceMappingURL=') || + text.startsWith('@ sourceMappingURL=') + ) { + sourceMappingURL = text.slice(19); + sourceMappingStart = start; + sourceMappingEnd = end; + sourceMappingLines = endLoc.line - startLoc.line; + } + }, + }); } catch (x) { // eslint-disable-next-line react-internal/no-production-logging console.error('Error parsing %s %s', url, x.message); @@ -405,6 +697,8 @@ async function transformModuleIfNeeded( let useClient = false; let useServer = false; + + const body = program.body; for (let i = 0; i < body.length; i++) { const node = body[i]; if (node.type !== 'ExpressionStatement' || !node.directive) { @@ -428,11 +722,38 @@ async function transformModuleIfNeeded( ); } + let sourceMap = null; + if (sourceMappingURL) { + const sourceMapResult = await loader( + sourceMappingURL, + // $FlowFixMe + { + format: 'json', + conditions: [], + importAssertions: {type: 'json'}, + importAttributes: {type: 'json'}, + }, + loader, + ); + const sourceMapString = + typeof sourceMapResult.source === 'string' + ? sourceMapResult.source + : // $FlowFixMe + sourceMapResult.source.toString('utf8'); + sourceMap = JSON.parse(sourceMapString); + + // Strip the source mapping comment. We'll re-add it below if needed. + source = + source.slice(0, sourceMappingStart) + + '\n'.repeat(sourceMappingLines) + + source.slice(sourceMappingEnd); + } + if (useClient) { - return transformClientModule(body, url, loader); + return transformClientModule(program, url, sourceMap, loader); } - return transformServerModule(source, body, url, loader); + return transformServerModule(source, program, url, sourceMap, loader); } export async function transformSource( diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index e1ebc1c945a0d..2afa0cc98b100 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -22,6 +22,9 @@ const importSideEffects = Object.freeze({ 'react-dom': HAS_NO_SIDE_EFFECTS_ON_IMPORT, url: HAS_NO_SIDE_EFFECTS_ON_IMPORT, ReactNativeInternalFeatureFlags: HAS_NO_SIDE_EFFECTS_ON_IMPORT, + 'webpack-sources/lib/helpers/createMappingsSerializer.js': + HAS_NO_SIDE_EFFECTS_ON_IMPORT, + 'webpack-sources/lib/helpers/readMappings.js': HAS_NO_SIDE_EFFECTS_ON_IMPORT, }); // Bundles exporting globals that other modules rely on. diff --git a/yarn.lock b/yarn.lock index 3ecf3e738e190..473020cfa1fd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11347,11 +11347,6 @@ lodash.omitby@4.6.0: resolved "https://registry.yarnpkg.com/lodash.omitby/-/lodash.omitby-4.6.0.tgz#5c15ff4754ad555016b53c041311e8f079204791" integrity sha1-XBX/R1StVVAWtTwEExHo8HkgR5E= -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= - lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -11603,11 +11598,6 @@ memfs@^3.4.3: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== -memoize-one@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" - integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== - memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -16499,7 +16489,7 @@ webpack-merge@^5.7.3: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@^3.2.3: +webpack-sources@^3.2.0, webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== From 1228a28398bbf7b4d15cd148496853342e63c041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 21 Aug 2024 09:58:31 -0400 Subject: [PATCH 031/191] Remove turbopack unbundled/register/loader (#30756) The unbundled form is just a way to show case a prototype for how an unbundled version of RSC can work. It's not really intended for every bundler combination to provide such a configuration. There's no configuration of Turbopack that supports this mode atm and possibly never will be since it's more of an integrated server/client experience. This removes the unbundled form and node register/loaders from the turbopack build. --- ...ClientConfig.dom-node-turbopack-bundled.js | 16 - ...ctFlightClientConfig.dom-node-turbopack.js | 3 +- .../client.node.unbundled.js | 10 - .../esm/package.json | 3 - ...er-dom-turbopack-node-loader.production.js | 10 - .../node-register.js | 10 - .../npm/client.node.unbundled.js | 7 - .../npm/esm/package.json | 3 - .../npm/node-register.js | 3 - .../npm/server.node.unbundled.js | 18 - .../npm/static.node.unbundled.js | 12 - .../react-server-dom-turbopack/package.json | 30 +- .../server.node.unbundled.js | 20 - .../src/ReactFlightTurbopackNodeLoader.js | 483 ------------------ .../src/ReactFlightTurbopackNodeRegister.js | 109 ---- .../__tests__/ReactFlightTurbopackDOM-test.js | 4 +- .../ReactFlightTurbopackDOMBrowser-test.js | 2 +- .../ReactFlightTurbopackDOMEdge-test.js | 2 +- .../ReactFlightTurbopackDOMForm-test.js | 147 ------ .../ReactFlightTurbopackDOMNode-test.js | 2 +- .../ReactFlightTurbopackDOMReply-test.js | 2 +- .../ReactFlightTurbopackDOMReplyEdge-test.js | 2 +- .../src/__tests__/utils/TurbopackMock.js | 62 +-- .../react-flight-dom-server.node.unbundled.js | 21 - ...flight-dom-server.node.unbundled.stable.js | 20 - .../static.node.unbundled.js | 10 - scripts/rollup/bundles.js | 50 -- scripts/shared/inlinedHostConfigs.js | 43 -- 28 files changed, 38 insertions(+), 1066 deletions(-) delete mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js delete mode 100644 packages/react-server-dom-turbopack/client.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/esm/package.json delete mode 100644 packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.js delete mode 100644 packages/react-server-dom-turbopack/node-register.js delete mode 100644 packages/react-server-dom-turbopack/npm/client.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/npm/esm/package.json delete mode 100644 packages/react-server-dom-turbopack/npm/node-register.js delete mode 100644 packages/react-server-dom-turbopack/npm/server.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/npm/static.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/server.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js delete mode 100644 packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js delete mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js delete mode 100644 packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js delete mode 100644 packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js delete mode 100644 packages/react-server-dom-turbopack/static.node.unbundled.js diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js deleted file mode 100644 index f4226a93d86bc..0000000000000 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js +++ /dev/null @@ -1,16 +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. - * - * @flow - */ - -export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactClientConsoleConfigServer'; -export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack'; -export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer'; -export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer'; -export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; -export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js index b6f2b77dd3686..f4226a93d86bc 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js @@ -9,7 +9,8 @@ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; -export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer'; export * from 'react-server-dom-turbopack/src/client/ReactFlightClientConfigTargetTurbopackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-server-dom-turbopack/client.node.unbundled.js b/packages/react-server-dom-turbopack/client.node.unbundled.js deleted file mode 100644 index c2e364f42f133..0000000000000 --- a/packages/react-server-dom-turbopack/client.node.unbundled.js +++ /dev/null @@ -1,10 +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. - * - * @flow - */ - -export * from './src/client/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-turbopack/esm/package.json b/packages/react-server-dom-turbopack/esm/package.json deleted file mode 100644 index 3dbc1ca591c05..0000000000000 --- a/packages/react-server-dom-turbopack/esm/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.js b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.js deleted file mode 100644 index ef6486656cafe..0000000000000 --- a/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.js +++ /dev/null @@ -1,10 +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. - * - * @flow - */ - -export * from '../src/ReactFlightTurbopackNodeLoader.js'; diff --git a/packages/react-server-dom-turbopack/node-register.js b/packages/react-server-dom-turbopack/node-register.js deleted file mode 100644 index 0d399f3842731..0000000000000 --- a/packages/react-server-dom-turbopack/node-register.js +++ /dev/null @@ -1,10 +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. - * - * @flow - */ - -module.exports = require('./src/ReactFlightTurbopackNodeRegister'); diff --git a/packages/react-server-dom-turbopack/npm/client.node.unbundled.js b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js deleted file mode 100644 index 04eb5b1972bed..0000000000000 --- a/packages/react-server-dom-turbopack/npm/client.node.unbundled.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.production.js'); -} else { - module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.development.js'); -} diff --git a/packages/react-server-dom-turbopack/npm/esm/package.json b/packages/react-server-dom-turbopack/npm/esm/package.json deleted file mode 100644 index 3dbc1ca591c05..0000000000000 --- a/packages/react-server-dom-turbopack/npm/esm/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/react-server-dom-turbopack/npm/node-register.js b/packages/react-server-dom-turbopack/npm/node-register.js deleted file mode 100644 index 7506743f033fa..0000000000000 --- a/packages/react-server-dom-turbopack/npm/node-register.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('./cjs/react-server-dom-turbopack-node-register.js'); diff --git a/packages/react-server-dom-turbopack/npm/server.node.unbundled.js b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js deleted file mode 100644 index 1e8648ccef133..0000000000000 --- a/packages/react-server-dom-turbopack/npm/server.node.unbundled.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var s; -if (process.env.NODE_ENV === 'production') { - s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.js'); -} else { - s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js'); -} - -exports.renderToPipeableStream = s.renderToPipeableStream; -exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; -exports.decodeReply = s.decodeReply; -exports.decodeAction = s.decodeAction; -exports.decodeFormState = s.decodeFormState; -exports.registerServerReference = s.registerServerReference; -exports.registerClientReference = s.registerClientReference; -exports.createClientModuleProxy = s.createClientModuleProxy; -exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; diff --git a/packages/react-server-dom-turbopack/npm/static.node.unbundled.js b/packages/react-server-dom-turbopack/npm/static.node.unbundled.js deleted file mode 100644 index e77863bf36a60..0000000000000 --- a/packages/react-server-dom-turbopack/npm/static.node.unbundled.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -var s; -if (process.env.NODE_ENV === 'production') { - s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.js'); -} else { - s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js'); -} - -if (s.prerenderToNodeStream) { - exports.prerenderToNodeStream = s.prerenderToNodeStream; -} diff --git a/packages/react-server-dom-turbopack/package.json b/packages/react-server-dom-turbopack/package.json index 93cd7d37a04ae..3b638137612da 100644 --- a/packages/react-server-dom-turbopack/package.json +++ b/packages/react-server-dom-turbopack/package.json @@ -16,20 +16,15 @@ "client.browser.js", "client.edge.js", "client.node.js", - "client.node.unbundled.js", "server.js", "server.browser.js", "server.edge.js", "server.node.js", - "server.node.unbundled.js", "static.js", "static.browser.js", "static.edge.js", "static.node.js", - "static.node.unbundled.js", - "node-register.js", - "cjs/", - "esm/" + "cjs/" ], "exports": { ".": "./index.js", @@ -37,11 +32,7 @@ "workerd": "./client.edge.js", "deno": "./client.edge.js", "worker": "./client.edge.js", - "node": { - "turbopack": "./client.node.js", - "webpack": "./client.node.js", - "default": "./client.node.unbundled.js" - }, + "node": "./client.node.js", "edge-light": "./client.edge.js", "browser": "./client.browser.js", "default": "./client.browser.js" @@ -49,16 +40,11 @@ "./client.browser": "./client.browser.js", "./client.edge": "./client.edge.js", "./client.node": "./client.node.js", - "./client.node.unbundled": "./client.node.unbundled.js", "./server": { "react-server": { "workerd": "./server.edge.js", "deno": "./server.browser.js", - "node": { - "turbopack": "./server.node.js", - "webpack": "./server.node.js", - "default": "./server.node.unbundled.js" - }, + "node": "./server.node.js", "edge-light": "./server.edge.js", "browser": "./server.browser.js" }, @@ -67,16 +53,11 @@ "./server.browser": "./server.browser.js", "./server.edge": "./server.edge.js", "./server.node": "./server.node.js", - "./server.node.unbundled": "./server.node.unbundled.js", "./static": { "react-server": { "workerd": "./static.edge.js", "deno": "./static.browser.js", - "node": { - "turbopack": "./static.node.js", - "webpack": "./static.node.js", - "default": "./static.node.unbundled.js" - }, + "node": "./static.node.js", "edge-light": "./static.edge.js", "browser": "./static.browser.js" }, @@ -85,9 +66,6 @@ "./static.browser": "./static.browser.js", "./static.edge": "./static.edge.js", "./static.node": "./static.node.js", - "./static.node.unbundled": "./static.node.unbundled.js", - "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.js", - "./node-register": "./node-register.js", "./src/*": "./src/*.js", "./package.json": "./package.json" }, diff --git a/packages/react-server-dom-turbopack/server.node.unbundled.js b/packages/react-server-dom-turbopack/server.node.unbundled.js deleted file mode 100644 index 9b8455bf66877..0000000000000 --- a/packages/react-server-dom-turbopack/server.node.unbundled.js +++ /dev/null @@ -1,20 +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. - * - * @flow - */ - -export { - renderToPipeableStream, - decodeReplyFromBusboy, - decodeReply, - decodeAction, - decodeFormState, - registerServerReference, - registerClientReference, - createClientModuleProxy, - createTemporaryReferenceSet, -} from './src/server/react-flight-dom-server.node.unbundled'; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js deleted file mode 100644 index b2f80e6b915f7..0000000000000 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js +++ /dev/null @@ -1,483 +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. - * - * @flow - */ - -import * as acorn from 'acorn-loose'; - -type ResolveContext = { - conditions: Array, - parentURL: string | void, -}; - -type ResolveFunction = ( - string, - ResolveContext, - ResolveFunction, -) => {url: string} | Promise<{url: string}>; - -type GetSourceContext = { - format: string, -}; - -type GetSourceFunction = ( - string, - GetSourceContext, - GetSourceFunction, -) => Promise<{source: Source}>; - -type TransformSourceContext = { - format: string, - url: string, -}; - -type TransformSourceFunction = ( - Source, - TransformSourceContext, - TransformSourceFunction, -) => Promise<{source: Source}>; - -type LoadContext = { - conditions: Array, - format: string | null | void, - importAssertions: Object, -}; - -type LoadFunction = ( - string, - LoadContext, - LoadFunction, -) => Promise<{format: string, shortCircuit?: boolean, source: Source}>; - -type Source = string | ArrayBuffer | Uint8Array; - -let warnedAboutConditionsFlag = false; - -let stashedGetSource: null | GetSourceFunction = null; -let stashedResolve: null | ResolveFunction = null; - -export async function resolve( - specifier: string, - context: ResolveContext, - defaultResolve: ResolveFunction, -): Promise<{url: string}> { - // We stash this in case we end up needing to resolve export * statements later. - stashedResolve = defaultResolve; - - if (!context.conditions.includes('react-server')) { - context = { - ...context, - conditions: [...context.conditions, 'react-server'], - }; - if (!warnedAboutConditionsFlag) { - warnedAboutConditionsFlag = true; - // eslint-disable-next-line react-internal/no-production-logging - console.warn( - 'You did not run Node.js with the `--conditions react-server` flag. ' + - 'Any "react-server" override will only work with ESM imports.', - ); - } - } - return await defaultResolve(specifier, context, defaultResolve); -} - -export async function getSource( - url: string, - context: GetSourceContext, - defaultGetSource: GetSourceFunction, -): Promise<{source: Source}> { - // We stash this in case we end up needing to resolve export * statements later. - stashedGetSource = defaultGetSource; - return defaultGetSource(url, context, defaultGetSource); -} - -function addLocalExportedNames(names: Map, node: any) { - switch (node.type) { - case 'Identifier': - names.set(node.name, node.name); - return; - case 'ObjectPattern': - for (let i = 0; i < node.properties.length; i++) - addLocalExportedNames(names, node.properties[i]); - return; - case 'ArrayPattern': - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i]; - if (element) addLocalExportedNames(names, element); - } - return; - case 'Property': - addLocalExportedNames(names, node.value); - return; - case 'AssignmentPattern': - addLocalExportedNames(names, node.left); - return; - case 'RestElement': - addLocalExportedNames(names, node.argument); - return; - case 'ParenthesizedExpression': - addLocalExportedNames(names, node.expression); - return; - } -} - -function transformServerModule( - source: string, - body: any, - url: string, - loader: LoadFunction, -): string { - // If the same local name is exported more than once, we only need one of the names. - const localNames: Map = new Map(); - const localTypes: Map = new Map(); - - for (let i = 0; i < body.length; i++) { - const node = body[i]; - switch (node.type) { - case 'ExportAllDeclaration': - // If export * is used, the other file needs to explicitly opt into "use server" too. - break; - case 'ExportDefaultDeclaration': - if (node.declaration.type === 'Identifier') { - localNames.set(node.declaration.name, 'default'); - } else if (node.declaration.type === 'FunctionDeclaration') { - if (node.declaration.id) { - localNames.set(node.declaration.id.name, 'default'); - localTypes.set(node.declaration.id.name, 'function'); - } else { - // TODO: This needs to be rewritten inline because it doesn't have a local name. - } - } - continue; - case 'ExportNamedDeclaration': - if (node.declaration) { - if (node.declaration.type === 'VariableDeclaration') { - const declarations = node.declaration.declarations; - for (let j = 0; j < declarations.length; j++) { - addLocalExportedNames(localNames, declarations[j].id); - } - } else { - const name = node.declaration.id.name; - localNames.set(name, name); - if (node.declaration.type === 'FunctionDeclaration') { - localTypes.set(name, 'function'); - } - } - } - if (node.specifiers) { - const specifiers = node.specifiers; - for (let j = 0; j < specifiers.length; j++) { - const specifier = specifiers[j]; - localNames.set(specifier.local.name, specifier.exported.name); - } - } - continue; - } - } - if (localNames.size === 0) { - return source; - } - let newSrc = source + '\n\n;'; - newSrc += - 'import {registerServerReference} from "react-server-dom-turbopack/server";\n'; - localNames.forEach(function (exported, local) { - if (localTypes.get(local) !== 'function') { - // We first check if the export is a function and if so annotate it. - newSrc += 'if (typeof ' + local + ' === "function") '; - } - newSrc += 'registerServerReference(' + local + ','; - newSrc += JSON.stringify(url) + ','; - newSrc += JSON.stringify(exported) + ');\n'; - }); - return newSrc; -} - -function addExportNames(names: Array, node: any) { - switch (node.type) { - case 'Identifier': - names.push(node.name); - return; - case 'ObjectPattern': - for (let i = 0; i < node.properties.length; i++) - addExportNames(names, node.properties[i]); - return; - case 'ArrayPattern': - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i]; - if (element) addExportNames(names, element); - } - return; - case 'Property': - addExportNames(names, node.value); - return; - case 'AssignmentPattern': - addExportNames(names, node.left); - return; - case 'RestElement': - addExportNames(names, node.argument); - return; - case 'ParenthesizedExpression': - addExportNames(names, node.expression); - return; - } -} - -function resolveClientImport( - specifier: string, - parentURL: string, -): {url: string} | Promise<{url: string}> { - // Resolve an import specifier as if it was loaded by the client. This doesn't use - // the overrides that this loader does but instead reverts to the default. - // This resolution algorithm will not necessarily have the same configuration - // as the actual client loader. It should mostly work and if it doesn't you can - // always convert to explicit exported names instead. - const conditions = ['node', 'import']; - if (stashedResolve === null) { - throw new Error( - 'Expected resolve to have been called before transformSource', - ); - } - return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); -} - -async function parseExportNamesInto( - body: any, - names: Array, - parentURL: string, - loader: LoadFunction, -): Promise { - for (let i = 0; i < body.length; i++) { - const node = body[i]; - switch (node.type) { - case 'ExportAllDeclaration': - if (node.exported) { - addExportNames(names, node.exported); - continue; - } else { - const {url} = await resolveClientImport(node.source.value, parentURL); - const {source} = await loader( - url, - {format: 'module', conditions: [], importAssertions: {}}, - loader, - ); - if (typeof source !== 'string') { - throw new Error('Expected the transformed source to be a string.'); - } - let childBody; - try { - childBody = acorn.parse(source, { - ecmaVersion: '2024', - sourceType: 'module', - }).body; - } catch (x) { - // eslint-disable-next-line react-internal/no-production-logging - console.error('Error parsing %s %s', url, x.message); - continue; - } - await parseExportNamesInto(childBody, names, url, loader); - continue; - } - case 'ExportDefaultDeclaration': - names.push('default'); - continue; - case 'ExportNamedDeclaration': - if (node.declaration) { - if (node.declaration.type === 'VariableDeclaration') { - const declarations = node.declaration.declarations; - for (let j = 0; j < declarations.length; j++) { - addExportNames(names, declarations[j].id); - } - } else { - addExportNames(names, node.declaration.id); - } - } - if (node.specifiers) { - const specifiers = node.specifiers; - for (let j = 0; j < specifiers.length; j++) { - addExportNames(names, specifiers[j].exported); - } - } - continue; - } - } -} - -async function transformClientModule( - body: any, - url: string, - loader: LoadFunction, -): Promise { - const names: Array = []; - - await parseExportNamesInto(body, names, url, loader); - - if (names.length === 0) { - return ''; - } - - let newSrc = - 'import {registerClientReference} from "react-server-dom-turbopack/server";\n'; - for (let i = 0; i < names.length; i++) { - const name = names[i]; - if (name === 'default') { - newSrc += 'export default '; - newSrc += 'registerClientReference(function() {'; - newSrc += - 'throw new Error(' + - JSON.stringify( - `Attempted to call the default export of ${url} from the server ` + - `but it's on the client. It's not possible to invoke a client function from ` + - `the server, it can only be rendered as a Component or passed to props of a ` + - `Client Component.`, - ) + - ');'; - } else { - newSrc += 'export const ' + name + ' = '; - newSrc += 'registerClientReference(function() {'; - newSrc += - 'throw new Error(' + - JSON.stringify( - `Attempted to call ${name}() from the server but ${name} is on the client. ` + - `It's not possible to invoke a client function from the server, it can ` + - `only be rendered as a Component or passed to props of a Client Component.`, - ) + - ');'; - } - newSrc += '},'; - newSrc += JSON.stringify(url) + ','; - newSrc += JSON.stringify(name) + ');\n'; - } - return newSrc; -} - -async function loadClientImport( - url: string, - defaultTransformSource: TransformSourceFunction, -): Promise<{format: string, shortCircuit?: boolean, source: Source}> { - if (stashedGetSource === null) { - throw new Error( - 'Expected getSource to have been called before transformSource', - ); - } - // TODO: Validate that this is another module by calling getFormat. - const {source} = await stashedGetSource( - url, - {format: 'module'}, - stashedGetSource, - ); - const result = await defaultTransformSource( - source, - {format: 'module', url}, - defaultTransformSource, - ); - return {format: 'module', source: result.source}; -} - -async function transformModuleIfNeeded( - source: string, - url: string, - loader: LoadFunction, -): Promise { - // Do a quick check for the exact string. If it doesn't exist, don't - // bother parsing. - if ( - source.indexOf('use client') === -1 && - source.indexOf('use server') === -1 - ) { - return source; - } - - let body; - try { - body = acorn.parse(source, { - ecmaVersion: '2024', - sourceType: 'module', - }).body; - } catch (x) { - // eslint-disable-next-line react-internal/no-production-logging - console.error('Error parsing %s %s', url, x.message); - return source; - } - - let useClient = false; - let useServer = false; - for (let i = 0; i < body.length; i++) { - const node = body[i]; - if (node.type !== 'ExpressionStatement' || !node.directive) { - break; - } - if (node.directive === 'use client') { - useClient = true; - } - if (node.directive === 'use server') { - useServer = true; - } - } - - if (!useClient && !useServer) { - return source; - } - - if (useClient && useServer) { - throw new Error( - 'Cannot have both "use client" and "use server" directives in the same file.', - ); - } - - if (useClient) { - return transformClientModule(body, url, loader); - } - - return transformServerModule(source, body, url, loader); -} - -export async function transformSource( - source: Source, - context: TransformSourceContext, - defaultTransformSource: TransformSourceFunction, -): Promise<{source: Source}> { - const transformed = await defaultTransformSource( - source, - context, - defaultTransformSource, - ); - if (context.format === 'module') { - const transformedSource = transformed.source; - if (typeof transformedSource !== 'string') { - throw new Error('Expected source to have been transformed to a string.'); - } - const newSrc = await transformModuleIfNeeded( - transformedSource, - context.url, - (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => { - return loadClientImport(url, defaultTransformSource); - }, - ); - return {source: newSrc}; - } - return transformed; -} - -export async function load( - url: string, - context: LoadContext, - defaultLoad: LoadFunction, -): Promise<{format: string, shortCircuit?: boolean, source: Source}> { - const result = await defaultLoad(url, context, defaultLoad); - if (result.format === 'module') { - if (typeof result.source !== 'string') { - throw new Error('Expected source to have been loaded into a string.'); - } - const newSrc = await transformModuleIfNeeded( - result.source, - url, - defaultLoad, - ); - return {format: 'module', source: newSrc}; - } - return result; -} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js deleted file mode 100644 index fa7965cc89945..0000000000000 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js +++ /dev/null @@ -1,109 +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. - * - * @flow - */ - -const acorn = require('acorn-loose'); - -const url = require('url'); - -const Module = require('module'); - -module.exports = function register() { - const Server: any = require('react-server-dom-turbopack/server'); - const registerServerReference = Server.registerServerReference; - const createClientModuleProxy = Server.createClientModuleProxy; - - // $FlowFixMe[prop-missing] found when upgrading Flow - const originalCompile = Module.prototype._compile; - - // $FlowFixMe[prop-missing] found when upgrading Flow - Module.prototype._compile = function ( - this: any, - content: string, - filename: string, - ): void { - // Do a quick check for the exact string. If it doesn't exist, don't - // bother parsing. - if ( - content.indexOf('use client') === -1 && - content.indexOf('use server') === -1 - ) { - return originalCompile.apply(this, arguments); - } - - let body; - try { - body = acorn.parse(content, { - ecmaVersion: '2024', - sourceType: 'source', - }).body; - } catch (x) { - console['error']('Error parsing %s %s', url, x.message); - return originalCompile.apply(this, arguments); - } - - let useClient = false; - let useServer = false; - for (let i = 0; i < body.length; i++) { - const node = body[i]; - if (node.type !== 'ExpressionStatement' || !node.directive) { - break; - } - if (node.directive === 'use client') { - useClient = true; - } - if (node.directive === 'use server') { - useServer = true; - } - } - - if (!useClient && !useServer) { - return originalCompile.apply(this, arguments); - } - - if (useClient && useServer) { - throw new Error( - 'Cannot have both "use client" and "use server" directives in the same file.', - ); - } - - if (useClient) { - const moduleId: string = (url.pathToFileURL(filename).href: any); - this.exports = createClientModuleProxy(moduleId); - } - - if (useServer) { - originalCompile.apply(this, arguments); - - const moduleId: string = (url.pathToFileURL(filename).href: any); - - const exports = this.exports; - - // This module is imported server to server, but opts in to exposing functions by - // reference. If there are any functions in the export. - if (typeof exports === 'function') { - // The module exports a function directly, - registerServerReference( - (exports: any), - moduleId, - // Represents the whole Module object instead of a particular import. - null, - ); - } else { - const keys = Object.keys(exports); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = exports[keys[i]]; - if (typeof value === 'function') { - registerServerReference((value: any), moduleId, key); - } - } - } - } - }; -}; diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js index eef2e824543e7..0cf2876fedba6 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -30,7 +30,7 @@ let Suspense; let ReactServerScheduler; let reactServerAct; -describe('ReactFlightDOM', () => { +describe('ReactFlightTurbopackDOM', () => { beforeEach(() => { // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server // This can be thought of as essentially being the React Server Components scope with react-server @@ -43,7 +43,7 @@ describe('ReactFlightDOM', () => { // Simulate the condition resolution jest.mock('react-server-dom-turbopack/server', () => - require('react-server-dom-turbopack/server.node.unbundled'), + require('react-server-dom-turbopack/server.node'), ); jest.mock('react', () => require('react/react.react-server')); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js index a47cca7068801..d90e1189d508e 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js @@ -23,7 +23,7 @@ let ReactServerDOMClient; let ReactServerScheduler; let reactServerAct; -describe('ReactFlightDOMBrowser', () => { +describe('ReactFlightTurbopackDOMBrowser', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js index 67d25c967f472..767e1f1ed0ebc 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js @@ -28,7 +28,7 @@ let ReactServerDOMServer; let ReactServerDOMClient; let use; -describe('ReactFlightDOMEdge', () => { +describe('ReactFlightTurbopackDOMEdge', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js deleted file mode 100644 index e7da697fad8e9..0000000000000 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMForm-test.js +++ /dev/null @@ -1,147 +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. - * - * @emails react-core - */ - -'use strict'; - -import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils'; - -// Polyfills for test environment -global.ReadableStream = - require('web-streams-polyfill/ponyfill/es6').ReadableStream; -global.TextEncoder = require('util').TextEncoder; -global.TextDecoder = require('util').TextDecoder; - -// Don't wait before processing work on the server. -// TODO: we can replace this with FlightServer.act(). -global.setTimeout = cb => cb(); - -let container; -let serverExports; -let turbopackServerMap; -let React; -let ReactDOMServer; -let ReactServerDOMServer; -let ReactServerDOMClient; - -describe('ReactFlightDOMForm', () => { - beforeEach(() => { - jest.resetModules(); - // Simulate the condition resolution - jest.mock('react', () => require('react/react.react-server')); - jest.mock('react-server-dom-turbopack/server', () => - require('react-server-dom-turbopack/server.edge'), - ); - ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); - const TurbopackMock = require('./utils/TurbopackMock'); - serverExports = TurbopackMock.serverExports; - turbopackServerMap = TurbopackMock.turbopackServerMap; - __unmockReact(); - jest.resetModules(); - React = require('react'); - ReactServerDOMClient = require('react-server-dom-turbopack/client.edge'); - ReactDOMServer = require('react-dom/server.edge'); - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - - async function POST(formData) { - const boundAction = await ReactServerDOMServer.decodeAction( - formData, - turbopackServerMap, - ); - return boundAction(); - } - - function submit(submitter) { - const form = submitter.form || submitter; - if (!submitter.form) { - submitter = undefined; - } - const submitEvent = new Event('submit', {bubbles: true, cancelable: true}); - submitEvent.submitter = submitter; - const returnValue = form.dispatchEvent(submitEvent); - if (!returnValue) { - return; - } - const action = - (submitter && submitter.getAttribute('formaction')) || form.action; - if (!/\s*javascript:/i.test(action)) { - const method = (submitter && submitter.formMethod) || form.method; - const encType = (submitter && submitter.formEnctype) || form.enctype; - if (method === 'post' && encType === 'multipart/form-data') { - let formData; - if (submitter) { - const temp = document.createElement('input'); - temp.name = submitter.name; - temp.value = submitter.value; - submitter.parentNode.insertBefore(temp, submitter); - formData = new FormData(form); - temp.parentNode.removeChild(temp); - } else { - formData = new FormData(form); - } - return POST(formData); - } - throw new Error('Navigate to: ' + action); - } - } - - async function readIntoContainer(stream) { - const reader = stream.getReader(); - let result = ''; - while (true) { - const {done, value} = await reader.read(); - if (done) { - break; - } - result += Buffer.from(value).toString('utf8'); - } - const temp = document.createElement('div'); - temp.innerHTML = result; - insertNodesAndExecuteScripts(temp, container, null); - } - - it('can submit a passed server action without hydrating it', async () => { - let foo = null; - - const serverAction = serverExports(function action(formData) { - foo = formData.get('foo'); - return 'hello'; - }); - function App() { - return ( -
- -
- ); - } - const rscStream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(rscStream, { - ssrManifest: { - moduleMap: null, - moduleLoading: null, - }, - }); - const ssrStream = await ReactDOMServer.renderToReadableStream(response); - await readIntoContainer(ssrStream); - - const form = container.firstChild; - - expect(foo).toBe(null); - - const result = await submit(form); - - expect(result).toBe('hello'); - expect(foo).toBe('bar'); - }); -}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js index 1276d4d0be40b..a7b6ff75d57ba 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js @@ -24,7 +24,7 @@ let use; let ReactServerScheduler; let reactServerAct; -describe('ReactFlightDOMNode', () => { +describe('ReactFlightTurbopackDOMNode', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js index cf328ab2e8fe3..04abb546df394 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js @@ -23,7 +23,7 @@ let ReactServerDOMServer; let ReactServerDOMClient; let ReactServerScheduler; -describe('ReactFlightDOMReply', () => { +describe('ReactFlightTurbopackDOMReply', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js index 0cd8605c7e8d6..cbc06ef80d503 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReplyEdge-test.js @@ -20,7 +20,7 @@ let turbopackServerMap; let ReactServerDOMServer; let ReactServerDOMClient; -describe('ReactFlightDOMReply', () => { +describe('ReactFlightDOMTurbopackReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution diff --git a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js index 35c1aae80a02b..5ed6fa5357408 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js +++ b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js @@ -8,7 +8,6 @@ 'use strict'; const url = require('url'); -const Module = require('module'); let turbopackModuleIdx = 0; const turbopackServerModules = {}; @@ -23,21 +22,9 @@ global.__turbopack_require__ = function (id) { return turbopackClientModules[id] || turbopackServerModules[id]; }; -const previousCompile = Module.prototype._compile; - -const register = require('react-server-dom-turbopack/node-register'); -// Register node compile -register(); - -const nodeCompile = Module.prototype._compile; - -if (previousCompile === nodeCompile) { - throw new Error( - 'Expected the Node loader to register the _compile extension', - ); -} - -Module.prototype._compile = previousCompile; +const Server = require('react-server-dom-turbopack/server'); +const registerServerReference = Server.registerServerReference; +const createClientModuleProxy = Server.createClientModuleProxy; exports.turbopackMap = turbopackClientMap; exports.turbopackModules = turbopackClientModules; @@ -46,20 +33,6 @@ exports.moduleLoading = { prefix: '/prefix/', }; -exports.clientModuleError = function clientModuleError(moduleError) { - const idx = '' + turbopackModuleIdx++; - turbopackErroredModules[idx] = moduleError; - const path = url.pathToFileURL(idx).href; - turbopackClientMap[path] = { - id: idx, - chunks: [], - name: '*', - }; - const mod = new Module(); - nodeCompile.call(mod, '"use client"', idx); - return mod.exports; -}; - exports.clientExports = function clientExports(moduleExports, chunkUrl) { const chunks = []; if (chunkUrl !== undefined) { @@ -107,9 +80,7 @@ exports.clientExports = function clientExports(moduleExports, chunkUrl) { name: 's', }; } - const mod = new Module(); - nodeCompile.call(mod, '"use client"', idx); - return mod.exports; + return createClientModuleProxy(path); }; // This tests server to server references. There's another case of client to server references. @@ -142,8 +113,25 @@ exports.serverExports = function serverExports(moduleExports) { name: 's', }; } - const mod = new Module(); - mod.exports = moduleExports; - nodeCompile.call(mod, '"use server"', idx); - return mod.exports; + + if (typeof exports === 'function') { + // The module exports a function directly, + registerServerReference( + (exports: any), + idx, + // Represents the whole Module object instead of a particular import. + null, + ); + } else { + const keys = Object.keys(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = exports[keys[i]]; + if (typeof value === 'function') { + registerServerReference((value: any), idx, key); + } + } + } + + return moduleExports; }; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js deleted file mode 100644 index badc2ed50b691..0000000000000 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js +++ /dev/null @@ -1,21 +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. - * - * @flow - */ - -export { - renderToPipeableStream, - prerenderToNodeStream, - decodeReplyFromBusboy, - decodeReply, - decodeAction, - decodeFormState, - registerServerReference, - registerClientReference, - createClientModuleProxy, - createTemporaryReferenceSet, -} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js deleted file mode 100644 index 0d159704067ea..0000000000000 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js +++ /dev/null @@ -1,20 +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. - * - * @flow - */ - -export { - renderToPipeableStream, - decodeReplyFromBusboy, - decodeReply, - decodeAction, - decodeFormState, - registerServerReference, - registerClientReference, - createClientModuleProxy, - createTemporaryReferenceSet, -} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/static.node.unbundled.js b/packages/react-server-dom-turbopack/static.node.unbundled.js deleted file mode 100644 index b2134459afc7a..0000000000000 --- a/packages/react-server-dom-turbopack/static.node.unbundled.js +++ /dev/null @@ -1,10 +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. - * - * @flow - */ - -export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node.unbundled'; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index ea4d1a034bf47..081b91b9d0b80 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -534,18 +534,6 @@ const bundles = [ wrapWithModuleBoundaries: false, externals: ['react', 'util', 'async_hooks', 'react-dom'], }, - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: RENDERER, - entry: - 'react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled', - name: 'react-server-dom-turbopack-server.node.unbundled', - condition: 'react-server', - global: 'ReactServerDOMServer', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: ['react', 'util', 'async_hooks', 'react-dom'], - }, { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, @@ -577,15 +565,6 @@ const bundles = [ wrapWithModuleBoundaries: false, externals: ['react', 'react-dom', 'util'], }, - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: RENDERER, - entry: 'react-server-dom-turbopack/client.node.unbundled', - global: 'ReactServerDOMClient', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: ['react', 'react-dom', 'util'], - }, { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, @@ -596,35 +575,6 @@ const bundles = [ externals: ['react', 'react-dom'], }, - /******* React Server DOM Turbopack Plugin *******/ - // There is no plugin the moment because Turbopack - // does not expose a plugin interface yet. - - /******* React Server DOM Turbopack Node.js Loader *******/ - { - bundleTypes: [ESM_PROD], - moduleType: RENDERER_UTILS, - entry: 'react-server-dom-turbopack/node-loader', - condition: 'react-server', - global: 'ReactServerTurbopackNodeLoader', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: ['acorn'], - }, - - /******* React Server DOM Turbopack Node.js CommonJS Loader *******/ - { - bundleTypes: [NODE_ES2015], - moduleType: RENDERER_UTILS, - entry: 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister', - name: 'react-server-dom-turbopack-node-register', - condition: 'react-server', - global: 'ReactFlightWebpackNodeRegister', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: ['url', 'module', 'react-server-dom-turbopack/server'], - }, - /******* React Server DOM ESM Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index be5706c927c7c..514ecc4567b05 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -104,49 +104,6 @@ module.exports = [ }, { shortName: 'dom-node-turbopack', - entryPoints: [ - 'react-server-dom-turbopack/client.node.unbundled', - 'react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled', - ], - paths: [ - 'react-dom', - 'react-dom-bindings', - 'react-dom/client', - 'react-dom/profiling', - 'react-dom/server', - 'react-dom/server.node', - 'react-dom/static', - 'react-dom/static.node', - 'react-dom/src/server/react-dom-server.node', - 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node - 'react-dom/src/server/ReactDOMFizzStaticNode.js', - 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', - 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', - 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', - 'react-server-dom-turbopack', - 'react-server-dom-turbopack/client.node.unbundled', - 'react-server-dom-turbopack/server', - 'react-server-dom-turbopack/server.node.unbundled', - 'react-server-dom-turbopack/static', - 'react-server-dom-turbopack/static.node.unbundled', - 'react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node.unbundled - 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerNode.js', - 'react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled', - 'react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled - 'react-server-dom-turbopack/node-register', - 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js', - 'react-devtools', - 'react-devtools-core', - 'react-devtools-shell', - 'react-devtools-shared', - 'shared/ReactDOMSharedInternals', - 'react-server/src/ReactFlightServerConfigDebugNode.js', - ], - isFlowTyped: true, - isServerSupported: true, - }, - { - shortName: 'dom-node-turbopack-bundled', entryPoints: [ 'react-server-dom-turbopack/client.node', 'react-server-dom-turbopack/src/server/react-flight-dom-server.node', From ab24f643d0809ee09a7499862fef135fb09a0225 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 21 Aug 2024 07:55:56 -0700 Subject: [PATCH 032/191] [Fizz] use microtasks rather than tasks when scheduling work while prerendering (#30770) Similar to https://github.com/facebook/react/pull/30768 we want to schedule work during prerendering in microtasks both for the root task and pings. We continue to schedule flushes as Tasks to allow as much work to be batched up as possible. --- packages/react-server/src/ReactFizzServer.js | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index daea492db5b8e..7ccbd65d16b74 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -38,6 +38,7 @@ import {describeObjectForErrorMessage} from 'shared/ReactSerializationErrors'; import { scheduleWork, + scheduleMicrotask, beginWriting, writeChunk, writeChunkAndReturn, @@ -669,7 +670,11 @@ function pingTask(request: Request, task: Task): void { pingedTasks.push(task); if (request.pingedTasks.length === 1) { request.flushScheduled = request.destination !== null; - scheduleWork(() => performWork(request)); + if (request.trackedPostpones !== null) { + scheduleMicrotask(() => performWork(request)); + } else { + scheduleWork(() => performWork(request)); + } } } @@ -4893,12 +4898,22 @@ function flushCompletedQueues( export function startWork(request: Request): void { request.flushScheduled = request.destination !== null; - if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request, performWork, request)); + if (request.trackedPostpones !== null) { + // When prerendering we use microtasks for pinging work + if (supportsRequestStorage) { + scheduleMicrotask(() => + requestStorage.run(request, performWork, request), + ); + } else { + scheduleMicrotask(() => performWork(request)); + } } else { - scheduleWork(() => performWork(request)); - } - if (request.trackedPostpones === null) { + // When rendering/resuming we use regular tasks and we also emit early preloads + if (supportsRequestStorage) { + scheduleWork(() => requestStorage.run(request, performWork, request)); + } else { + scheduleWork(() => performWork(request)); + } // this is either a regular render or a resume. For regular render we want // to call emitEarlyPreloads after the first performWork because we want // are responding to a live request and need to balance sending something early From 1d989965a6aac11d71ecf28030796f5475a86642 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 21 Aug 2024 11:41:55 -0400 Subject: [PATCH 033/191] [ez] Add noformat etc headers into some files These are only needed internally so I'm opting to just do it in the commit artifacts job instead of amending the build config. ghstack-source-id: 6a5382b0287d679f4515d79b140ab8248ce90c6b Pull Request resolved: https://github.com/facebook/react/pull/30775 --- .github/workflows/runtime_commit_artifacts.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 9f786b9b5eccc..073c289843fe1 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -85,6 +85,11 @@ jobs: sed -i -e 's/ @license React*//' \ build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js + - name: Insert @headers into eslint plugin and react-refresh + run: | + sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n * \n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \ + build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js - name: Move relevant files for React in www into compiled run: | # Move the facebook-www folder into compiled From 985747f81033833dca22f30b0c04704dd4bd3714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 21 Aug 2024 18:17:29 -0400 Subject: [PATCH 034/191] [DevTools] Support REACT_LEGACY_ELEMENT_TYPE for formatting JSX (#30779) DevTools shouldn't use react-is since that's versioned to one version of React. We don't need to since we use all the symbols from shared/ReactSymbols anyway and have a fork of typeOf that can cover both. Now JSX of old React versions show up with proper JSX formatting when inspecting. --- .../__tests__/legacy/inspectElement-test.js | 20 +------ packages/react-devtools-shared/src/utils.js | 57 ++++++++----------- 2 files changed, 27 insertions(+), 50 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index 243759e5b02e7..b5941ee5c6ccc 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -289,23 +289,9 @@ describe('InspectedElementContext', () => { "preview_long": {boolean: true, number: 123, string: "abc"}, }, }, - "react_element": { - "$$typeof": Dehydrated { - "preview_short": Symbol(react.element), - "preview_long": Symbol(react.element), - }, - "_owner": null, - "_store": Dehydrated { - "preview_short": {…}, - "preview_long": {}, - }, - "key": null, - "props": Dehydrated { - "preview_short": {…}, - "preview_long": {}, - }, - "ref": null, - "type": "span", + "react_element": Dehydrated { + "preview_short": , + "preview_long": , }, "regexp": Dehydrated { "preview_short": /abc/giu, diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 8e6f52f6c0f8a..62ae8070bf020 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -8,26 +8,13 @@ */ import LRU from 'lru-cache'; -import { - isElement, - typeOf, - ContextConsumer, - ContextProvider, - ForwardRef, - Fragment, - Lazy, - Memo, - Portal, - Profiler, - StrictMode, - Suspense, -} from 'react-is'; import { REACT_CONSUMER_TYPE, REACT_CONTEXT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, + REACT_ELEMENT_TYPE, REACT_LEGACY_ELEMENT_TYPE, REACT_MEMO_TYPE, REACT_PORTAL_TYPE, @@ -35,9 +22,8 @@ import { REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_LIST_TYPE, - REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_SUSPENSE_TYPE, - REACT_TRACING_MARKER_TYPE as TracingMarker, + REACT_TRACING_MARKER_TYPE, } from 'shared/ReactSymbols'; import {enableRenderableContext} from 'shared/ReactFeatureFlags'; import { @@ -632,10 +618,6 @@ export function getDataType(data: Object): DataType { return 'undefined'; } - if (isElement(data)) { - return 'react_element'; - } - if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { return 'html_element'; } @@ -657,6 +639,12 @@ export function getDataType(data: Object): DataType { return 'number'; } case 'object': + if ( + data.$$typeof === REACT_ELEMENT_TYPE || + data.$$typeof === REACT_LEGACY_ELEMENT_TYPE + ) { + return 'react_element'; + } if (isArray(data)) { return 'array'; } else if (ArrayBuffer.isView(data)) { @@ -717,6 +705,7 @@ function typeOfWithLegacyElementSymbol(object: any): mixed { if (typeof object === 'object' && object !== null) { const $$typeof = object.$$typeof; switch ($$typeof) { + case REACT_ELEMENT_TYPE: case REACT_LEGACY_ELEMENT_TYPE: const type = object.type; @@ -761,31 +750,33 @@ function typeOfWithLegacyElementSymbol(object: any): mixed { export function getDisplayNameForReactElement( element: React$Element, ): string | null { - const elementType = typeOf(element) || typeOfWithLegacyElementSymbol(element); + const elementType = typeOfWithLegacyElementSymbol(element); switch (elementType) { - case ContextConsumer: + case REACT_CONSUMER_TYPE: return 'ContextConsumer'; - case ContextProvider: + case REACT_PROVIDER_TYPE: return 'ContextProvider'; - case ForwardRef: + case REACT_CONTEXT_TYPE: + return 'Context'; + case REACT_FORWARD_REF_TYPE: return 'ForwardRef'; - case Fragment: + case REACT_FRAGMENT_TYPE: return 'Fragment'; - case Lazy: + case REACT_LAZY_TYPE: return 'Lazy'; - case Memo: + case REACT_MEMO_TYPE: return 'Memo'; - case Portal: + case REACT_PORTAL_TYPE: return 'Portal'; - case Profiler: + case REACT_PROFILER_TYPE: return 'Profiler'; - case StrictMode: + case REACT_STRICT_MODE_TYPE: return 'StrictMode'; - case Suspense: + case REACT_SUSPENSE_TYPE: return 'Suspense'; - case SuspenseList: + case REACT_SUSPENSE_LIST_TYPE: return 'SuspenseList'; - case TracingMarker: + case REACT_TRACING_MARKER_TYPE: return 'TracingMarker'; default: const {type} = element; From eb3ad065a10e542eb501bcb7dba7f9617e8c363e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 22 Aug 2024 10:17:19 -0400 Subject: [PATCH 035/191] Feature flag: enableSiblingPrerendering (#30761) Adds a new feature flag for an upcoming experiment. No implementation yet. --- packages/shared/ReactFeatureFlags.js | 2 ++ packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js | 1 + packages/shared/forks/ReactFeatureFlags.native-fb.js | 1 + packages/shared/forks/ReactFeatureFlags.native-oss.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.js | 1 + .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 1 + packages/shared/forks/ReactFeatureFlags.www-dynamic.js | 1 + packages/shared/forks/ReactFeatureFlags.www.js | 1 + 9 files changed, 10 insertions(+) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c5351d6d92631..673ff9d7e8450 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -148,6 +148,8 @@ export const enableOwnerStacks = __EXPERIMENTAL__; export const enableShallowPropDiffing = false; +export const enableSiblingPrerendering = __EXPERIMENTAL__; + /** * Enables an expiration time for retry lanes to avoid starvation. */ diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index 2426206bc825d..2d49c56377a2f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -25,3 +25,4 @@ export const enableShallowPropDiffing = __VARIANT__; export const passChildrenWhenCloningPersistedNodes = __VARIANT__; export const enableFabricCompleteRootInCommitPhase = __VARIANT__; export const enableLazyContextPropagation = __VARIANT__; +export const enableSiblingPrerendering = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3618aa70e7d67..6ba5a79ff0f39 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -27,6 +27,7 @@ export const { enableShallowPropDiffing, passChildrenWhenCloningPersistedNodes, enableLazyContextPropagation, + enableSiblingPrerendering, } = dynamicFlags; // The rest of the flags are static for better dead code elimination. diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 2aae8bd3d1c65..4e50442c2579f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -84,6 +84,7 @@ export const retryLaneExpirationMs = 5000; export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const useModernStrictMode = true; +export const enableSiblingPrerendering = false; // Profiling Only export const enableProfilerTimer = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index c44e7014fc444..ae211b9ec4639 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -80,6 +80,7 @@ export const enableAddPropertiesFastPath = false; export const renameElementSymbol = true; export const enableShallowPropDiffing = false; +export const enableSiblingPrerendering = __EXPERIMENTAL__; // TODO: This must be in sync with the main ReactFeatureFlags file because // the Test Renderer's value must be the same as the one used by the diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index bc7ddf85acc03..ebc3ddab2a551 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -80,6 +80,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const useModernStrictMode = true; export const enableFabricCompleteRootInCommitPhase = false; +export const enableSiblingPrerendering = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 57f60c24aef45..a378ab3edf17c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -94,6 +94,7 @@ export const renameElementSymbol = false; export const enableObjectFiber = false; export const enableOwnerStacks = false; export const enableShallowPropDiffing = false; +export const enableSiblingPrerendering = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index e2f2751f0c2c8..d6e11f92649ba 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -41,6 +41,7 @@ export const enableDebugTracing = __EXPERIMENTAL__; export const enableSchedulingProfiler = __VARIANT__; export const enableInfiniteRenderLoopDetection = __VARIANT__; +export const enableSiblingPrerendering = __VARIANT__; // TODO: These flags are hard-coded to the default values used in open source. // Update the tests so that they pass in either mode, then set these diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 465fa58590bcc..e207dbb71b669 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -35,6 +35,7 @@ export const { retryLaneExpirationMs, syncLaneExpirationMs, transitionLaneExpirationMs, + enableSiblingPrerendering, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. From f7bb717e9e9f876c6a466a5f6d31004c7f7590c5 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 14:07:11 -0700 Subject: [PATCH 036/191] [compiler] Repro for missing memoization due to inferred mutation This fixture bails out on ValidatePreserveExistingMemo but would ideally memoize since the original memoization is safe. It's trivial to make it pass by commenting out the commented line (`LogEvent.log(() => object)`). I would expect the compiler to infer this as possible mutation of `logData`, since `object` captures a reference to `logData`. But somehow `logData` is getting memoized successfully, but we still infer the callback, `setCurrentIndex`, as having a mutable range that extends to the `setCurrentIndex()` call after the useCallback. ghstack-source-id: 4f82e345102f82f6da74de3f9014af263d016762 Pull Request resolved: https://github.com/facebook/react/pull/30764 --- .../ValidatePreservedManualMemoization.ts | 6 +- ...from-inferred-mutation-in-logger.expect.md | 83 +++++++++++++++++++ ...zation-from-inferred-mutation-in-logger.js | 46 ++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 50d1c986cea5a..7af4aaaccd7ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -23,7 +23,7 @@ import { ScopeId, SourceLocation, } from '../HIR'; -import {printManualMemoDependency} from '../HIR/PrintHIR'; +import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization'; import { @@ -537,7 +537,9 @@ class Visitor extends ReactiveFunctionVisitor { state.errors.push({ reason: 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.', - description: null, + description: DEBUG + ? `${printIdentifier(identifier)} was not memoized` + : null, severity: ErrorSeverity.CannotPreserveMemoization, loc, suggestions: null, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md new file mode 100644 index 0000000000000..9010607496478 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +// @flow @validatePreserveExistingMemoizationGuarantees +import {useFragment} from 'react-relay'; +import LogEvent from 'LogEvent'; +import {useCallback, useMemo} from 'react'; + +component Component(id) { + const {data} = useFragment(); + const items = data.items.edges; + + const [prevId, setPrevId] = useState(id); + const [index, setIndex] = useState(0); + + const logData = useMemo(() => { + const item = items[index]; + return { + key: item.key ?? '', + }; + }, [index, items]); + + const setCurrentIndex = useCallback( + (index: number) => { + const object = { + tracking: logData.key, + }; + // We infer that this may mutate `object`, which in turn aliases + // data from `logData`, such that `logData` may be mutated. + LogEvent.log(() => object); + setIndex(index); + }, + [index, logData, items] + ); + + if (prevId !== id) { + setPrevId(id); + setCurrentIndex(0); + } + + return ( + + ); +} + +``` + + +## Error + +``` + 19 | + 20 | const setCurrentIndex = useCallback( +> 21 | (index: number) => { + | ^^^^^^^^^^^^^^^^^^^^ +> 22 | const object = { + | ^^^^^^^^^^^^^^^^^^^^^^ +> 23 | tracking: logData.key, + | ^^^^^^^^^^^^^^^^^^^^^^ +> 24 | }; + | ^^^^^^^^^^^^^^^^^^^^^^ +> 25 | // We infer that this may mutate `object`, which in turn aliases + | ^^^^^^^^^^^^^^^^^^^^^^ +> 26 | // data from `logData`, such that `logData` may be mutated. + | ^^^^^^^^^^^^^^^^^^^^^^ +> 27 | LogEvent.log(() => object); + | ^^^^^^^^^^^^^^^^^^^^^^ +> 28 | setIndex(index); + | ^^^^^^^^^^^^^^^^^^^^^^ +> 29 | }, + | ^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (21:29) + 30 | [index, logData, items] + 31 | ); + 32 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js new file mode 100644 index 0000000000000..b4b33303767c1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js @@ -0,0 +1,46 @@ +// @flow @validatePreserveExistingMemoizationGuarantees +import {useFragment} from 'react-relay'; +import LogEvent from 'LogEvent'; +import {useCallback, useMemo} from 'react'; + +component Component(id) { + const {data} = useFragment(); + const items = data.items.edges; + + const [prevId, setPrevId] = useState(id); + const [index, setIndex] = useState(0); + + const logData = useMemo(() => { + const item = items[index]; + return { + key: item.key ?? '', + }; + }, [index, items]); + + const setCurrentIndex = useCallback( + (index: number) => { + const object = { + tracking: logData.key, + }; + // We infer that this may mutate `object`, which in turn aliases + // data from `logData`, such that `logData` may be mutated. + LogEvent.log(() => object); + setIndex(index); + }, + [index, logData, items] + ); + + if (prevId !== id) { + setPrevId(id); + setCurrentIndex(0); + } + + return ( + + ); +} From 0ef00b3e17447ae94dc5701a5ad410c137680d86 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 14:07:12 -0700 Subject: [PATCH 037/191] [compiler] Transitively freezing functions marks values as frozen, not effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture from the previous PR was getting inconsistent behavior because of the following: 1. Create an object in a useMemo 2. Create a callback in a useCallback, where the callback captures the object from (1) into a local object, then passes that local object into a logging method. We have to assume the logging method could modify the local object, and transitively, the object from (1). 3. Call the callback during render. 4. Pass the callback to JSX. We correctly infer that the object from (1) is captured and modified in (2). However, in (4) we transitively freeze the callback. When transitively freezing functions we were previously doing two things: updating our internal abstract model of the program values to reflect the values as being frozen *and* also updating function operands to change their effects to freeze. As the case above demonstrates, that can clobber over information about real potential mutability. The potential fix here is to only walk our abstract value model to mark values as frozen, but _not_ override operand effects. Conceptually, this is a forward data flow propagation — but walking backward to update effects is pushing information backwards in the algorithm. An alternative would be to mark that data was propagated backwards, and trigger another loop over the CFG to propagate information forward again given the updated effects. But the fix in this PR is more correct. ghstack-source-id: c05e716f37827cb5515a059a1f0e8e8ff94b91df Pull Request resolved: https://github.com/facebook/react/pull/30766 --- .../src/Inference/InferReferenceEffects.ts | 59 +++++++++++-------- ...mutate-global-in-effect-fixpoint.expect.md | 53 +++++++++-------- ...from-inferred-mutation-in-logger.expect.md | 51 ++++++++-------- ...zation-from-inferred-mutation-in-logger.js | 7 +-- 4 files changed, 89 insertions(+), 81 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index 4cce942c18154..8aa82469bdec4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -453,6 +453,37 @@ class InferenceState { } } + freezeValues(values: Set, reason: Set): void { + for (const value of values) { + this.#values.set(value, { + kind: ValueKind.Frozen, + reason, + context: new Set(), + }); + if (value.kind === 'FunctionExpression') { + if ( + this.#env.config.enablePreserveExistingMemoizationGuarantees || + this.#env.config.enableTransitivelyFreezeFunctionExpressions + ) { + if (value.kind === 'FunctionExpression') { + /* + * We want to freeze the captured values, not mark the operands + * themselves as frozen. There could be mutations that occur + * before the freeze we are processing, and it would be invalid + * to overwrite those mutations as a freeze. + */ + for (const operand of eachInstructionValueOperand(value)) { + const operandValues = this.#variables.get(operand.identifier.id); + if (operandValues !== undefined) { + this.freezeValues(operandValues, reason); + } + } + } + } + } + } + } + reference( place: Place, effectKind: Effect, @@ -482,29 +513,7 @@ class InferenceState { reason: reasonSet, context: new Set(), }; - values.forEach(value => { - this.#values.set(value, { - kind: ValueKind.Frozen, - reason: reasonSet, - context: new Set(), - }); - - if ( - this.#env.config.enablePreserveExistingMemoizationGuarantees || - this.#env.config.enableTransitivelyFreezeFunctionExpressions - ) { - if (value.kind === 'FunctionExpression') { - for (const operand of eachInstructionValueOperand(value)) { - this.referenceAndRecordEffects( - operand, - Effect.Freeze, - ValueReason.Other, - [], - ); - } - } - } - }); + this.freezeValues(values, reasonSet); } else { effect = Effect.Read; } @@ -1241,6 +1250,7 @@ function inferBlock( case 'ObjectMethod': case 'FunctionExpression': { let hasMutableOperand = false; + const mutableOperands: Array = []; for (const operand of eachInstructionOperand(instr)) { state.referenceAndRecordEffects( operand, @@ -1248,6 +1258,9 @@ function inferBlock( ValueReason.Other, [], ); + if (isMutableEffect(operand.effect, operand.loc)) { + mutableOperands.push(operand); + } hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc); /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md index 670236e1f7498..942daec1dd08c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md @@ -46,54 +46,57 @@ import { useEffect, useState } from "react"; let someGlobal = { value: null }; function Component() { - const $ = _c(6); + const $ = _c(7); const [state, setState] = useState(someGlobal); - - let x = someGlobal; - while (x == null) { - x = someGlobal; - } - - const y = x; let t0; let t1; + let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { + let x = someGlobal; + while (x == null) { + x = someGlobal; + } + + const y = x; + t0 = useEffect; + t1 = () => { y.value = "hello"; }; - t1 = []; + t2 = []; $[0] = t0; $[1] = t1; + $[2] = t2; } else { t0 = $[0]; t1 = $[1]; + t2 = $[2]; } - useEffect(t0, t1); - let t2; + t0(t1, t2); let t3; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { + let t4; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = () => { setState(someGlobal.value); }; - t3 = [someGlobal]; - $[2] = t2; + t4 = [someGlobal]; $[3] = t3; + $[4] = t4; } else { - t2 = $[2]; t3 = $[3]; + t4 = $[4]; } - useEffect(t2, t3); + useEffect(t3, t4); - const t4 = String(state); - let t5; - if ($[4] !== t4) { - t5 =
{t4}
; - $[4] = t4; + const t5 = String(state); + let t6; + if ($[5] !== t5) { + t6 =
{t5}
; $[5] = t5; + $[6] = t6; } else { - t5 = $[5]; + t6 = $[6]; } - return t5; + return t6; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md index 9010607496478..39e4c5f0c3253 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.expect.md @@ -8,16 +8,14 @@ import LogEvent from 'LogEvent'; import {useCallback, useMemo} from 'react'; component Component(id) { - const {data} = useFragment(); - const items = data.items.edges; + const items = useFragment(); - const [prevId, setPrevId] = useState(id); const [index, setIndex] = useState(0); const logData = useMemo(() => { const item = items[index]; return { - key: item.key ?? '', + key: item.key, }; }, [index, items]); @@ -35,7 +33,6 @@ component Component(id) { ); if (prevId !== id) { - setPrevId(id); setCurrentIndex(0); } @@ -55,29 +52,27 @@ component Component(id) { ## Error ``` - 19 | - 20 | const setCurrentIndex = useCallback( -> 21 | (index: number) => { - | ^^^^^^^^^^^^^^^^^^^^ -> 22 | const object = { - | ^^^^^^^^^^^^^^^^^^^^^^ -> 23 | tracking: logData.key, - | ^^^^^^^^^^^^^^^^^^^^^^ -> 24 | }; - | ^^^^^^^^^^^^^^^^^^^^^^ -> 25 | // We infer that this may mutate `object`, which in turn aliases - | ^^^^^^^^^^^^^^^^^^^^^^ -> 26 | // data from `logData`, such that `logData` may be mutated. - | ^^^^^^^^^^^^^^^^^^^^^^ -> 27 | LogEvent.log(() => object); - | ^^^^^^^^^^^^^^^^^^^^^^ -> 28 | setIndex(index); - | ^^^^^^^^^^^^^^^^^^^^^^ -> 29 | }, - | ^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (21:29) - 30 | [index, logData, items] - 31 | ); - 32 | + 9 | const [index, setIndex] = useState(0); + 10 | +> 11 | const logData = useMemo(() => { + | ^^^^^^^^^^^^^^^ +> 12 | const item = items[index]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 13 | return { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 14 | key: item.key, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 15 | }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 16 | }, [index, items]); + | ^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:16) + +CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (28:28) + +CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (19:27) + 17 | + 18 | const setCurrentIndex = useCallback( + 19 | (index: number) => { ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js index b4b33303767c1..34a9a12882188 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-inferred-mutation-in-logger.js @@ -4,16 +4,14 @@ import LogEvent from 'LogEvent'; import {useCallback, useMemo} from 'react'; component Component(id) { - const {data} = useFragment(); - const items = data.items.edges; + const items = useFragment(); - const [prevId, setPrevId] = useState(id); const [index, setIndex] = useState(0); const logData = useMemo(() => { const item = items[index]; return { - key: item.key ?? '', + key: item.key, }; }, [index, items]); @@ -31,7 +29,6 @@ component Component(id) { ); if (prevId !== id) { - setPrevId(id); setCurrentIndex(0); } From 689c6bd3fd138ec6c21c54e741da168bdd0c0616 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 15:45:30 -0700 Subject: [PATCH 038/191] [compiler][wip] Environment option for resolving imported module types Adds a new Environment config option which allows specifying a function that is called to resolve types of imported modules. The function is passed the name of the imported module (the RHS of the import stmt) and can return a TypeConfig, which is a recursive type of the following form: * Object of valid identifier keys (or "*" for wildcard) and values that are TypeConfigs * Function with various properties, whose return type is a TypeConfig * or a reference to a builtin type using one of a small list (currently Ref, Array, MixedReadonly, Primitive) Rather than have to eagerly supply all known types (most of which may not be used) when creating the config, this function can do so lazily. During InferTypes we call `getGlobalDeclaration()` to resolve global types. Originally this was just for known react modules, but if the new config option is passed we also call it to see if it can resolve a type. For `import {name} from 'module'` syntax, we first resolve the module type and then call `getPropertyType(moduleType, 'name')` to attempt to retrieve the property of the module (the module would obviously have to be typed as an object type for this to have a chance of yielding a result). If the module type is returned as null, or the property doesn't exist, we fall through to the original checking of whether the name was hook-like. TODO: * testing * cache the results of modules so we don't have to re-parse/install their types on each LoadGlobal of the same module * decide what to do if the module types are invalid. probably better to fatal rather than bail out, since this would indicate an invalid configuration. ghstack-source-id: bfdbf67e3dd0cbfd511bed0bd6ba92266cf99ab8 Pull Request resolved: https://github.com/facebook/react/pull/30771 --- .../src/HIR/Environment.ts | 71 ++++++- .../src/HIR/Globals.ts | 76 +++++++ .../src/HIR/HIR.ts | 19 ++ .../src/HIR/TypeSchema.ts | 105 ++++++++++ .../src/Inference/DropManualMemoization.ts | 2 +- .../src/TypeInference/InferTypes.ts | 2 +- ...ed-scope-declarations-and-locals.expect.md | 4 + ...ing-mixed-scope-declarations-and-locals.js | 2 + .../compiler/optional-call-logical.expect.md | 4 + .../compiler/optional-call-logical.js | 2 + ...ject-method-calls-mutable-lambda.expect.md | 4 + ...only-object-method-calls-mutable-lambda.js | 2 + .../readonly-object-method-calls.expect.md | 4 + .../compiler/readonly-object-method-calls.js | 2 + .../tagged-template-in-hook.expect.md | 4 + .../compiler/tagged-template-in-hook.js | 2 + ...type-provider-log-default-import.expect.md | 147 ++++++++++++++ .../type-provider-log-default-import.tsx | 30 +++ .../compiler/type-provider-log.expect.md | 145 ++++++++++++++ .../fixtures/compiler/type-provider-log.tsx | 29 +++ ...r-store-capture-namespace-import.expect.md | 185 ++++++++++++++++++ ...rovider-store-capture-namespace-import.tsx | 35 ++++ .../type-provider-store-capture.expect.md | 185 ++++++++++++++++++ .../compiler/type-provider-store-capture.tsx | 35 ++++ compiler/packages/snap/src/compiler.ts | 45 ++--- compiler/packages/snap/src/constants.ts | 1 + compiler/packages/snap/src/runner-worker.ts | 6 + .../sprout/shared-runtime-type-provider.ts | 69 +++++++ .../snap/src/sprout/shared-runtime.ts | 9 + 29 files changed, 1190 insertions(+), 36 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.tsx create mode 100644 compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index a5614ac244a14..12c741641c7e0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -17,6 +17,7 @@ import { Global, GlobalRegistry, installReAnimatedTypes, + installTypeConfig, } from './Globals'; import { BlockId, @@ -28,6 +29,7 @@ import { NonLocalBinding, PolyType, ScopeId, + SourceLocation, Type, ValidatedIdentifier, ValueKind, @@ -45,6 +47,7 @@ import { addHook, } from './ObjectShape'; import {Scope as BabelScope} from '@babel/traverse'; +import {TypeSchema} from './TypeSchema'; export const ExternalFunctionSchema = z.object({ // Source for the imported module that exports the `importSpecifierName` functions @@ -137,6 +140,12 @@ export type Hook = z.infer; const EnvironmentConfigSchema = z.object({ customHooks: z.map(z.string(), HookSchema).optional().default(new Map()), + /** + * A function that, given the name of a module, can optionally return a description + * of that module's type signature. + */ + moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null), + /** * A list of functions which the application compiles as macros, where * the compiler must ensure they are not compiled to rename the macro or separate the @@ -577,6 +586,7 @@ export function printFunctionType(type: ReactFunctionType): string { export class Environment { #globals: GlobalRegistry; #shapes: ShapeRegistry; + #moduleTypes: Map = new Map(); #nextIdentifer: number = 0; #nextBlock: number = 0; #nextScope: number = 0; @@ -698,7 +708,40 @@ export class Environment { return this.#outlinedFunctions; } - getGlobalDeclaration(binding: NonLocalBinding): Global | null { + #resolveModuleType(moduleName: string, loc: SourceLocation): Global | null { + if (this.config.moduleTypeProvider == null) { + return null; + } + let moduleType = this.#moduleTypes.get(moduleName); + if (moduleType === undefined) { + const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName); + if (unparsedModuleConfig != null) { + const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig); + if (!parsedModuleConfig.success) { + CompilerError.throwInvalidConfig({ + reason: `Could not parse module type, the configured \`moduleTypeProvider\` function returned an invalid module description`, + description: parsedModuleConfig.error.toString(), + loc, + }); + } + const moduleConfig = parsedModuleConfig.data; + moduleType = installTypeConfig( + this.#globals, + this.#shapes, + moduleConfig, + ); + } else { + moduleType = null; + } + this.#moduleTypes.set(moduleName, moduleType); + } + return moduleType; + } + + getGlobalDeclaration( + binding: NonLocalBinding, + loc: SourceLocation, + ): Global | null { if (this.config.hookPattern != null) { const match = new RegExp(this.config.hookPattern).exec(binding.name); if ( @@ -736,6 +779,17 @@ export class Environment { (isHookName(binding.imported) ? this.#getCustomHookType() : null) ); } else { + const moduleType = this.#resolveModuleType(binding.module, loc); + if (moduleType !== null) { + const importedType = this.getPropertyType( + moduleType, + binding.imported, + ); + if (importedType != null) { + return importedType; + } + } + /** * For modules we don't own, we look at whether the original name or import alias * are hook-like. Both of the following are likely hooks so we would return a hook @@ -758,6 +812,17 @@ export class Environment { (isHookName(binding.name) ? this.#getCustomHookType() : null) ); } else { + const moduleType = this.#resolveModuleType(binding.module, loc); + if (moduleType !== null) { + if (binding.kind === 'ImportDefault') { + const defaultType = this.getPropertyType(moduleType, 'default'); + if (defaultType !== null) { + return defaultType; + } + } else { + return moduleType; + } + } return isHookName(binding.name) ? this.#getCustomHookType() : null; } } @@ -767,9 +832,7 @@ export class Environment { #isKnownReactModule(moduleName: string): boolean { return ( moduleName.toLowerCase() === 'react' || - moduleName.toLowerCase() === 'react-dom' || - (this.config.enableSharedRuntime__testonly && - moduleName === 'shared-runtime') + moduleName.toLowerCase() === 'react-dom' ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index e9066f85b8193..2812394300ad5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR'; import { BUILTIN_SHAPES, BuiltInArrayId, + BuiltInMixedReadonlyId, BuiltInUseActionStateId, BuiltInUseContextHookId, BuiltInUseEffectHookId, @@ -25,6 +26,8 @@ import { addObject, } from './ObjectShape'; import {BuiltInType, PolyType} from './Types'; +import {TypeConfig} from './TypeSchema'; +import {assertExhaustive} from '../Utils/utils'; /* * This file exports types and defaults for JavaScript global objects. @@ -528,6 +531,79 @@ DEFAULT_GLOBALS.set( addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS), ); +export function installTypeConfig( + globals: GlobalRegistry, + shapes: ShapeRegistry, + typeConfig: TypeConfig, +): Global { + switch (typeConfig.kind) { + case 'type': { + switch (typeConfig.name) { + case 'Array': { + return {kind: 'Object', shapeId: BuiltInArrayId}; + } + case 'MixedReadonly': { + return {kind: 'Object', shapeId: BuiltInMixedReadonlyId}; + } + case 'Primitive': { + return {kind: 'Primitive'}; + } + case 'Ref': { + return {kind: 'Object', shapeId: BuiltInUseRefId}; + } + case 'Any': { + return {kind: 'Poly'}; + } + default: { + assertExhaustive( + typeConfig.name, + `Unexpected type '${(typeConfig as any).name}'`, + ); + } + } + } + case 'function': { + return addFunction(shapes, [], { + positionalParams: typeConfig.positionalParams, + restParam: typeConfig.restParam, + calleeEffect: typeConfig.calleeEffect, + returnType: installTypeConfig(globals, shapes, typeConfig.returnType), + returnValueKind: typeConfig.returnValueKind, + noAlias: typeConfig.noAlias === true, + mutableOnlyIfOperandsAreMutable: + typeConfig.mutableOnlyIfOperandsAreMutable === true, + }); + } + case 'hook': { + return addHook(shapes, { + hookKind: 'Custom', + positionalParams: typeConfig.positionalParams ?? [], + restParam: typeConfig.restParam ?? Effect.Freeze, + calleeEffect: Effect.Read, + returnType: installTypeConfig(globals, shapes, typeConfig.returnType), + returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, + noAlias: typeConfig.noAlias === true, + }); + } + case 'object': { + return addObject( + shapes, + null, + Object.entries(typeConfig.properties ?? {}).map(([key, value]) => [ + key, + installTypeConfig(globals, shapes, value), + ]), + ); + } + default: { + assertExhaustive( + typeConfig, + `Unexpected type kind '${(typeConfig as any).kind}'`, + ); + } + } +} + export function installReAnimatedTypes( globals: GlobalRegistry, registry: ShapeRegistry, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 0810130102b0e..e56c002c513bd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -12,6 +12,7 @@ import {assertExhaustive} from '../Utils/utils'; import {Environment, ReactFunctionType} from './Environment'; import {HookKind} from './ObjectShape'; import {Type, makeType} from './Types'; +import {z} from 'zod'; /* * ******************************************************************************************* @@ -1360,6 +1361,15 @@ export enum ValueKind { Context = 'context', } +export const ValueKindSchema = z.enum([ + ValueKind.MaybeFrozen, + ValueKind.Frozen, + ValueKind.Primitive, + ValueKind.Global, + ValueKind.Mutable, + ValueKind.Context, +]); + // The effect with which a value is modified. export enum Effect { // Default value: not allowed after lifetime inference @@ -1389,6 +1399,15 @@ export enum Effect { Store = 'store', } +export const EffectSchema = z.enum([ + Effect.Read, + Effect.Mutate, + Effect.ConditionallyMutate, + Effect.Capture, + Effect.Store, + Effect.Freeze, +]); + export function isMutableEffect( effect: Effect, location: SourceLocation, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts new file mode 100644 index 0000000000000..362328db72155 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -0,0 +1,105 @@ +/** + * 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. + */ + +import {isValidIdentifier} from '@babel/types'; +import {z} from 'zod'; +import {Effect, ValueKind} from '..'; +import {EffectSchema, ValueKindSchema} from './HIR'; + +export type ObjectPropertiesConfig = {[key: string]: TypeConfig}; +export const ObjectPropertiesSchema: z.ZodType = z + .record( + z.string(), + z.lazy(() => TypeSchema), + ) + .refine(record => { + return Object.keys(record).every( + key => key === '*' || key === 'default' || isValidIdentifier(key), + ); + }, 'Expected all "object" property names to be valid identifier, `*` to match any property, of `default` to define a module default export'); + +export type ObjectTypeConfig = { + kind: 'object'; + properties: ObjectPropertiesConfig | null; +}; +export const ObjectTypeSchema: z.ZodType = z.object({ + kind: z.literal('object'), + properties: ObjectPropertiesSchema.nullable(), +}); + +export type FunctionTypeConfig = { + kind: 'function'; + positionalParams: Array; + restParam: Effect | null; + calleeEffect: Effect; + returnType: TypeConfig; + returnValueKind: ValueKind; + noAlias?: boolean | null | undefined; + mutableOnlyIfOperandsAreMutable?: boolean | null | undefined; +}; +export const FunctionTypeSchema: z.ZodType = z.object({ + kind: z.literal('function'), + positionalParams: z.array(EffectSchema), + restParam: EffectSchema.nullable(), + calleeEffect: EffectSchema, + returnType: z.lazy(() => TypeSchema), + returnValueKind: ValueKindSchema, + noAlias: z.boolean().nullable().optional(), + mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(), +}); + +export type HookTypeConfig = { + kind: 'hook'; + positionalParams?: Array | null | undefined; + restParam?: Effect | null | undefined; + returnType: TypeConfig; + returnValueKind?: ValueKind | null | undefined; + noAlias?: boolean | null | undefined; +}; +export const HookTypeSchema: z.ZodType = z.object({ + kind: z.literal('hook'), + positionalParams: z.array(EffectSchema).nullable().optional(), + restParam: EffectSchema.nullable().optional(), + returnType: z.lazy(() => TypeSchema), + returnValueKind: ValueKindSchema.nullable().optional(), + noAlias: z.boolean().nullable().optional(), +}); + +export type BuiltInTypeConfig = + | 'Any' + | 'Ref' + | 'Array' + | 'Primitive' + | 'MixedReadonly'; +export const BuiltInTypeSchema: z.ZodType = z.union([ + z.literal('Any'), + z.literal('Ref'), + z.literal('Array'), + z.literal('Primitive'), + z.literal('MixedReadonly'), +]); + +export type TypeReferenceConfig = { + kind: 'type'; + name: BuiltInTypeConfig; +}; +export const TypeReferenceSchema: z.ZodType = z.object({ + kind: z.literal('type'), + name: BuiltInTypeSchema, +}); + +export type TypeConfig = + | ObjectTypeConfig + | FunctionTypeConfig + | HookTypeConfig + | TypeReferenceConfig; +export const TypeSchema: z.ZodType = z.union([ + ObjectTypeSchema, + FunctionTypeSchema, + HookTypeSchema, + TypeReferenceSchema, +]); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index 2d9e21af1d61b..c9d2a7e1412c3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -127,7 +127,7 @@ function collectTemporaries( break; } case 'LoadGlobal': { - const global = env.getGlobalDeclaration(value.binding); + const global = env.getGlobalDeclaration(value.binding, value.loc); const hookKind = global !== null ? getHookKindForType(env, global) : null; const lvalId = instr.lvalue.identifier.id; if (hookKind === 'useMemo' || hookKind === 'useCallback') { diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 0b8949e1977ff..4dfeb676a3d0b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -227,7 +227,7 @@ function* generateInstructionTypes( } case 'LoadGlobal': { - const globalType = env.getGlobalDeclaration(value.binding); + const globalType = env.getGlobalDeclaration(value.binding, value.loc); if (globalType) { yield equation(left, globalType); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md index ed566a605fd5a..f69149aba4175 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md @@ -2,6 +2,8 @@ ## Input ```javascript +import {useFragment} from 'shared-runtime'; + function Component(props) { const post = useFragment( graphql` @@ -36,6 +38,8 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useFragment } from "shared-runtime"; + function Component(props) { const $ = _c(4); const post = useFragment( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.js index f44fe5c57baf3..5d1377c458855 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.js @@ -1,3 +1,5 @@ +import {useFragment} from 'shared-runtime'; + function Component(props) { const post = useFragment( graphql` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.expect.md index 4dc011c85f420..fa6f8cd9bc66c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.expect.md @@ -2,6 +2,8 @@ ## Input ```javascript +import {useFragment} from 'shared-runtime'; + function Component(props) { const item = useFragment( graphql` @@ -20,6 +22,8 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useFragment } from "shared-runtime"; + function Component(props) { const $ = _c(2); const item = useFragment( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.js index fac4efc9ba90b..6fa11a2dab494 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-logical.js @@ -1,3 +1,5 @@ +import {useFragment} from 'shared-runtime'; + function Component(props) { const item = useFragment( graphql` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md index 0cbd9caf169f0..3abd8cac949c3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md @@ -2,6 +2,8 @@ ## Input ```javascript +import {useFragment} from 'shared-runtime'; + function Component(props) { const x = makeObject(); const user = useFragment( @@ -28,6 +30,8 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useFragment } from "shared-runtime"; + function Component(props) { const $ = _c(3); const x = makeObject(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js index d78be74ac9957..2658a048939d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js @@ -1,3 +1,5 @@ +import {useFragment} from 'shared-runtime'; + function Component(props) { const x = makeObject(); const user = useFragment( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md index 1a5f49631f661..05ab1c533b9b1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md @@ -2,6 +2,8 @@ ## Input ```javascript +import {useFragment} from 'shared-runtime'; + function Component(props) { const user = useFragment( graphql` @@ -26,6 +28,8 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useFragment } from "shared-runtime"; + function Component(props) { const $ = _c(5); const user = useFragment( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.js index 7cccb397cabb2..bb1e52dc7645c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/readonly-object-method-calls.js @@ -1,3 +1,5 @@ +import {useFragment} from 'shared-runtime'; + function Component(props) { const user = useFragment( graphql` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md index 3ffc93ada58ea..d52a7713bb875 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md @@ -2,6 +2,8 @@ ## Input ```javascript +import {useFragment} from 'shared-runtime'; + function Component(props) { const user = useFragment( graphql` @@ -19,6 +21,8 @@ function Component(props) { ## Code ```javascript +import { useFragment } from "shared-runtime"; + function Component(props) { const user = useFragment( graphql` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.js index cb31d2978a610..dbcd2b6d21803 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/tagged-template-in-hook.js @@ -1,3 +1,5 @@ +import {useFragment} from 'shared-runtime'; + function Component(props) { const user = useFragment( graphql` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md new file mode 100644 index 0000000000000..54d5be2d6bf44 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md @@ -0,0 +1,147 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {ValidateMemoization} from 'shared-runtime'; +import typedLog from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + typedLog(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { ValidateMemoization } from "shared-runtime"; +import typedLog from "shared-runtime"; + +export function Component(t0) { + const $ = _c(17); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + typedLog(item1, item2); + let t5; + if ($[4] !== a) { + t5 = [a]; + $[4] = a; + $[5] = t5; + } else { + t5 = $[5]; + } + let t6; + if ($[6] !== t5 || $[7] !== item1) { + t6 = ; + $[6] = t5; + $[7] = item1; + $[8] = t6; + } else { + t6 = $[8]; + } + let t7; + if ($[9] !== b) { + t7 = [b]; + $[9] = b; + $[10] = t7; + } else { + t7 = $[10]; + } + let t8; + if ($[11] !== t7 || $[12] !== item2) { + t8 = ; + $[11] = t7; + $[12] = item2; + $[13] = t8; + } else { + t8 = $[13]; + } + let t9; + if ($[14] !== t6 || $[15] !== t8) { + t9 = ( + <> + {t6} + {t8} + + ); + $[14] = t6; + $[15] = t8; + $[16] = t9; + } else { + t9 = $[16]; + } + return t9; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+logs: [{ a: 0 },{ b: 0 },{ a: 1 },{ b: 0 },{ a: 1 },{ b: 1 },{ a: 1 },{ b: 2 },{ a: 2 },{ b: 2 },{ a: 3 },{ b: 2 },{ a: 0 },{ b: 0 }] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.tsx new file mode 100644 index 0000000000000..ec5dcf41e004c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.tsx @@ -0,0 +1,30 @@ +import {useMemo} from 'react'; +import {ValidateMemoization} from 'shared-runtime'; +import typedLog from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + typedLog(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md new file mode 100644 index 0000000000000..072c6d03d9adb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md @@ -0,0 +1,145 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {typedLog, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + typedLog(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { typedLog, ValidateMemoization } from "shared-runtime"; + +export function Component(t0) { + const $ = _c(17); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + typedLog(item1, item2); + let t5; + if ($[4] !== a) { + t5 = [a]; + $[4] = a; + $[5] = t5; + } else { + t5 = $[5]; + } + let t6; + if ($[6] !== t5 || $[7] !== item1) { + t6 = ; + $[6] = t5; + $[7] = item1; + $[8] = t6; + } else { + t6 = $[8]; + } + let t7; + if ($[9] !== b) { + t7 = [b]; + $[9] = b; + $[10] = t7; + } else { + t7 = $[10]; + } + let t8; + if ($[11] !== t7 || $[12] !== item2) { + t8 = ; + $[11] = t7; + $[12] = item2; + $[13] = t8; + } else { + t8 = $[13]; + } + let t9; + if ($[14] !== t6 || $[15] !== t8) { + t9 = ( + <> + {t6} + {t8} + + ); + $[14] = t6; + $[15] = t8; + $[16] = t9; + } else { + t9 = $[16]; + } + return t9; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
+
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
+logs: [{ a: 0 },{ b: 0 },{ a: 1 },{ b: 0 },{ a: 1 },{ b: 1 },{ a: 1 },{ b: 2 },{ a: 2 },{ b: 2 },{ a: 3 },{ b: 2 },{ a: 0 },{ b: 0 }] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.tsx new file mode 100644 index 0000000000000..5fb53d9ca85d4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.tsx @@ -0,0 +1,29 @@ +import {useMemo} from 'react'; +import {typedLog, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + typedLog(item1, item2); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md new file mode 100644 index 0000000000000..caa74267f326b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md @@ -0,0 +1,185 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import * as SharedRuntime from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + const items = useMemo(() => { + const items = []; + SharedRuntime.typedArrayPush(items, item1); + SharedRuntime.typedArrayPush(items, item2); + return items; + }, [item1, item2]); + + return ( + <> + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import * as SharedRuntime from "shared-runtime"; + +export function Component(t0) { + const $ = _c(27); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + let t5; + let items; + if ($[4] !== item1 || $[5] !== item2) { + items = []; + SharedRuntime.typedArrayPush(items, item1); + SharedRuntime.typedArrayPush(items, item2); + $[4] = item1; + $[5] = item2; + $[6] = items; + } else { + items = $[6]; + } + t5 = items; + const items_0 = t5; + let t6; + if ($[7] !== a) { + t6 = [a]; + $[7] = a; + $[8] = t6; + } else { + t6 = $[8]; + } + const t7 = items_0[0]; + let t8; + if ($[9] !== t6 || $[10] !== t7) { + t8 = ; + $[9] = t6; + $[10] = t7; + $[11] = t8; + } else { + t8 = $[11]; + } + let t9; + if ($[12] !== b) { + t9 = [b]; + $[12] = b; + $[13] = t9; + } else { + t9 = $[13]; + } + const t10 = items_0[1]; + let t11; + if ($[14] !== t9 || $[15] !== t10) { + t11 = ; + $[14] = t9; + $[15] = t10; + $[16] = t11; + } else { + t11 = $[16]; + } + let t12; + if ($[17] !== a || $[18] !== b) { + t12 = [a, b]; + $[17] = a; + $[18] = b; + $[19] = t12; + } else { + t12 = $[19]; + } + let t13; + if ($[20] !== t12 || $[21] !== items_0) { + t13 = ; + $[20] = t12; + $[21] = items_0; + $[22] = t13; + } else { + t13 = $[22]; + } + let t14; + if ($[23] !== t8 || $[24] !== t11 || $[25] !== t13) { + t14 = ( + <> + {t8} + {t11} + {t13} + + ); + $[23] = t8; + $[24] = t11; + $[25] = t13; + $[26] = t14; + } else { + t14 = $[26]; + } + return t14; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[0,0],"output":[{"a":0},{"b":0}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[1,0],"output":[{"a":1},{"b":0}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
{"inputs":[1,1],"output":[{"a":1},{"b":1}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[1,2],"output":[{"a":1},{"b":2}]}
+
{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[2,2],"output":[{"a":2},{"b":2}]}
+
{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[3,2],"output":[{"a":3},{"b":2}]}
+
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[0,0],"output":[{"a":0},{"b":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.tsx new file mode 100644 index 0000000000000..6479df9a5a86e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.tsx @@ -0,0 +1,35 @@ +import {useMemo} from 'react'; +import * as SharedRuntime from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + const items = useMemo(() => { + const items = []; + SharedRuntime.typedArrayPush(items, item1); + SharedRuntime.typedArrayPush(items, item2); + return items; + }, [item1, item2]); + + return ( + <> + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md new file mode 100644 index 0000000000000..a92abd4ca597c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md @@ -0,0 +1,185 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {typedArrayPush, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + const items = useMemo(() => { + const items = []; + typedArrayPush(items, item1); + typedArrayPush(items, item2); + return items; + }, [item1, item2]); + + return ( + <> + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { typedArrayPush, ValidateMemoization } from "shared-runtime"; + +export function Component(t0) { + const $ = _c(27); + const { a, b } = t0; + let t1; + let t2; + if ($[0] !== a) { + t2 = { a }; + $[0] = a; + $[1] = t2; + } else { + t2 = $[1]; + } + t1 = t2; + const item1 = t1; + let t3; + let t4; + if ($[2] !== b) { + t4 = { b }; + $[2] = b; + $[3] = t4; + } else { + t4 = $[3]; + } + t3 = t4; + const item2 = t3; + let t5; + let items; + if ($[4] !== item1 || $[5] !== item2) { + items = []; + typedArrayPush(items, item1); + typedArrayPush(items, item2); + $[4] = item1; + $[5] = item2; + $[6] = items; + } else { + items = $[6]; + } + t5 = items; + const items_0 = t5; + let t6; + if ($[7] !== a) { + t6 = [a]; + $[7] = a; + $[8] = t6; + } else { + t6 = $[8]; + } + const t7 = items_0[0]; + let t8; + if ($[9] !== t6 || $[10] !== t7) { + t8 = ; + $[9] = t6; + $[10] = t7; + $[11] = t8; + } else { + t8 = $[11]; + } + let t9; + if ($[12] !== b) { + t9 = [b]; + $[12] = b; + $[13] = t9; + } else { + t9 = $[13]; + } + const t10 = items_0[1]; + let t11; + if ($[14] !== t9 || $[15] !== t10) { + t11 = ; + $[14] = t9; + $[15] = t10; + $[16] = t11; + } else { + t11 = $[16]; + } + let t12; + if ($[17] !== a || $[18] !== b) { + t12 = [a, b]; + $[17] = a; + $[18] = b; + $[19] = t12; + } else { + t12 = $[19]; + } + let t13; + if ($[20] !== t12 || $[21] !== items_0) { + t13 = ; + $[20] = t12; + $[21] = items_0; + $[22] = t13; + } else { + t13 = $[22]; + } + let t14; + if ($[23] !== t8 || $[24] !== t11 || $[25] !== t13) { + t14 = ( + <> + {t8} + {t11} + {t13} + + ); + $[23] = t8; + $[24] = t11; + $[25] = t13; + $[26] = t14; + } else { + t14 = $[26]; + } + return t14; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 2 }, + { a: 3, b: 2 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[0,0],"output":[{"a":0},{"b":0}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[1,0],"output":[{"a":1},{"b":0}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[1],"output":{"b":1}}
{"inputs":[1,1],"output":[{"a":1},{"b":1}]}
+
{"inputs":[1],"output":{"a":1}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[1,2],"output":[{"a":1},{"b":2}]}
+
{"inputs":[2],"output":{"a":2}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[2,2],"output":[{"a":2},{"b":2}]}
+
{"inputs":[3],"output":{"a":3}}
{"inputs":[2],"output":{"b":2}}
{"inputs":[3,2],"output":[{"a":3},{"b":2}]}
+
{"inputs":[0],"output":{"a":0}}
{"inputs":[0],"output":{"b":0}}
{"inputs":[0,0],"output":[{"a":0},{"b":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.tsx new file mode 100644 index 0000000000000..3afef5439bec3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.tsx @@ -0,0 +1,35 @@ +import {useMemo} from 'react'; +import {typedArrayPush, ValidateMemoization} from 'shared-runtime'; + +export function Component({a, b}) { + const item1 = useMemo(() => ({a}), [a]); + const item2 = useMemo(() => ({b}), [b]); + const items = useMemo(() => { + const items = []; + typedArrayPush(items, item1); + typedArrayPush(items, item2); + return items; + }, [item1, item2]); + + return ( + <> + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 1, b: 2}, + {a: 2, b: 2}, + {a: 3, b: 2}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index de0184f0bddb1..417a657d28012 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -31,6 +31,7 @@ import path from 'path'; import prettier from 'prettier'; import SproutTodoFilter from './SproutTodoFilter'; import {isExpectError} from './fixture-utils'; +import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider'; export function parseLanguage(source: string): 'flow' | 'typescript' { return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript'; } @@ -38,6 +39,8 @@ export function parseLanguage(source: string): 'flow' | 'typescript' { function makePluginOptions( firstLine: string, parseConfigPragmaFn: typeof ParseConfigPragma, + EffectEnum: typeof Effect, + ValueKindEnum: typeof ValueKind, ): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] { let gating = null; let enableEmitInstrumentForget = null; @@ -212,35 +215,10 @@ function makePluginOptions( const options = { environment: { ...config, - customHooks: new Map([ - [ - 'useFreeze', - { - valueKind: 'frozen' as ValueKind, - effectKind: 'freeze' as Effect, - transitiveMixedData: false, - noAlias: false, - }, - ], - [ - 'useFragment', - { - valueKind: 'frozen' as ValueKind, - effectKind: 'freeze' as Effect, - transitiveMixedData: true, - noAlias: true, - }, - ], - [ - 'useNoAlias', - { - valueKind: 'mutable' as ValueKind, - effectKind: 'read' as Effect, - transitiveMixedData: false, - noAlias: true, - }, - ], - ]), + moduleTypeProvider: makeSharedRuntimeTypeProvider({ + EffectEnum, + ValueKindEnum, + }), customMacros, enableEmitFreeze, enableEmitInstrumentForget, @@ -383,6 +361,8 @@ export async function transformFixtureInput( parseConfigPragmaFn: typeof ParseConfigPragma, plugin: BabelCore.PluginObj, includeEvaluator: boolean, + EffectEnum: typeof Effect, + ValueKindEnum: typeof ValueKind, ): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> { // Extract the first line to quickly check for custom test directives const firstLine = input.substring(0, input.indexOf('\n')); @@ -405,7 +385,12 @@ export async function transformFixtureInput( /** * Get Forget compiled code */ - const [options, logs] = makePluginOptions(firstLine, parseConfigPragmaFn); + const [options, logs] = makePluginOptions( + firstLine, + parseConfigPragmaFn, + EffectEnum, + ValueKindEnum, + ); const forgetResult = transformFromAstSync(inputAst, input, { filename: virtualFilepath, highlightCode: false, diff --git a/compiler/packages/snap/src/constants.ts b/compiler/packages/snap/src/constants.ts index bcc0b0ff1ca03..abee06c55be8a 100644 --- a/compiler/packages/snap/src/constants.ts +++ b/compiler/packages/snap/src/constants.ts @@ -17,6 +17,7 @@ export const COMPILER_PATH = path.join( 'Babel', 'BabelPlugin.js', ); +export const COMPILER_INDEX_PATH = path.join(process.cwd(), 'dist', 'index'); export const LOGGER_PATH = path.join( process.cwd(), 'dist', diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index 55c85b9466925..9447b2cddc52c 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -11,6 +11,7 @@ import type {parseConfigPragma as ParseConfigPragma} from 'babel-plugin-react-co import {TransformResult, transformFixtureInput} from './compiler'; import { COMPILER_PATH, + COMPILER_INDEX_PATH, LOGGER_PATH, PARSE_CONFIG_PRAGMA_PATH, } from './constants'; @@ -60,6 +61,9 @@ async function compile( const {default: BabelPluginReactCompiler} = require(COMPILER_PATH) as { default: PluginObj; }; + const {Effect: EffectEnum, ValueKind: ValueKindEnum} = require( + COMPILER_INDEX_PATH, + ); const {toggleLogging} = require(LOGGER_PATH); const {parseConfigPragma} = require(PARSE_CONFIG_PRAGMA_PATH) as { parseConfigPragma: typeof ParseConfigPragma; @@ -74,6 +78,8 @@ async function compile( parseConfigPragma, BabelPluginReactCompiler, includeEvaluator, + EffectEnum, + ValueKindEnum, ); if (result.kind === 'err') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts new file mode 100644 index 0000000000000..fb0877d11474f --- /dev/null +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -0,0 +1,69 @@ +/** + * 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. + */ + +import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src'; +import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema'; + +export function makeSharedRuntimeTypeProvider({ + EffectEnum, + ValueKindEnum, +}: { + EffectEnum: typeof Effect; + ValueKindEnum: typeof ValueKind; +}) { + return function sharedRuntimeTypeProvider( + moduleName: string, + ): TypeConfig | null { + if (moduleName !== 'shared-runtime') { + return null; + } + return { + kind: 'object', + properties: { + default: { + kind: 'function', + calleeEffect: EffectEnum.Read, + positionalParams: [], + restParam: EffectEnum.Read, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + }, + typedArrayPush: { + kind: 'function', + calleeEffect: EffectEnum.Read, + positionalParams: [EffectEnum.Store, EffectEnum.Capture], + restParam: EffectEnum.Capture, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + }, + typedLog: { + kind: 'function', + calleeEffect: EffectEnum.Read, + positionalParams: [], + restParam: EffectEnum.Read, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + }, + useFreeze: { + kind: 'hook', + returnType: {kind: 'type', name: 'Any'}, + }, + useFragment: { + kind: 'hook', + returnType: {kind: 'type', name: 'MixedReadonly'}, + noAlias: true, + }, + useNoAlias: { + kind: 'hook', + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + noAlias: true, + }, + }, + }; + }; +} diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index f15aaaaa4a2e5..bb1c65a6574ac 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -347,3 +347,12 @@ export function useFragment(..._args: Array): object { b: {c: {d: 4}}, }; } + +export function typedArrayPush(array: Array, item: T): void { + array.push(item); +} + +export function typedLog(...values: Array): void { + console.log(...values); +} +export default typedLog; From 8a20fc3b19b600a3b8666203f1877230c62becf9 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 17:31:47 -0700 Subject: [PATCH 039/191] [compiler] Repro of missing memoization due to capturing w/o mutation If you have a function expression which _captures_ a mutable value (but does not mutate it), and that function is invoked during render, we infer the invocation as a mutation of the captured value. But in some circumstances we can prove that the captured value cannot have been mutated, and could in theory avoid inferring a mutation. ghstack-source-id: 47664e48ce8c51a6edf4d714d1acd1ec4781df80 Pull Request resolved: https://github.com/facebook/react/pull/30783 --- ...ed-function-inferred-as-mutation.expect.md | 54 +++++++++++++++++++ ...n-invoked-function-inferred-as-mutation.js | 33 ++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md new file mode 100644 index 0000000000000..1eb9b98b09529 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @flow @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {logValue, useFragment, useHook, typedLog} from 'shared-runtime'; + +component Component() { + const data = useFragment(); + + const getIsEnabled = () => { + if (data != null) { + return true; + } else { + return false; + } + }; + + // We infer that getIsEnabled returns a mutable value, such that + // isEnabled is mutable + const isEnabled = useMemo(() => getIsEnabled(), [getIsEnabled]); + + // We then infer getLoggingData as capturing that mutable value, + // so any calls to this function are then inferred as extending + // the mutable range of isEnabled + const getLoggingData = () => { + return { + isEnabled, + }; + }; + + // The call here is then inferred as an indirect mutation of isEnabled + useHook(getLoggingData()); + + return
typedLog(getLoggingData())} />; +} + +``` + + +## Error + +``` + 16 | // We infer that getIsEnabled returns a mutable value, such that + 17 | // isEnabled is mutable +> 18 | const isEnabled = useMemo(() => getIsEnabled(), [getIsEnabled]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (18:18) + 19 | + 20 | // We then infer getLoggingData as capturing that mutable value, + 21 | // so any calls to this function are then inferred as extending +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js new file mode 100644 index 0000000000000..02114e26530c5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js @@ -0,0 +1,33 @@ +// @flow @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {logValue, useFragment, useHook, typedLog} from 'shared-runtime'; + +component Component() { + const data = useFragment(); + + const getIsEnabled = () => { + if (data != null) { + return true; + } else { + return false; + } + }; + + // We infer that getIsEnabled returns a mutable value, such that + // isEnabled is mutable + const isEnabled = useMemo(() => getIsEnabled(), [getIsEnabled]); + + // We then infer getLoggingData as capturing that mutable value, + // so any calls to this function are then inferred as extending + // the mutable range of isEnabled + const getLoggingData = () => { + return { + isEnabled, + }; + }; + + // The call here is then inferred as an indirect mutation of isEnabled + useHook(getLoggingData()); + + return
typedLog(getLoggingData())} />; +} From 217a0efcd90ef04556e0256e0eff9313bdbbcaca Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 18:21:27 -0700 Subject: [PATCH 040/191] [compiler] Add returnIdentifier to function expressions This gives us a place to store type information, used in follow-up PRs. ghstack-source-id: ee0bfa253f63c30ccaac083b9f1f72b76617f19c Pull Request resolved: https://github.com/facebook/react/pull/30784 --- .../packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts | 3 +++ compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts | 1 + .../src/Optimization/LowerContextAccess.ts | 2 ++ ...epro-named-function-with-shadowed-local-same-name.expect.md | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 20fac9d610a08..4fec81c91146f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -211,11 +211,14 @@ export function lower( null, ); + const returnIdentifier = builder.makeTemporary(func.node.loc ?? GeneratedSource); + return Ok({ id, params, fnType: parent == null ? env.fnType : 'Other', returnType: null, // TODO: extract the actual return type node if present + returnIdentifier, body: builder.build(), context, generator: func.node.generator === true, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index e56c002c513bd..8c8682aa8cc2b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -286,6 +286,7 @@ export type HIRFunction = { env: Environment; params: Array; returnType: t.FlowType | t.TSType | null; + returnIdentifier: Identifier; context: Array; effects: Array | null; body: HIR; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 8455a094f05e7..5a21104d1d201 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -238,6 +238,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { phis: new Set(), }; + const returnIdentifier = createTemporaryPlace(env, GeneratedSource).identifier; const fn: HIRFunction = { loc: GeneratedSource, id: null, @@ -245,6 +246,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { env, params: [obj], returnType: null, + returnIdentifier, context: [], effects: null, body: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md index db3a192eaf604..51b0bfd506973 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md @@ -22,7 +22,7 @@ function Component(props) { 7 | return hasErrors; 8 | } > 9 | return hasErrors(); - | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$16 (9:9) + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$17 (9:9) 10 | } 11 | ``` From 8410c8b959b8e20adc5577cb7211702cfba0f78f Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 21 Aug 2024 21:17:29 -0700 Subject: [PATCH 041/191] [compiler] Infer return types of function expressions Uses the returnIdentifier added in the previous PR to provide a stable identifier for which we can infer a return type for functions, then wires up the equations in InferTypes to infer the type. ghstack-source-id: 22c0a9ea096daa5f72821fca2a5ff5b199f65c8b Pull Request resolved: https://github.com/facebook/react/pull/30785 --- .../src/HIR/BuildHIR.ts | 4 +- .../src/HIR/PrintHIR.ts | 6 ++- .../src/Optimization/LowerContextAccess.ts | 7 +++- ...rgeReactiveScopesThatInvalidateTogether.ts | 20 +++++---- .../src/TypeInference/InferTypes.ts | 20 ++++++++- ...ed-function-inferred-as-mutation.expect.md | 2 +- ...n-invoked-function-inferred-as-mutation.js | 2 +- ...oisting-simple-const-declaration.expect.md | 11 +++-- .../hoisting-simple-let-declaration.expect.md | 11 +++-- ...invoked-callback-escaping-return.expect.md | 41 ++++++++----------- 10 files changed, 74 insertions(+), 50 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 4fec81c91146f..cf2c113d8c418 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -211,7 +211,9 @@ export function lower( null, ); - const returnIdentifier = builder.makeTemporary(func.node.loc ?? GeneratedSource); + const returnIdentifier = builder.makeTemporary( + func.node.loc ?? GeneratedSource, + ); return Ok({ id, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 59f067787359f..19a3a718285f4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -72,6 +72,7 @@ export function printFunction(fn: HIRFunction): string { if (definition.length !== 0) { output.push(definition); } + output.push(printType(fn.returnIdentifier.type)); output.push(printHIR(fn.body)); output.push(...fn.directives); return output.join('\n'); @@ -555,7 +556,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string { } }) .join(', ') ?? ''; - value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]:\n${fn}`; + const type = printType( + instrValue.loweredFunc.func.returnIdentifier.type, + ).trim(); + value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; break; } case 'TaggedTemplateExpression': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 5a21104d1d201..5c6d19a0f2cfa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -238,7 +238,10 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { phis: new Set(), }; - const returnIdentifier = createTemporaryPlace(env, GeneratedSource).identifier; + const returnIdentifier = createTemporaryPlace( + env, + GeneratedSource, + ).identifier; const fn: HIRFunction = { loc: GeneratedSource, id: null, @@ -246,7 +249,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { env, params: [obj], returnType: null, - returnIdentifier, + returnIdentifier, context: [], effects: null, body: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts index 2c9004e6ad9a6..1e73697783f0b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts @@ -481,14 +481,20 @@ function canMergeScopes( } function isAlwaysInvalidatingType(type: Type): boolean { - if (type.kind === 'Object') { - switch (type.shapeId) { - case BuiltInArrayId: - case BuiltInObjectId: - case BuiltInFunctionId: - case BuiltInJsxId: { - return true; + switch (type.kind) { + case 'Object': { + switch (type.shapeId) { + case BuiltInArrayId: + case BuiltInObjectId: + case BuiltInFunctionId: + case BuiltInJsxId: { + return true; + } } + break; + } + case 'Function': { + return true; } } return false; diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 4dfeb676a3d0b..a8859f129bcfc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -88,6 +88,7 @@ function apply(func: HIRFunction, unifier: Unifier): void { } } } + func.returnIdentifier.type = unifier.get(func.returnIdentifier.type); } type TypeEquation = { @@ -122,6 +123,7 @@ function* generate( } const names = new Map(); + const returnTypes: Array = []; for (const [_, block] of func.body.blocks) { for (const phi of block.phis) { yield equation(phi.type, { @@ -133,6 +135,18 @@ function* generate( for (const instr of block.instructions) { yield* generateInstructionTypes(func.env, names, instr); } + const terminal = block.terminal; + if (terminal.kind === 'return') { + returnTypes.push(terminal.value.identifier.type); + } + } + if (returnTypes.length > 1) { + yield equation(func.returnIdentifier.type, { + kind: 'Phi', + operands: returnTypes, + }); + } else if (returnTypes.length === 1) { + yield equation(func.returnIdentifier.type, returnTypes[0]!); } } @@ -346,7 +360,11 @@ function* generateInstructionTypes( case 'FunctionExpression': { yield* generate(value.loweredFunc.func); - yield equation(left, {kind: 'Object', shapeId: BuiltInFunctionId}); + yield equation(left, { + kind: 'Function', + shapeId: BuiltInFunctionId, + return: value.loweredFunc.func.returnIdentifier.type, + }); break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md index 1eb9b98b09529..5205445751249 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.expect.md @@ -13,7 +13,7 @@ component Component() { if (data != null) { return true; } else { - return false; + return {}; } }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js index 02114e26530c5..1621ab41c9050 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-missed-memoization-from-capture-in-invoked-function-inferred-as-mutation.js @@ -9,7 +9,7 @@ component Component() { if (data != null) { return true; } else { - return false; + return {}; } }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.expect.md index ae5bf41e3227c..7939c9143d1d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.expect.md @@ -25,18 +25,17 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function hoisting() { const $ = _c(1); - let t0; + let foo; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const foo = () => bar + baz; + foo = () => bar + baz; const bar = 3; const baz = 2; - t0 = foo(); - $[0] = t0; + $[0] = foo; } else { - t0 = $[0]; + foo = $[0]; } - return t0; + return foo(); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md index 8974664b0515f..8d694a984aed5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md @@ -25,18 +25,17 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function hoisting() { const $ = _c(1); - let t0; + let foo; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const foo = () => bar + baz; + foo = () => bar + baz; let bar = 3; let baz = 2; - t0 = foo(); - $[0] = t0; + $[0] = foo; } else { - t0 = $[0]; + foo = $[0]; } - return t0; + return foo(); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md index 14913665c5474..6d9183915467f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-nonescaping-invoked-callback-escaping-return.expect.md @@ -40,7 +40,7 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { useCallback } from "react"; function Component(t0) { - const $ = _c(11); + const $ = _c(9); const { entity, children } = t0; let t1; if ($[0] !== entity) { @@ -51,46 +51,39 @@ function Component(t0) { t1 = $[1]; } const showMessage = t1; + + const shouldShowMessage = showMessage(); let t2; - if ($[2] !== showMessage) { - t2 = showMessage(); - $[2] = showMessage; + if ($[2] !== shouldShowMessage) { + t2 =
{shouldShowMessage}
; + $[2] = shouldShowMessage; $[3] = t2; } else { t2 = $[3]; } - const shouldShowMessage = t2; let t3; - if ($[4] !== shouldShowMessage) { - t3 =
{shouldShowMessage}
; - $[4] = shouldShowMessage; + if ($[4] !== children) { + t3 =
{children}
; + $[4] = children; $[5] = t3; } else { t3 = $[5]; } let t4; - if ($[6] !== children) { - t4 =
{children}
; - $[6] = children; - $[7] = t4; - } else { - t4 = $[7]; - } - let t5; - if ($[8] !== t3 || $[9] !== t4) { - t5 = ( + if ($[6] !== t2 || $[7] !== t3) { + t4 = (
+ {t2} {t3} - {t4}
); - $[8] = t3; - $[9] = t4; - $[10] = t5; + $[6] = t2; + $[7] = t3; + $[8] = t4; } else { - t5 = $[10]; + t4 = $[8]; } - return t5; + return t4; } export const FIXTURE_ENTRYPOINT = { From 98b57408216c80ec75723773524466657b4956b6 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 22 Aug 2024 09:06:58 -0700 Subject: [PATCH 042/191] [compiler] Rename HIRFunction.returnType Rename this field so we can use it for the actual return type. ghstack-source-id: 118d7dcfbbcc40911bf6d13f14e70053e436738d Pull Request resolved: https://github.com/facebook/react/pull/30789 --- .../packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts | 2 +- compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts | 2 +- .../src/Optimization/LowerContextAccess.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index cf2c113d8c418..1b0d9f455900d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -219,7 +219,7 @@ export function lower( id, params, fnType: parent == null ? env.fnType : 'Other', - returnType: null, // TODO: extract the actual return type node if present + returnTypeAnnotation: null, // TODO: extract the actual return type node if present returnIdentifier, body: builder.build(), context, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 8c8682aa8cc2b..1b866bb4c38a7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -285,7 +285,7 @@ export type HIRFunction = { fnType: ReactFunctionType; env: Environment; params: Array; - returnType: t.FlowType | t.TSType | null; + returnTypeAnnotation: t.FlowType | t.TSType | null; returnIdentifier: Identifier; context: Array; effects: Array | null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 5c6d19a0f2cfa..7ae39d695ad53 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -248,7 +248,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { fnType: 'Other', env, params: [obj], - returnType: null, + returnTypeAnnotation: null, returnIdentifier, context: [], effects: null, From 7a3fcc9898d57a723613814bd19ec1d60805e5c8 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 22 Aug 2024 09:07:01 -0700 Subject: [PATCH 043/191] [compiler] Flatten returnIdentifier to just returnType We don't a full Identifier object for the return type, we can just store the type. ghstack-source-id: 4594d64ce3900ced3e461945697926489898318e Pull Request resolved: https://github.com/facebook/react/pull/30790 --- .../babel-plugin-react-compiler/src/HIR/BuildHIR.ts | 6 +----- .../packages/babel-plugin-react-compiler/src/HIR/HIR.ts | 2 +- .../babel-plugin-react-compiler/src/HIR/PrintHIR.ts | 6 ++---- .../src/Optimization/LowerContextAccess.ts | 7 ++----- .../src/TypeInference/InferTypes.ts | 8 ++++---- ...named-function-with-shadowed-local-same-name.expect.md | 2 +- 6 files changed, 11 insertions(+), 20 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 1b0d9f455900d..06fcfbea7ecc0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -211,16 +211,12 @@ export function lower( null, ); - const returnIdentifier = builder.makeTemporary( - func.node.loc ?? GeneratedSource, - ); - return Ok({ id, params, fnType: parent == null ? env.fnType : 'Other', returnTypeAnnotation: null, // TODO: extract the actual return type node if present - returnIdentifier, + returnType: makeType(), body: builder.build(), context, generator: func.node.generator === true, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 1b866bb4c38a7..ea121c6fcd727 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -286,7 +286,7 @@ export type HIRFunction = { env: Environment; params: Array; returnTypeAnnotation: t.FlowType | t.TSType | null; - returnIdentifier: Identifier; + returnType: Type; context: Array; effects: Array | null; body: HIR; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 19a3a718285f4..a5ad303d7a587 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -72,7 +72,7 @@ export function printFunction(fn: HIRFunction): string { if (definition.length !== 0) { output.push(definition); } - output.push(printType(fn.returnIdentifier.type)); + output.push(printType(fn.returnType)); output.push(printHIR(fn.body)); output.push(...fn.directives); return output.join('\n'); @@ -556,9 +556,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string { } }) .join(', ') ?? ''; - const type = printType( - instrValue.loweredFunc.func.returnIdentifier.type, - ).trim(); + const type = printType(instrValue.loweredFunc.func.returnType).trim(); value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 7ae39d695ad53..e27b8f952148a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -23,6 +23,7 @@ import { isUseContextHookType, makeBlockId, makeInstructionId, + makeType, markInstructionIds, promoteTemporary, reversePostorderBlocks, @@ -238,10 +239,6 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { phis: new Set(), }; - const returnIdentifier = createTemporaryPlace( - env, - GeneratedSource, - ).identifier; const fn: HIRFunction = { loc: GeneratedSource, id: null, @@ -249,7 +246,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { env, params: [obj], returnTypeAnnotation: null, - returnIdentifier, + returnType: makeType(), context: [], effects: null, body: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index a8859f129bcfc..d0d23f0823df8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -88,7 +88,7 @@ function apply(func: HIRFunction, unifier: Unifier): void { } } } - func.returnIdentifier.type = unifier.get(func.returnIdentifier.type); + func.returnType = unifier.get(func.returnType); } type TypeEquation = { @@ -141,12 +141,12 @@ function* generate( } } if (returnTypes.length > 1) { - yield equation(func.returnIdentifier.type, { + yield equation(func.returnType, { kind: 'Phi', operands: returnTypes, }); } else if (returnTypes.length === 1) { - yield equation(func.returnIdentifier.type, returnTypes[0]!); + yield equation(func.returnType, returnTypes[0]!); } } @@ -363,7 +363,7 @@ function* generateInstructionTypes( yield equation(left, { kind: 'Function', shapeId: BuiltInFunctionId, - return: value.loweredFunc.func.returnIdentifier.type, + return: value.loweredFunc.func.returnType, }); break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md index 51b0bfd506973..db3a192eaf604 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md @@ -22,7 +22,7 @@ function Component(props) { 7 | return hasErrors; 8 | } > 9 | return hasErrors(); - | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$17 (9:9) + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$16 (9:9) 10 | } 11 | ``` From e483df4658473ca9c917a42be4869d445be00807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 22 Aug 2024 12:34:48 -0400 Subject: [PATCH 044/191] [Flight ESM] Wire up Source Maps in the flight-esm fixture (#30758) Same as #29708 but for the flight-esm fixture. --- fixtures/flight-esm/server/global.js | 39 +++++++++++++ fixtures/flight-esm/server/region.js | 84 ++++++++++++++++++++++++++++ fixtures/flight-esm/src/index.js | 11 ++++ fixtures/flight/src/index.js | 21 ++++--- 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/fixtures/flight-esm/server/global.js b/fixtures/flight-esm/server/global.js index 1088d42967a2f..c0b148fc063c4 100644 --- a/fixtures/flight-esm/server/global.js +++ b/fixtures/flight-esm/server/global.js @@ -131,6 +131,45 @@ app.use( express.static('node_modules/react-server-dom-esm/esm') ); +if (process.env.NODE_ENV === 'development') { + app.get('/source-maps', async function (req, res, next) { + // Proxy the request to the regional server. + const proxiedHeaders = { + 'X-Forwarded-Host': req.hostname, + 'X-Forwarded-For': req.ips, + 'X-Forwarded-Port': 3000, + 'X-Forwarded-Proto': req.protocol, + }; + + const promiseForData = request( + { + host: '127.0.0.1', + port: 3001, + method: req.method, + path: req.originalUrl, + headers: proxiedHeaders, + }, + req + ); + + try { + const rscResponse = await promiseForData; + res.set('Content-type', 'application/json'); + rscResponse.on('data', data => { + res.write(data); + res.flush(); + }); + rscResponse.on('end', data => { + res.end(); + }); + } catch (e) { + console.error(`Failed to proxy request: ${e.stack}`); + res.statusCode = 500; + res.end(); + } + }); +} + app.listen(3000, () => { console.log('Global Fizz/Webpack Server listening on port 3000...'); }); diff --git a/fixtures/flight-esm/server/region.js b/fixtures/flight-esm/server/region.js index c7e8d9aad33cc..fe992b6daf538 100644 --- a/fixtures/flight-esm/server/region.js +++ b/fixtures/flight-esm/server/region.js @@ -17,6 +17,8 @@ const app = express(); const compress = require('compression'); const {Readable} = require('node:stream'); +const nodeModule = require('node:module'); + app.use(compress()); // Application @@ -116,6 +118,88 @@ app.get('/todos', function (req, res) { ]); }); +if (process.env.NODE_ENV === 'development') { + const rootDir = path.resolve(__dirname, '../'); + + app.get('/source-maps', async function (req, res, next) { + try { + res.set('Content-type', 'application/json'); + let requestedFilePath = req.query.name; + + let isCompiledOutput = false; + if (requestedFilePath.startsWith('file://')) { + // We assume that if it was prefixed with file:// it's referring to the compiled output + // and if it's a direct file path we assume it's source mapped back to original format. + isCompiledOutput = true; + requestedFilePath = url.fileURLToPath(requestedFilePath); + } + + const relativePath = path.relative(rootDir, requestedFilePath); + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + // This is outside the root directory of the app. Forbid it to be served. + res.status = 403; + res.write('{}'); + res.end(); + return; + } + + const sourceMap = nodeModule.findSourceMap(requestedFilePath); + let map; + if (requestedFilePath.startsWith('node:')) { + // This is a node internal. We don't include any source code for this but we still + // generate a source map for it so that we can add it to an ignoreList automatically. + map = { + version: 3, + // We use the node:// protocol convention to teach Chrome DevTools that this is + // on a different protocol and not part of the current page. + sources: ['node:///' + requestedFilePath.slice(5)], + sourcesContent: ['// Node Internals'], + mappings: 'AAAA', + ignoreList: [0], + sourceRoot: '', + }; + } else if (!sourceMap || !isCompiledOutput) { + // If a file doesn't have a source map, such as this file, then we generate a blank + // source map that just contains the original content and segments pointing to the + // original lines. If a line number points to uncompiled output, like if source mapping + // was already applied we also use this path. + const sourceContent = await readFile(requestedFilePath, 'utf8'); + const lines = sourceContent.split('\n').length; + // We ensure to absolute + const sourceURL = url.pathToFileURL(requestedFilePath); + map = { + version: 3, + sources: [sourceURL], + sourcesContent: [sourceContent], + // Note: This approach to mapping each line only lets you jump to each line + // not jump to a column within a line. To do that, you need a proper source map + // generated for each parsed segment or add a segment for each column. + mappings: 'AAAA' + ';AACA'.repeat(lines - 1), + sourceRoot: '', + // Add any node_modules to the ignore list automatically. + ignoreList: requestedFilePath.includes('node_modules') + ? [0] + : undefined, + }; + } else { + // We always set prepareStackTrace before reading the stack so that we get the stack + // without source maps applied. Therefore we have to use the original source map. + // If something read .stack before we did, we might observe the line/column after + // source mapping back to the original file. We use the isCompiledOutput check above + // in that case. + map = sourceMap.payload; + } + res.write(JSON.stringify(map)); + res.end(); + } catch (x) { + res.status = 500; + res.write('{}'); + res.end(); + console.error(x); + } + }); +} + app.listen(3001, () => { console.log('Regional Flight Server listening on port 3001...'); }); diff --git a/fixtures/flight-esm/src/index.js b/fixtures/flight-esm/src/index.js index 30d060af6342c..6cef6c6c3c547 100644 --- a/fixtures/flight-esm/src/index.js +++ b/fixtures/flight-esm/src/index.js @@ -4,6 +4,15 @@ import ReactDOM from 'react-dom/client'; import {createFromFetch, encodeReply} from 'react-server-dom-esm/client'; const moduleBaseURL = '/src/'; + +function findSourceMapURL(fileName) { + return ( + document.location.origin + + '/source-maps?name=' + + encodeURIComponent(fileName) + ); +} + let updateRoot; async function callServer(id, args) { const response = fetch('/', { @@ -17,6 +26,7 @@ async function callServer(id, args) { const {returnValue, root} = await createFromFetch(response, { callServer, moduleBaseURL, + findSourceMapURL, }); // Refresh the tree with the new RSC payload. startTransition(() => { @@ -34,6 +44,7 @@ let data = createFromFetch( { callServer, moduleBaseURL, + findSourceMapURL, } ); diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index b4b538a3a9992..755551047535b 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -6,6 +6,14 @@ import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client'; // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet. import './style.css'; +function findSourceMapURL(fileName) { + return ( + document.location.origin + + '/source-maps?name=' + + encodeURIComponent(fileName) + ); +} + let updateRoot; async function callServer(id, args) { const response = fetch('/', { @@ -16,7 +24,10 @@ async function callServer(id, args) { }, body: await encodeReply(args), }); - const {returnValue, root} = await createFromFetch(response, {callServer}); + const {returnValue, root} = await createFromFetch(response, { + callServer, + findSourceMapURL, + }); // Refresh the tree with the new RSC payload. startTransition(() => { updateRoot(root); @@ -39,13 +50,7 @@ async function hydrateApp() { }), { callServer, - findSourceMapURL(fileName) { - return ( - document.location.origin + - '/source-maps?name=' + - encodeURIComponent(fileName) - ); - }, + findSourceMapURL, } ); From 97e2ce6a003db070d1d14ca25ac4b30e1df4a8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 22 Aug 2024 12:35:16 -0400 Subject: [PATCH 045/191] [Flight] Enable Server Action Source Maps in flight-esm Fixture (#30763) Stacked on #30758 and #30755. This is copy paste from #30755 into the ESM package. We use the `webpack-sources` package for the source map utility but it's not actually dependent on Webpack itself. Could probably inline it in the build. --- fixtures/flight-esm/package.json | 5 +- fixtures/flight-esm/yarn.lock | 5 + packages/react-server-dom-esm/package.json | 3 +- .../src/ReactFlightESMNodeLoader.js | 403 ++++++++++++++++-- 4 files changed, 372 insertions(+), 44 deletions(-) diff --git a/fixtures/flight-esm/package.json b/fixtures/flight-esm/package.json index 8188df5abbd59..cb4ca1ea30b82 100644 --- a/fixtures/flight-esm/package.json +++ b/fixtures/flight-esm/package.json @@ -13,14 +13,15 @@ "prompts": "^2.4.2", "react": "experimental", "react-dom": "experimental", - "undici": "^5.20.0" + "undici": "^5.20.0", + "webpack-sources": "^3.2.0" }, "scripts": { "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/", "dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"", "dev:global": "NODE_ENV=development BUILD_PATH=dist node server/global", - "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/region.js --conditions=react-server server/region", + "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server server/region", "start": "concurrently \"npm run start:region\" \"npm run start:global\"", "start:global": "NODE_ENV=production node server/global", "start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region" diff --git a/fixtures/flight-esm/yarn.lock b/fixtures/flight-esm/yarn.lock index 8d336e519453b..2eae2a7a934a6 100644 --- a/fixtures/flight-esm/yarn.lock +++ b/fixtures/flight-esm/yarn.lock @@ -755,6 +755,11 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +webpack-sources@^3.2.0: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" diff --git a/packages/react-server-dom-esm/package.json b/packages/react-server-dom-esm/package.json index a1f8f17f45014..a0cb32c34c9e8 100644 --- a/packages/react-server-dom-esm/package.json +++ b/packages/react-server-dom-esm/package.json @@ -58,6 +58,7 @@ "react-dom": "^19.0.0" }, "dependencies": { - "acorn-loose": "^8.3.0" + "acorn-loose": "^8.3.0", + "webpack-sources": "^3.2.0" } } diff --git a/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js b/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js index f54d5449fbbec..450f1b25d022a 100644 --- a/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js +++ b/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js @@ -9,6 +9,9 @@ import * as acorn from 'acorn-loose'; +import readMappings from 'webpack-sources/lib/helpers/readMappings.js'; +import createMappingsSerializer from 'webpack-sources/lib/helpers/createMappingsSerializer.js'; + type ResolveContext = { conditions: Array, parentURL: string | void, @@ -95,45 +98,102 @@ export async function getSource( return defaultGetSource(url, context, defaultGetSource); } -function addLocalExportedNames(names: Map, node: any) { +type ExportedEntry = { + localName: string, + exportedName: string, + type: null | string, + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, + originalLine: number, + originalColumn: number, + originalSource: number, + nameIndex: number, +}; + +function addExportedEntry( + exportedEntries: Array, + localNames: Set, + localName: string, + exportedName: string, + type: null | 'function', + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, +) { + if (localNames.has(localName)) { + // If the same local name is exported more than once, we only need one of the names. + return; + } + exportedEntries.push({ + localName, + exportedName, + type, + loc, + originalLine: -1, + originalColumn: -1, + originalSource: -1, + nameIndex: -1, + }); +} + +function addLocalExportedNames( + exportedEntries: Array, + localNames: Set, + node: any, +) { switch (node.type) { case 'Identifier': - names.set(node.name, node.name); + addExportedEntry( + exportedEntries, + localNames, + node.name, + node.name, + null, + node.loc, + ); return; case 'ObjectPattern': for (let i = 0; i < node.properties.length; i++) - addLocalExportedNames(names, node.properties[i]); + addLocalExportedNames(exportedEntries, localNames, node.properties[i]); return; case 'ArrayPattern': for (let i = 0; i < node.elements.length; i++) { const element = node.elements[i]; - if (element) addLocalExportedNames(names, element); + if (element) + addLocalExportedNames(exportedEntries, localNames, element); } return; case 'Property': - addLocalExportedNames(names, node.value); + addLocalExportedNames(exportedEntries, localNames, node.value); return; case 'AssignmentPattern': - addLocalExportedNames(names, node.left); + addLocalExportedNames(exportedEntries, localNames, node.left); return; case 'RestElement': - addLocalExportedNames(names, node.argument); + addLocalExportedNames(exportedEntries, localNames, node.argument); return; case 'ParenthesizedExpression': - addLocalExportedNames(names, node.expression); + addLocalExportedNames(exportedEntries, localNames, node.expression); return; } } function transformServerModule( source: string, - body: any, + program: any, url: string, + sourceMap: any, loader: LoadFunction, ): string { - // If the same local name is exported more than once, we only need one of the names. - const localNames: Map = new Map(); - const localTypes: Map = new Map(); + const body = program.body; + + // This entry list needs to be in source location order. + const exportedEntries: Array = []; + // Dedupe set. + const localNames: Set = new Set(); for (let i = 0; i < body.length; i++) { const node = body[i]; @@ -143,11 +203,24 @@ function transformServerModule( break; case 'ExportDefaultDeclaration': if (node.declaration.type === 'Identifier') { - localNames.set(node.declaration.name, 'default'); + addExportedEntry( + exportedEntries, + localNames, + node.declaration.name, + 'default', + null, + node.declaration.loc, + ); } else if (node.declaration.type === 'FunctionDeclaration') { if (node.declaration.id) { - localNames.set(node.declaration.id.name, 'default'); - localTypes.set(node.declaration.id.name, 'function'); + addExportedEntry( + exportedEntries, + localNames, + node.declaration.id.name, + 'default', + 'function', + node.declaration.id.loc, + ); } else { // TODO: This needs to be rewritten inline because it doesn't have a local name. } @@ -158,41 +231,230 @@ function transformServerModule( if (node.declaration.type === 'VariableDeclaration') { const declarations = node.declaration.declarations; for (let j = 0; j < declarations.length; j++) { - addLocalExportedNames(localNames, declarations[j].id); + addLocalExportedNames( + exportedEntries, + localNames, + declarations[j].id, + ); } } else { const name = node.declaration.id.name; - localNames.set(name, name); - if (node.declaration.type === 'FunctionDeclaration') { - localTypes.set(name, 'function'); - } + addExportedEntry( + exportedEntries, + localNames, + name, + name, + + node.declaration.type === 'FunctionDeclaration' + ? 'function' + : null, + node.declaration.id.loc, + ); } } if (node.specifiers) { const specifiers = node.specifiers; for (let j = 0; j < specifiers.length; j++) { const specifier = specifiers[j]; - localNames.set(specifier.local.name, specifier.exported.name); + addExportedEntry( + exportedEntries, + localNames, + specifier.local.name, + specifier.exported.name, + null, + specifier.local.loc, + ); } } continue; } } - if (localNames.size === 0) { - return source; - } - let newSrc = source + '\n\n;'; - newSrc += - 'import {registerServerReference} from "react-server-dom-esm/server";\n'; - localNames.forEach(function (exported, local) { - if (localTypes.get(local) !== 'function') { - // We first check if the export is a function and if so annotate it. - newSrc += 'if (typeof ' + local + ' === "function") '; + + let mappings = + sourceMap && typeof sourceMap.mappings === 'string' + ? sourceMap.mappings + : ''; + let newSrc = source; + + if (exportedEntries.length > 0) { + let lastSourceIndex = 0; + let lastOriginalLine = 0; + let lastOriginalColumn = 0; + let lastNameIndex = 0; + let sourceLineCount = 0; + let lastMappedLine = 0; + + if (sourceMap) { + // We iterate source mapping entries and our matched exports in parallel to source map + // them to their original location. + let nextEntryIdx = 0; + let nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + let nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + readMappings( + mappings, + ( + generatedLine: number, + generatedColumn: number, + sourceIndex: number, + originalLine: number, + originalColumn: number, + nameIndex: number, + ) => { + if ( + generatedLine > nextEntryLine || + (generatedLine === nextEntryLine && + generatedColumn > nextEntryColumn) + ) { + // We're past the entry which means that the best match we have is the previous entry. + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } else { + // Skip if we didn't have any mappings on the exported line. + } + nextEntryIdx++; + if (nextEntryIdx < exportedEntries.length) { + nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + } else { + nextEntryLine = -1; + nextEntryColumn = -1; + } + } + lastMappedLine = generatedLine; + if (sourceIndex > -1) { + lastSourceIndex = sourceIndex; + } + if (originalLine > -1) { + lastOriginalLine = originalLine; + } + if (originalColumn > -1) { + lastOriginalColumn = originalColumn; + } + if (nameIndex > -1) { + lastNameIndex = nameIndex; + } + }, + ); + if (nextEntryIdx < exportedEntries.length) { + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } + } + + for ( + let lastIdx = mappings.length - 1; + lastIdx >= 0 && mappings[lastIdx] === ';'; + lastIdx-- + ) { + // If the last mapped lines don't contain any segments, we don't get a callback from readMappings + // so we need to pad the number of mapped lines, with one for each empty line. + lastMappedLine++; + } + + sourceLineCount = program.loc.end.line; + if (sourceLineCount < lastMappedLine) { + throw new Error( + 'The source map has more mappings than there are lines.', + ); + } + // If the original source string had more lines than there are mappings in the source map. + // Add some extra padding of unmapped lines so that any lines that we add line up. + for ( + let extraLines = sourceLineCount - lastMappedLine; + extraLines > 0; + extraLines-- + ) { + mappings += ';'; + } + } else { + // If a file doesn't have a source map then we generate a blank source map that just + // contains the original content and segments pointing to the original lines. + sourceLineCount = 1; + let idx = -1; + while ((idx = source.indexOf('\n', idx + 1)) !== -1) { + sourceLineCount++; + } + mappings = 'AAAA' + ';AACA'.repeat(sourceLineCount - 1); + sourceMap = { + version: 3, + sources: [url], + sourcesContent: [source], + mappings: mappings, + sourceRoot: '', + }; + lastSourceIndex = 0; + lastOriginalLine = sourceLineCount; + lastOriginalColumn = 0; + lastNameIndex = -1; + lastMappedLine = sourceLineCount; + + for (let i = 0; i < exportedEntries.length; i++) { + // Point each entry to original location. + const entry = exportedEntries[i]; + entry.originalSource = 0; + entry.originalLine = entry.loc.start.line; + // We use column zero since we do the short-hand line-only source maps above. + entry.originalColumn = 0; // entry.loc.start.column; + } } - newSrc += 'registerServerReference(' + local + ','; - newSrc += JSON.stringify(url) + ','; - newSrc += JSON.stringify(exported) + ');\n'; - }); + + newSrc += '\n\n;'; + newSrc += + 'import {registerServerReference} from "react-server-dom-esm/server";\n'; + if (mappings) { + mappings += ';;'; + } + + const createMapping = createMappingsSerializer(); + + // Create an empty mapping pointing to where we last left off to reset the counters. + let generatedLine = 1; + createMapping( + generatedLine, + 0, + lastSourceIndex, + lastOriginalLine, + lastOriginalColumn, + lastNameIndex, + ); + for (let i = 0; i < exportedEntries.length; i++) { + const entry = exportedEntries[i]; + generatedLine++; + if (entry.type !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + entry.localName + ' === "function") '; + } + newSrc += 'registerServerReference(' + entry.localName + ','; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(entry.exportedName) + ');\n'; + + mappings += createMapping( + generatedLine, + 0, + entry.originalSource, + entry.originalLine, + entry.originalColumn, + entry.nameIndex, + ); + } + } + + if (sourceMap) { + // Override with an new mappings and serialize an inline source map. + sourceMap.mappings = mappings; + newSrc += + '//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + + Buffer.from(JSON.stringify(sourceMap)).toString('base64'); + } + return newSrc; } @@ -307,10 +569,13 @@ async function parseExportNamesInto( } async function transformClientModule( - body: any, + program: any, url: string, + sourceMap: any, loader: LoadFunction, ): Promise { + const body = program.body; + const names: Array = []; await parseExportNamesInto(body, names, url, loader); @@ -351,6 +616,9 @@ async function transformClientModule( newSrc += JSON.stringify(url) + ','; newSrc += JSON.stringify(name) + ');\n'; } + + // TODO: Generate source maps for Client Reference functions so they can point to their + // original locations. return newSrc; } @@ -391,12 +659,36 @@ async function transformModuleIfNeeded( return source; } - let body; + let sourceMappingURL = null; + let sourceMappingStart = 0; + let sourceMappingEnd = 0; + let sourceMappingLines = 0; + + let program; try { - body = acorn.parse(source, { + program = acorn.parse(source, { ecmaVersion: '2024', sourceType: 'module', - }).body; + locations: true, + onComment( + block: boolean, + text: string, + start: number, + end: number, + startLoc: {line: number, column: number}, + endLoc: {line: number, column: number}, + ) { + if ( + text.startsWith('# sourceMappingURL=') || + text.startsWith('@ sourceMappingURL=') + ) { + sourceMappingURL = text.slice(19); + sourceMappingStart = start; + sourceMappingEnd = end; + sourceMappingLines = endLoc.line - startLoc.line; + } + }, + }); } catch (x) { // eslint-disable-next-line react-internal/no-production-logging console.error('Error parsing %s %s', url, x.message); @@ -405,6 +697,8 @@ async function transformModuleIfNeeded( let useClient = false; let useServer = false; + + const body = program.body; for (let i = 0; i < body.length; i++) { const node = body[i]; if (node.type !== 'ExpressionStatement' || !node.directive) { @@ -428,11 +722,38 @@ async function transformModuleIfNeeded( ); } + let sourceMap = null; + if (sourceMappingURL) { + const sourceMapResult = await loader( + sourceMappingURL, + // $FlowFixMe + { + format: 'json', + conditions: [], + importAssertions: {type: 'json'}, + importAttributes: {type: 'json'}, + }, + loader, + ); + const sourceMapString = + typeof sourceMapResult.source === 'string' + ? sourceMapResult.source + : // $FlowFixMe + sourceMapResult.source.toString('utf8'); + sourceMap = JSON.parse(sourceMapString); + + // Strip the source mapping comment. We'll re-add it below if needed. + source = + source.slice(0, sourceMappingStart) + + '\n'.repeat(sourceMappingLines) + + source.slice(sourceMappingEnd); + } + if (useClient) { - return transformClientModule(body, url, loader); + return transformClientModule(program, url, sourceMap, loader); } - return transformServerModule(source, body, url, loader); + return transformServerModule(source, program, url, sourceMap, loader); } export async function transformSource( From 36c04348d7c6179bac4e7f27af823a67289432f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 22 Aug 2024 12:35:49 -0400 Subject: [PATCH 046/191] [DevTools] Make Functions Clickable to Jump to Definition (#30769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently you can jump to definition of a function by right clicking through the context menu. However, it's pretty difficult to discover. This makes the functions clickable to jump to definition - like links. This uses the same styling as we do for links (which are btw only clickable if they're not editable). Including cursor: pointer. I added a background on hover which follows the same pattern as the owners list. I also dropped the ƒ prefix when displaying functions. This is a cute short cut and there's precedence in how Chrome prints functions in the console *if* the function's toString would've had a function prefix like if it was a function declaration or expression. It does not do this for arrow functions or object methods. Elsewhere in the JS ecosystem this isn't really used anywhere. It invites more questions than it answers. The parenthesis and curlies are enough. There's no ambiguity here since strings have quotations. It looks better with just its object method form. Keeping it simple seems best. To my eyes this flows better because I'm used to looking at function syntax but not weird "f"s. Before: Screenshot 2024-08-20 at 11 55 09 PM After: Screenshot 2024-08-20 at 11 46 01 PM After (Hover): Screenshot 2024-08-20 at 11 46 31 PM --- .../src/__tests__/inspectedElement-test.js | 28 ++++++------- .../__tests__/legacy/inspectElement-test.js | 12 +++--- .../devtools/views/Components/KeyValue.css | 8 ++++ .../src/devtools/views/Components/KeyValue.js | 42 +++++++++++++++++-- packages/react-devtools-shared/src/utils.js | 7 ++-- 5 files changed, 70 insertions(+), 27 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 843c6dff9c08f..cd777f8333763 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -697,8 +697,8 @@ describe('InspectedElement', () => { expect(inspectedElement.props).toMatchInlineSnapshot(` { "anonymous_fn": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, "array_buffer": Dehydrated { "preview_short": ArrayBuffer(3), @@ -715,8 +715,8 @@ describe('InspectedElement', () => { "preview_long": 123n, }, "bound_fn": Dehydrated { - "preview_short": ƒ bound exampleFunction() {}, - "preview_long": ƒ bound exampleFunction() {}, + "preview_short": bound exampleFunction() {}, + "preview_long": bound exampleFunction() {}, }, "data_view": Dehydrated { "preview_short": DataView(3), @@ -727,8 +727,8 @@ describe('InspectedElement', () => { "preview_long": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time), }, "fn": Dehydrated { - "preview_short": ƒ exampleFunction() {}, - "preview_long": ƒ exampleFunction() {}, + "preview_short": exampleFunction() {}, + "preview_long": exampleFunction() {}, }, "html_element": Dehydrated { "preview_short":
, @@ -778,8 +778,8 @@ describe('InspectedElement', () => { "Symbol(name)": "hello", }, "proxy": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, "react_element": Dehydrated { "preview_short": , @@ -2018,16 +2018,16 @@ describe('InspectedElement', () => { { "proxy": { "$$typeof": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, "Symbol(Symbol.iterator)": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, "constructor": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, }, } diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index b5941ee5c6ccc..cf1ce1ffa3e38 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -212,8 +212,8 @@ describe('InspectedElementContext', () => { expect(inspectedElement.props).toMatchInlineSnapshot(` { "anonymous_fn": Dehydrated { - "preview_short": ƒ () {}, - "preview_long": ƒ () {}, + "preview_short": () => {}, + "preview_long": () => {}, }, "array_buffer": Dehydrated { "preview_short": ArrayBuffer(3), @@ -230,8 +230,8 @@ describe('InspectedElementContext', () => { "preview_long": 123n, }, "bound_fn": Dehydrated { - "preview_short": ƒ bound exampleFunction() {}, - "preview_long": ƒ bound exampleFunction() {}, + "preview_short": bound exampleFunction() {}, + "preview_long": bound exampleFunction() {}, }, "data_view": Dehydrated { "preview_short": DataView(3), @@ -242,8 +242,8 @@ describe('InspectedElementContext', () => { "preview_long": Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time), }, "fn": Dehydrated { - "preview_short": ƒ exampleFunction() {}, - "preview_long": ƒ exampleFunction() {}, + "preview_short": exampleFunction() {}, + "preview_long": exampleFunction() {}, }, "html_element": Dehydrated { "preview_short":
, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css index d6851be672cff..0c0f5e852d9ee 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css @@ -34,8 +34,16 @@ overflow: hidden; text-overflow: ellipsis; flex: 1; + cursor: pointer; + border-radius: 0.125rem; + padding: 0px 2px; } +.Link:hover { + background-color: var(--color-background-hover); +} + + .ExpandCollapseToggleSpacer { flex: 0 0 1rem; width: 1rem; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index 16e619cfb0847..af9e4b27e6181 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -27,7 +27,9 @@ import isArray from 'react-devtools-shared/src/isArray'; import {InspectedElementContext} from './InspectedElementContext'; import {PROTOCOLS_SUPPORTED_AS_LINKS_IN_KEY_VALUE} from './constants'; import KeyValueContextMenuContainer from './KeyValueContextMenuContainer'; +import {ContextMenuContext} from '../context'; +import type {ContextMenuContextType} from '../context'; import type {InspectedElement} from 'react-devtools-shared/src/frontend/types'; import type {Element} from 'react-devtools-shared/src/frontend/types'; import type {Element as ReactElement} from 'react'; @@ -91,6 +93,8 @@ export default function KeyValue({ const contextMenuTriggerRef = useRef(null); const {inspectPaths} = useContext(InspectedElementContext); + const {viewAttributeSourceFunction} = + useContext(ContextMenuContext); let isInspectable = false; let isReadOnlyBasedOnMetadata = false; @@ -268,8 +272,8 @@ export default function KeyValue({ ); + } else if (pathIsFunction && viewAttributeSourceFunction != null) { + children = ( + + )} {listItems.length === 0 && ( -
Did not render during this profiling session.
+
Did not render on the client during this profiling session.
)}
From 9690b9ad749c30eab1900c99e7c25a7ed7e1d9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 27 Aug 2024 12:05:24 -0400 Subject: [PATCH 060/191] [DevTools] Remove findCurrentFiberUsingSlowPathByFiberInstance (#30818) We always track the last committed Fiber on `FiberInstance.data`. https://github.com/facebook/react/blob/dcae56f8b72f625d8affe5729ca9991b31a492ac/packages/react-devtools-shared/src/backend/fiber/renderer.js#L3068 So we can now remove this complex slow path to get the current fiber. --- .../src/backend/fiber/renderer.js | 221 +----------------- 1 file changed, 7 insertions(+), 214 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index bb5b1b559d5f3..9e5a35686b34f 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -3550,7 +3550,7 @@ export function attach( fiberInstance: FiberInstance, ): $ReadOnlyArray { const hostInstances = []; - const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance); + const fiber = fiberInstance.data; if (!fiber) { return hostInstances; } @@ -3601,8 +3601,7 @@ export function attach( // TODO: Handle VirtualInstance. return null; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); + const fiber = devtoolsInstance.data; if (fiber === null) { return null; } @@ -3710,208 +3709,6 @@ export function attach( return null; } - // This function is copied from React and should be kept in sync: - // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js - function assertIsMounted(fiber: Fiber) { - if (getNearestMountedFiber(fiber) !== fiber) { - throw new Error('Unable to find node on an unmounted component.'); - } - } - - // This function is copied from React and should be kept in sync: - // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js - function getNearestMountedFiber(fiber: Fiber): null | Fiber { - let node = fiber; - let nearestMounted: null | Fiber = fiber; - if (!fiber.alternate) { - // If there is no alternate, this might be a new tree that isn't inserted - // yet. If it is, then it will have a pending insertion effect on it. - let nextNode: Fiber = node; - do { - node = nextNode; - // TODO: This function, and these flags, are a leaked implementation - // detail. Once we start releasing DevTools in lockstep with React, we - // should import a function from the reconciler instead. - const Placement = 0b000000000000000000000000010; - const Hydrating = 0b000000000000001000000000000; - if ((node.flags & (Placement | Hydrating)) !== 0) { - // This is an insertion or in-progress hydration. The nearest possible - // mounted fiber is the parent but we need to continue to figure out - // if that one is still mounted. - nearestMounted = node.return; - } - // $FlowFixMe[incompatible-type] we bail out when we get a null - nextNode = node.return; - } while (nextNode); - } else { - while (node.return) { - node = node.return; - } - } - if (node.tag === HostRoot) { - // TODO: Check if this was a nested HostRoot when used with - // renderContainerIntoSubtree. - return nearestMounted; - } - // If we didn't hit the root, that means that we're in an disconnected tree - // that has been unmounted. - return null; - } - - // This function is copied from React and should be kept in sync: - // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js - // It would be nice if we updated React to inject this function directly (vs just indirectly via findDOMNode). - // BEGIN copied code - function findCurrentFiberUsingSlowPathByFiberInstance( - fiberInstance: FiberInstance, - ): Fiber | null { - const fiber = fiberInstance.data; - const alternate = fiber.alternate; - if (!alternate) { - // If there is no alternate, then we only need to check if it is mounted. - const nearestMounted = getNearestMountedFiber(fiber); - - if (nearestMounted === null) { - throw new Error('Unable to find node on an unmounted component.'); - } - - if (nearestMounted !== fiber) { - return null; - } - return fiber; - } - // If we have two possible branches, we'll walk backwards up to the root - // to see what path the root points to. On the way we may hit one of the - // special cases and we'll deal with them. - let a: Fiber = fiber; - let b: Fiber = alternate; - while (true) { - const parentA = a.return; - if (parentA === null) { - // We're at the root. - break; - } - const parentB = parentA.alternate; - if (parentB === null) { - // There is no alternate. This is an unusual case. Currently, it only - // happens when a Suspense component is hidden. An extra fragment fiber - // is inserted in between the Suspense fiber and its children. Skip - // over this extra fragment fiber and proceed to the next parent. - const nextParent = parentA.return; - if (nextParent !== null) { - a = b = nextParent; - continue; - } - // If there's no parent, we're at the root. - break; - } - - // If both copies of the parent fiber point to the same child, we can - // assume that the child is current. This happens when we bailout on low - // priority: the bailed out fiber's child reuses the current child. - if (parentA.child === parentB.child) { - let child = parentA.child; - while (child) { - if (child === a) { - // We've determined that A is the current branch. - assertIsMounted(parentA); - return fiber; - } - if (child === b) { - // We've determined that B is the current branch. - assertIsMounted(parentA); - return alternate; - } - child = child.sibling; - } - - // We should never have an alternate for any mounting node. So the only - // way this could possibly happen is if this was unmounted, if at all. - throw new Error('Unable to find node on an unmounted component.'); - } - - if (a.return !== b.return) { - // The return pointer of A and the return pointer of B point to different - // fibers. We assume that return pointers never criss-cross, so A must - // belong to the child set of A.return, and B must belong to the child - // set of B.return. - a = parentA; - b = parentB; - } else { - // The return pointers point to the same fiber. We'll have to use the - // default, slow path: scan the child sets of each parent alternate to see - // which child belongs to which set. - // - // Search parent A's child set - let didFindChild = false; - let child = parentA.child; - while (child) { - if (child === a) { - didFindChild = true; - a = parentA; - b = parentB; - break; - } - if (child === b) { - didFindChild = true; - b = parentA; - a = parentB; - break; - } - child = child.sibling; - } - if (!didFindChild) { - // Search parent B's child set - child = parentB.child; - while (child) { - if (child === a) { - didFindChild = true; - a = parentB; - b = parentA; - break; - } - if (child === b) { - didFindChild = true; - b = parentB; - a = parentA; - break; - } - child = child.sibling; - } - - if (!didFindChild) { - throw new Error( - 'Child was not found in either parent set. This indicates a bug ' + - 'in React related to the return pointer. Please file an issue.', - ); - } - } - } - - if (a.alternate !== b) { - throw new Error( - "Return fibers should always be each others' alternates. " + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - } - } - - // If the root is not a host container, we're in a disconnected tree. I.e. - // unmounted. - if (a.tag !== HostRoot) { - throw new Error('Unable to find node on an unmounted component.'); - } - - if (a.stateNode.current === a) { - // We've determined that A is the current branch. - return fiber; - } - // Otherwise B has to be current branch. - return alternate; - } - - // END copied code - function getElementAttributeByPath( id: number, path: Array, @@ -4096,8 +3893,7 @@ export function attach( return {instance, style}; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); + const fiber = devtoolsInstance.data; if (fiber !== null) { instance = fiber.stateNode; @@ -4153,7 +3949,7 @@ export function attach( function inspectFiberInstanceRaw( fiberInstance: FiberInstance, ): InspectedElement | null { - const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance); + const fiber = fiberInstance.data; if (fiber == null) { return null; } @@ -4913,8 +4709,7 @@ export function attach( // TODO: Handle VirtualInstance. return; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); + const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; @@ -4979,8 +4774,7 @@ export function attach( // TODO: Handle VirtualInstance. return; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); + const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; @@ -5055,8 +4849,7 @@ export function attach( // TODO: Handle VirtualInstance. return; } - const fiber = - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); + const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; From f90a6bcc4c988f7524ce2be675b3257a530a51e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 27 Aug 2024 12:05:47 -0400 Subject: [PATCH 061/191] [DevTools] Reconcile Fibers Against Previous Children Instances (#30822) This loops over the remainingReconcilingChildren to find existing FiberInstances that match the updated Fiber. This is the same thing we already do for virtual instances. This avoids the need for a `fiberToFiberInstanceMap`. This loop is fast but there is a downside when the children set is very large and gets reordered with keys since we might have to loop over the set multiple times to get to the instances in the bottom. If that becomes a problem we can optimize it the same way ReactChildFiber does which is to create a temporary Map only when the children don't line up properly. That way everything except the first pass can use the Map but there's no need to create it eagerly. Now that we have the loop we don't need the previousSibling field so we can save some memory there. --- .../src/backend/fiber/renderer.js | 166 ++++++++++-------- 1 file changed, 89 insertions(+), 77 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 9e5a35686b34f..729c8fce5a7a5 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -158,7 +158,6 @@ type FiberInstance = { id: number, parent: null | DevToolsInstance, // filtered parent, including virtual firstChild: null | DevToolsInstance, // filtered first child, including virtual - previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual flags: number, // Force Error/Suspense source: null | string | Error | Source, // source location of this component function, or owned child stack @@ -174,7 +173,6 @@ function createFiberInstance(fiber: Fiber): FiberInstance { id: getUID(), parent: null, firstChild: null, - previousSibling: null, nextSibling: null, flags: 0, source: null, @@ -195,7 +193,6 @@ type VirtualInstance = { id: number, parent: null | DevToolsInstance, // filtered parent, including virtual firstChild: null | DevToolsInstance, // filtered first child, including virtual - previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual flags: number, source: null | string | Error | Source, // source location of this server component, or owned child stack @@ -218,7 +215,6 @@ function createVirtualInstance( id: getUID(), parent: null, firstChild: null, - previousSibling: null, nextSibling: null, flags: 0, source: null, @@ -1088,8 +1084,6 @@ export function attach( ' '.repeat(indent) + '- ' + instance.id + ' (' + name + ')', 'parent', instance.parent === null ? ' ' : instance.parent.id, - 'prev', - instance.previousSibling === null ? ' ' : instance.previousSibling.id, 'next', instance.nextSibling === null ? ' ' : instance.nextSibling.id, ); @@ -2321,21 +2315,25 @@ export function attach( if (previouslyReconciledSibling === null) { previouslyReconciledSibling = instance; parentInstance.firstChild = instance; - instance.previousSibling = null; } else { previouslyReconciledSibling.nextSibling = instance; - instance.previousSibling = previouslyReconciledSibling; previouslyReconciledSibling = instance; } instance.nextSibling = null; } - function moveChild(instance: DevToolsInstance): void { - removeChild(instance); + function moveChild( + instance: DevToolsInstance, + previousSibling: null | DevToolsInstance, + ): void { + removeChild(instance, previousSibling); insertChild(instance); } - function removeChild(instance: DevToolsInstance): void { + function removeChild( + instance: DevToolsInstance, + previousSibling: null | DevToolsInstance, + ): void { if (instance.parent === null) { if (remainingReconcilingChildren === instance) { throw new Error( @@ -2343,8 +2341,6 @@ export function attach( ); } else if (instance.nextSibling !== null) { throw new Error('A deleted instance should not have next siblings'); - } else if (instance.previousSibling !== null) { - throw new Error('A deleted instance should not have previous siblings'); } // Already deleted. return; @@ -2360,7 +2356,7 @@ export function attach( } // Remove an existing child from its current position, which we assume is in the // remainingReconcilingChildren set. - if (instance.previousSibling === null) { + if (previousSibling === null) { // We're first in the remaining set. Remove us. if (remainingReconcilingChildren !== instance) { throw new Error( @@ -2369,13 +2365,9 @@ export function attach( } remainingReconcilingChildren = instance.nextSibling; } else { - instance.previousSibling.nextSibling = instance.nextSibling; - } - if (instance.nextSibling !== null) { - instance.nextSibling.previousSibling = instance.previousSibling; + previousSibling.nextSibling = instance.nextSibling; } instance.nextSibling = null; - instance.previousSibling = null; instance.parent = null; } @@ -2655,7 +2647,7 @@ export function attach( } else { recordVirtualUnmount(instance); } - removeChild(instance); + removeChild(instance, null); } function recordProfilingDurations(fiberInstance: FiberInstance) { @@ -2889,8 +2881,7 @@ export function attach( ); } } - // TODO: Find the best matching existing child based on the key if defined. - + let previousSiblingOfBestMatch = null; let bestMatch = remainingReconcilingChildren; if (componentInfo.key != null) { // If there is a key try to find a matching key in the set. @@ -2902,6 +2893,7 @@ export function attach( ) { break; } + previousSiblingOfBestMatch = bestMatch; bestMatch = bestMatch.nextSibling; } } @@ -2916,7 +2908,7 @@ export function attach( // with the same name, then we claim it and reuse it for this update. // Update it with the latest entry. bestMatch.data = componentInfo; - moveChild(bestMatch); + moveChild(bestMatch, previousSiblingOfBestMatch); previousVirtualInstance = bestMatch; previousVirtualInstanceWasMount = false; } else { @@ -2965,42 +2957,93 @@ export function attach( } previousVirtualInstance = null; } + // We've reached the end of the virtual levels, but not beyond, // and now continue with the regular fiber. + + // Do a fast pass over the remaining children to find the previous instance. + // TODO: This doesn't have the best O(n) for a large set of children that are + // reordered. Consider using a temporary map if it's not the very next one. + let prevChild; if (prevChildAtSameIndex === nextChild) { // This set is unchanged. We're just going through it to place all the // children again. + prevChild = nextChild; + } else { + // We don't actually need to rely on the alternate here. We could also + // reconcile against stateNode, key or whatever. Doesn't have to be same + // Fiber pair. + prevChild = nextChild.alternate; + } + let previousSiblingOfExistingInstance = null; + let existingInstance = null; + if (prevChild !== null) { + existingInstance = remainingReconcilingChildren; + while (existingInstance !== null) { + if (existingInstance.data === prevChild) { + break; + } + previousSiblingOfExistingInstance = existingInstance; + existingInstance = existingInstance.nextSibling; + } + } + if (existingInstance !== null) { + // Common case. Match in the same parent. + const fiberInstance: FiberInstance = (existingInstance: any); // Only matches if it's a Fiber. + + // We keep track if the order of the children matches the previous order. + // They are always different referentially, but if the instances line up + // conceptually we'll want to know that. + if (prevChild !== prevChildAtSameIndex) { + shouldResetChildren = true; + } + + // Register the new alternate in case it's not already in. + fiberToFiberInstanceMap.set(nextChild, fiberInstance); + + // Update the Fiber so we that we always keep the current Fiber on the data. + fiberInstance.data = nextChild; + moveChild(fiberInstance, previousSiblingOfExistingInstance); + if ( updateFiberRecursively( + fiberInstance, nextChild, - nextChild, + (prevChild: any), traceNearestHostComponentUpdate, ) ) { - throw new Error('Updating the same fiber should not cause reorder'); + // If a nested tree child order changed but it can't handle its own + // child order invalidation (e.g. because it's filtered out like host nodes), + // propagate the need to reset child order upwards to this Fiber. + shouldResetChildren = true; } - } else if (nextChild.alternate) { - const prevChild = nextChild.alternate; + } else if (prevChild !== null && shouldFilterFiber(nextChild)) { + // If this Fiber should be filtered, we need to still update its children. + // This relies on an alternate since we don't have an Instance with the previous + // child on it. Ideally, the reconciliation wouldn't need previous Fibers that + // are filtered from the tree. if ( updateFiberRecursively( + null, nextChild, prevChild, traceNearestHostComponentUpdate, ) ) { - // If a nested tree child order changed but it can't handle its own - // child order invalidation (e.g. because it's filtered out like host nodes), - // propagate the need to reset child order upwards to this Fiber. - shouldResetChildren = true; - } - // However we also keep track if the order of the children matches - // the previous order. They are always different referentially, but - // if the instances line up conceptually we'll want to know that. - if (prevChild !== prevChildAtSameIndex) { shouldResetChildren = true; } } else { + // It's possible for a FiberInstance to be reparented when virtual parents + // get their sequence split or change structure with the same render result. + // In this case we unmount the and remount the FiberInstances. + // This might cause us to lose the selection but it's an edge case. + + // We let the previous instance remain in the "remaining queue" it is + // in to be deleted at the end since it'll have no match. + mountFiberRecursively(nextChild, traceNearestHostComponentUpdate); + // Need to mark the parent set to remount the new instance. shouldResetChildren = true; } } @@ -3059,6 +3102,7 @@ export function attach( // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateFiberRecursively( + fiberInstance: null | FiberInstance, // null if this should be filtered nextFiber: Fiber, prevFiber: Fiber, traceNearestHostComponentUpdate: boolean, @@ -3092,34 +3136,10 @@ export function attach( } } - let fiberInstance: null | FiberInstance = null; - const shouldIncludeInTree = !shouldFilterFiber(nextFiber); - if (shouldIncludeInTree) { - const entry = fiberToFiberInstanceMap.get(prevFiber); - if (entry !== undefined && entry.parent === reconcilingParent) { - // Common case. Match in the same parent. - fiberInstance = entry; - // Register the new alternate in case it's not already in. - fiberToFiberInstanceMap.set(nextFiber, fiberInstance); - - // Update the Fiber so we that we always keep the current Fiber on the data. - fiberInstance.data = nextFiber; - moveChild(fiberInstance); - } else { - // It's possible for a FiberInstance to be reparented when virtual parents - // get their sequence split or change structure with the same render result. - // In this case we unmount the and remount the FiberInstances. - // This might cause us to lose the selection but it's an edge case. - - // We let the previous instance remain in the "remaining queue" it is - // in to be deleted at the end since it'll have no match. - - mountFiberRecursively(nextFiber, traceNearestHostComponentUpdate); - - // Need to mark the parent set to remount the new instance. - return true; - } - + const stashedParent = reconcilingParent; + const stashedPrevious = previouslyReconciledSibling; + const stashedRemaining = remainingReconcilingChildren; + if (fiberInstance !== null) { if ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === fiberInstance.id && @@ -3129,12 +3149,6 @@ export function attach( // If it is inspected again, it may need to be re-run to obtain updated hooks values. hasElementUpdatedSinceLastInspected = true; } - } - - const stashedParent = reconcilingParent; - const stashedPrevious = previouslyReconciledSibling; - const stashedRemaining = remainingReconcilingChildren; - if (fiberInstance !== null) { // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = fiberInstance; previouslyReconciledSibling = null; @@ -3189,7 +3203,7 @@ export function attach( if ( nextFallbackChildSet != null && prevFallbackChildSet != null && - updateFiberRecursively( + updateChildrenRecursively( nextFallbackChildSet, prevFallbackChildSet, traceNearestHostComponentUpdate, @@ -3284,10 +3298,8 @@ export function attach( if (shouldResetChildren) { // We need to crawl the subtree for closest non-filtered Fibers // so that we can display them in a flat children set. - if (shouldIncludeInTree) { - if (reconcilingParent !== null) { - recordResetChildren(reconcilingParent); - } + if (fiberInstance !== null) { + recordResetChildren(fiberInstance); // We've handled the child order change for this Fiber. // Since it's included, there's no need to invalidate parent child order. return false; @@ -3299,7 +3311,7 @@ export function attach( return false; } } finally { - if (shouldIncludeInTree) { + if (fiberInstance !== null) { unmountRemainingChildren(); reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; @@ -3489,7 +3501,7 @@ export function attach( mountFiberRecursively(current, false); } else if (wasMounted && isMounted) { // Update an existing root. - updateFiberRecursively(current, alternate, false); + updateFiberRecursively(rootInstance, current, alternate, false); } else if (wasMounted && !isMounted) { // Unmount an existing root. removeRootPseudoKey(currentRootID); From 96aca5f4f3d7fbe0c13350f90031d8ec4c060ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 27 Aug 2024 13:10:37 -0400 Subject: [PATCH 062/191] Spawn new task if we hit stack overflow (#30419) If we see the "Maximum call stack size exceeded" error we know we've hit stack overflow. We can recover from this by spawning a new task and trying again. Effectively a zero-cost trampoline in the normal case. The new task will have a clean stack. If you have a lot of siblings at the same depth that hits the limit you can end up hitting this once for each sibling but within that new sibling you're unlikely to hit this again. So it's not too expensive. If it errors again in the retryTask pass, the other error handling takes over which causes this to be able to still not infinitely stall. E.g. when the component itself throws an error like this. It's still better to increase the stack limit for performance if you have a really deep tree but it doesn't really hurt to be able to recover since it's zero cost when it doesn't happen. We could do the same thing for Flight. Those trees don't tend to be as deep but could happen. --- .../src/__tests__/ReactDOMFizzServer-test.js | 61 ++++++++++++ packages/react-server/src/ReactFizzServer.js | 92 +++++++++++++++---- 2 files changed, 137 insertions(+), 16 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 5a763ffe949ab..a109011b35e98 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -8677,4 +8677,65 @@ describe('ReactDOMFizzServer', () => { '\n in Bar (at **)' + '\n in Foo (at **)', ); }); + + it('can recover from very deep trees to avoid stack overflow', async () => { + function Recursive({n}) { + if (n > 0) { + return ; + } + return hi; + } + + // Recursively render a component tree deep enough to trigger stack overflow. + // Don't make this too short to not hit the limit but also not too deep to slow + // down the test. + await act(() => { + const {pipe} = renderToPipeableStream( +
+ +
, + ); + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual( +
+ hi +
, + ); + }); + + it('handles stack overflows inside components themselves', async () => { + function StackOverflow() { + // This component is recursive inside itself and is therefore an error. + // Assuming no tail-call optimizations. + function recursive(n, a0, a1, a2, a3) { + if (n > 0) { + return recursive(n - 1, a0, a1, a2, a3) + a0 + a1 + a2 + a3; + } + return a0; + } + return recursive(10000, 'should', 'not', 'resolve', 'this'); + } + + let caughtError; + + await expect(async () => { + await act(() => { + const {pipe} = renderToPipeableStream( +
+ +
, + { + onError(error, errorInfo) { + caughtError = error; + }, + }, + ); + pipe(writable); + }); + }).rejects.toThrow('Maximum call stack size exceeded'); + + expect(caughtError.message).toBe('Maximum call stack size exceeded'); + }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 7ccbd65d16b74..aeffa6b813b8f 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -3320,9 +3320,8 @@ function spawnNewSuspendedReplayTask( request: Request, task: ReplayTask, thenableState: ThenableState | null, - x: Wakeable, -): void { - const newTask = createReplayTask( +): ReplayTask { + return createReplayTask( request, thenableState, task.replay, @@ -3340,17 +3339,13 @@ function spawnNewSuspendedReplayTask( !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, ); - - const ping = newTask.ping; - x.then(ping, ping); } function spawnNewSuspendedRenderTask( request: Request, task: RenderTask, thenableState: ThenableState | null, - x: Wakeable, -): void { +): RenderTask { // Something suspended, we'll need to create a new segment and resolve it later. const segment = task.blockedSegment; const insertionIndex = segment.chunks.length; @@ -3367,7 +3362,7 @@ function spawnNewSuspendedRenderTask( segment.children.push(newSegment); // Reset lastPushedText for current Segment since the new Segment "consumed" it segment.lastPushedText = false; - const newTask = createRenderTask( + return createRenderTask( request, thenableState, task.node, @@ -3385,9 +3380,6 @@ function spawnNewSuspendedRenderTask( !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, ); - - const ping = newTask.ping; - x.then(ping, ping); } // This is a non-destructive form of rendering a node. If it suspends it spawns @@ -3436,14 +3428,48 @@ function renderNode( if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); const thenableState = getThenableStateAfterSuspending(); - spawnNewSuspendedReplayTask( + const newTask = spawnNewSuspendedReplayTask( + request, + // $FlowFixMe: Refined. + task, + thenableState, + ); + const ping = newTask.ping; + wakeable.then(ping, ping); + + // Restore the context. We assume that this will be restored by the inner + // functions in case nothing throws so we don't use "finally" here. + task.formatContext = previousFormatContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } + task.context = previousContext; + task.keyPath = previousKeyPath; + task.treeContext = previousTreeContext; + task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } + // Restore all active ReactContexts to what they were before. + switchContext(previousContext); + return; + } + if (x.message === 'Maximum call stack size exceeded') { + // This was a stack overflow. We do a lot of recursion in React by default for + // performance but it can lead to stack overflows in extremely deep trees. + // We do have the ability to create a trampoile if this happens which makes + // this kind of zero-cost. + const thenableState = getThenableStateAfterSuspending(); + const newTask = spawnNewSuspendedReplayTask( request, // $FlowFixMe: Refined. task, thenableState, - wakeable, ); + // Immediately schedule the task for retrying. + request.pingedTasks.push(newTask); + // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; @@ -3493,13 +3519,14 @@ function renderNode( if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); const thenableState = getThenableStateAfterSuspending(); - spawnNewSuspendedRenderTask( + const newTask = spawnNewSuspendedRenderTask( request, // $FlowFixMe: Refined. task, thenableState, - wakeable, ); + const ping = newTask.ping; + wakeable.then(ping, ping); // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. @@ -3540,6 +3567,39 @@ function renderNode( ); trackPostpone(request, trackedPostpones, task, postponedSegment); + // Restore the context. We assume that this will be restored by the inner + // functions in case nothing throws so we don't use "finally" here. + task.formatContext = previousFormatContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } + task.context = previousContext; + task.keyPath = previousKeyPath; + task.treeContext = previousTreeContext; + task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } + // Restore all active ReactContexts to what they were before. + switchContext(previousContext); + return; + } + if (x.message === 'Maximum call stack size exceeded') { + // This was a stack overflow. We do a lot of recursion in React by default for + // performance but it can lead to stack overflows in extremely deep trees. + // We do have the ability to create a trampoile if this happens which makes + // this kind of zero-cost. + const thenableState = getThenableStateAfterSuspending(); + const newTask = spawnNewSuspendedRenderTask( + request, + // $FlowFixMe: Refined. + task, + thenableState, + ); + + // Immediately schedule the task for retrying. + request.pingedTasks.push(newTask); + // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; From f2841c2a490b4b776b98568871b69693fedf985c Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Tue, 27 Aug 2024 10:11:50 -0700 Subject: [PATCH 063/191] [compiler] Fixture to demonstrate issue with returning object containing ref Summary: We currently can return a ref from a hook but not an object containing a ref. ghstack-source-id: 8b1de4991eb2731b7f758e685ba62d9f07d584b2 Pull Request resolved: https://github.com/facebook/react/pull/30820 --- ...or.return-ref-callback-structure.expect.md | 45 +++++++++++++++++++ .../error.return-ref-callback-structure.js | 20 +++++++++ 2 files changed, 65 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md new file mode 100644 index 0000000000000..866d2e2fea657 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +import {useRef} from 'react'; + +component Foo(cond: boolean, cond2: boolean) { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + if (cond) return [s]; + else if (cond2) return {s}; + else return {s: [s]}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: false, cond2: false}], +}; + +``` + + +## Error + +``` + 10 | }; + 11 | +> 12 | if (cond) return [s]; + | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) + 13 | else if (cond2) return {s}; + 14 | else return {s: [s]}; + 15 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js new file mode 100644 index 0000000000000..e37acbde348d1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js @@ -0,0 +1,20 @@ +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +import {useRef} from 'react'; + +component Foo(cond: boolean, cond2: boolean) { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + if (cond) return [s]; + else if (cond2) return {s}; + else return {s: [s]}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: false, cond2: false}], +}; From 7771d3a7972cc2483c45fde51b7ec2d926cba097 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Tue, 27 Aug 2024 10:11:50 -0700 Subject: [PATCH 064/191] [compiler] Track refs through object expressions and property lookups Summary: This addresses the issue of the compiler being overly restrictive about refs escaping into object expressions. Rather than erroring whenever a ref flows into an object, we will now treat the object itself as a ref, and apply the same escape rules to it. Whenever we look up a property from a ref value, we now don't know whether that value is itself a ref or a ref value, so we assume it's both. The same logic applies to ref-accessing functions--if such a function is stored in an object, we'll propagate that property to the object itself and any properties looked up from it. ghstack-source-id: 5c6fcb895d4a1658ce9dddec286aad3a57a4c9f1 Pull Request resolved: https://github.com/facebook/react/pull/30821 --- .../Validation/ValidateNoRefAccesInRender.ts | 253 ++++++++++++------ ...f-added-to-dep-without-type-info.expect.md | 16 +- ...or.return-ref-callback-structure.expect.md | 45 ---- .../return-ref-callback-structure.expect.md | 87 ++++++ ...re.js => return-ref-callback-structure.js} | 0 5 files changed, 273 insertions(+), 128 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.return-ref-callback-structure.js => return-ref-callback-structure.js} (100%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index df6241a73f448..8a65b4709c174 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -11,12 +11,12 @@ import { IdentifierId, Place, SourceLocation, - isRefOrRefValue, isRefValueType, isUseRefType, } from '../HIR'; import { eachInstructionValueOperand, + eachPatternOperand, eachTerminalOperand, } from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; @@ -42,58 +42,165 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies'; * In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref) * or based on property name alone (`foo.current` might be a ref). */ +type State = { + refs: Set; + refValues: Map; + refAccessingFunctions: Set; +}; + export function validateNoRefAccessInRender(fn: HIRFunction): void { - const refAccessingFunctions: Set = new Set(); - validateNoRefAccessInRenderImpl(fn, refAccessingFunctions).unwrap(); + const state = { + refs: new Set(), + refValues: new Map(), + refAccessingFunctions: new Set(), + }; + validateNoRefAccessInRenderImpl(fn, state).unwrap(); } function validateNoRefAccessInRenderImpl( fn: HIRFunction, - refAccessingFunctions: Set, + state: State, ): Result { + let place; + for (const param of fn.params) { + if (param.kind === 'Identifier') { + place = param; + } else { + place = param.place; + } + + if (isRefValueType(place.identifier)) { + state.refValues.set(place.identifier.id, null); + } + if (isUseRefType(place.identifier)) { + state.refs.add(place.identifier.id); + } + } const errors = new CompilerError(); - const lookupLocations: Map = new Map(); for (const [, block] of fn.body.blocks) { + for (const phi of block.phis) { + phi.operands.forEach(operand => { + if (state.refs.has(operand.id) || isUseRefType(phi.id)) { + state.refs.add(phi.id.id); + } + const refValue = state.refValues.get(operand.id); + if (refValue !== undefined || isRefValueType(operand)) { + state.refValues.set( + phi.id.id, + refValue ?? state.refValues.get(phi.id.id) ?? null, + ); + } + if (state.refAccessingFunctions.has(operand.id)) { + state.refAccessingFunctions.add(phi.id.id); + } + }); + } + for (const instr of block.instructions) { + for (const operand of eachInstructionValueOperand(instr.value)) { + if (isRefValueType(operand.identifier)) { + CompilerError.invariant(state.refValues.has(operand.identifier.id), { + reason: 'Expected ref value to be in state', + loc: operand.loc, + }); + } + if (isUseRefType(operand.identifier)) { + CompilerError.invariant(state.refs.has(operand.identifier.id), { + reason: 'Expected ref to be in state', + loc: operand.loc, + }); + } + } + switch (instr.value.kind) { case 'JsxExpression': case 'JsxFragment': { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoDirectRefValueAccess(errors, operand, lookupLocations); + validateNoDirectRefValueAccess(errors, operand, state); } break; } + case 'ComputedLoad': case 'PropertyLoad': { + if (typeof instr.value.property !== 'string') { + validateNoRefValueAccess(errors, state, instr.value.property); + } if ( - isRefValueType(instr.lvalue.identifier) && - instr.value.property === 'current' + state.refAccessingFunctions.has(instr.value.object.identifier.id) ) { - lookupLocations.set(instr.lvalue.identifier.id, instr.loc); + state.refAccessingFunctions.add(instr.lvalue.identifier.id); + } + if (state.refs.has(instr.value.object.identifier.id)) { + /* + * Once an object contains a ref at any level, we treat it as a ref. + * If we look something up from it, that value may either be a ref + * or the ref value (or neither), so we conservatively assume it's both. + */ + state.refs.add(instr.lvalue.identifier.id); + state.refValues.set(instr.lvalue.identifier.id, instr.loc); } break; } + case 'LoadContext': case 'LoadLocal': { - if (refAccessingFunctions.has(instr.value.place.identifier.id)) { - refAccessingFunctions.add(instr.lvalue.identifier.id); + if ( + state.refAccessingFunctions.has(instr.value.place.identifier.id) + ) { + state.refAccessingFunctions.add(instr.lvalue.identifier.id); } - if (isRefValueType(instr.lvalue.identifier)) { - const loc = lookupLocations.get(instr.value.place.identifier.id); - if (loc !== undefined) { - lookupLocations.set(instr.lvalue.identifier.id, loc); - } + const refValue = state.refValues.get(instr.value.place.identifier.id); + if (refValue !== undefined) { + state.refValues.set(instr.lvalue.identifier.id, refValue); + } + if (state.refs.has(instr.value.place.identifier.id)) { + state.refs.add(instr.lvalue.identifier.id); } break; } + case 'StoreContext': case 'StoreLocal': { - if (refAccessingFunctions.has(instr.value.value.identifier.id)) { - refAccessingFunctions.add(instr.value.lvalue.place.identifier.id); - refAccessingFunctions.add(instr.lvalue.identifier.id); + if ( + state.refAccessingFunctions.has(instr.value.value.identifier.id) + ) { + state.refAccessingFunctions.add( + instr.value.lvalue.place.identifier.id, + ); + state.refAccessingFunctions.add(instr.lvalue.identifier.id); + } + const refValue = state.refValues.get(instr.value.value.identifier.id); + if ( + refValue !== undefined || + isRefValueType(instr.value.lvalue.place.identifier) + ) { + state.refValues.set( + instr.value.lvalue.place.identifier.id, + refValue ?? null, + ); + state.refValues.set(instr.lvalue.identifier.id, refValue ?? null); + } + if (state.refs.has(instr.value.value.identifier.id)) { + state.refs.add(instr.value.lvalue.place.identifier.id); + state.refs.add(instr.lvalue.identifier.id); } - if (isRefValueType(instr.value.lvalue.place.identifier)) { - const loc = lookupLocations.get(instr.value.value.identifier.id); - if (loc !== undefined) { - lookupLocations.set(instr.value.lvalue.place.identifier.id, loc); - lookupLocations.set(instr.lvalue.identifier.id, loc); + break; + } + case 'Destructure': { + const destructuredFunction = state.refAccessingFunctions.has( + instr.value.value.identifier.id, + ); + const destructuredRef = state.refs.has( + instr.value.value.identifier.id, + ); + for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) { + if (isUseRefType(lval.identifier)) { + state.refs.add(lval.identifier.id); + } + if (destructuredRef || isRefValueType(lval.identifier)) { + state.refs.add(lval.identifier.id); + state.refValues.set(lval.identifier.id, null); + } + if (destructuredFunction) { + state.refAccessingFunctions.add(lval.identifier.id); } } break; @@ -107,32 +214,27 @@ function validateNoRefAccessInRenderImpl( */ [...eachInstructionValueOperand(instr.value)].some( operand => - isRefValueType(operand.identifier) || - refAccessingFunctions.has(operand.identifier.id), + state.refValues.has(operand.identifier.id) || + state.refAccessingFunctions.has(operand.identifier.id), ) || // check for cases where .current is accessed through an aliased ref ([...eachInstructionValueOperand(instr.value)].some(operand => - isUseRefType(operand.identifier), + state.refs.has(operand.identifier.id), ) && validateNoRefAccessInRenderImpl( instr.value.loweredFunc.func, - refAccessingFunctions, + state, ).isErr()) ) { // This function expression unconditionally accesses a ref - refAccessingFunctions.add(instr.lvalue.identifier.id); + state.refAccessingFunctions.add(instr.lvalue.identifier.id); } break; } case 'MethodCall': { if (!isEffectHook(instr.value.property.identifier)) { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefAccess( - errors, - refAccessingFunctions, - operand, - operand.loc, - ); + validateNoRefAccess(errors, state, operand, operand.loc); } } break; @@ -142,7 +244,7 @@ function validateNoRefAccessInRenderImpl( const isUseEffect = isEffectHook(callee.identifier); if (!isUseEffect) { // Report a more precise error when calling a local function that accesses a ref - if (refAccessingFunctions.has(callee.identifier.id)) { + if (state.refAccessingFunctions.has(callee.identifier.id)) { errors.push({ severity: ErrorSeverity.InvalidReact, reason: @@ -159,9 +261,9 @@ function validateNoRefAccessInRenderImpl( for (const operand of eachInstructionValueOperand(instr.value)) { validateNoRefAccess( errors, - refAccessingFunctions, + state, operand, - lookupLocations.get(operand.identifier.id) ?? operand.loc, + state.refValues.get(operand.identifier.id) ?? operand.loc, ); } } @@ -170,12 +272,17 @@ function validateNoRefAccessInRenderImpl( case 'ObjectExpression': case 'ArrayExpression': { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefAccess( - errors, - refAccessingFunctions, - operand, - lookupLocations.get(operand.identifier.id) ?? operand.loc, - ); + validateNoDirectRefValueAccess(errors, operand, state); + if (state.refAccessingFunctions.has(operand.identifier.id)) { + state.refAccessingFunctions.add(instr.lvalue.identifier.id); + } + if (state.refs.has(operand.identifier.id)) { + state.refs.add(instr.lvalue.identifier.id); + } + const refValue = state.refValues.get(operand.identifier.id); + if (refValue !== undefined) { + state.refValues.set(instr.lvalue.identifier.id, refValue); + } } break; } @@ -185,20 +292,15 @@ function validateNoRefAccessInRenderImpl( case 'ComputedStore': { validateNoRefAccess( errors, - refAccessingFunctions, + state, instr.value.object, - lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc, + state.refValues.get(instr.value.object.identifier.id) ?? instr.loc, ); for (const operand of eachInstructionValueOperand(instr.value)) { if (operand === instr.value.object) { continue; } - validateNoRefValueAccess( - errors, - refAccessingFunctions, - lookupLocations, - operand, - ); + validateNoRefValueAccess(errors, state, operand); } break; } @@ -207,28 +309,27 @@ function validateNoRefAccessInRenderImpl( break; default: { for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess( - errors, - refAccessingFunctions, - lookupLocations, - operand, - ); + validateNoRefValueAccess(errors, state, operand); } break; } } + if (isUseRefType(instr.lvalue.identifier)) { + state.refs.add(instr.lvalue.identifier.id); + } + if ( + isRefValueType(instr.lvalue.identifier) && + !state.refValues.has(instr.lvalue.identifier.id) + ) { + state.refValues.set(instr.lvalue.identifier.id, instr.loc); + } } for (const operand of eachTerminalOperand(block.terminal)) { if (block.terminal.kind !== 'return') { - validateNoRefValueAccess( - errors, - refAccessingFunctions, - lookupLocations, - operand, - ); + validateNoRefValueAccess(errors, state, operand); } else { // Allow functions containing refs to be returned, but not direct ref values - validateNoDirectRefValueAccess(errors, operand, lookupLocations); + validateNoDirectRefValueAccess(errors, operand, state); } } } @@ -242,19 +343,18 @@ function validateNoRefAccessInRenderImpl( function validateNoRefValueAccess( errors: CompilerError, - refAccessingFunctions: Set, - lookupLocations: Map, + state: State, operand: Place, ): void { if ( - isRefValueType(operand.identifier) || - refAccessingFunctions.has(operand.identifier.id) + state.refValues.has(operand.identifier.id) || + state.refAccessingFunctions.has(operand.identifier.id) ) { errors.push({ severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + loc: state.refValues.get(operand.identifier.id) ?? operand.loc, description: operand.identifier.name !== null && operand.identifier.name.kind === 'named' @@ -267,13 +367,14 @@ function validateNoRefValueAccess( function validateNoRefAccess( errors: CompilerError, - refAccessingFunctions: Set, + state: State, operand: Place, loc: SourceLocation, ): void { if ( - isRefOrRefValue(operand.identifier) || - refAccessingFunctions.has(operand.identifier.id) + state.refs.has(operand.identifier.id) || + state.refValues.has(operand.identifier.id) || + state.refAccessingFunctions.has(operand.identifier.id) ) { errors.push({ severity: ErrorSeverity.InvalidReact, @@ -293,14 +394,14 @@ function validateNoRefAccess( function validateNoDirectRefValueAccess( errors: CompilerError, operand: Place, - lookupLocations: Map, + state: State, ): void { - if (isRefValueType(operand.identifier)) { + if (state.refValues.has(operand.identifier.id)) { errors.push({ severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: lookupLocations.get(operand.identifier.id) ?? operand.loc, + loc: state.refValues.get(operand.identifier.id) ?? operand.loc, description: operand.identifier.name !== null && operand.identifier.name.kind === 'named' diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md index a28a74730bfb8..f576bac764613 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md @@ -22,13 +22,15 @@ function Foo({a}) { ## Error ``` - 3 | const ref = useRef(); - 4 | // type information is lost here as we don't track types of fields -> 5 | const val = {ref}; - | ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) - 6 | // without type info, we don't know that val.ref.current is a ref value so we - 7 | // *would* end up depending on val.ref.current - 8 | // however, this is an instance of accessing a ref during render and is disallowed + 8 | // however, this is an instance of accessing a ref during render and is disallowed + 9 | // under React's rules, so we reject this input +> 10 | const x = {a, val: val.ref.current}; + | ^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10) + 11 | + 12 | return ; + 13 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md deleted file mode 100644 index 866d2e2fea657..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md +++ /dev/null @@ -1,45 +0,0 @@ - -## Input - -```javascript -// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees - -import {useRef} from 'react'; - -component Foo(cond: boolean, cond2: boolean) { - const ref = useRef(); - - const s = () => { - return ref.current; - }; - - if (cond) return [s]; - else if (cond2) return {s}; - else return {s: [s]}; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{cond: false, cond2: false}], -}; - -``` - - -## Error - -``` - 10 | }; - 11 | -> 12 | if (cond) return [s]; - | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) - -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13) - -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14) - 13 | else if (cond2) return {s}; - 14 | else return {s: [s]}; - 15 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.expect.md new file mode 100644 index 0000000000000..95976383cbff8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.expect.md @@ -0,0 +1,87 @@ + +## Input + +```javascript +// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees + +import {useRef} from 'react'; + +component Foo(cond: boolean, cond2: boolean) { + const ref = useRef(); + + const s = () => { + return ref.current; + }; + + if (cond) return [s]; + else if (cond2) return {s}; + else return {s: [s]}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: false, cond2: false}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { useRef } from "react"; + +function Foo(t0) { + const $ = _c(4); + const { cond, cond2 } = t0; + const ref = useRef(); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => ref.current; + $[0] = t1; + } else { + t1 = $[0]; + } + const s = t1; + if (cond) { + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t2 = [s]; + $[1] = t2; + } else { + t2 = $[1]; + } + return t2; + } else { + if (cond2) { + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = { s }; + $[2] = t2; + } else { + t2 = $[2]; + } + return t2; + } else { + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 = { s: [s] }; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; + } + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ cond: false, cond2: false }], +}; + +``` + +### Eval output +(kind: ok) {"s":["[[ function params=0 ]]"]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.js From 5e51d767d179fda586f28e1118fb9ec5c200e35e Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:44:44 -0700 Subject: [PATCH 065/191] [compiler] Stop reusing ScopeDep type in AnalyzeFunctions AnalyzeFunctions was reusing the `ReactiveScopeDependency` type since it happened to have a convenient shape, but we need to change this type to represent optionality. We now use a locally defined type instead. ghstack-source-id: e305c6ede4bcbdffce606336c572cdc6dc1556c3 Pull Request resolved: https://github.com/facebook/react/pull/30811 --- .../src/Inference/AnalyseFunctions.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index fbb24ea492c0f..684acaf298388 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -13,7 +13,6 @@ import { IdentifierName, LoweredFunction, Place, - ReactiveScopeDependency, isRefOrRefValue, makeInstructionId, } from '../HIR'; @@ -25,9 +24,14 @@ import {inferMutableContextVariables} from './InferMutableContextVariables'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; +type Dependency = { + identifier: Identifier; + path: Array; +}; + // Helper class to track indirections such as LoadLocal and PropertyLoad. export class IdentifierState { - properties: Map = new Map(); + properties: Map = new Map(); resolve(identifier: Identifier): Identifier { const resolved = this.properties.get(identifier); @@ -39,7 +43,7 @@ export class IdentifierState { declareProperty(lvalue: Place, object: Place, property: string): void { const objectDependency = this.properties.get(object.identifier); - let nextDependency: ReactiveScopeDependency; + let nextDependency: Dependency; if (objectDependency === undefined) { nextDependency = {identifier: object.identifier, path: [property]}; } else { @@ -52,9 +56,7 @@ export class IdentifierState { } declareTemporary(lvalue: Place, value: Place): void { - const resolved: ReactiveScopeDependency = this.properties.get( - value.identifier, - ) ?? { + const resolved: Dependency = this.properties.get(value.identifier) ?? { identifier: value.identifier, path: [], }; From 4759161ed8d8f77bad654b6c23a063c8ad8d4864 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:33 -0700 Subject: [PATCH 066/191] [compiler] Wrap ReactiveScopeDep path tokens in object Previously the path of a ReactiveScopeDependency was `Array`. We need to track whether each property access is optional or not, so as a first step we change this to `Array<{property: string}>`, making space for an additional property in a subsequent PR. ghstack-source-id: c5d38d72f6b9d084a5df69ad23178794468f5f8b Pull Request resolved: https://github.com/facebook/react/pull/30812 --- .../src/HIR/HIR.ts | 12 +++- .../src/Inference/DropManualMemoization.ts | 2 +- .../ReactiveScopes/CodegenReactiveFunction.ts | 4 +- .../DeriveMinimalDependencies.ts | 70 ++++--------------- ...rgeReactiveScopesThatInvalidateTogether.ts | 5 +- .../ReactiveScopes/PrintReactiveFunction.ts | 2 +- .../PropagateScopeDependencies.ts | 27 ++----- .../PruneInitializationDependencies.ts | 4 +- .../ValidatePreservedManualMemoization.ts | 8 +-- 9 files changed, 40 insertions(+), 94 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 06fa48a73656c..e74ec52203bab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -776,7 +776,7 @@ export type ManualMemoDependency = { value: Place; } | {kind: 'Global'; identifierName: string}; - path: Array; + path: DependencyPath; }; export type StartMemoize = { @@ -1494,9 +1494,17 @@ export type ReactiveScopeDeclaration = { export type ReactiveScopeDependency = { identifier: Identifier; - path: Array; + path: DependencyPath; }; +export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean { + return ( + a.length === b.length && + a.every((item, ix) => item.property === b[ix].property) + ); +} +export type DependencyPath = Array<{property: string}>; + /* * Simulated opaque type for BlockIds to prevent using normal numbers as block ids * accidentally. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index c9d2a7e1412c3..e60d8a9914583 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -68,7 +68,7 @@ export function collectMaybeMemoDependencies( if (object != null) { return { root: object.root, - path: [...object.path, value.property], + path: [...object.path, {property: value.property}], }; } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 624a4b604d66f..73330b959e018 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1411,7 +1411,7 @@ function printDependencyComment(dependency: ReactiveScopeDependency): string { let name = identifier.name; if (dependency.path !== null) { for (const path of dependency.path) { - name += `.${path}`; + name += `.${path.property}`; } } return name; @@ -1448,7 +1448,7 @@ function codegenDependency( let object: t.Expression = convertIdentifier(dependency.identifier); if (dependency.path !== null) { for (const path of dependency.path) { - object = t.memberExpression(object, t.identifier(path)); + object = t.memberExpression(object, t.identifier(path.property)); } } return object; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts index 8c2e31fa9666e..13420ee140e63 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts @@ -14,20 +14,8 @@ import {assertExhaustive} from '../Utils/utils'; * We need to understand optional member expressions only when determining * dependencies of a ReactiveScope (i.e. in {@link PropagateScopeDependencies}), * hence why this type lives here (not in HIR.ts) - * - * {@link ReactiveScopePropertyDependency.optionalPath} is populated only if the Property - * represents an optional member expression, and it represents the property path - * loaded conditionally. - * e.g. the member expr a.b.c?.d.e?.f is represented as - * { - * identifier: 'a'; - * path: ['b', 'c'], - * optionalPath: ['d', 'e', 'f']. - * } */ -export type ReactiveScopePropertyDependency = ReactiveScopeDependency & { - optionalPath: Array; -}; +export type ReactiveScopePropertyDependency = ReactiveScopeDependency; /* * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional @@ -69,59 +57,29 @@ export class ReactiveScopeDependencyTree { } add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void { - const {path, optionalPath} = dep; + const {path} = dep; let currNode = this.#getOrCreateRoot(dep.identifier); const accessType = inConditional ? PropertyAccessType.ConditionalAccess : PropertyAccessType.UnconditionalAccess; - for (const property of path) { + for (const item of path) { // all properties read 'on the way' to a dependency are marked as 'access' - let currChild = getOrMakeProperty(currNode, property); + let currChild = getOrMakeProperty(currNode, item.property); currChild.accessType = merge(currChild.accessType, accessType); currNode = currChild; } - if (optionalPath.length === 0) { - /* - * If this property does not have a conditional path (i.e. a.b.c), the - * final property node should be marked as an conditional/unconditional - * `dependency` as based on control flow. - */ - const depType = inConditional - ? PropertyAccessType.ConditionalDependency - : PropertyAccessType.UnconditionalDependency; - - currNode.accessType = merge(currNode.accessType, depType); - } else { - /* - * Technically, we only depend on whether unconditional path `dep.path` - * is nullish (not its actual value). As long as we preserve the nullthrows - * behavior of `dep.path`, we can keep it as an access (and not promote - * to a dependency). - * See test `reduce-reactive-cond-memberexpr-join` for example. - */ - - /* - * If this property has an optional path (i.e. a?.b.c), all optional - * nodes should be marked accordingly. - */ - for (const property of optionalPath) { - let currChild = getOrMakeProperty(currNode, property); - currChild.accessType = merge( - currChild.accessType, - PropertyAccessType.ConditionalAccess, - ); - currNode = currChild; - } + /** + * The final property node should be marked as an conditional/unconditional + * `dependency` as based on control flow. + */ + const depType = inConditional + ? PropertyAccessType.ConditionalDependency + : PropertyAccessType.UnconditionalDependency; - // The final node should be marked as a conditional dependency. - currNode.accessType = merge( - currNode.accessType, - PropertyAccessType.ConditionalDependency, - ); - } + currNode.accessType = merge(currNode.accessType, depType); } deriveMinimalDependencies(): Set { @@ -294,7 +252,7 @@ type DependencyNode = { }; type ReduceResultNode = { - relativePath: Array; + relativePath: Array<{property: string}>; accessType: PropertyAccessType; }; @@ -325,7 +283,7 @@ function deriveMinimalDependenciesInSubtree( const childResult = deriveMinimalDependenciesInSubtree(childNode).map( ({relativePath, accessType}) => { return { - relativePath: [childName, ...relativePath], + relativePath: [{property: childName}, ...relativePath], accessType, }; }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts index 1e73697783f0b..08d2212d86b95 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts @@ -19,6 +19,7 @@ import { ReactiveScopeDependency, ReactiveStatement, Type, + areEqualPaths, makeInstructionId, } from '../HIR'; import { @@ -525,10 +526,6 @@ function areEqualDependencies( return true; } -export function areEqualPaths(a: Array, b: Array): boolean { - return a.length === b.length && a.every((item, ix) => item === b[ix]); -} - /** * Is this scope eligible for merging with subsequent scopes? In general this * is only true if the scope's output values are guaranteed to change when its diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts index f85f7071f1c49..80395a2e0ea41 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts @@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string { const identifier = printIdentifier(dependency.identifier) + printType(dependency.identifier.type); - return `${identifier}${dependency.path.map(prop => `.${prop}`).join('')}`; + return `${identifier}${dependency.path.map(token => `.${token.property}`).join('')}`; } export function printReactiveInstructions( diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts index 690bdb758839d..0e47a8dcedfb2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -7,6 +7,7 @@ import {CompilerError} from '../CompilerError'; import { + areEqualPaths, BlockId, DeclarationId, GeneratedSource, @@ -35,7 +36,6 @@ import { ReactiveScopeDependencyTree, ReactiveScopePropertyDependency, } from './DeriveMinimalDependencies'; -import {areEqualPaths} from './MergeReactiveScopesThatInvalidateTogether'; import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors'; /* @@ -465,7 +465,6 @@ class Context { #getProperty( object: Place, property: string, - isConditional: boolean, ): ReactiveScopePropertyDependency { const resolvedObject = this.resolveTemporary(object); const resolvedDependency = this.#properties.get(resolvedObject.identifier); @@ -478,36 +477,21 @@ class Context { objectDependency = { identifier: resolvedObject.identifier, path: [], - optionalPath: [], }; } else { objectDependency = { identifier: resolvedDependency.identifier, path: [...resolvedDependency.path], - optionalPath: [...resolvedDependency.optionalPath], }; } - // (2) Determine whether property is an optional access - if (objectDependency.optionalPath.length > 0) { - /* - * If the base property dependency represents a optional member expression, - * property is on the optionalPath (regardless of whether this PropertyLoad - * itself was conditional) - * e.g. for `a.b?.c.d`, `d` should be added to optionalPath - */ - objectDependency.optionalPath.push(property); - } else if (isConditional) { - objectDependency.optionalPath.push(property); - } else { - objectDependency.path.push(property); - } + objectDependency.path.push({property}); return objectDependency; } declareProperty(lvalue: Place, object: Place, property: string): void { - const nextDependency = this.#getProperty(object, property, false); + const nextDependency = this.#getProperty(object, property); this.#properties.set(lvalue.identifier, nextDependency); } @@ -516,7 +500,7 @@ class Context { // ref.current access is not a valid dep if ( isUseRefType(maybeDependency.identifier) && - maybeDependency.path.at(0) === 'current' + maybeDependency.path.at(0)?.property === 'current' ) { return false; } @@ -577,7 +561,6 @@ class Context { let dependency: ReactiveScopePropertyDependency = { identifier: resolved.identifier, path: [], - optionalPath: [], }; if (resolved.identifier.name === null) { const propertyDependency = this.#properties.get(resolved.identifier); @@ -589,7 +572,7 @@ class Context { } visitProperty(object: Place, property: string): void { - const nextDependency = this.#getProperty(object, property, false); + const nextDependency = this.#getProperty(object, property); this.visitDependency(nextDependency); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts index 721fa7b0ec65d..2a9d0b9793d9f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts @@ -180,8 +180,8 @@ class Visitor extends ReactiveFunctionVisitor { [...scope.scope.dependencies].forEach(ident => { let target: undefined | IdentifierId = this.aliases.find(ident.identifier.id) ?? ident.identifier.id; - ident.path.forEach(key => { - target &&= this.paths.get(target)?.get(key); + ident.path.forEach(token => { + target &&= this.paths.get(target)?.get(token.property); }); if (target && this.map.get(target) === 'Create') { scope.scope.dependencies.delete(ident); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 7af4aaaccd7ab..43a477f3418e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -167,7 +167,7 @@ function compareDeps( let isSubpath = true; for (let i = 0; i < Math.min(inferred.path.length, source.path.length); i++) { - if (inferred.path[i] !== source.path[i]) { + if (inferred.path[i].property !== source.path[i].property) { isSubpath = false; break; } @@ -177,14 +177,14 @@ function compareDeps( isSubpath && (source.path.length === inferred.path.length || (inferred.path.length >= source.path.length && - !inferred.path.includes('current'))) + !inferred.path.some(token => token.property === 'current'))) ) { return CompareDependencyResult.Ok; } else { if (isSubpath) { if ( - source.path.includes('current') || - inferred.path.includes('current') + source.path.some(token => token.property === 'current') || + inferred.path.some(token => token.property === 'current') ) { return CompareDependencyResult.RefAccessDifference; } else { From a718da0b23c3f72ba6fb8e1bd087aca85f2b0b4a Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:33 -0700 Subject: [PATCH 067/191] [compiler] Add DependencyPath optional property Adds an `optional: boolean` property to each token in a DependencyPath, currently always set to false. Also updates the equality and printing logic for paths to account for this field. Subsequent PRs will update our logic to determine which manual dependencies were optional, then we can start inferring optional deps as well. ghstack-source-id: 66c2da2cfab5e5ba6c2ac5e20adae5e4f615ad29 Pull Request resolved: https://github.com/facebook/react/pull/30813 --- .../src/HIR/HIR.ts | 7 ++-- .../src/HIR/PrintHIR.ts | 2 +- .../src/Inference/DropManualMemoization.ts | 3 +- .../DeriveMinimalDependencies.ts | 9 ++++-- .../PropagateScopeDependencies.ts | 2 +- .../ValidatePreservedManualMemoization.ts | 2 +- ...al-member-expression-as-memo-dep.expect.md | 32 +++++++++++++++++++ ...-optional-member-expression-as-memo-dep.js | 7 ++++ 8 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index e74ec52203bab..3bc7b135b6ced 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1500,10 +1500,13 @@ export type ReactiveScopeDependency = { export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean { return ( a.length === b.length && - a.every((item, ix) => item.property === b[ix].property) + a.every( + (item, ix) => + item.property === b[ix].property && item.optional === b[ix].optional, + ) ); } -export type DependencyPath = Array<{property: string}>; +export type DependencyPath = Array<{property: string; optional: boolean}>; /* * Simulated opaque type for BlockIds to prevent using normal numbers as block ids diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 8bcab1d7b25ae..ecf0b5f0c6041 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -869,7 +869,7 @@ export function printManualMemoDependency( ? val.root.value.identifier.name.value : printIdentifier(val.root.value.identifier); } - return `${rootStr}${val.path.length > 0 ? '.' : ''}${val.path.join('.')}`; + return `${rootStr}${val.path.map(v => `${v.optional ? '?.' : '.'}${v.property}`).join('')}`; } export function printType(type: Type): string { if (type.kind === 'Type') return ''; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index e60d8a9914583..c580b3e8b759f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -68,7 +68,8 @@ export function collectMaybeMemoDependencies( if (object != null) { return { root: object.root, - path: [...object.path, {property: value.property}], + // TODO: determine if the access is optional + path: [...object.path, {property: value.property, optional: false}], }; } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts index 13420ee140e63..dcf67a36e5c6b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts @@ -6,7 +6,7 @@ */ import {CompilerError} from '../CompilerError'; -import {Identifier, ReactiveScopeDependency} from '../HIR'; +import {DependencyPath, Identifier, ReactiveScopeDependency} from '../HIR'; import {printIdentifier} from '../HIR/PrintHIR'; import {assertExhaustive} from '../Utils/utils'; @@ -252,7 +252,7 @@ type DependencyNode = { }; type ReduceResultNode = { - relativePath: Array<{property: string}>; + relativePath: DependencyPath; accessType: PropertyAccessType; }; @@ -283,7 +283,10 @@ function deriveMinimalDependenciesInSubtree( const childResult = deriveMinimalDependenciesInSubtree(childNode).map( ({relativePath, accessType}) => { return { - relativePath: [{property: childName}, ...relativePath], + relativePath: [ + {property: childName, optional: false}, + ...relativePath, + ], accessType, }; }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts index 0e47a8dcedfb2..8fd324ae2c9aa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -485,7 +485,7 @@ class Context { }; } - objectDependency.path.push({property}); + objectDependency.path.push({property, optional: false}); return objectDependency; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 43a477f3418e6..5a9a947d88bfd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -116,7 +116,7 @@ function prettyPrintScopeDependency(val: ReactiveScopeDependency): string { } else { rootStr = '[unnamed]'; } - return `${rootStr}${val.path.length > 0 ? '.' : ''}${val.path.join('.')}`; + return `${rootStr}${val.path.map(v => `${v.optional ? '?.' : '.'}${v.property}`).join('')}`; } enum CompareDependencyResult { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md new file mode 100644 index 0000000000000..7e4145a27c16b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const data = useMemo(() => { + return props.items?.edges?.nodes ?? []; + }, [props.items?.edges?.nodes]); + return ; +} + +``` + + +## Error + +``` + 1 | // @validatePreserveExistingMemoizationGuarantees + 2 | function Component(props) { +> 3 | const data = useMemo(() => { + | ^^^^^^^ +> 4 | return props.items?.edges?.nodes ?? []; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 5 | }, [props.items?.edges?.nodes]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:5) + 6 | return ; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js new file mode 100644 index 0000000000000..fd8cf0214c87b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js @@ -0,0 +1,7 @@ +// @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const data = useMemo(() => { + return props.items?.edges?.nodes ?? []; + }, [props.items?.edges?.nodes]); + return ; +} From 925c20a20674254391b7752aa216ec417c8f52a3 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:34 -0700 Subject: [PATCH 068/191] [compiler] Add fallthrough to branch terminal Branch terminals didn't have a fallthrough because they correspond to an outer terminal (optional, logical, etc) that has the "real" fallthrough. But understanding how branch terminals correspond to these outer terminals requires knowing the branch fallthrough. For example, `foo?.bar?.baz` creates terminals along the lines of: ``` bb0: optional fallthrough=bb4 bb1: optional fallthrough=bb3 bb2: ... branch ... (fallthrough=bb3) ... bb3: ... branch ... (fallthrough=bb4) ... bb4: ... ``` Without a fallthrough on `branch` terminals, it's unclear that the optional from bb0 has its branch node in bb3. With the fallthroughs, we can see look for a branch with the same fallthrough as the outer optional terminal to match them up. ghstack-source-id: d48c6232899864716eef71798a278b487d30eafc Pull Request resolved: https://github.com/facebook/react/pull/30814 --- .../src/HIR/BuildHIR.ts | 14 +++-- .../src/HIR/HIR.ts | 2 +- .../src/HIR/visitors.ts | 4 +- .../src/Inference/DropManualMemoization.ts | 55 ++++++++++++++++++- .../AlignReactiveScopesToBlockScopesHIR.ts | 2 +- .../ValidatePreservedManualMemoization.ts | 11 +++- 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 06fcfbea7ecc0..7fb12d4624c10 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -607,6 +607,7 @@ function lowerStatement( ), consequent: bodyBlock, alternate: continuationBlock.id, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc: stmt.node.loc ?? GeneratedSource, }, @@ -656,16 +657,13 @@ function lowerStatement( }, conditionalBlock, ); - /* - * The conditional block is empty and exists solely as conditional for - * (re)entering or exiting the loop - */ const test = lowerExpressionToTemporary(builder, stmt.get('test')); const terminal: BranchTerminal = { kind: 'branch', test, consequent: loopBlock, alternate: continuationBlock.id, + fallthrough: conditionalBlock.id, id: makeInstructionId(0), loc: stmt.node.loc ?? GeneratedSource, }; @@ -975,6 +973,7 @@ function lowerStatement( test, consequent: loopBlock, alternate: continuationBlock.id, + fallthrough: conditionalBlock.id, id: makeInstructionId(0), loc, }; @@ -1118,6 +1117,7 @@ function lowerStatement( consequent: loopBlock, alternate: continuationBlock.id, loc: stmt.node.loc ?? GeneratedSource, + fallthrough: continuationBlock.id, }, continuationBlock, ); @@ -1203,6 +1203,7 @@ function lowerStatement( test, consequent: loopBlock, alternate: continuationBlock.id, + fallthrough: continuationBlock.id, loc: stmt.node.loc ?? GeneratedSource, }, continuationBlock, @@ -1800,6 +1801,7 @@ function lowerExpression( test: {...testPlace}, consequent: consequentBlock, alternate: alternateBlock, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc: exprLoc, }, @@ -1878,6 +1880,7 @@ function lowerExpression( test: {...leftPlace}, consequent, alternate, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc: exprLoc, }, @@ -2611,6 +2614,7 @@ function lowerOptionalMemberExpression( test: {...object}, consequent: consequent.id, alternate, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc, }; @@ -2750,6 +2754,7 @@ function lowerOptionalCallExpression( test: {...testPlace}, consequent: consequent.id, alternate, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc, }; @@ -4025,6 +4030,7 @@ function lowerAssignment( test: {...test}, consequent, alternate, + fallthrough: continuationBlock.id, id: makeInstructionId(0), loc, }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 3bc7b135b6ced..3a04a4c3c9dce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -491,7 +491,7 @@ export type BranchTerminal = { alternate: BlockId; id: InstructionId; loc: SourceLocation; - fallthrough?: never; + fallthrough: BlockId; }; export type SwitchTerminal = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index 0df8478b39c45..904b7a4038dec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -660,11 +660,13 @@ export function mapTerminalSuccessors( case 'branch': { const consequent = fn(terminal.consequent); const alternate = fn(terminal.alternate); + const fallthrough = fn(terminal.fallthrough); return { kind: 'branch', test: terminal.test, consequent, alternate, + fallthrough, id: makeInstructionId(0), loc: terminal.loc, }; @@ -883,7 +885,6 @@ export function terminalHasFallthrough< >(terminal: T): terminal is U { switch (terminal.kind) { case 'maybe-throw': - case 'branch': case 'goto': case 'return': case 'throw': @@ -892,6 +893,7 @@ export function terminalHasFallthrough< const _: undefined = terminal.fallthrough; return false; } + case 'branch': case 'try': case 'do-while': case 'for-of': diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index c580b3e8b759f..fdd9bcc968aef 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -42,6 +42,7 @@ type IdentifierSidemap = { react: Set; maybeDepsLists: Map>; maybeDeps: Map; + optionals: Set; }; /** @@ -52,6 +53,7 @@ type IdentifierSidemap = { export function collectMaybeMemoDependencies( value: InstructionValue, maybeDeps: Map, + optional: boolean, ): ManualMemoDependency | null { switch (value.kind) { case 'LoadGlobal': { @@ -69,7 +71,7 @@ export function collectMaybeMemoDependencies( return { root: object.root, // TODO: determine if the access is optional - path: [...object.path, {property: value.property, optional: false}], + path: [...object.path, {property: value.property, optional}], }; } break; @@ -162,7 +164,11 @@ function collectTemporaries( break; } } - const maybeDep = collectMaybeMemoDependencies(value, sidemap.maybeDeps); + const maybeDep = collectMaybeMemoDependencies( + value, + sidemap.maybeDeps, + sidemap.optionals.has(lvalue.identifier.id), + ); // We don't expect named lvalues during this pass (unlike ValidatePreservingManualMemo) if (maybeDep != null) { sidemap.maybeDeps.set(lvalue.identifier.id, maybeDep); @@ -338,12 +344,14 @@ export function dropManualMemoization(func: HIRFunction): void { func.env.config.validatePreserveExistingMemoizationGuarantees || func.env.config.validateNoSetStateInRender || func.env.config.enablePreserveExistingMemoizationGuarantees; + const optionals = findOptionalPlaces(func); const sidemap: IdentifierSidemap = { functions: new Map(), manualMemos: new Map(), react: new Set(), maybeDeps: new Map(), maybeDepsLists: new Map(), + optionals, }; let nextManualMemoId = 0; @@ -476,3 +484,46 @@ export function dropManualMemoization(func: HIRFunction): void { } } } + +function findOptionalPlaces(fn: HIRFunction): Set { + const optionals = new Set(); + for (const [, block] of fn.body.blocks) { + if (block.terminal.kind === 'optional') { + const optionalTerminal = block.terminal; + let testBlock = fn.body.blocks.get(block.terminal.test)!; + loop: while (true) { + const terminal = testBlock.terminal; + switch (terminal.kind) { + case 'branch': { + if (terminal.fallthrough === optionalTerminal.fallthrough) { + // found it + const consequent = fn.body.blocks.get(terminal.consequent)!; + const last = consequent.instructions.at(-1); + if (last !== undefined && last.value.kind === 'StoreLocal') { + optionals.add(last.value.value.identifier.id); + } + break loop; + } else { + testBlock = fn.body.blocks.get(terminal.fallthrough)!; + } + break; + } + case 'optional': + case 'logical': + case 'sequence': + case 'ternary': { + testBlock = fn.body.blocks.get(terminal.fallthrough)!; + break; + } + default: { + CompilerError.invariant(false, { + reason: `Unexpected terminal in optional`, + loc: terminal.loc, + }); + } + } + } + } + } + return optionals; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts index 6517918d02e16..3e8329679cfe2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts @@ -140,7 +140,7 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { } const fallthrough = terminalFallthrough(terminal); - if (fallthrough !== null) { + if (fallthrough !== null && terminal.kind !== 'branch') { /* * Any currently active scopes that overlaps the block-fallthrough range * need their range extended to at least the first instruction of the diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 5a9a947d88bfd..6d948fad9711a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -167,7 +167,10 @@ function compareDeps( let isSubpath = true; for (let i = 0; i < Math.min(inferred.path.length, source.path.length); i++) { - if (inferred.path[i].property !== source.path[i].property) { + if ( + inferred.path[i].property !== source.path[i].property || + inferred.path[i].optional !== source.path[i].optional + ) { isSubpath = false; break; } @@ -339,7 +342,11 @@ class Visitor extends ReactiveFunctionVisitor { return null; } default: { - const dep = collectMaybeMemoDependencies(value, this.temporaries); + const dep = collectMaybeMemoDependencies( + value, + this.temporaries, + false, + ); if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') { const storeTarget = value.lvalue.place; state.manualMemoState?.decls.add( From 9180a37fba0c9ad642bfc6e1c2839f88f66485ab Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:35 -0700 Subject: [PATCH 069/191] [compiler] Allow inferred non-optional paths when manual deps were optional If the inferred deps are more precise (non-optional) than the manual deps (optional) it should pass validation. The other direction also seems like it would be fine - inferring optional deps when the original was non-optional - but for now let's keep the "at least as precise" rule. ghstack-source-id: 9f7a99ee5f7caa2c2d96f70f360e4320bac3de2d Pull Request resolved: https://github.com/facebook/react/pull/30816 --- .../ValidatePreservedManualMemoization.ts | 12 +++-- ...as-memo-dep-non-optional-in-body.expect.md | 50 +++++++++++++++++++ ...ession-as-memo-dep-non-optional-in-body.js | 9 ++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 6d948fad9711a..4f0c756585cf0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -167,12 +167,16 @@ function compareDeps( let isSubpath = true; for (let i = 0; i < Math.min(inferred.path.length, source.path.length); i++) { - if ( - inferred.path[i].property !== source.path[i].property || - inferred.path[i].optional !== source.path[i].optional - ) { + if (inferred.path[i].property !== source.path[i].property) { isSubpath = false; break; + } else if (inferred.path[i].optional && !source.path[i].optional) { + /** + * The inferred path must be at least as precise as the manual path: + * if the inferred path is optional, then the source path must have + * been optional too. + */ + return CompareDependencyResult.PathDifference; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md new file mode 100644 index 0000000000000..7cf1bcd90b339 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const data = useMemo(() => { + // actual code is non-optional + return props.items.edges.nodes ?? []; + // deps are optional + }, [props.items?.edges?.nodes]); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const $ = _c(4); + + props.items?.edges?.nodes; + let t0; + let t1; + if ($[0] !== props.items.edges.nodes) { + t1 = props.items.edges.nodes ?? []; + $[0] = props.items.edges.nodes; + $[1] = t1; + } else { + t1 = $[1]; + } + t0 = t1; + const data = t0; + let t2; + if ($[2] !== data) { + t2 = ; + $[2] = data; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js new file mode 100644 index 0000000000000..1a6196a494e66 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js @@ -0,0 +1,9 @@ +// @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const data = useMemo(() => { + // actual code is non-optional + return props.items.edges.nodes ?? []; + // deps are optional + }, [props.items?.edges?.nodes]); + return ; +} From 7475d568da137b661ce23edc24446871d58c67ef Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:36 -0700 Subject: [PATCH 070/191] [wip][compiler] Infer optional dependencies Updates PropagateScopeDeps and DeriveMinimalDeps to understand optional dependency paths (`a?.b`). There a few key pieces to this: In PropagateScopeDeps we jump through some hoops to work around the awkward structure of nested OptionalExpressions. This is much easier in HIR form, but I managed to get this pretty close and i think it will be landable with further cleanup. A good chunk of this is avoiding prematurely registering a value as a dependency - there are a bunch of indirections in the ReactiveFunction structure: ``` t0 = OptionalExpression SequenceExpression t0 = Sequence ... LoadLocal t0 ``` Where if at any point we call `visitOperand()` we'll prematurely register a dependency instead of declareProperty(). The other bit is that optionals can be optional=false for nested member expressions where not all the parts are actually optional (`foo.bar?.bar.call()`). And of course, parts of an optional chain can still be conditional even when optional=true (for example the `x` in `foo.bar?.[x]?.baz`). Not all of this is tested yet so there are likely bugs still. The other bit is DeriveMinimalDeps, which is thankfully easier. We add OptionalAccess and OptionalDep and update the merge and reducing logic for these cases. There is probably still more to update though, for things like merging subtrees. There are a lot of ternaries that assume a result can be exactly one of two states (conditional/unconditional, dependency/access) and these assumptions don't hold anymore. I'd like to refactor to dependency/access separate from conditional/optional/unconditional. Also, the reducing logic isn't quite right: once a child is optional we keep inferring all the parents as optional too, losing some precision. I need to adjust the reducing logic to let children decide whether their path token is optional or not. ghstack-source-id: 207842ac64560cf0f93ec96eb9ae1f17c62493ac Pull Request resolved: https://github.com/facebook/react/pull/30819 --- .../src/HIR/Environment.ts | 8 + .../src/HIR/PrintHIR.ts | 2 +- .../ReactiveScopes/CodegenReactiveFunction.ts | 14 +- .../DeriveMinimalDependencies.ts | 165 ++++++++--- .../ReactiveScopes/PrintReactiveFunction.ts | 2 +- .../PropagateScopeDependencies.ts | 271 ++++++++++++++---- ...al-member-expression-as-memo-dep.expect.md | 32 --- ...-optional-member-expression-as-memo-dep.js | 7 - ...al-member-expression-as-memo-dep.expect.md | 48 ++++ .../optional-member-expression-as-memo-dep.js | 7 + ...ession-single-with-unconditional.expect.md | 65 +++++ ...er-expression-single-with-unconditional.js | 11 + ...ptional-member-expression-single.expect.md | 63 ++++ .../optional-member-expression-single.js | 10 + ...ession-with-conditional-optional.expect.md | 74 +++++ ...er-expression-with-conditional-optional.js | 15 + ...mber-expression-with-conditional.expect.md | 74 +++++ ...onal-member-expression-with-conditional.js | 15 + 18 files changed, 755 insertions(+), 128 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 12c741641c7e0..a13ebb6aac878 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -224,6 +224,14 @@ const EnvironmentConfigSchema = z.object({ enableReactiveScopesInHIR: z.boolean().default(true), + /** + * Enables inference of optional dependency chains. Without this flag + * a property chain such as `props?.items?.foo` will infer as a dep on + * just `props`. With this flag enabled, we'll infer that full path as + * the dependency. + */ + enableOptionalDependencies: z.boolean().default(false), + /* * Enable validation of hooks to partially check that the component honors the rules of hooks. * When disabled, the component is assumed to follow the rules (though the Babel plugin looks diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index ecf0b5f0c6041..c2db20c5099a1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -191,7 +191,7 @@ export function printTerminal(terminal: Terminal): Array | string { case 'branch': { value = `[${terminal.id}] Branch (${printPlace(terminal.test)}) then:bb${ terminal.consequent - } else:bb${terminal.alternate}`; + } else:bb${terminal.alternate} fallthrough:bb${terminal.fallthrough}`; break; } case 'logical': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 73330b959e018..2df7b5ed1c7fd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1446,9 +1446,19 @@ function codegenDependency( dependency: ReactiveScopeDependency, ): t.Expression { let object: t.Expression = convertIdentifier(dependency.identifier); - if (dependency.path !== null) { + if (dependency.path.length !== 0) { + const hasOptional = dependency.path.some(path => path.optional); for (const path of dependency.path) { - object = t.memberExpression(object, t.identifier(path.property)); + if (hasOptional) { + object = t.optionalMemberExpression( + object, + t.identifier(path.property), + false, + path.optional, + ); + } else { + object = t.memberExpression(object, t.identifier(path.property)); + } } } return object; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts index dcf67a36e5c6b..c7e16cce7ac86 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts @@ -60,13 +60,14 @@ export class ReactiveScopeDependencyTree { const {path} = dep; let currNode = this.#getOrCreateRoot(dep.identifier); - const accessType = inConditional - ? PropertyAccessType.ConditionalAccess - : PropertyAccessType.UnconditionalAccess; - for (const item of path) { // all properties read 'on the way' to a dependency are marked as 'access' let currChild = getOrMakeProperty(currNode, item.property); + const accessType = inConditional + ? PropertyAccessType.ConditionalAccess + : item.optional + ? PropertyAccessType.OptionalAccess + : PropertyAccessType.UnconditionalAccess; currChild.accessType = merge(currChild.accessType, accessType); currNode = currChild; } @@ -77,7 +78,9 @@ export class ReactiveScopeDependencyTree { */ const depType = inConditional ? PropertyAccessType.ConditionalDependency - : PropertyAccessType.UnconditionalDependency; + : isOptional(currNode.accessType) + ? PropertyAccessType.OptionalDependency + : PropertyAccessType.UnconditionalDependency; currNode.accessType = merge(currNode.accessType, depType); } @@ -85,10 +88,12 @@ export class ReactiveScopeDependencyTree { deriveMinimalDependencies(): Set { const results = new Set(); for (const [rootId, rootNode] of this.#roots.entries()) { - const deps = deriveMinimalDependenciesInSubtree(rootNode); + const deps = deriveMinimalDependenciesInSubtree(rootNode, null); CompilerError.invariant( deps.every( - dep => dep.accessType === PropertyAccessType.UnconditionalDependency, + dep => + dep.accessType === PropertyAccessType.UnconditionalDependency || + dep.accessType == PropertyAccessType.OptionalDependency, ), { reason: @@ -173,6 +178,27 @@ export class ReactiveScopeDependencyTree { } return res.flat().join('\n'); } + + debug(): string { + const buf: Array = [`tree() [`]; + for (const [rootId, rootNode] of this.#roots) { + buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`); + this.#debugImpl(buf, rootNode, 1); + } + buf.push(']'); + return buf.length > 2 ? buf.join('\n') : buf.join(''); + } + + #debugImpl( + buf: Array, + node: DependencyNode, + depth: number = 0, + ): void { + for (const [property, childNode] of node.properties) { + buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`); + this.#debugImpl(buf, childNode, depth + 1); + } + } } /* @@ -196,8 +222,10 @@ export class ReactiveScopeDependencyTree { */ enum PropertyAccessType { ConditionalAccess = 'ConditionalAccess', + OptionalAccess = 'OptionalAccess', UnconditionalAccess = 'UnconditionalAccess', ConditionalDependency = 'ConditionalDependency', + OptionalDependency = 'OptionalDependency', UnconditionalDependency = 'UnconditionalDependency', } @@ -211,9 +239,16 @@ function isUnconditional(access: PropertyAccessType): boolean { function isDependency(access: PropertyAccessType): boolean { return ( access === PropertyAccessType.ConditionalDependency || + access === PropertyAccessType.OptionalDependency || access === PropertyAccessType.UnconditionalDependency ); } +function isOptional(access: PropertyAccessType): boolean { + return ( + access === PropertyAccessType.OptionalAccess || + access === PropertyAccessType.OptionalDependency + ); +} function merge( access1: PropertyAccessType, @@ -222,6 +257,7 @@ function merge( const resultIsUnconditional = isUnconditional(access1) || isUnconditional(access2); const resultIsDependency = isDependency(access1) || isDependency(access2); + const resultIsOptional = isOptional(access1) || isOptional(access2); /* * Straightforward merge. @@ -237,6 +273,12 @@ function merge( } else { return PropertyAccessType.UnconditionalAccess; } + } else if (resultIsOptional) { + if (resultIsDependency) { + return PropertyAccessType.OptionalDependency; + } else { + return PropertyAccessType.OptionalAccess; + } } else { if (resultIsDependency) { return PropertyAccessType.ConditionalDependency; @@ -256,19 +298,34 @@ type ReduceResultNode = { accessType: PropertyAccessType; }; -const promoteUncondResult = [ - { +function promoteResult( + accessType: PropertyAccessType, + path: {property: string; optional: boolean} | null, +): Array { + const result: ReduceResultNode = { relativePath: [], - accessType: PropertyAccessType.UnconditionalDependency, - }, -]; + accessType, + }; + if (path !== null) { + result.relativePath.push(path); + } + return [result]; +} -const promoteCondResult = [ - { - relativePath: [], - accessType: PropertyAccessType.ConditionalDependency, - }, -]; +function prependPath( + results: Array, + path: {property: string; optional: boolean} | null, +): Array { + if (path === null) { + return results; + } + return results.map(result => { + return { + accessType: result.accessType, + relativePath: [path, ...result.relativePath], + }; + }); +} /* * Recursively calculates minimal dependencies in a subtree. @@ -277,42 +334,76 @@ const promoteCondResult = [ */ function deriveMinimalDependenciesInSubtree( dep: DependencyNode, + property: string | null, ): Array { const results: Array = []; for (const [childName, childNode] of dep.properties) { - const childResult = deriveMinimalDependenciesInSubtree(childNode).map( - ({relativePath, accessType}) => { - return { - relativePath: [ - {property: childName, optional: false}, - ...relativePath, - ], - accessType, - }; - }, + const childResult = deriveMinimalDependenciesInSubtree( + childNode, + childName, ); results.push(...childResult); } switch (dep.accessType) { case PropertyAccessType.UnconditionalDependency: { - return promoteUncondResult; + return promoteResult( + PropertyAccessType.UnconditionalDependency, + property !== null ? {property, optional: false} : null, + ); } case PropertyAccessType.UnconditionalAccess: { if ( results.every( ({accessType}) => - accessType === PropertyAccessType.UnconditionalDependency, + accessType === PropertyAccessType.UnconditionalDependency || + accessType === PropertyAccessType.OptionalDependency, ) ) { // all children are unconditional dependencies, return them to preserve granularity - return results; + return prependPath( + results, + property !== null ? {property, optional: false} : null, + ); } else { /* * at least one child is accessed conditionally, so this node needs to be promoted to * unconditional dependency */ - return promoteUncondResult; + return promoteResult( + PropertyAccessType.UnconditionalDependency, + property !== null ? {property, optional: false} : null, + ); + } + } + case PropertyAccessType.OptionalDependency: { + return promoteResult( + PropertyAccessType.OptionalDependency, + property !== null ? {property, optional: true} : null, + ); + } + case PropertyAccessType.OptionalAccess: { + if ( + results.every( + ({accessType}) => + accessType === PropertyAccessType.UnconditionalDependency || + accessType === PropertyAccessType.OptionalDependency, + ) + ) { + // all children are unconditional dependencies, return them to preserve granularity + return prependPath( + results, + property !== null ? {property, optional: true} : null, + ); + } else { + /* + * at least one child is accessed conditionally, so this node needs to be promoted to + * unconditional dependency + */ + return promoteResult( + PropertyAccessType.OptionalDependency, + property !== null ? {property, optional: true} : null, + ); } } case PropertyAccessType.ConditionalAccess: @@ -328,13 +419,19 @@ function deriveMinimalDependenciesInSubtree( * unconditional access. * Truncate results of child nodes here, since we shouldn't access them anyways */ - return promoteCondResult; + return promoteResult( + PropertyAccessType.ConditionalDependency, + property !== null ? {property, optional: true} : null, + ); } else { /* * at least one child is accessed unconditionally, so this node can be promoted to * unconditional dependency */ - return promoteUncondResult; + return promoteResult( + PropertyAccessType.UnconditionalDependency, + property !== null ? {property, optional: true} : null, + ); } } default: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts index 80395a2e0ea41..b5aa44ead095d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts @@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string { const identifier = printIdentifier(dependency.identifier) + printType(dependency.identifier.type); - return `${identifier}${dependency.path.map(token => `.${token.property}`).join('')}`; + return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`; } export function printReactiveInstructions( diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts index 8fd324ae2c9aa..4a054ab84c0bf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -6,6 +6,7 @@ */ import {CompilerError} from '../CompilerError'; +import {Environment} from '../HIR'; import { areEqualPaths, BlockId, @@ -22,6 +23,7 @@ import { PrunedReactiveScopeBlock, ReactiveFunction, ReactiveInstruction, + ReactiveOptionalCallValue, ReactiveScope, ReactiveScopeBlock, ReactiveScopeDependency, @@ -65,11 +67,7 @@ export function propagateScopeDependencies(fn: ReactiveFunction): void { }); } } - visitReactiveFunction( - fn, - new PropagationVisitor(fn.env.config.enableTreatFunctionDepsAsConditional), - context, - ); + visitReactiveFunction(fn, new PropagationVisitor(fn.env), context); } type TemporariesUsedOutsideDefiningScope = { @@ -465,6 +463,7 @@ class Context { #getProperty( object: Place, property: string, + optional: boolean, ): ReactiveScopePropertyDependency { const resolvedObject = this.resolveTemporary(object); const resolvedDependency = this.#properties.get(resolvedObject.identifier); @@ -485,13 +484,18 @@ class Context { }; } - objectDependency.path.push({property, optional: false}); + objectDependency.path.push({property, optional}); return objectDependency; } - declareProperty(lvalue: Place, object: Place, property: string): void { - const nextDependency = this.#getProperty(object, property); + declareProperty( + lvalue: Place, + object: Place, + property: string, + optional: boolean, + ): void { + const nextDependency = this.#getProperty(object, property, optional); this.#properties.set(lvalue.identifier, nextDependency); } @@ -571,8 +575,8 @@ class Context { this.visitDependency(dependency); } - visitProperty(object: Place, property: string): void { - const nextDependency = this.#getProperty(object, property); + visitProperty(object: Place, property: string, optional: boolean): void { + const nextDependency = this.#getProperty(object, property, optional); this.visitDependency(nextDependency); } @@ -671,12 +675,11 @@ class Context { } class PropagationVisitor extends ReactiveFunctionVisitor { - enableTreatFunctionDepsAsConditional = false; + env: Environment; - constructor(enableTreatFunctionDepsAsConditional: boolean) { + constructor(env: Environment) { super(); - this.enableTreatFunctionDepsAsConditional = - enableTreatFunctionDepsAsConditional; + this.env = env; } override visitScope(scope: ReactiveScopeBlock, context: Context): void { @@ -744,51 +747,212 @@ class PropagationVisitor extends ReactiveFunctionVisitor { }); } + extractOptionalProperty( + context: Context, + optionalValue: ReactiveOptionalCallValue, + lvalue: Place, + ): { + lvalue: Place; + object: Place; + property: string; + optional: boolean; + } | null { + const sequence = optionalValue.value; + CompilerError.invariant(sequence.kind === 'SequenceExpression', { + reason: 'Expected OptionalExpression value to be a SequenceExpression', + description: `Found a \`${sequence.kind}\``, + loc: sequence.loc, + }); + /** + * Base case: inner ` "." or "?."" ` + *``` + * = OptionalExpression optional=true (`optionalValue` is here) + * Sequence (`sequence` is here) + * t0 = LoadLocal + * Sequence + * t1 = PropertyLoad t0 . + * LoadLocal t1 + * ``` + */ + if ( + sequence.instructions.length === 1 && + sequence.instructions[0].value.kind === 'LoadLocal' && + sequence.instructions[0].lvalue !== null && + sequence.instructions[0].value.place.identifier.name !== null && + !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) && + sequence.value.kind === 'SequenceExpression' && + sequence.value.instructions.length === 1 && + sequence.value.instructions[0].value.kind === 'PropertyLoad' && + sequence.value.instructions[0].value.object.identifier.id === + sequence.instructions[0].lvalue.identifier.id && + sequence.value.instructions[0].lvalue !== null && + sequence.value.value.kind === 'LoadLocal' && + sequence.value.value.place.identifier.id === + sequence.value.instructions[0].lvalue.identifier.id + ) { + context.declareTemporary( + sequence.instructions[0].lvalue, + sequence.instructions[0].value.place, + ); + const propertyLoad = sequence.value.instructions[0].value; + return { + lvalue, + object: propertyLoad.object, + property: propertyLoad.property, + optional: optionalValue.optional, + }; + } + /** + * Composed case: ` "." or "?." ` + * + * This case is convoluted, note how `t0` appears as an lvalue *twice* + * and then is an operand of an intermediate LoadLocal and then the + * object of the final PropertyLoad: + * + * ``` + * = OptionalExpression optional=false (`optionalValue` is here) + * Sequence (`sequence` is here) + * t0 = Sequence + * t0 = + * + * LoadLocal t0 + * Sequence + * t1 = PropertyLoad t0. + * LoadLocal t1 + * ``` + */ + if ( + sequence.instructions.length === 1 && + sequence.instructions[0].value.kind === 'SequenceExpression' && + sequence.instructions[0].value.instructions.length === 1 && + sequence.instructions[0].value.instructions[0].lvalue !== null && + sequence.instructions[0].value.instructions[0].value.kind === + 'OptionalExpression' && + sequence.instructions[0].value.value.kind === 'LoadLocal' && + sequence.instructions[0].value.value.place.identifier.id === + sequence.instructions[0].value.instructions[0].lvalue.identifier.id && + sequence.value.kind === 'SequenceExpression' && + sequence.value.instructions.length === 1 && + sequence.value.instructions[0].lvalue !== null && + sequence.value.instructions[0].value.kind === 'PropertyLoad' && + sequence.value.instructions[0].value.object.identifier.id === + sequence.instructions[0].value.value.place.identifier.id && + sequence.value.value.kind === 'LoadLocal' && + sequence.value.value.place.identifier.id === + sequence.value.instructions[0].lvalue.identifier.id + ) { + const {lvalue: innerLvalue, value: innerOptional} = + sequence.instructions[0].value.instructions[0]; + const innerProperty = this.extractOptionalProperty( + context, + innerOptional, + innerLvalue, + ); + if (innerProperty === null) { + return null; + } + context.declareProperty( + innerProperty.lvalue, + innerProperty.object, + innerProperty.property, + innerProperty.optional, + ); + const propertyLoad = sequence.value.instructions[0].value; + return { + lvalue, + object: propertyLoad.object, + property: propertyLoad.property, + optional: optionalValue.optional, + }; + } + return null; + } + + visitOptionalExpression( + context: Context, + id: InstructionId, + value: ReactiveOptionalCallValue, + lvalue: Place | null, + ): void { + /** + * If this is the first optional=true optional in a recursive OptionalExpression + * subtree, we check to see if the subtree is of the form: + * ``` + * NestedOptional = + * ` . / ?. ` + * ` . / ?. ` + * ``` + * + * Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains + * any other types of expressions - for example `foo?.[makeKey(a)]` - then this + * will return null and we'll go to the default handling below. + * + * If the tree does match the NestedOptional shape, then we'll have recorded + * a sequence of declareProperty calls, and the final visitProperty call here + * will record that optional chain as a dependency (since we know it's about + * to be referenced via its lvalue which is non-null). + */ + if ( + lvalue !== null && + value.optional && + this.env.config.enableOptionalDependencies + ) { + const inner = this.extractOptionalProperty(context, value, lvalue); + if (inner !== null) { + context.visitProperty(inner.object, inner.property, inner.optional); + return; + } + } + + // Otherwise we treat everything after the optional as conditional + const inner = value.value; + /* + * OptionalExpression value is a SequenceExpression where the instructions + * represent the code prior to the `?` and the final value represents the + * conditional code that follows. + */ + CompilerError.invariant(inner.kind === 'SequenceExpression', { + reason: 'Expected OptionalExpression value to be a SequenceExpression', + description: `Found a \`${value.kind}\``, + loc: value.loc, + suggestions: null, + }); + // Instructions are the unconditionally executed portion before the `?` + for (const instr of inner.instructions) { + this.visitInstruction(instr, context); + } + // The final value is the conditional portion following the `?` + context.enterConditional(() => { + this.visitReactiveValue(context, id, inner.value, null); + }); + } + visitReactiveValue( context: Context, id: InstructionId, value: ReactiveValue, + lvalue: Place | null, ): void { switch (value.kind) { case 'OptionalExpression': { - const inner = value.value; - /* - * OptionalExpression value is a SequenceExpression where the instructions - * represent the code prior to the `?` and the final value represents the - * conditional code that follows. - */ - CompilerError.invariant(inner.kind === 'SequenceExpression', { - reason: - 'Expected OptionalExpression value to be a SequenceExpression', - description: `Found a \`${value.kind}\``, - loc: value.loc, - suggestions: null, - }); - // Instructions are the unconditionally executed portion before the `?` - for (const instr of inner.instructions) { - this.visitInstruction(instr, context); - } - // The final value is the conditional portion following the `?` - context.enterConditional(() => { - this.visitReactiveValue(context, id, inner.value); - }); + this.visitOptionalExpression(context, id, value, lvalue); break; } case 'LogicalExpression': { - this.visitReactiveValue(context, id, value.left); + this.visitReactiveValue(context, id, value.left, null); context.enterConditional(() => { - this.visitReactiveValue(context, id, value.right); + this.visitReactiveValue(context, id, value.right, null); }); break; } case 'ConditionalExpression': { - this.visitReactiveValue(context, id, value.test); + this.visitReactiveValue(context, id, value.test, null); const consequentDeps = context.enterConditional(() => { - this.visitReactiveValue(context, id, value.consequent); + this.visitReactiveValue(context, id, value.consequent, null); }); const alternateDeps = context.enterConditional(() => { - this.visitReactiveValue(context, id, value.alternate); + this.visitReactiveValue(context, id, value.alternate, null); }); context.promoteDepsFromExhaustiveConditionals([ consequentDeps, @@ -804,7 +968,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor { break; } case 'FunctionExpression': { - if (this.enableTreatFunctionDepsAsConditional) { + if (this.env.config.enableTreatFunctionDepsAsConditional) { context.enterConditional(() => { for (const operand of eachInstructionValueOperand(value)) { context.visitOperand(operand); @@ -851,9 +1015,9 @@ class PropagationVisitor extends ReactiveFunctionVisitor { } } else if (value.kind === 'PropertyLoad') { if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) { - context.declareProperty(lvalue, value.object, value.property); + context.declareProperty(lvalue, value.object, value.property, false); } else { - context.visitProperty(value.object, value.property); + context.visitProperty(value.object, value.property, false); } } else if (value.kind === 'StoreLocal') { context.visitOperand(value.value); @@ -896,7 +1060,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor { }); } } else { - this.visitReactiveValue(context, id, value); + this.visitReactiveValue(context, id, value, lvalue); } } @@ -947,25 +1111,30 @@ class PropagationVisitor extends ReactiveFunctionVisitor { break; } case 'for': { - this.visitReactiveValue(context, terminal.id, terminal.init); - this.visitReactiveValue(context, terminal.id, terminal.test); + this.visitReactiveValue(context, terminal.id, terminal.init, null); + this.visitReactiveValue(context, terminal.id, terminal.test, null); context.enterConditional(() => { this.visitBlock(terminal.loop, context); if (terminal.update !== null) { - this.visitReactiveValue(context, terminal.id, terminal.update); + this.visitReactiveValue( + context, + terminal.id, + terminal.update, + null, + ); } }); break; } case 'for-of': { - this.visitReactiveValue(context, terminal.id, terminal.init); + this.visitReactiveValue(context, terminal.id, terminal.init, null); context.enterConditional(() => { this.visitBlock(terminal.loop, context); }); break; } case 'for-in': { - this.visitReactiveValue(context, terminal.id, terminal.init); + this.visitReactiveValue(context, terminal.id, terminal.init, null); context.enterConditional(() => { this.visitBlock(terminal.loop, context); }); @@ -974,12 +1143,12 @@ class PropagationVisitor extends ReactiveFunctionVisitor { case 'do-while': { this.visitBlock(terminal.loop, context); context.enterConditional(() => { - this.visitReactiveValue(context, terminal.id, terminal.test); + this.visitReactiveValue(context, terminal.id, terminal.test, null); }); break; } case 'while': { - this.visitReactiveValue(context, terminal.id, terminal.test); + this.visitReactiveValue(context, terminal.id, terminal.test, null); context.enterConditional(() => { this.visitBlock(terminal.loop, context); }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md deleted file mode 100644 index 7e4145a27c16b..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.expect.md +++ /dev/null @@ -1,32 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees -function Component(props) { - const data = useMemo(() => { - return props.items?.edges?.nodes ?? []; - }, [props.items?.edges?.nodes]); - return ; -} - -``` - - -## Error - -``` - 1 | // @validatePreserveExistingMemoizationGuarantees - 2 | function Component(props) { -> 3 | const data = useMemo(() => { - | ^^^^^^^ -> 4 | return props.items?.edges?.nodes ?? []; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 5 | }, [props.items?.edges?.nodes]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:5) - 6 | return ; - 7 | } - 8 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js deleted file mode 100644 index fd8cf0214c87b..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-member-expression-as-memo-dep.js +++ /dev/null @@ -1,7 +0,0 @@ -// @validatePreserveExistingMemoizationGuarantees -function Component(props) { - const data = useMemo(() => { - return props.items?.edges?.nodes ?? []; - }, [props.items?.edges?.nodes]); - return ; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md new file mode 100644 index 0000000000000..c34b79a848ba8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md @@ -0,0 +1,48 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +function Component(props) { + const data = useMemo(() => { + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +function Component(props) { + const $ = _c(4); + + props?.items.edges?.nodes; + let t0; + let t1; + if ($[0] !== props?.items.edges?.nodes) { + t1 = props?.items.edges?.nodes.map(); + $[0] = props?.items.edges?.nodes; + $[1] = t1; + } else { + t1 = $[1]; + } + t0 = t1; + const data = t0; + let t2; + if ($[2] !== data) { + t2 = ; + $[2] = data; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js new file mode 100644 index 0000000000000..d82d36b547970 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js @@ -0,0 +1,7 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +function Component(props) { + const data = useMemo(() => { + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md new file mode 100644 index 0000000000000..d0c4afe459da2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + x.push(props.items); + return x; + }, [props?.items]); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import { ValidateMemoization } from "shared-runtime"; +function Component(props) { + const $ = _c(7); + + props?.items; + let t0; + let x; + if ($[0] !== props.items) { + x = []; + x.push(props?.items); + x.push(props.items); + $[0] = props.items; + $[1] = x; + } else { + x = $[1]; + } + t0 = x; + const data = t0; + const t1 = props?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== data) { + t3 = ; + $[4] = t2; + $[5] = data; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js new file mode 100644 index 0000000000000..c630dd6bb4b9d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js @@ -0,0 +1,11 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + x.push(props.items); + return x; + }, [props?.items]); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md new file mode 100644 index 0000000000000..a4cf6d767d1c3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + return x; + }, [props?.items]); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import { ValidateMemoization } from "shared-runtime"; +function Component(props) { + const $ = _c(7); + + props?.items; + let t0; + let x; + if ($[0] !== props?.items) { + x = []; + x.push(props?.items); + $[0] = props?.items; + $[1] = x; + } else { + x = $[1]; + } + t0 = x; + const data = t0; + const t1 = props?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== data) { + t3 = ; + $[4] = t2; + $[5] = data; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js new file mode 100644 index 0000000000000..5750d7af3a0e0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js @@ -0,0 +1,10 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + return x; + }, [props?.items]); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md new file mode 100644 index 0000000000000..77ded20d939bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props?.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import { ValidateMemoization } from "shared-runtime"; +function Component(props) { + const $ = _c(9); + + props?.items; + let t0; + let x; + if ($[0] !== props?.items || $[1] !== props.cond) { + x = []; + x.push(props?.items); + if (props.cond) { + x.push(props?.items); + } + $[0] = props?.items; + $[1] = props.cond; + $[2] = x; + } else { + x = $[2]; + } + t0 = x; + const data = t0; + + const t1 = props?.items; + let t2; + if ($[3] !== t1 || $[4] !== props.cond) { + t2 = [t1, props.cond]; + $[3] = t1; + $[4] = props.cond; + $[5] = t2; + } else { + t2 = $[5]; + } + let t3; + if ($[6] !== t2 || $[7] !== data) { + t3 = ; + $[6] = t2; + $[7] = data; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.js new file mode 100644 index 0000000000000..760f345e90210 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.js @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props?.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md new file mode 100644 index 0000000000000..10c23085d8e6b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import { ValidateMemoization } from "shared-runtime"; +function Component(props) { + const $ = _c(9); + + props?.items; + let t0; + let x; + if ($[0] !== props?.items || $[1] !== props.cond) { + x = []; + x.push(props?.items); + if (props.cond) { + x.push(props.items); + } + $[0] = props?.items; + $[1] = props.cond; + $[2] = x; + } else { + x = $[2]; + } + t0 = x; + const data = t0; + + const t1 = props?.items; + let t2; + if ($[3] !== t1 || $[4] !== props.cond) { + t2 = [t1, props.cond]; + $[3] = t1; + $[4] = props.cond; + $[5] = t2; + } else { + t2 = $[5]; + } + let t3; + if ($[6] !== t2 || $[7] !== data) { + t3 = ; + $[6] = t2; + $[7] = data; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.js new file mode 100644 index 0000000000000..3f773f4fe4e4b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.js @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} From 99a4b26e18a71a2ed5af5ec11f4b9bace3882f7e Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 10:52:37 -0700 Subject: [PATCH 071/191] [compiler] Handle optional where innermost property access is non-optional Handles an additional case as part of testing combinations of the same path being accessed in different places with different segments as optional/unconditional. ghstack-source-id: ace777fcbb98fa8f41b977d0aec8418f3f58fb7b Pull Request resolved: https://github.com/facebook/react/pull/30836 --- .../PropagateScopeDependencies.ts | 82 ++++++++++++++++++- ...nverted-optionals-parallel-paths.expect.md | 46 +++++++++++ ...ssion-inverted-optionals-parallel-paths.js | 11 +++ 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts index 4a054ab84c0bf..dc1142b271e77 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -764,7 +764,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor { loc: sequence.loc, }); /** - * Base case: inner ` "." or "?."" ` + * Base case: inner ` "?." ` *``` * = OptionalExpression optional=true (`optionalValue` is here) * Sequence (`sequence` is here) @@ -776,8 +776,8 @@ class PropagationVisitor extends ReactiveFunctionVisitor { */ if ( sequence.instructions.length === 1 && - sequence.instructions[0].value.kind === 'LoadLocal' && sequence.instructions[0].lvalue !== null && + sequence.instructions[0].value.kind === 'LoadLocal' && sequence.instructions[0].value.place.identifier.name !== null && !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) && sequence.value.kind === 'SequenceExpression' && @@ -803,7 +803,83 @@ class PropagationVisitor extends ReactiveFunctionVisitor { }; } /** - * Composed case: ` "." or "?." ` + * Base case 2: inner ` "." "?." + * ``` + * = OptionalExpression optional=true (`optionalValue` is here) + * Sequence (`sequence` is here) + * t0 = Sequence + * t1 = LoadLocal + * ... // see note + * PropertyLoad t1 . + * [46] Sequence + * t2 = PropertyLoad t0 . + * [46] LoadLocal t2 + * ``` + * + * Note that it's possible to have additional inner chained non-optional + * property loads at "...", from an expression like `a?.b.c.d.e`. We could + * expand to support this case by relaxing the check on the inner sequence + * length, ensuring all instructions after the first LoadLocal are PropertyLoad + * and then iterating to ensure that the lvalue of the previous is always + * the object of the next PropertyLoad, w the final lvalue as the object + * of the sequence.value's object. + * + * But this case is likely rare in practice, usually once you're optional + * chaining all property accesses are optional (not `a?.b.c` but `a?.b?.c`). + * Also, HIR-based PropagateScopeDeps will handle this case so it doesn't + * seem worth it to optimize for that edge-case here. + */ + if ( + sequence.instructions.length === 1 && + sequence.instructions[0].lvalue !== null && + sequence.instructions[0].value.kind === 'SequenceExpression' && + sequence.instructions[0].value.instructions.length === 1 && + sequence.instructions[0].value.instructions[0].lvalue !== null && + sequence.instructions[0].value.instructions[0].value.kind === + 'LoadLocal' && + sequence.instructions[0].value.instructions[0].value.place.identifier + .name !== null && + !context.isUsedOutsideDeclaringScope( + sequence.instructions[0].value.instructions[0].lvalue, + ) && + sequence.instructions[0].value.value.kind === 'PropertyLoad' && + sequence.instructions[0].value.value.object.identifier.id === + sequence.instructions[0].value.instructions[0].lvalue.identifier.id && + sequence.value.kind === 'SequenceExpression' && + sequence.value.instructions.length === 1 && + sequence.value.instructions[0].lvalue !== null && + sequence.value.instructions[0].value.kind === 'PropertyLoad' && + sequence.value.instructions[0].value.object.identifier.id === + sequence.instructions[0].lvalue.identifier.id && + sequence.value.value.kind === 'LoadLocal' && + sequence.value.value.place.identifier.id === + sequence.value.instructions[0].lvalue.identifier.id + ) { + // LoadLocal + context.declareTemporary( + sequence.instructions[0].value.instructions[0].lvalue, + sequence.instructions[0].value.instructions[0].value.place, + ); + // PropertyLoad . (the inner non-optional property) + context.declareProperty( + sequence.instructions[0].lvalue, + sequence.instructions[0].value.value.object, + sequence.instructions[0].value.value.property, + false, + ); + const propertyLoad = sequence.value.instructions[0].value; + return { + lvalue, + object: propertyLoad.object, + property: propertyLoad.property, + optional: optionalValue.optional, + }; + } + + /** + * Composed case: + * - ` "." or "?." ` + * - ` "." or "?>" ` * * This case is convoluted, note how `t0` appears as an lvalue *twice* * and then is an operand of an intermediate LoadLocal and then the diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md new file mode 100644 index 0000000000000..98fcfbe7f0f6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.a.b?.c.d?.e); + x.push(props.a?.b.c?.d.e); + return x; + }, [props.a.b.c.d.e]); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import { ValidateMemoization } from "shared-runtime"; +function Component(props) { + const $ = _c(2); + let t0; + + const x$0 = []; + x$0.push(props?.a.b?.c.d?.e); + x$0.push(props.a?.b.c?.d.e); + t0 = x$0; + let t1; + if ($[0] !== props.a.b.c.d.e) { + t1 = ; + $[0] = props.a.b.c.d.e; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.js new file mode 100644 index 0000000000000..563b0bbf0f418 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.js @@ -0,0 +1,11 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.a.b?.c.d?.e); + x.push(props.a?.b.c?.d.e); + return x; + }, [props.a.b.c.d.e]); + return ; +} From 3a45ba241c028cd0af7bf17bb4c6487d0095a10f Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 12:12:29 -0700 Subject: [PATCH 072/191] [compiler] Enable optional dependencies by default Per title. This gives us much more granular memoization when the source used optional member expressions. Note that we only infer optional deps when the source used optionals: we don't (yet) infer optional dependencies from conditionals. ghstack-source-id: 104d0b712d09498239e926e306c4623d546463b1 Pull Request resolved: https://github.com/facebook/react/pull/30838 --- .../src/HIR/Environment.ts | 2 +- ...call-with-optional-property-load.expect.md | 4 ++-- ...less-specific-conditional-access.expect.md | 2 -- .../conditional-member-expr.expect.md | 4 ++-- .../memberexpr-join-optional-chain.expect.md | 4 ++-- .../memberexpr-join-optional-chain2.expect.md | 21 +++++++++++++------ ...epro-scope-missing-mutable-range.expect.md | 4 ++-- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index a13ebb6aac878..2b007f9659b99 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -230,7 +230,7 @@ const EnvironmentConfigSchema = z.object({ * just `props`. With this flag enabled, we'll infer that full path as * the dependency. */ - enableOptionalDependencies: z.boolean().default(false), + enableOptionalDependencies: z.boolean().default(true), /* * Enable validation of hooks to partially check that the component honors the rules of hooks. diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-with-optional-property-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-with-optional-property-load.expect.md index 795ab61cca997..d95461adf90e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-with-optional-property-load.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-call-with-optional-property-load.expect.md @@ -15,9 +15,9 @@ import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(2); let t0; - if ($[0] !== props) { + if ($[0] !== props?.items) { t0 = props?.items?.map?.(render)?.filter(Boolean) ?? []; - $[0] = props; + $[0] = props?.items; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md index 0cda150692955..25d7bf5f7ccce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-infer-less-specific-conditional-access.expect.md @@ -44,8 +44,6 @@ function Component({propA, propB}) { | ^^^^^^^^^^^^^^^^^ > 14 | }, [propA?.a, propB.x.y]); | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14) - -CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (6:14) 15 | } 16 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/conditional-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/conditional-member-expr.expect.md index 59b48898fda16..2dd61732f689f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/conditional-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/conditional-member-expr.expect.md @@ -29,10 +29,10 @@ import { c as _c } from "react/compiler-runtime"; // To preserve the nullthrows function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a) { + if ($[0] !== props.a?.b) { x = []; x.push(props.a?.b); - $[0] = props.a; + $[0] = props.a?.b; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md index 7362cd8317091..a66540655ab79 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md @@ -44,11 +44,11 @@ import { c as _c } from "react/compiler-runtime"; // To preserve the nullthrows function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a) { + if ($[0] !== props.a.b) { x = []; x.push(props.a?.b); x.push(props.a.b.c); - $[0] = props.a; + $[0] = props.a.b; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md index f25ea2a31e552..8d69c008c573b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md @@ -21,16 +21,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(5); let x; - if ($[0] !== props.items) { + if ($[0] !== props.items?.length || $[1] !== props.items?.edges) { x = []; x.push(props.items?.length); - x.push(props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []); - $[0] = props.items; - $[1] = x; + let t0; + if ($[3] !== props.items?.edges) { + t0 = props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []; + $[3] = props.items?.edges; + $[4] = t0; + } else { + t0 = $[4]; + } + x.push(t0); + $[0] = props.items?.length; + $[1] = props.items?.edges; + $[2] = x; } else { - x = $[1]; + x = $[2]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md index 2ce8ffbe4c92e..d8e59c486a55b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md @@ -23,14 +23,14 @@ function HomeDiscoStoreItemTileRating(props) { const $ = _c(4); const item = useFragment(); let count; - if ($[0] !== item) { + if ($[0] !== item?.aggregates) { count = 0; const aggregates = item?.aggregates || []; aggregates.forEach((aggregate) => { count = count + (aggregate.count || 0); count; }); - $[0] = item; + $[0] = item?.aggregates; $[1] = count; } else { count = $[1]; From fc0df475c4417670272b819bad92590b310bcdaa Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 28 Aug 2024 15:16:02 -0700 Subject: [PATCH 073/191] [compiler] Inferred deps must match exact optionality of manual deps To prevent any difference in behavior, we check that the optionality of the inferred deps exactly matches the optionality of the manual dependencies. This required a fix, I was incorrectly inferring optionality of manual deps (they're only optional if OptionalTerminal.optional is true) - for nested cases of mixed optional/non-optional. ghstack-source-id: afd49e89cc3194eb3c317ca7434d3fa948896bff Pull Request resolved: https://github.com/facebook/react/pull/30840 --- .../src/Inference/DropManualMemoization.ts | 2 +- .../ValidatePreservedManualMemoization.ts | 2 +- ...as-memo-dep-non-optional-in-body.expect.md | 38 ++++++++++++++ ...ssion-as-memo-dep-non-optional-in-body.js} | 0 ...as-memo-dep-non-optional-in-body.expect.md | 50 ------------------- ...ession-single-with-unconditional.expect.md | 33 ++++++------ ...er-expression-single-with-unconditional.js | 4 +- 7 files changed, 57 insertions(+), 72 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{optional-member-expression-as-memo-dep-non-optional-in-body.js => error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.js} (100%) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index fdd9bcc968aef..4dcdc21e15ac5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -488,7 +488,7 @@ export function dropManualMemoization(func: HIRFunction): void { function findOptionalPlaces(fn: HIRFunction): Set { const optionals = new Set(); for (const [, block] of fn.body.blocks) { - if (block.terminal.kind === 'optional') { + if (block.terminal.kind === 'optional' && block.terminal.optional) { const optionalTerminal = block.terminal; let testBlock = fn.body.blocks.get(block.terminal.test)!; loop: while (true) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 4f0c756585cf0..e7615320c7b95 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -170,7 +170,7 @@ function compareDeps( if (inferred.path[i].property !== source.path[i].property) { isSubpath = false; break; - } else if (inferred.path[i].optional && !source.path[i].optional) { + } else if (inferred.path[i].optional !== source.path[i].optional) { /** * The inferred path must be at least as precise as the manual path: * if the inferred path is optional, then the source path must have diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md new file mode 100644 index 0000000000000..ba5b30418069a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +function Component(props) { + const data = useMemo(() => { + // actual code is non-optional + return props.items.edges.nodes ?? []; + // deps are optional + }, [props.items?.edges?.nodes]); + return ; +} + +``` + + +## Error + +``` + 1 | // @validatePreserveExistingMemoizationGuarantees + 2 | function Component(props) { +> 3 | const data = useMemo(() => { + | ^^^^^^^ +> 4 | // actual code is non-optional + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 5 | return props.items.edges.nodes ?? []; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 6 | // deps are optional + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 7 | }, [props.items?.edges?.nodes]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:7) + 8 | return ; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-optional-member-expression-as-memo-dep-non-optional-in-body.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md deleted file mode 100644 index 7cf1bcd90b339..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep-non-optional-in-body.expect.md +++ /dev/null @@ -1,50 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees -function Component(props) { - const data = useMemo(() => { - // actual code is non-optional - return props.items.edges.nodes ?? []; - // deps are optional - }, [props.items?.edges?.nodes]); - return ; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees -function Component(props) { - const $ = _c(4); - - props.items?.edges?.nodes; - let t0; - let t1; - if ($[0] !== props.items.edges.nodes) { - t1 = props.items.edges.nodes ?? []; - $[0] = props.items.edges.nodes; - $[1] = t1; - } else { - t1 = $[1]; - } - t0 = t1; - const data = t0; - let t2; - if ($[2] !== data) { - t2 = ; - $[2] = data; - $[3] = t2; - } else { - t2 = $[3]; - } - return t2; -} - -``` - -### Eval output -(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md index d0c4afe459da2..46767056bdcdf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md @@ -10,8 +10,8 @@ function Component(props) { x.push(props?.items); x.push(props.items); return x; - }, [props?.items]); - return ; + }, [props.items]); + return ; } ``` @@ -23,8 +23,6 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); - - props?.items; let t0; let x; if ($[0] !== props.items) { @@ -38,25 +36,24 @@ function Component(props) { } t0 = x; const data = t0; - const t1 = props?.items; - let t2; - if ($[2] !== t1) { - t2 = [t1]; - $[2] = t1; - $[3] = t2; + let t1; + if ($[2] !== props.items) { + t1 = [props.items]; + $[2] = props.items; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== t2 || $[5] !== data) { - t3 = ; - $[4] = t2; + let t2; + if ($[4] !== t1 || $[5] !== data) { + t2 = ; + $[4] = t1; $[5] = data; - $[6] = t3; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js index c630dd6bb4b9d..8e6275bf921eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.js @@ -6,6 +6,6 @@ function Component(props) { x.push(props?.items); x.push(props.items); return x; - }, [props?.items]); - return ; + }, [props.items]); + return ; } From 537c74e16a394df16a4b368caa09ea5755f78dfb Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 29 Aug 2024 11:28:35 +0100 Subject: [PATCH 074/191] feat[react-devtools]: support Manifest v3 for Firefox extension (#30824) Firefox [finally supports `ExecutionWorld.MAIN`](https://bugzilla.mozilla.org/show_bug.cgi?id=1736575) in content scripts, which means we can migrate the browser extension to Manifest V3. This PR also removes a bunch of no longer required explicit branching for Firefox case, when we are using Manifest V3-only APIs. We are also removing XMLHttpRequest injection, which is no longer needed and restricted in Manifest V3. The new standardized approach (same as in Chromium) doesn't violate CSP rules, which means that extension can finally be used for apps running in production mode. --- .../firefox/manifest.json | 40 +++++---- .../dynamicallyInjectContentScripts.js | 90 +++++++------------ .../src/background/executeScript.js | 39 -------- .../background/setExtensionIconAndPopup.js | 6 +- .../src/background/tabsManager.js | 24 ++--- .../src/contentScripts/prepareInjection.js | 40 --------- .../react-devtools-shared/babel.config.js | 2 +- 7 files changed, 66 insertions(+), 175 deletions(-) diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index ffa48634e0e0d..3c2d417d585af 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -1,12 +1,12 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", "version": "5.3.1", - "applications": { + "browser_specific_settings": { "gecko": { "id": "@react-devtools", - "strict_min_version": "102.0" + "strict_min_version": "128.0" } }, "icons": { @@ -15,22 +15,32 @@ "48": "icons/48-production.png", "128": "icons/128-production.png" }, - "browser_action": { + "action": { "default_icon": { "16": "icons/16-disabled.png", "32": "icons/32-disabled.png", "48": "icons/48-disabled.png", "128": "icons/128-disabled.png" }, - "default_popup": "popups/disabled.html", - "browser_style": true + "default_popup": "popups/disabled.html" }, "devtools_page": "main.html", - "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'", + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'" + }, "web_accessible_resources": [ - "main.html", - "panel.html", - "build/*.js" + { + "resources": [ + "main.html", + "panel.html", + "build/*.js", + "build/*.js.map" + ], + "matches": [ + "" + ], + "extension_ids": [] + } ], "background": { "scripts": [ @@ -38,12 +48,10 @@ ] }, "permissions": [ - "file:///*", - "http://*/*", - "https://*/*", - "clipboardWrite", - "scripting", - "devtools" + "scripting" + ], + "host_permissions": [ + "" ], "content_scripts": [ { diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index e19030457a89c..b1888b4e7cc59 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -1,58 +1,39 @@ /* global chrome */ -// Firefox doesn't support ExecutionWorld.MAIN yet -// equivalent logic for Firefox is in prepareInjection.js -const contentScriptsToInject = __IS_FIREFOX__ - ? [ - { - id: '@react-devtools/proxy', - js: ['build/proxy.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - }, - { - id: '@react-devtools/file-fetcher', - js: ['build/fileFetcher.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - }, - ] - : [ - { - id: '@react-devtools/proxy', - js: ['build/proxy.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - world: chrome.scripting.ExecutionWorld.ISOLATED, - }, - { - id: '@react-devtools/file-fetcher', - js: ['build/fileFetcher.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - world: chrome.scripting.ExecutionWorld.ISOLATED, - }, - { - id: '@react-devtools/hook', - js: ['build/installHook.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_start', - world: chrome.scripting.ExecutionWorld.MAIN, - }, - { - id: '@react-devtools/renderer', - js: ['build/renderer.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_start', - world: chrome.scripting.ExecutionWorld.MAIN, - }, - ]; +const contentScriptsToInject = [ + { + id: '@react-devtools/proxy', + js: ['build/proxy.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_end', + world: chrome.scripting.ExecutionWorld.ISOLATED, + }, + { + id: '@react-devtools/file-fetcher', + js: ['build/fileFetcher.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_end', + world: chrome.scripting.ExecutionWorld.ISOLATED, + }, + { + id: '@react-devtools/hook', + js: ['build/installHook.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + world: chrome.scripting.ExecutionWorld.MAIN, + }, + { + id: '@react-devtools/renderer', + js: ['build/renderer.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + world: chrome.scripting.ExecutionWorld.MAIN, + }, +]; async function dynamicallyInjectContentScripts() { try { @@ -61,9 +42,6 @@ async function dynamicallyInjectContentScripts() { // This fixes registering proxy content script in incognito mode await chrome.scripting.unregisterContentScripts(); - // equivalent logic for Firefox is in prepareInjection.js - // Manifest V3 method of injecting content script - // TODO(hoxyq): migrate Firefox to V3 manifests // Note: the "world" option in registerContentScripts is only available in Chrome v102+ // It's critical since it allows us to directly run scripts on the "main" world on the page // "document_start" allows it to run before the page's scripts diff --git a/packages/react-devtools-extensions/src/background/executeScript.js b/packages/react-devtools-extensions/src/background/executeScript.js index efe73229ecff5..8b80095d33c2e 100644 --- a/packages/react-devtools-extensions/src/background/executeScript.js +++ b/packages/react-devtools-extensions/src/background/executeScript.js @@ -1,40 +1,5 @@ /* global chrome */ -// Firefox doesn't support ExecutionWorld.MAIN yet -// https://bugzilla.mozilla.org/show_bug.cgi?id=1736575 -function executeScriptForFirefoxInMainWorld({target, files}) { - return chrome.scripting.executeScript({ - target, - func: fileNames => { - function injectScriptSync(src) { - let code = ''; - const request = new XMLHttpRequest(); - request.addEventListener('load', function () { - code = this.responseText; - }); - request.open('GET', src, false); - request.send(); - - const script = document.createElement('script'); - script.textContent = code; - - // This script runs before the element is created, - // so we add the script to instead. - if (document.documentElement) { - document.documentElement.appendChild(script); - } - - if (script.parentNode) { - script.parentNode.removeChild(script); - } - } - - fileNames.forEach(file => injectScriptSync(chrome.runtime.getURL(file))); - }, - args: [files], - }); -} - export function executeScriptInIsolatedWorld({target, files}) { return chrome.scripting.executeScript({ target, @@ -44,10 +9,6 @@ export function executeScriptInIsolatedWorld({target, files}) { } export function executeScriptInMainWorld({target, files}) { - if (__IS_FIREFOX__) { - return executeScriptForFirefoxInMainWorld({target, files}); - } - return chrome.scripting.executeScript({ target, files, diff --git a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js index 5c6e011114014..51f233e284f0e 100644 --- a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js +++ b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js @@ -3,9 +3,7 @@ 'use strict'; function setExtensionIconAndPopup(reactBuildType, tabId) { - const action = __IS_FIREFOX__ ? chrome.browserAction : chrome.action; - - action.setIcon({ + chrome.action.setIcon({ tabId, path: { '16': chrome.runtime.getURL(`icons/16-${reactBuildType}.png`), @@ -15,7 +13,7 @@ function setExtensionIconAndPopup(reactBuildType, tabId) { }, }); - action.setPopup({ + chrome.action.setPopup({ tabId, popup: chrome.runtime.getURL(`popups/${reactBuildType}.html`), }); diff --git a/packages/react-devtools-extensions/src/background/tabsManager.js b/packages/react-devtools-extensions/src/background/tabsManager.js index 23b566502a269..192a6ce42ce28 100644 --- a/packages/react-devtools-extensions/src/background/tabsManager.js +++ b/packages/react-devtools-extensions/src/background/tabsManager.js @@ -18,26 +18,12 @@ function checkAndHandleRestrictedPageIfSo(tab) { // we can't update for any other types (prod,dev,outdated etc) // as the content script needs to be injected at document_start itself for those kinds of detection // TODO: Show a different popup page(to reload current page probably) for old tabs, opened before the extension is installed -if (__IS_CHROME__ || __IS_EDGE__) { - chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); - chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => - checkAndHandleRestrictedPageIfSo(tab), - ); -} +chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); +chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => + checkAndHandleRestrictedPageIfSo(tab), +); // Listen to URL changes on the active tab and update the DevTools icon. chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (__IS_FIREFOX__) { - // We don't properly detect protected URLs in Firefox at the moment. - // However, we can reset the DevTools icon to its loading state when the URL changes. - // It will be updated to the correct icon by the onMessage callback below. - if (tab.active && changeInfo.status === 'loading') { - setExtensionIconAndPopup('disabled', tabId); - } - } else { - // Don't reset the icon to the loading state for Chrome or Edge. - // The onUpdated callback fires more frequently for these browsers, - // often after onMessage has been called. - checkAndHandleRestrictedPageIfSo(tab); - } + checkAndHandleRestrictedPageIfSo(tab); }); diff --git a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js index d67ea7c405a1e..1b9962a9a826f 100644 --- a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js +++ b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js @@ -1,31 +1,5 @@ /* global chrome */ -import nullthrows from 'nullthrows'; - -// We run scripts on the page via the service worker (background/index.js) for -// Manifest V3 extensions (Chrome & Edge). -// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld -// In this content script we have access to DOM, but don't have access to the webpage's window, -// so we inject this inline script tag into the webpage (allowed in Manifest V2). -function injectScriptSync(src) { - let code = ''; - const request = new XMLHttpRequest(); - request.addEventListener('load', function () { - code = this.responseText; - }); - request.open('GET', src, false); - request.send(); - - const script = document.createElement('script'); - script.textContent = code; - - // This script runs before the element is created, - // so we add the script to instead. - nullthrows(document.documentElement).appendChild(script); - nullthrows(script.parentNode).removeChild(script); -} - let lastSentDevToolsHookMessage; // We want to detect when a renderer attaches, and notify the "background page" @@ -60,17 +34,3 @@ window.addEventListener('pageshow', function ({target}) { chrome.runtime.sendMessage(lastSentDevToolsHookMessage); }); - -if (__IS_FIREFOX__) { - injectScriptSync(chrome.runtime.getURL('build/renderer.js')); - - // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with. - // Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs. - switch (document.contentType) { - case 'text/html': - case 'application/xhtml+xml': { - injectScriptSync(chrome.runtime.getURL('build/installHook.js')); - break; - } - } -} diff --git a/packages/react-devtools-shared/babel.config.js b/packages/react-devtools-shared/babel.config.js index ca877aa683afd..78af34817e0a9 100644 --- a/packages/react-devtools-shared/babel.config.js +++ b/packages/react-devtools-shared/babel.config.js @@ -3,7 +3,7 @@ const firefoxManifest = require('../react-devtools-extensions/firefox/manifest.j const minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10); const minFirefoxVersion = parseInt( - firefoxManifest.applications.gecko.strict_min_version, + firefoxManifest.browser_specific_settings.gecko.strict_min_version, 10, ); validateVersion(minChromeVersion); From 795b3207ce5ea25c80749e367c61e5f56ac09856 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 29 Aug 2024 11:31:43 +0100 Subject: [PATCH 075/191] fix[react-devtools/extensions]: fixed tabs API calls and displaying restricted access popup (#30825) Stacked on https://github.com/facebook/react/pull/30824. See [this commit](https://github.com/facebook/react/pull/30825/commits/c9830d64749cf8fd592ea30a1cd65842cf83f6df). Turns out we should be listing `tabs` in our permissions, if we want to be able to receive tab url, once its updated. This also fixes `chrome.tabs.onCreated` event subscription, because [it should receive only tab object](https://developer.chrome.com/docs/extensions/reference/api/tabs#event-onCreated), and not 3 arguments, as expected in the previous implementation. --- .../chrome/manifest.json | 3 ++- .../edge/manifest.json | 3 ++- .../firefox/manifest.json | 3 ++- .../src/background/tabsManager.js | 20 ++++++++++--------- .../src/main/registerEventsLogger.js | 10 ++-------- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 4dcd951a480ac..e7b9b19b6a9df 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -42,7 +42,8 @@ }, "permissions": [ "storage", - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index fd19f1c5df532..496d4e92c3b63 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -42,7 +42,8 @@ }, "permissions": [ "storage", - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 3c2d417d585af..930c1ab11083e 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -48,7 +48,8 @@ ] }, "permissions": [ - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/src/background/tabsManager.js b/packages/react-devtools-extensions/src/background/tabsManager.js index 192a6ce42ce28..d46c14c6ea7c3 100644 --- a/packages/react-devtools-extensions/src/background/tabsManager.js +++ b/packages/react-devtools-extensions/src/background/tabsManager.js @@ -5,7 +5,12 @@ import setExtensionIconAndPopup from './setExtensionIconAndPopup'; function isRestrictedBrowserPage(url) { - return !url || new URL(url).protocol === 'chrome:'; + if (!url) { + return true; + } + + const urlProtocol = new URL(url).protocol; + return urlProtocol === 'chrome:' || urlProtocol === 'about:'; } function checkAndHandleRestrictedPageIfSo(tab) { @@ -14,16 +19,13 @@ function checkAndHandleRestrictedPageIfSo(tab) { } } -// update popup page of any existing open tabs, if they are restricted browser pages. -// we can't update for any other types (prod,dev,outdated etc) -// as the content script needs to be injected at document_start itself for those kinds of detection -// TODO: Show a different popup page(to reload current page probably) for old tabs, opened before the extension is installed +// Update popup page of any existing open tabs, if they are restricted browser pages chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); -chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => - checkAndHandleRestrictedPageIfSo(tab), -); +chrome.tabs.onCreated.addListener(tab => checkAndHandleRestrictedPageIfSo(tab)); // Listen to URL changes on the active tab and update the DevTools icon. chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - checkAndHandleRestrictedPageIfSo(tab); + if (changeInfo.url && isRestrictedBrowserPage(changeInfo.url)) { + setExtensionIconAndPopup('restricted', tabId); + } }); diff --git a/packages/react-devtools-extensions/src/main/registerEventsLogger.js b/packages/react-devtools-extensions/src/main/registerEventsLogger.js index 5234866fd546c..ec57d173e42ca 100644 --- a/packages/react-devtools-extensions/src/main/registerEventsLogger.js +++ b/packages/react-devtools-extensions/src/main/registerEventsLogger.js @@ -4,14 +4,8 @@ import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDev function registerEventsLogger() { registerDevToolsEventLogger('extension', async () => { - // TODO: after we upgrade to Firefox Manifest V3, chrome.tabs.query returns a Promise without the callback. - return new Promise(resolve => { - chrome.tabs.query({active: true}, tabs => { - resolve({ - page_url: tabs[0]?.url, - }); - }); - }); + const tabs = await chrome.tabs.query({active: true}); + return {page_url: tabs[0]?.url}; }); } From 233d63c497d3a5f669a1bae1ee1d3f389e12a42a Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 29 Aug 2024 11:32:18 +0100 Subject: [PATCH 076/191] chore[react-devtools/extensions]: remove unused storage permission (#30826) Stacked on https://github.com/facebook/react/pull/30825. See [this commit](https://github.com/facebook/react/pull/30826/commits/b2130701cf6b25d7a96c1e92b44f41affa56bb35). We are not using `storage` anywhere yet, but will be soon. This permission is not needed. --- packages/react-devtools-extensions/chrome/manifest.json | 1 - packages/react-devtools-extensions/edge/manifest.json | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index e7b9b19b6a9df..e61ebd1e57ed0 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -41,7 +41,6 @@ "service_worker": "build/background.js" }, "permissions": [ - "storage", "scripting", "tabs" ], diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 496d4e92c3b63..48a56c7400ce4 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -41,7 +41,6 @@ "service_worker": "build/background.js" }, "permissions": [ - "storage", "scripting", "tabs" ], From a19a8ab44f53f189745015a6d2e6bf8955f98170 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 29 Aug 2024 11:34:31 +0100 Subject: [PATCH 077/191] chore[react-devtools/hook]: remove unused native values (#30827) Stacked on https://github.com/facebook/react/pull/30826. See [this commit](https://github.com/facebook/react/pull/30827/commits/ec0e48ed7a47dbbdafb5e2530ccba1f2e5b17bad). This is unused. --- .../src/contentScripts/installHook.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index 2d33cdd89036d..ff7e041627f0e 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -20,10 +20,4 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { ); }, ); - - // save native values - window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create; - window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map; - window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap; - window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set; } From 18bf7bf5002450ce7daa281e8be1c3216bd871ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 29 Aug 2024 12:44:48 -0400 Subject: [PATCH 078/191] [DevTools] Remove displayName from inspected data (#30841) This just clarifies that this is actually unused in the front end. We use the name from the original instance as the canonical name. --- .../src/backend/fiber/renderer.js | 13 +++---------- .../src/backend/legacy/renderer.js | 8 ++++---- packages/react-devtools-shared/src/backend/types.js | 2 -- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 729c8fce5a7a5..09b599a4d09c4 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -4212,7 +4212,6 @@ export function attach( key: key != null ? key : null, - displayName: getDisplayNameForFiber(fiber), type: elementType, // Inspectable properties. @@ -4252,13 +4251,6 @@ export function attach( typeof componentInfo.key === 'string' ? componentInfo.key : null; const props = null; // TODO: Track props on ReactComponentInfo; - const env = componentInfo.env; - let displayName = componentInfo.name || ''; - if (typeof env === 'string') { - // We model environment as an HoC name for now. - displayName = env + '(' + displayName + ')'; - } - const owners: null | Array = getOwnersListFromInstance(virtualInstance); @@ -4311,7 +4303,6 @@ export function attach( key: key, - displayName: displayName, type: ElementTypeVirtual, // Inspectable properties. @@ -4675,10 +4666,12 @@ export function attach( return; } + const displayName = getDisplayNameForElementID(id); + const supportsGroup = typeof console.groupCollapsed === 'function'; if (supportsGroup) { console.groupCollapsed( - `[Click to expand] %c<${result.displayName || 'Component'} />`, + `[Click to expand] %c<${displayName || 'Component'} />`, // --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console. 'color: var(--dom-tag-name-color); font-weight: normal;', ); diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index f8aa548a0573a..523fbbeba3d8e 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -775,7 +775,7 @@ export function attach( return null; } - const {displayName, key} = getData(internalInstance); + const {key} = getData(internalInstance); const type = getElementType(internalInstance); let context = null; @@ -842,8 +842,6 @@ export function attach( // Only legacy context exists in legacy versions. hasLegacyContext: true, - displayName: displayName, - type: type, key: key != null ? key : null, @@ -876,10 +874,12 @@ export function attach( return; } + const displayName = getDisplayNameForElementID(id); + const supportsGroup = typeof console.groupCollapsed === 'function'; if (supportsGroup) { console.groupCollapsed( - `[Click to expand] %c<${result.displayName || 'Component'} />`, + `[Click to expand] %c<${displayName || 'Component'} />`, // --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console. 'color: var(--dom-tag-name-color); font-weight: normal;', ); diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 87b0f2048b9db..41c278d02ebdf 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -239,8 +239,6 @@ export type OwnersList = { export type InspectedElement = { id: number, - displayName: string | null, - // Does the current renderer support editable hooks and function props? canEditHooks: boolean, canEditFunctionProps: boolean, From e33a7233a76e1164bd1a9c4b8115abb575b48c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 29 Aug 2024 12:45:03 -0400 Subject: [PATCH 079/191] [DevTools] Track virtual instances on the tracked path for selections (#30802) This appends a (filtered) virtual instance path at the end of the fiber path. If a virtual instance is selected inside the fiber. The main part of the path is still just the fiber path since that's the semantically stateful part. Then we just tack on a few virtual path frames at the end if we're currently selecting a specific Server Component within the nearest Fiber. I also took the opportunity to fix a bug which caused selections inside Suspense boundaries to not be tracked. --- .../src/backend/fiber/renderer.js | 160 +++++++++++++----- 1 file changed, 121 insertions(+), 39 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 09b599a4d09c4..0a19c79718c74 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2266,16 +2266,11 @@ export function attach( debug('recordUnmount()', fiber, null); } - if (trackedPathMatchFiber !== null) { + if (trackedPathMatchInstance === fiberInstance) { // We're in the process of trying to restore previous selection. // If this fiber matched but is being unmounted, there's no use trying. // Reset the state so we don't keep holding onto it. - if ( - fiber === trackedPathMatchFiber || - fiber === trackedPathMatchFiber.alternate - ) { - setTrackedPath(null); - } + setTrackedPath(null); } const id = fiberInstance.id; @@ -2386,6 +2381,14 @@ export function attach( traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): void { + // If we have the tree selection from previous reload, try to match this Instance. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = + updateVirtualTrackedPathStateBeforeMount( + virtualInstance, + reconcilingParent, + ); + const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; @@ -2406,13 +2409,16 @@ export function attach( reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); } } function recordVirtualUnmount(instance: VirtualInstance) { - if (trackedPathMatchFiber !== null) { + if (trackedPathMatchInstance === instance) { // We're in the process of trying to restore previous selection. - // TODO: Handle virtual instances on the tracked path. + // If this fiber matched but is being unmounted, there's no use trying. + // Reset the state so we don't keep holding onto it. + setTrackedPath(null); } const id = instance.id; @@ -2521,17 +2527,20 @@ export function attach( debug('mountFiberRecursively()', fiber, reconcilingParent); } - // If we have the tree selection from previous reload, try to match this Fiber. - // Also remember whether to do the same for siblings. - const mightSiblingsBeOnTrackedPath = - updateTrackedPathStateBeforeMount(fiber); - const shouldIncludeInTree = !shouldFilterFiber(fiber); let newInstance = null; if (shouldIncludeInTree) { newInstance = recordMount(fiber, reconcilingParent); insertChild(newInstance); } + + // If we have the tree selection from previous reload, try to match this Fiber. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount( + fiber, + newInstance, + ); + const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; @@ -2570,14 +2579,15 @@ export function attach( const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null; - const fallbackChild = fallbackChildFragment - ? fallbackChildFragment.child - : null; - if (fallbackChild !== null) { - mountChildrenRecursively( - fallbackChild, - traceNearestHostComponentUpdate, - ); + if (fallbackChildFragment) { + const fallbackChild = fallbackChildFragment.child; + if (fallbackChild !== null) { + updateTrackedPathStateBeforeMount(fallbackChildFragment, null); + mountChildrenRecursively( + fallbackChild, + traceNearestHostComponentUpdate, + ); + } } } else { let primaryChild: Fiber | null = null; @@ -2587,6 +2597,7 @@ export function attach( primaryChild = fiber.child; } else if (fiber.child !== null) { primaryChild = fiber.child.child; + updateTrackedPathStateBeforeMount(fiber.child, null); } if (primaryChild !== null) { mountChildrenRecursively( @@ -5262,13 +5273,15 @@ export function attach( // Remember if we're trying to restore the selection after reload. // In that case, we'll do some extra checks for matching mounts. let trackedPath: Array | null = null; - let trackedPathMatchFiber: Fiber | null = null; + let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber. + let trackedPathMatchInstance: DevToolsInstance | null = null; // This is the deepest matched filtered Instance. let trackedPathMatchDepth = -1; let mightBeOnTrackedPath = false; function setTrackedPath(path: Array | null) { if (path === null) { trackedPathMatchFiber = null; + trackedPathMatchInstance = null; trackedPathMatchDepth = -1; mightBeOnTrackedPath = false; } @@ -5278,7 +5291,10 @@ export function attach( // We call this before traversing a new mount. // It remembers whether this Fiber is the next best match for tracked path. // The return value signals whether we should keep matching siblings or not. - function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean { + function updateTrackedPathStateBeforeMount( + fiber: Fiber, + fiberInstance: null | FiberInstance, + ): boolean { if (trackedPath === null || !mightBeOnTrackedPath) { // Fast path: there's nothing to track so do nothing and ignore siblings. return false; @@ -5306,6 +5322,9 @@ export function attach( ) { // We have our next match. trackedPathMatchFiber = fiber; + if (fiberInstance !== null) { + trackedPathMatchInstance = fiberInstance; + } trackedPathMatchDepth++; // Are we out of frames to match? // $FlowFixMe[incompatible-use] found when upgrading Flow @@ -5322,6 +5341,11 @@ export function attach( return false; } } + if (trackedPathMatchFiber === null && fiberInstance === null) { + // We're now looking for a Virtual Instance. It might be inside filtered Fibers + // so we keep looking below. + return true; + } // This Fiber's parent is on the path, but this Fiber itself isn't. // There's no need to check its children--they won't be on the path either. mightBeOnTrackedPath = false; @@ -5329,6 +5353,57 @@ export function attach( return true; } + function updateVirtualTrackedPathStateBeforeMount( + virtualInstance: VirtualInstance, + parentInstance: null | DevToolsInstance, + ): boolean { + if (trackedPath === null || !mightBeOnTrackedPath) { + // Fast path: there's nothing to track so do nothing and ignore siblings. + return false; + } + // Check if we've matched our nearest unfiltered parent so far. + if (trackedPathMatchInstance === parentInstance) { + const actualFrame = getVirtualPathFrame(virtualInstance); + // $FlowFixMe[incompatible-use] found when upgrading Flow + const expectedFrame = trackedPath[trackedPathMatchDepth + 1]; + if (expectedFrame === undefined) { + throw new Error('Expected to see a frame at the next depth.'); + } + if ( + actualFrame.index === expectedFrame.index && + actualFrame.key === expectedFrame.key && + actualFrame.displayName === expectedFrame.displayName + ) { + // We have our next match. + trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now. + trackedPathMatchInstance = virtualInstance; + trackedPathMatchDepth++; + // Are we out of frames to match? + // $FlowFixMe[incompatible-use] found when upgrading Flow + if (trackedPathMatchDepth === trackedPath.length - 1) { + // There's nothing that can possibly match afterwards. + // Don't check the children. + mightBeOnTrackedPath = false; + } else { + // Check the children, as they might reveal the next match. + mightBeOnTrackedPath = true; + } + // In either case, since we have a match, we don't need + // to check the siblings. They'll never match. + return false; + } + } + if (trackedPathMatchFiber !== null) { + // We're still looking for a Fiber which might be underneath this instance. + return true; + } + // This Instance's parent is on the path, but this Instance itself isn't. + // There's no need to check its children--they won't be on the path either. + mightBeOnTrackedPath = false; + // However, one of its siblings may be on the path so keep searching. + return true; + } + function updateTrackedPathStateAfterMount( mightSiblingsBeOnTrackedPath: boolean, ) { @@ -5428,6 +5503,14 @@ export function attach( }; } + function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame { + return { + displayName: virtualInstance.data.name || '', + key: virtualInstance.data.key == null ? null : virtualInstance.data.key, + index: -1, // We use -1 to indicate that this is a virtual path frame. + }; + } + // Produces a serializable representation that does a best effort // of identifying a particular Fiber between page reloads. // The return path will contain Fibers that are "invisible" to the store @@ -5437,13 +5520,20 @@ export function attach( if (devtoolsInstance === undefined) { return null; } - if (devtoolsInstance.kind !== FIBER_INSTANCE) { - // TODO: Handle VirtualInstance. - return null; - } - let fiber: null | Fiber = devtoolsInstance.data; const keyPath = []; + + let inst: DevToolsInstance = devtoolsInstance; + while (inst.kind === VIRTUAL_INSTANCE) { + keyPath.push(getVirtualPathFrame(inst)); + if (inst.parent === null) { + // This is a bug but non-essential. We should've found a root instance. + return null; + } + inst = inst.parent; + } + + let fiber: null | Fiber = inst.data; while (fiber !== null) { // $FlowFixMe[incompatible-call] found when upgrading Flow keyPath.push(getPathFrame(fiber)); @@ -5459,20 +5549,12 @@ export function attach( // Nothing to match. return null; } - if (trackedPathMatchFiber === null) { + if (trackedPathMatchInstance === null) { // We didn't find anything. return null; } - // Find the closest Fiber store is aware of. - let fiber: null | Fiber = trackedPathMatchFiber; - while (fiber !== null && shouldFilterFiber(fiber)) { - fiber = fiber.return; - } - if (fiber === null) { - return null; - } return { - id: getFiberIDThrows(fiber), + id: trackedPathMatchInstance.id, // $FlowFixMe[incompatible-use] found when upgrading Flow isFullMatch: trackedPathMatchDepth === trackedPath.length - 1, }; From 61739a8a0fd23adf18336d96f9c307a1cd897354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 29 Aug 2024 12:49:30 -0400 Subject: [PATCH 080/191] [DevTools] Filter Server Components (#30839) Support filtering Virtual Instances with existing filters. Server Components are considered "Functions". In a follow up I'll a new filter for "Environment" which will let you filter by Client vs Server (and more). --- .../src/backend/fiber/renderer.js | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 0a19c79718c74..08158086def33 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1216,7 +1216,26 @@ export function attach( } function shouldFilterVirtual(data: ReactComponentInfo): boolean { - // TODO: Apply filters to VirtualInstances. + // For purposes of filtering Server Components are always Function Components. + // Environment will be used to filter Server vs Client. + // Technically they can be forwardRef and memo too but those filters will go away + // as those become just plain user space function components like any HoC. + if (hideElementsWithTypes.has(ElementTypeFunction)) { + return true; + } + + if (hideElementsWithDisplayNames.size > 0) { + const displayName = data.name; + if (displayName != null) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const displayNameRegExp of hideElementsWithDisplayNames) { + if (displayNameRegExp.test(displayName)) { + return true; + } + } + } + } + return false; } @@ -2446,6 +2465,10 @@ export function attach( continue; } const componentInfo: ReactComponentInfo = (debugEntry: any); + if (shouldFilterVirtual(componentInfo)) { + // Skip. + continue; + } if (level === virtualLevel) { if ( previousVirtualInstance === null || @@ -2864,6 +2887,9 @@ export function attach( continue; } const componentInfo: ReactComponentInfo = (debugEntry: any); + if (shouldFilterVirtual(componentInfo)) { + continue; + } if (level === virtualLevel) { if ( previousVirtualInstance === null || @@ -3686,19 +3712,16 @@ export function attach( } // We couldn't use this Fiber but we might have a VirtualInstance // that is the nearest unfiltered instance. - let parentInstance = fiberInstance.parent; - while (parentInstance !== null) { - if (parentInstance.kind === FIBER_INSTANCE) { - // If we find a parent Fiber, it might not be the nearest parent - // so we break out and continue walking the Fiber tree instead. - break; - } else { - if (!shouldFilterVirtual(parentInstance.data)) { - return parentInstance.id; - } - } - parentInstance = parentInstance.parent; + const parentInstance = fiberInstance.parent; + if ( + parentInstance !== null && + parentInstance.kind === VIRTUAL_INSTANCE + ) { + // Virtual Instances only exist if they're unfiltered. + return parentInstance.id; } + // If we find a parent Fiber, it might not be the nearest parent + // so we break out and continue walking the Fiber tree instead. } fiber = fiber.return; } From 071dd00366b3accb649e3f5978454e993e0b11aa Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 29 Aug 2024 22:40:41 -0700 Subject: [PATCH 081/191] [compiler] Errors in earlier functions dont stop subsequent compilation Errors in an earlier component/hook shouldn't stop later components from compiling. ghstack-source-id: 6e04a5bb2e2045303cbddad6d6d4bd38d5f7990b Pull Request resolved: https://github.com/facebook/react/pull/30844 --- .../src/Entrypoint/Program.ts | 13 ++--- .../src/Entrypoint/Suppression.ts | 10 ++-- ...alid-unclosed-eslint-suppression.expect.md | 2 - ...iple-components-first-is-invalid.expect.md | 50 +++++++++++++++++++ .../multiple-components-first-is-invalid.js | 13 +++++ ...suppression-skips-all-components.expect.md | 39 +++++++++++++++ ...eslint-suppression-skips-all-components.js | 12 +++++ 7 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 979e9f88d1b57..c2c7d8d640846 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -310,8 +310,6 @@ export function compileProgram( pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS, pass.opts.flowSuppressions, ); - const lintError = suppressionsToCompilerError(suppressions); - let hasCriticalError = lintError != null; const queue: Array<{ kind: 'original' | 'outlined'; fn: BabelFn; @@ -385,7 +383,8 @@ export function compileProgram( ); } - if (lintError != null) { + let compiledFn: CodegenFunction; + try { /** * Note that Babel does not attach comment nodes to nodes; they are dangling off of the * Program node itself. We need to figure out whether an eslint suppression range @@ -396,16 +395,15 @@ export function compileProgram( fn, ); if (suppressionsInFunction.length > 0) { + const lintError = suppressionsToCompilerError(suppressionsInFunction); if (optOutDirectives.length > 0) { logError(lintError, pass, fn.node.loc ?? null); } else { handleError(lintError, pass, fn.node.loc ?? null); } + return null; } - } - let compiledFn: CodegenFunction; - try { compiledFn = compileFn( fn, environment, @@ -436,7 +434,6 @@ export function compileProgram( return null; } } - hasCriticalError ||= isCriticalError(err); handleError(err, pass, fn.node.loc ?? null); return null; } @@ -470,7 +467,7 @@ export function compileProgram( return null; } - if (!pass.opts.noEmit && !hasCriticalError) { + if (!pass.opts.noEmit) { return compiledFn; } return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts index 71341989a7592..4d0369f5210ca 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts @@ -14,6 +14,7 @@ import { ErrorSeverity, } from '../CompilerError'; import {assertExhaustive} from '../Utils/utils'; +import {GeneratedSource} from '../HIR'; /** * Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the @@ -148,10 +149,11 @@ export function findProgramSuppressions( export function suppressionsToCompilerError( suppressionRanges: Array, -): CompilerError | null { - if (suppressionRanges.length === 0) { - return null; - } +): CompilerError { + CompilerError.invariant(suppressionRanges.length !== 0, { + reason: `Expected at least suppression comment source range`, + loc: GeneratedSource, + }); const error = new CompilerError(); for (const suppressionRange of suppressionRanges) { if ( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unclosed-eslint-suppression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unclosed-eslint-suppression.expect.md index b81dadf409301..9f8e15592df6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unclosed-eslint-suppression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unclosed-eslint-suppression.expect.md @@ -39,8 +39,6 @@ function CrimesAgainstReact() { 1 | // Note: Everything below this is sketchy > 2 | /* eslint-disable react-hooks/rules-of-hooks */ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable react-hooks/rules-of-hooks (2:2) - -InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable-next-line react-hooks/rules-of-hooks (25:25) 3 | function lowercasecomponent() { 4 | 'use forget'; 5 | const x = []; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.expect.md new file mode 100644 index 0000000000000..3224997b40343 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @panicThreshold(none) +import {useHook} from 'shared-runtime'; + +function InvalidComponent(props) { + if (props.cond) { + useHook(); + } + return
Hello World!
; +} + +function ValidComponent(props) { + return
{props.greeting}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @panicThreshold(none) +import { useHook } from "shared-runtime"; + +function InvalidComponent(props) { + if (props.cond) { + useHook(); + } + return
Hello World!
; +} + +function ValidComponent(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.greeting) { + t0 =
{props.greeting}
; + $[0] = props.greeting; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.js new file mode 100644 index 0000000000000..6a3d52c86406a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/multiple-components-first-is-invalid.js @@ -0,0 +1,13 @@ +// @panicThreshold(none) +import {useHook} from 'shared-runtime'; + +function InvalidComponent(props) { + if (props.cond) { + useHook(); + } + return
Hello World!
; +} + +function ValidComponent(props) { + return
{props.greeting}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.expect.md new file mode 100644 index 0000000000000..f0c6bce34222e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @panicThreshold(none) + +// unclosed disable rule should affect all components +/* eslint-disable react-hooks/rules-of-hooks */ + +function ValidComponent1(props) { + return
Hello World!
; +} + +function ValidComponent2(props) { + return
{props.greeting}
; +} + +``` + +## Code + +```javascript +// @panicThreshold(none) + +// unclosed disable rule should affect all components +/* eslint-disable react-hooks/rules-of-hooks */ + +function ValidComponent1(props) { + return
Hello World!
; +} + +function ValidComponent2(props) { + return
{props.greeting}
; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.js new file mode 100644 index 0000000000000..121f10041821f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unclosed-eslint-suppression-skips-all-components.js @@ -0,0 +1,12 @@ +// @panicThreshold(none) + +// unclosed disable rule should affect all components +/* eslint-disable react-hooks/rules-of-hooks */ + +function ValidComponent1(props) { + return
Hello World!
; +} + +function ValidComponent2(props) { + return
{props.greeting}
; +} From 394e75d9a9af26dc00074f2b8c2978d8c2dfbbb9 Mon Sep 17 00:00:00 2001 From: Rune Botten Date: Fri, 30 Aug 2024 02:34:27 -0700 Subject: [PATCH 082/191] [DevTools] Increase max payload for websocket in standalone app (#30848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When debugging applications that are experiencing runaway re-rendering, it is helpful to profile them in the React Developer Tools. Unfortunately there is a size limit on the captured profile which can make them impossible to inspect or save. The limitations I have found are in `postMessage` for the Chrome extension and in the `ws` websocket server for the standalone app. Profiling an app that produces a large profile artifact will simply show that no profiling data was captured and output an error in the console, here shown for the standalone app: ```text standalone.js:92 [React DevTools] Error with websocket connection i {target: H, type: 'error', message: 'Max payload size exceeded', error: RangeError: Max payload size exceeded at e.exports.haveLength (/Users/rune/.npm/_npx/8ea6ac5c50…}error: RangeError: Max payload size exceeded ``` This change simply increases the max payload of the websocket server in the standalone app so that larger profiles may be captured and inspected. ## How did you test this change? I verified that I could capture and inspect profiling data that previously exceeded the default limitation for a particular app --- packages/react-devtools-core/src/standalone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 541efa0d37e96..22aea529dd8e3 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -329,7 +329,7 @@ function startServer( const httpServer = useHttps ? require('https').createServer(httpsOptions) : require('http').createServer(); - const server = new Server({server: httpServer}); + const server = new Server({server: httpServer, maxPayload: 1e9}); let connected: WebSocket | null = null; server.on('connection', (socket: WebSocket) => { if (connected !== null) { From 8308d2f1fe90ec0b5a5cde147b97c6e78581710a Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Fri, 30 Aug 2024 10:34:52 +0100 Subject: [PATCH 083/191] fix[react-devtools/ReactDebugHooks]: support unstable prefixes in hooks and useContextWithBailout (#30837) Related - https://github.com/facebook/react/pull/30407. This is experimental-only and FB-only hook. Without these changes, inspecting an element that is using this hook will throw an error, because this hook is missing in Dispatcher implementation from React DevTools, which overrides the original one to build the hook tree. ![Screenshot 2024-08-28 at 18 42 55](https://github.com/user-attachments/assets/e3bccb92-74fb-4e4a-8181-03d13f8512c0) One nice thing from it is that in case of any potential regressions related to this experiment, we can quickly triage which implementation of `useContext` is used by inspecting an element in React DevTools. Ideally, I should've added some component that is using this hook to `react-devtools-shell`, so it can be manually tested, but I can't do it without rewriting the infra for it. This is because this hook is only available from fb-www builds, and not experimental. --- .../react-debug-tools/src/ReactDebugHooks.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index edbef05e259d1..c54e591321dee 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -20,6 +20,7 @@ import type { Dependencies, Fiber, Dispatcher as DispatcherType, + ContextDependencyWithSelect, } from 'react-reconciler/src/ReactInternalTypes'; import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; @@ -37,7 +38,6 @@ import { REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import hasOwnProperty from 'shared/hasOwnProperty'; -import type {ContextDependencyWithSelect} from '../../react-reconciler/src/ReactInternalTypes'; type CurrentDispatcherRef = typeof ReactSharedInternals; @@ -76,6 +76,13 @@ function getPrimitiveStackCache(): Map> { try { // Use all hooks here to add them to the hook log. Dispatcher.useContext(({_currentValue: null}: any)); + if (typeof Dispatcher.unstable_useContextWithBailout === 'function') { + // This type check is for Flow only. + Dispatcher.unstable_useContextWithBailout( + ({_currentValue: null}: any), + null, + ); + } Dispatcher.useState(null); Dispatcher.useReducer((s: mixed, a: mixed) => s, null); Dispatcher.useRef(null); @@ -280,6 +287,22 @@ function useContext(context: ReactContext): T { return value; } +function unstable_useContextWithBailout( + context: ReactContext, + select: (T => Array) | null, +): T { + const value = readContext(context); + hookLog.push({ + displayName: context.displayName || null, + primitive: 'ContextWithBailout', + stackError: new Error(), + value: value, + debugInfo: null, + dispatcherHookName: 'ContextWithBailout', + }); + return value; +} + function useState( initialState: (() => S) | S, ): [S, Dispatch>] { @@ -753,6 +776,7 @@ const Dispatcher: DispatcherType = { useCacheRefresh, useCallback, useContext, + unstable_useContextWithBailout, useEffect, useImperativeHandle, useDebugValue, @@ -954,6 +978,11 @@ function parseHookName(functionName: void | string): string { } else { startIndex += 1; } + + if (functionName.slice(startIndex).startsWith('unstable_')) { + startIndex += 'unstable_'.length; + } + if (functionName.slice(startIndex, startIndex + 3) === 'use') { if (functionName.length - startIndex === 3) { return 'Use'; From e56f4ae38d118168e0561f1b86ecbdef592138e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 30 Aug 2024 10:05:19 -0400 Subject: [PATCH 084/191] [DevTools] Support secondary environment name when it changes (#30842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We currently support the Environment Name change within a Component. #29867 If this happens, we give it two HoCs. The problem with this is that we only show one followed by `+1` in the list. Before: Screenshot 2024-08-28 at 6 50 31 PM After: Screenshot 2024-08-28 at 7 16 21 PM I could potentially instead badge this case as `A/B` in a single badge. --- .../src/backend/fiber/renderer.js | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 08158086def33..47cb12bf17ae0 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes'; import { ComponentFilterDisplayName, @@ -2226,6 +2226,7 @@ export function attach( function recordVirtualMount( instance: VirtualInstance, parentInstance: DevToolsInstance | null, + secondaryEnv: null | string, ): void { const id = instance.id; @@ -2239,6 +2240,9 @@ export function attach( let displayName = componentInfo.name || ''; if (typeof env === 'string') { // We model environment as an HoC name for now. + if (secondaryEnv !== null) { + displayName = secondaryEnv + '(' + displayName + ')'; + } displayName = env + '(' + displayName + ')'; } const elementType = ElementTypeVirtual; @@ -2444,6 +2448,25 @@ export function attach( pendingRealUnmountedIDs.push(id); } + function getSecondaryEnvironmentName( + debugInfo: ?ReactDebugInfo, + index: number, + ): null | string { + if (debugInfo != null) { + const componentInfo: ReactComponentInfo = (debugInfo[index]: any); + for (let i = index + 1; i < debugInfo.length; i++) { + const debugEntry = debugInfo[i]; + if (typeof debugEntry.env === 'string') { + // If the next environment is different then this component was the boundary + // and it changed before entering the next component. So we assign this + // component a secondary environment. + return componentInfo.env !== debugEntry.env ? debugEntry.env : null; + } + } + } + return null; + } + function mountVirtualChildrenRecursively( firstChild: Fiber, lastChild: null | Fiber, // non-inclusive @@ -2464,6 +2487,7 @@ export function attach( // Not a Component. Some other Debug Info. continue; } + // Scan up until the next Component to see if this component changed environment. const componentInfo: ReactComponentInfo = (debugEntry: any); if (shouldFilterVirtual(componentInfo)) { // Skip. @@ -2487,7 +2511,15 @@ export function attach( ); } previousVirtualInstance = createVirtualInstance(componentInfo); - recordVirtualMount(previousVirtualInstance, reconcilingParent); + const secondaryEnv = getSecondaryEnvironmentName( + fiber._debugInfo, + i, + ); + recordVirtualMount( + previousVirtualInstance, + reconcilingParent, + secondaryEnv, + ); insertChild(previousVirtualInstance); previousVirtualInstanceFirstFiber = fiber; } @@ -2951,7 +2983,15 @@ export function attach( } else { // Otherwise we create a new instance. const newVirtualInstance = createVirtualInstance(componentInfo); - recordVirtualMount(newVirtualInstance, reconcilingParent); + const secondaryEnv = getSecondaryEnvironmentName( + nextChild._debugInfo, + i, + ); + recordVirtualMount( + newVirtualInstance, + reconcilingParent, + secondaryEnv, + ); insertChild(newVirtualInstance); previousVirtualInstance = newVirtualInstance; previousVirtualInstanceWasMount = true; From 4f604941569d2e8947ce1460a0b2997e835f37b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 30 Aug 2024 15:11:57 -0400 Subject: [PATCH 085/191] [Flight] Ship DEV-only enableServerComponentLogs flag in Stable/Canary (#30847) To recap. This only affects DEV and RSC. It patches console on the server in DEV (similar to how React DevTools already does and what we did for the double logging). Then replays those logs with a `[Server]` badge on the client so you don't need a server terminal open. This has been on for over 6 months now in our experimental channel and we've had a lot of coverage in Next.js due to various experimental flags like taint and ppr. It's non-invasive in that even if something throws we just serialize that as an unknown value. The main feedback we've gotten was: - The serialization depth wasn't deep enough which I addressed in #30294 and haven't really had any issues since. This could still be an issue or the inverse that you serialize too many logs that are also too deep. This is not so much an issue with intentional logging and things like accidental errors don't typically have unbounded arguments (e.g. React errors are always string arguments). The ideal would be some way to retain objects and then load them on-demand but that needs more plumbing. Which can be later. - The other was that double logging on the server is annoying if the same terminal does both the RSC render and SSR render which was addressed in #30207. It is now off by default in node/edge-builds of the client, on by default in browser builds. With the `replayConsole` option to either opt-in or out. We've reached a good spot now I think. These are better with `enableOwnerStacks` but that's a separate track and not needed. The only thing to document here, other than maybe that we're doing it, is the `replayConsole` option but that's part of the RSC renderers that themselves are not documented so nowhere to document it. --- packages/shared/ReactFeatureFlags.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 673ff9d7e8450..10084be3cf2ef 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -134,7 +134,7 @@ export const alwaysThrottleRetries = true; export const passChildrenWhenCloningPersistedNodes = false; -export const enableServerComponentLogs = __EXPERIMENTAL__; +export const enableServerComponentLogs = true; /** * Enables a new Fiber flag used in persisted mode to reduce the number From 04ec50efa941a7f07e8231a87e72d6d851948b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 3 Sep 2024 12:29:15 -0400 Subject: [PATCH 086/191] [DevTools] Add Filtering of Environment Names (#30850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30842. This adds a filter to be able to exclude Components from a certain environment. Default to Client or Server. The available options are computed into a dropdown based on the names that are currently used on the page (or an option that were previously used). In addition to the hardcoded "Client". Meaning that if you have Server Components on the page you see "Server" or "Client" as possible options but it can be anything if there are multiple RSC environments on the page. "Client" in this case means Function and Class Components in Fiber - excluding built-ins. If a Server Component has two environments (primary and secondary) then both have to be filtered to exclude it. We don't show the option at all if there are no Server Components used in the page to avoid confusing existing users that are just using Client Components and wouldn't know the difference between Server vs Client. Screenshot 2024-08-30 at 12 56 42 AM --- .../src/__tests__/utils.js | 13 +++ .../src/backend/agent.js | 19 ++++ .../src/backend/fiber/renderer.js | 80 +++++++++++++--- .../src/backend/legacy/renderer.js | 6 ++ .../src/backend/types.js | 1 + packages/react-devtools-shared/src/bridge.js | 2 + .../views/Settings/ComponentsSettings.js | 93 ++++++++++++++++++- .../devtools/views/Settings/SettingsModal.js | 5 +- .../views/Settings/SettingsModalContext.js | 47 ++++++++-- .../src/frontend/types.js | 13 ++- 10 files changed, 253 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index 4a42cf2703ffb..c22ac6e05dc11 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -284,6 +284,19 @@ export function createHOCFilter(isEnabled: boolean = true) { }; } +export function createEnvironmentNameFilter( + env: string, + isEnabled: boolean = true, +) { + const Types = require('react-devtools-shared/src/frontend/types'); + return { + type: Types.ComponentFilterEnvironmentName, + isEnabled, + isValid: true, + value: env, + }; +} + export function createElementTypeFilter( elementType: ElementType, isEnabled: boolean = true, diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index a71b259441e98..9de4115b21d97 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -220,6 +220,7 @@ export default class Agent extends EventEmitter<{ this.updateConsolePatchSettings, ); bridge.addListener('updateComponentFilters', this.updateComponentFilters); + bridge.addListener('getEnvironmentNames', this.getEnvironmentNames); // Temporarily support older standalone front-ends sending commands to newer embedded backends. // We do this because React Native embeds the React DevTools backend, @@ -814,6 +815,24 @@ export default class Agent extends EventEmitter<{ } }; + getEnvironmentNames: () => void = () => { + let accumulatedNames = null; + for (const rendererID in this._rendererInterfaces) { + const renderer = this._rendererInterfaces[+rendererID]; + const names = renderer.getEnvironmentNames(); + if (accumulatedNames === null) { + accumulatedNames = names; + } else { + for (let i = 0; i < names.length; i++) { + if (accumulatedNames.indexOf(names[i]) === -1) { + accumulatedNames.push(names[i]); + } + } + } + } + this._bridge.send('environmentNames', accumulatedNames || []); + }; + onTraceUpdates: (nodes: Set) => void = nodes => { this.emit('traceUpdates', nodes); }; diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 47cb12bf17ae0..7960f68fb54a3 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -14,6 +14,7 @@ import { ComponentFilterElementType, ComponentFilterHOC, ComponentFilterLocation, + ComponentFilterEnvironmentName, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -721,6 +722,11 @@ export function getInternalReactConstants(version: string): { }; } +// All environment names we've seen so far. This lets us create a list of filters to apply. +// This should ideally include env of filtered Components too so that you can add those as +// filters at the same time as removing some other filter. +const knownEnvironmentNames: Set = new Set(); + // Map of one or more Fibers in a pair to their unique id number. // We track both Fibers to support Fast Refresh, // which may forcefully replace one of the pair as part of hot reloading. @@ -1099,6 +1105,7 @@ export function attach( const hideElementsWithDisplayNames: Set = new Set(); const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); + const hideElementsWithEnvs: Set = new Set(); // Highlight updates let traceUpdatesEnabled: boolean = false; @@ -1108,6 +1115,7 @@ export function attach( hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); hideElementsWithPaths.clear(); + hideElementsWithEnvs.clear(); componentFilters.forEach(componentFilter => { if (!componentFilter.isEnabled) { @@ -1133,6 +1141,9 @@ export function attach( case ComponentFilterHOC: hideElementsWithDisplayNames.add(new RegExp('\\(')); break; + case ComponentFilterEnvironmentName: + hideElementsWithEnvs.add(componentFilter.value); + break; default: console.warn( `Invalid component filter type "${componentFilter.type}"`, @@ -1215,7 +1226,14 @@ export function attach( flushPendingEvents(); } - function shouldFilterVirtual(data: ReactComponentInfo): boolean { + function getEnvironmentNames(): Array { + return Array.from(knownEnvironmentNames); + } + + function shouldFilterVirtual( + data: ReactComponentInfo, + secondaryEnv: null | string, + ): boolean { // For purposes of filtering Server Components are always Function Components. // Environment will be used to filter Server vs Client. // Technically they can be forwardRef and memo too but those filters will go away @@ -1236,6 +1254,14 @@ export function attach( } } + if ( + (data.env == null || hideElementsWithEnvs.has(data.env)) && + (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv)) + ) { + // If a Component has two environments, you have to filter both for it not to appear. + return true; + } + return false; } @@ -1294,6 +1320,26 @@ export function attach( } } + if (hideElementsWithEnvs.has('Client')) { + // If we're filtering out the Client environment we should filter out all + // "Client Components". Technically that also includes the built-ins but + // since that doesn't actually include any additional code loading it's + // useful to not filter out the built-ins. Those can be filtered separately. + // There's no other way to filter out just Function components on the Client. + // Therefore, this only filters Class and Function components. + switch (tag) { + case ClassComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + return true; + } + } + /* DISABLED: https://github.com/facebook/react/pull/28417 if (hideElementsWithPaths.size > 0) { const source = getSourceForFiber(fiber); @@ -2489,7 +2535,14 @@ export function attach( } // Scan up until the next Component to see if this component changed environment. const componentInfo: ReactComponentInfo = (debugEntry: any); - if (shouldFilterVirtual(componentInfo)) { + const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i); + if (componentInfo.env != null) { + knownEnvironmentNames.add(componentInfo.env); + } + if (secondaryEnv !== null) { + knownEnvironmentNames.add(secondaryEnv); + } + if (shouldFilterVirtual(componentInfo, secondaryEnv)) { // Skip. continue; } @@ -2511,10 +2564,6 @@ export function attach( ); } previousVirtualInstance = createVirtualInstance(componentInfo); - const secondaryEnv = getSecondaryEnvironmentName( - fiber._debugInfo, - i, - ); recordVirtualMount( previousVirtualInstance, reconcilingParent, @@ -2919,7 +2968,17 @@ export function attach( continue; } const componentInfo: ReactComponentInfo = (debugEntry: any); - if (shouldFilterVirtual(componentInfo)) { + const secondaryEnv = getSecondaryEnvironmentName( + nextChild._debugInfo, + i, + ); + if (componentInfo.env != null) { + knownEnvironmentNames.add(componentInfo.env); + } + if (secondaryEnv !== null) { + knownEnvironmentNames.add(secondaryEnv); + } + if (shouldFilterVirtual(componentInfo, secondaryEnv)) { continue; } if (level === virtualLevel) { @@ -2983,10 +3042,6 @@ export function attach( } else { // Otherwise we create a new instance. const newVirtualInstance = createVirtualInstance(componentInfo); - const secondaryEnv = getSecondaryEnvironmentName( - nextChild._debugInfo, - i, - ); recordVirtualMount( newVirtualInstance, reconcilingParent, @@ -3925,7 +3980,7 @@ export function attach( owner = ownerFiber._debugOwner; } else { const ownerInfo: ReactComponentInfo = (owner: any); // Refined - if (!shouldFilterVirtual(ownerInfo)) { + if (!shouldFilterVirtual(ownerInfo, null)) { return ownerInfo; } owner = ownerInfo.owner; @@ -5750,5 +5805,6 @@ export function attach( storeAsGlobal, unpatchConsoleForStrictMode, updateComponentFilters, + getEnvironmentNames, }; } diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 523fbbeba3d8e..ccff5ef07a1b6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -1078,6 +1078,11 @@ export function attach( // Not implemented. } + function getEnvironmentNames(): Array { + // No RSC support. + return []; + } + function setTraceUpdatesEnabled(enabled: boolean) { // Not implemented. } @@ -1152,5 +1157,6 @@ export function attach( storeAsGlobal, unpatchConsoleForStrictMode, updateComponentFilters, + getEnvironmentNames, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 41c278d02ebdf..2f1482fb1cbd2 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -416,6 +416,7 @@ export type RendererInterface = { ) => void, unpatchConsoleForStrictMode: () => void, updateComponentFilters: (componentFilters: Array) => void, + getEnvironmentNames: () => Array, // Timeline profiler interface diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index f4da08be6bdbe..1e9b3c222d623 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -189,6 +189,7 @@ export type BackendEvents = { operations: [Array], ownersList: [OwnersList], overrideComponentFilters: [Array], + environmentNames: [Array], profilingData: [ProfilingDataBackend], profilingStatus: [boolean], reloadAppForProfiling: [], @@ -237,6 +238,7 @@ type FrontendEvents = { stopProfiling: [], storeAsGlobal: [StoreAsGlobalParams], updateComponentFilters: [Array], + getEnvironmentNames: [], updateConsolePatchSettings: [ConsolePatchSettings], viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index a2a6a1d681819..33552daa262d8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -15,6 +15,7 @@ import { useMemo, useRef, useState, + use, } from 'react'; import { LOCAL_STORAGE_OPEN_IN_EDITOR_URL, @@ -31,6 +32,7 @@ import { ComponentFilterElementType, ComponentFilterHOC, ComponentFilterLocation, + ComponentFilterEnvironmentName, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -52,11 +54,16 @@ import type { ElementType, ElementTypeComponentFilter, RegExpComponentFilter, + EnvironmentNameComponentFilter, } from 'react-devtools-shared/src/frontend/types'; const vscodeFilepath = 'vscode://file/{path}:{line}'; -export default function ComponentsSettings(_: {}): React.Node { +export default function ComponentsSettings({ + environmentNames, +}: { + environmentNames: Promise>, +}): React.Node { const store = useContext(StoreContext); const {parseHookNames, setParseHookNames} = useContext(SettingsContext); @@ -101,6 +108,30 @@ export default function ComponentsSettings(_: {}): React.Node { Array, >(() => [...store.componentFilters]); + const usedEnvironmentNames = use(environmentNames); + + const resolvedEnvironmentNames = useMemo(() => { + const set = new Set(usedEnvironmentNames); + // If there are other filters already specified but are not currently + // on the page, we still allow them as options. + for (let i = 0; i < componentFilters.length; i++) { + const filter = componentFilters[i]; + if (filter.type === ComponentFilterEnvironmentName) { + set.add(filter.value); + } + } + // Client is special and is always available as a default. + if (set.size > 0) { + // Only show any options at all if there's any other option already + // used by a filter or if any environments are used by the page. + // Note that "Client" can have been added above which would mean + // that we should show it as an option regardless if it's the only + // option. + set.add('Client'); + } + return Array.from(set).sort(); + }, [usedEnvironmentNames, componentFilters]); + const addFilter = useCallback(() => { setComponentFilters(prevComponentFilters => { return [ @@ -146,6 +177,13 @@ export default function ComponentsSettings(_: {}): React.Node { isEnabled: componentFilter.isEnabled, isValid: true, }; + } else if (type === ComponentFilterEnvironmentName) { + cloned[index] = { + type: ComponentFilterEnvironmentName, + isEnabled: componentFilter.isEnabled, + isValid: true, + value: 'Client', + }; } } return cloned; @@ -210,6 +248,29 @@ export default function ComponentsSettings(_: {}): React.Node { [], ); + const updateFilterValueEnvironmentName = useCallback( + (componentFilter: ComponentFilter, value: string) => { + if (componentFilter.type !== ComponentFilterEnvironmentName) { + throw Error('Invalid value for environment name filter'); + } + + setComponentFilters(prevComponentFilters => { + const cloned: Array = [...prevComponentFilters]; + if (componentFilter.type === ComponentFilterEnvironmentName) { + const index = prevComponentFilters.indexOf(componentFilter); + if (index >= 0) { + cloned[index] = { + ...componentFilter, + value, + }; + } + } + return cloned; + }); + }, + [], + ); + const removeFilter = useCallback((index: number) => { setComponentFilters(prevComponentFilters => { const cloned: Array = [...prevComponentFilters]; @@ -246,6 +307,11 @@ export default function ComponentsSettings(_: {}): React.Node { ...((cloned[index]: any): BooleanComponentFilter), isEnabled, }; + } else if (componentFilter.type === ComponentFilterEnvironmentName) { + cloned[index] = { + ...((cloned[index]: any): EnvironmentNameComponentFilter), + isEnabled, + }; } } return cloned; @@ -380,10 +446,16 @@ export default function ComponentsSettings(_: {}): React.Node { + {resolvedEnvironmentNames.length > 0 && ( + + )} - {componentFilter.type === ComponentFilterElementType && + {(componentFilter.type === ComponentFilterElementType || + componentFilter.type === ComponentFilterEnvironmentName) && 'equals'} {(componentFilter.type === ComponentFilterLocation || componentFilter.type === ComponentFilterDisplayName) && @@ -428,6 +500,23 @@ export default function ComponentsSettings(_: {}): React.Node { value={componentFilter.value} /> )} + {componentFilter.type === ComponentFilterEnvironmentName && ( + + )} + ))} +
+ ); + }; + + await act(() => { + root.render(); + }); + + newWindow.document.getElementById('a').focus(); + await act(() => { + newWindow.document.getElementById('a').click(); + }); + + expect(newWindow.document.activeElement).not.toBe(newWindow.document.body); + expect(newWindow.document.activeElement.innerHTML).toBe('a'); + }); }); From 5deb78223a269a6cb1706da8ec6aad8c007cab03 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 13 Sep 2024 22:33:05 +0200 Subject: [PATCH 141/191] [Flight] Respect `async` flag in client manifest (#30959) In #26624, the ability to mark a client reference module as `async` in the React client manifest was removed because it was not utilized by Webpack, neither in `ReactFlightWebpackPlugin` nor in Next.js. However, some bundlers and frameworks are sophisticated enough to properly handle and identify async ESM modules (e.g., client component modules with top-level `await`), most notably Turbopack in Next.js. Therefore, we need to consider the `async` flag in the client manifest when resolving the client reference metadata on the server. The SSR manifest cannot override this flag, meaning that if a module is async, it must remain async in all client environments. x-ref: https://github.com/vercel/next.js/pull/70022 --- .../__tests__/ReactFlightTurbopackDOM-test.js | 120 ++++++++++++++++++ .../src/__tests__/utils/TurbopackMock.js | 60 +++++++++ ...ReactFlightServerConfigTurbopackBundler.js | 10 +- .../src/shared/ReactFlightImportMetadata.js | 1 + .../src/__tests__/ReactFlightDOM-test.js | 103 +++++++++++++++ .../src/__tests__/utils/WebpackMock.js | 63 +++++++++ .../ReactFlightServerConfigWebpackBundler.js | 10 +- .../src/shared/ReactFlightImportMetadata.js | 1 + 8 files changed, 366 insertions(+), 2 deletions(-) diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js index 0cf2876fedba6..d4fc59b826208 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -20,6 +20,7 @@ global.TextDecoder = require('util').TextDecoder; let act; let use; let clientExports; +let clientExportsESM; let turbopackMap; let Stream; let React; @@ -29,6 +30,7 @@ let ReactServerDOMClient; let Suspense; let ReactServerScheduler; let reactServerAct; +let ErrorBoundary; describe('ReactFlightTurbopackDOM', () => { beforeEach(() => { @@ -49,6 +51,7 @@ describe('ReactFlightTurbopackDOM', () => { const TurbopackMock = require('./utils/TurbopackMock'); clientExports = TurbopackMock.clientExports; + clientExportsESM = TurbopackMock.clientExportsESM; turbopackMap = TurbopackMock.turbopackMap; ReactServerDOMServer = require('react-server-dom-turbopack/server'); @@ -63,6 +66,22 @@ describe('ReactFlightTurbopackDOM', () => { Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); ReactServerDOMClient = require('react-server-dom-turbopack/client'); + + ErrorBoundary = class extends React.Component { + state = {hasError: false, error: null}; + static getDerivedStateFromError(error) { + return { + hasError: true, + error, + }; + } + render() { + if (this.state.hasError) { + return this.props.fallback(this.state.error); + } + return this.props.children; + } + }; }); async function serverAct(callback) { @@ -220,4 +239,105 @@ describe('ReactFlightTurbopackDOM', () => { }); expect(container.innerHTML).toBe('

Async: Module

'); }); + + it('should unwrap async ESM module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = await clientExportsESM(AsyncModule); + const AsyncModuleRef2 = await clientExportsESM(AsyncModule2); + + const {writable, readable} = getTestStream(); + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ), + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async: Module

'); + }); + + it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => { + const AsyncModule = Promise.resolve(function AsyncModule() { + return 'This should not be rendered'; + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + ( +

+ {__DEV__ ? error.message + ' + ' : null} + {error.digest} +

+ )}> + Loading...}> + + +
+ ); + } + + const AsyncModuleRef = await clientExportsESM(AsyncModule, { + forceClientModuleProxy: true, + }); + + const {writable, readable} = getTestStream(); + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + { + onError(error) { + return __DEV__ ? 'a dev digest' : `digest(${error.message})`; + }, + }, + ), + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const errorMessage = `The module "${Object.keys(turbopackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`; + + expect(container.innerHTML).toBe( + __DEV__ + ? `

${errorMessage} + a dev digest

` + : `

digest(${errorMessage})

`, + ); + }); }); diff --git a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js index 5ed6fa5357408..2e81d55fa692c 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js +++ b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js @@ -23,6 +23,7 @@ global.__turbopack_require__ = function (id) { }; const Server = require('react-server-dom-turbopack/server'); +const registerClientReference = Server.registerClientReference; const registerServerReference = Server.registerServerReference; const createClientModuleProxy = Server.createClientModuleProxy; @@ -83,6 +84,65 @@ exports.clientExports = function clientExports(moduleExports, chunkUrl) { return createClientModuleProxy(path); }; +exports.clientExportsESM = function clientExportsESM( + moduleExports, + options?: {forceClientModuleProxy?: boolean} = {}, +) { + const chunks = []; + const idx = '' + turbopackModuleIdx++; + turbopackClientModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + + const createClientReferencesForExports = ({exports, async}) => { + turbopackClientMap[path] = { + id: idx, + chunks, + name: '*', + async: true, + }; + + if (options.forceClientModuleProxy) { + return createClientModuleProxy(path); + } + + if (typeof exports === 'object') { + const references = {}; + + for (const name in exports) { + const id = path + '#' + name; + turbopackClientMap[path + '#' + name] = { + id: idx, + chunks, + name: name, + async, + }; + references[name] = registerClientReference(() => {}, id, name); + } + + return references; + } + + return registerClientReference(() => {}, path, '*'); + }; + + if ( + moduleExports && + typeof moduleExports === 'object' && + typeof moduleExports.then === 'function' + ) { + return moduleExports.then( + asyncModuleExports => + createClientReferencesForExports({ + exports: asyncModuleExports, + async: true, + }), + () => {}, + ); + } + + return createClientReferencesForExports({exports: moduleExports}); +}; + // This tests server to server references. There's another case of client to server references. exports.serverExports = function serverExports(moduleExports) { const idx = '' + turbopackModuleIdx++; diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js index d8224aff341dc..219391f8f819e 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js @@ -71,7 +71,15 @@ export function resolveClientReferenceMetadata( ); } } - if (clientReference.$$async === true) { + if (resolvedModuleData.async === true && clientReference.$$async === true) { + throw new Error( + 'The module "' + + modulePath + + '" is marked as an async ESM module but was loaded as a CJS proxy. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + if (resolvedModuleData.async === true || clientReference.$$async === true) { return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; } else { return [resolvedModuleData.id, resolvedModuleData.chunks, name]; diff --git a/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js index 60460d9c1d6d9..7cfce93deb25a 100644 --- a/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js +++ b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js @@ -12,6 +12,7 @@ export type ImportManifestEntry = { // chunks is an array of filenames chunks: Array, name: string, + async?: boolean, }; // This is the parsed shape of the wire format which is why it is diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 41fc0bfd41088..b59eb05c7b3fb 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -21,6 +21,7 @@ global.TextDecoder = require('util').TextDecoder; let act; let use; let clientExports; +let clientExportsESM; let clientModuleError; let webpackMap; let Stream; @@ -68,6 +69,7 @@ describe('ReactFlightDOM', () => { } const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; + clientExportsESM = WebpackMock.clientExportsESM; clientModuleError = WebpackMock.clientModuleError; webpackMap = WebpackMock.webpackMap; @@ -583,6 +585,107 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

Async Text

'); }); + it('should unwrap async ESM module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = await clientExportsESM(AsyncModule); + const AsyncModuleRef2 = await clientExportsESM(AsyncModule2); + + const {writable, readable} = getTestStream(); + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ), + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async: Module

'); + }); + + it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => { + const AsyncModule = Promise.resolve(function AsyncModule() { + return 'This should not be rendered'; + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + ( +

+ {__DEV__ ? error.message + ' + ' : null} + {error.digest} +

+ )}> + Loading...}> + + +
+ ); + } + + const AsyncModuleRef = await clientExportsESM(AsyncModule, { + forceClientModuleProxy: true, + }); + + const {writable, readable} = getTestStream(); + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + { + onError(error) { + return __DEV__ ? 'a dev digest' : `digest(${error.message})`; + }, + }, + ), + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`; + + expect(container.innerHTML).toBe( + __DEV__ + ? `

${errorMessage} + a dev digest

` + : `

digest(${errorMessage})

`, + ); + }); + it('should be able to import a name called "then"', async () => { const thenExports = { then: function then() { diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 4527118c1de8b..654bcdc9b6d5c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -44,6 +44,10 @@ if (previousCompile === nodeCompile) { Module.prototype._compile = previousCompile; +const Server = require('react-server-dom-webpack/server'); +const registerClientReference = Server.registerClientReference; +const createClientModuleProxy = Server.createClientModuleProxy; + exports.webpackMap = webpackClientMap; exports.webpackModules = webpackClientModules; exports.webpackServerMap = webpackServerMap; @@ -126,6 +130,65 @@ exports.clientExports = function clientExports( return mod.exports; }; +exports.clientExportsESM = function clientExportsESM( + moduleExports, + options?: {forceClientModuleProxy?: boolean} = {}, +) { + const chunks = []; + const idx = '' + webpackModuleIdx++; + webpackClientModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + + const createClientReferencesForExports = ({exports, async}) => { + webpackClientMap[path] = { + id: idx, + chunks, + name: '*', + async: true, + }; + + if (options.forceClientModuleProxy) { + return createClientModuleProxy(path); + } + + if (typeof exports === 'object') { + const references = {}; + + for (const name in exports) { + const id = path + '#' + name; + webpackClientMap[path + '#' + name] = { + id: idx, + chunks, + name: name, + async, + }; + references[name] = registerClientReference(() => {}, id, name); + } + + return references; + } + + return registerClientReference(() => {}, path, '*'); + }; + + if ( + moduleExports && + typeof moduleExports === 'object' && + typeof moduleExports.then === 'function' + ) { + return moduleExports.then( + asyncModuleExports => + createClientReferencesForExports({ + exports: asyncModuleExports, + async: true, + }), + () => {}, + ); + } + + return createClientReferencesForExports({exports: moduleExports}); +}; + // This tests server to server references. There's another case of client to server references. exports.serverExports = function serverExports(moduleExports, blockOnChunk) { const idx = '' + webpackModuleIdx++; diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js index d29516ff946ea..f9d9bf4ea9169 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js @@ -71,7 +71,15 @@ export function resolveClientReferenceMetadata( ); } } - if (clientReference.$$async === true) { + if (resolvedModuleData.async === true && clientReference.$$async === true) { + throw new Error( + 'The module "' + + modulePath + + '" is marked as an async ESM module but was loaded as a CJS proxy. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + if (resolvedModuleData.async === true || clientReference.$$async === true) { return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; } else { return [resolvedModuleData.id, resolvedModuleData.chunks, name]; diff --git a/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js index 08aafaf00c605..29b012f605204 100644 --- a/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js +++ b/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js @@ -12,6 +12,7 @@ export type ImportManifestEntry = { // chunks is a double indexed array of chunkId / chunkFilename pairs chunks: Array, name: string, + async?: boolean, }; // This is the parsed shape of the wire format which is why it is From 6774caa37973e3e26d60f100971e5e785fd12235 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 13 Sep 2024 15:55:42 -0700 Subject: [PATCH 142/191] [Flight] properly track pendingChunks when changing environment names (#30958) When the environment name changes for a chunk we issue a new debug chunk which updates the environment name. This chunk was not beign included in the pendingChunks count so the count was off when flushing --- packages/react-server/src/ReactFlightServer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 7ee275e060a82..b40c0628b2c28 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -3813,6 +3813,7 @@ function retryTask(request: Request, task: Task): void { if (__DEV__) { const currentEnv = (0, request.environmentName)(); if (currentEnv !== task.environmentName) { + request.pendingChunks++; // The environment changed since we last emitted any debug information for this // task. We emit an entry that just includes the environment name change. emitDebugChunk(request, task.id, {env: currentEnv}); @@ -3831,6 +3832,7 @@ function retryTask(request: Request, task: Task): void { if (__DEV__) { const currentEnv = (0, request.environmentName)(); if (currentEnv !== task.environmentName) { + request.pendingChunks++; // The environment changed since we last emitted any debug information for this // task. We emit an entry that just includes the environment name change. emitDebugChunk(request, task.id, {env: currentEnv}); From 3d95c43b8967d4dda1ec9a22f0d9ea4999fee8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 13 Sep 2024 21:51:52 -0400 Subject: [PATCH 143/191] [Fiber] Profiler - Use two separate functions instead of branch by flag (#30957) Nit: I don't trust flags in hot code. While it can take somewhat longer to compile two functions and JIT them. After that they don't need to check branches. Also makes it clearer the purpose. --- .../src/ReactFiberWorkLoop.js | 13 +++++----- .../src/ReactProfilerTimer.js | 26 ++++++++++++++----- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 8322fad7cd18b..3c0cafacfe202 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -225,7 +225,8 @@ import { recordCommitTime, resetNestedUpdateFlag, startProfilerTimer, - stopProfilerTimerIfRunningAndRecordDelta, + stopProfilerTimerIfRunningAndRecordDuration, + stopProfilerTimerIfRunningAndRecordIncompleteDuration, syncNestedUpdateFlag, } from './ReactProfilerTimer'; @@ -1844,7 +1845,7 @@ function handleThrow(root: FiberRoot, thrownValue: any): void { // Record the time spent rendering before an error was thrown. This // avoids inaccurate Profiler durations in the case of a // suspended render. - stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true); + stopProfilerTimerIfRunningAndRecordDuration(erroredWork); } if (enableSchedulingProfiler) { @@ -2516,7 +2517,7 @@ function performUnitOfWork(unitOfWork: Fiber): void { } else { next = beginWork(current, unitOfWork, entangledRenderLanes); } - stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + stopProfilerTimerIfRunningAndRecordDuration(unitOfWork); } else { if (__DEV__) { next = runWithFiberInDEV( @@ -2660,7 +2661,7 @@ function replayBeginWork(unitOfWork: Fiber): null | Fiber { } } if (isProfilingMode) { - stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); + stopProfilerTimerIfRunningAndRecordDuration(unitOfWork); } return next; @@ -2851,7 +2852,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { next = completeWork(current, completedWork, entangledRenderLanes); } // Update render duration assuming we didn't error. - stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); + stopProfilerTimerIfRunningAndRecordIncompleteDuration(completedWork); } if (next !== null) { @@ -2909,7 +2910,7 @@ function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void { if (enableProfilerTimer && (incompleteWork.mode & ProfileMode) !== NoMode) { // Record the render duration for the fiber that errored. - stopProfilerTimerIfRunningAndRecordDelta(incompleteWork, false); + stopProfilerTimerIfRunningAndRecordIncompleteDuration(incompleteWork); // Include the time spent working on failed children before continuing. let actualDuration = incompleteWork.actualDuration; diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index eaa24dc9fdc3b..e0c634b190001 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -29,7 +29,8 @@ export type ProfilerTimer = { recordCommitTime(): void, startProfilerTimer(fiber: Fiber): void, stopProfilerTimerIfRunning(fiber: Fiber): void, - stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void, + stopProfilerTimerIfRunningAndRecordDuration(fiber: Fiber): void, + stopProfilerTimerIfRunningAndRecordIncompleteDuration(fiber: Fiber): void, syncNestedUpdateFlag(): void, ... }; @@ -112,9 +113,21 @@ function stopProfilerTimerIfRunning(fiber: Fiber): void { profilerStartTime = -1; } -function stopProfilerTimerIfRunningAndRecordDelta( +function stopProfilerTimerIfRunningAndRecordDuration(fiber: Fiber): void { + if (!enableProfilerTimer) { + return; + } + + if (profilerStartTime >= 0) { + const elapsedTime = now() - profilerStartTime; + fiber.actualDuration += elapsedTime; + fiber.selfBaseDuration = elapsedTime; + profilerStartTime = -1; + } +} + +function stopProfilerTimerIfRunningAndRecordIncompleteDuration( fiber: Fiber, - overrideBaseTime: boolean, ): void { if (!enableProfilerTimer) { return; @@ -123,9 +136,7 @@ function stopProfilerTimerIfRunningAndRecordDelta( if (profilerStartTime >= 0) { const elapsedTime = now() - profilerStartTime; fiber.actualDuration += elapsedTime; - if (overrideBaseTime) { - fiber.selfBaseDuration = elapsedTime; - } + // We don't update the selfBaseDuration here because we errored. profilerStartTime = -1; } } @@ -233,7 +244,8 @@ export { startPassiveEffectTimer, startProfilerTimer, stopProfilerTimerIfRunning, - stopProfilerTimerIfRunningAndRecordDelta, + stopProfilerTimerIfRunningAndRecordDuration, + stopProfilerTimerIfRunningAndRecordIncompleteDuration, syncNestedUpdateFlag, transferActualDuration, }; From b75cc078c5fda0d57135523a7a2f4e8d1536472f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E5=B8=8C=E4=B8=8E=E5=AD=90=E6=99=B4?= Date: Sat, 14 Sep 2024 23:18:27 +0800 Subject: [PATCH 144/191] Fix nodeName to UPPERCASE in insertStylesheetIntoRoot (#28255) ## Summary image The condition `node.nodeName === 'link'` is always `false`, because `node.nodeName` is Uppercase in specification. And the condition `node.nodeName === 'LINK'` is unnecessary, because Fizz hoists tags when it's `media` attribute is `"not all"`, whether it is a `link` or a `style` (line 36): https://github.com/facebook/react/blob/18cbcbf783377c5a22277a63ae41af54504502e0/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js#L30-L44 https://github.com/facebook/react/blob/18cbcbf783377c5a22277a63ae41af54504502e0/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js#L30-L44 --- packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index b2eec77f6b4cb..0e39f988f813c 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -3520,7 +3520,7 @@ function insertStylesheetIntoRoot( for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if ( - node.nodeName === 'link' || + node.nodeName === 'LINK' || // We omit style tags with media="not all" because they are not in the right position // and will be hoisted by the Fizz runtime imminently. node.getAttribute('media') !== 'not all' From fc5ef50da8e975a569622d477f1fed54cb8b193d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sat, 14 Sep 2024 09:26:01 -0700 Subject: [PATCH 145/191] [Flight] Start initial work immediately (#30961) In a past update we made render and prerender have different work scheduling behavior because these methods are meant to be used in differeent environments with different performance tradeoffs in mind. For instance to prioritize streaming we want to allow as much IO to complete before triggering a round of work because we want to flush as few intermediate UI states. With Prerendering there will never be any intermediate UI states so we can more aggressively render tasks as they complete. One thing we've found is that even during render we should ideally kick off work immediately. This update normalizes the intitial work for render and prerender to start in a microtask. Choosing microtask over sync is somewhat arbitrary but there really isn't a reason to make them different between render/prerender so for now we'll unify them and keep it as a microtask for now. This change also updates pinging behavior. If the request is still in the initial task that spawned it then pings will schedule on the microtask queue. This allows immediately available async APIs to resolve right away. The concern with doing this for normal pings is that it might crowd out IO events but since this is the initial task there would be IO to already be scheduled. --- .../react-server/src/ReactFlightServer.js | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b40c0628b2c28..f0e632c5499b0 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -352,8 +352,17 @@ type Task = { interface Reference {} +const OPENING = 10; +const OPEN = 11; +const ABORTING = 12; +const CLOSING = 13; +const CLOSED = 14; + +const RENDER = 20; +const PRERENDER = 21; + export type Request = { - status: 10 | 11 | 12 | 13, + status: 10 | 11 | 12 | 13 | 14, type: 20 | 21, flushScheduled: boolean, fatalError: mixed, @@ -426,14 +435,6 @@ function defaultPostponeHandler(reason: string) { // Noop } -const OPEN = 10; -const ABORTING = 11; -const CLOSING = 12; -const CLOSED = 13; - -const RENDER = 20; -const PRERENDER = 21; - function RequestInstance( this: $FlowFixMe, type: 20 | 21, @@ -472,7 +473,7 @@ function RequestInstance( } const hints = createHints(); this.type = type; - this.status = OPEN; + this.status = OPENING; this.flushScheduled = false; this.fatalError = null; this.destination = null; @@ -1794,7 +1795,7 @@ function pingTask(request: Request, task: Task): void { pingedTasks.push(task); if (pingedTasks.length === 1) { request.flushScheduled = request.destination !== null; - if (request.type === PRERENDER) { + if (request.type === PRERENDER || request.status === OPENING) { scheduleMicrotask(() => performWork(request)); } else { scheduleWork(() => performWork(request)); @@ -4062,21 +4063,18 @@ function flushCompletedChunks( export function startWork(request: Request): void { request.flushScheduled = request.destination !== null; - if (request.type === PRERENDER) { - if (supportsRequestStorage) { - scheduleMicrotask(() => { - requestStorage.run(request, performWork, request); - }); - } else { - scheduleMicrotask(() => performWork(request)); - } + if (supportsRequestStorage) { + scheduleMicrotask(() => { + requestStorage.run(request, performWork, request); + }); } else { - if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request, performWork, request)); - } else { - scheduleWork(() => performWork(request)); - } + scheduleMicrotask(() => performWork(request)); } + scheduleWork(() => { + if (request.status === OPENING) { + request.status = OPEN; + } + }); } function enqueueFlush(request: Request): void { @@ -4129,7 +4127,8 @@ export function stopFlowing(request: Request): void { export function abort(request: Request, reason: mixed): void { try { - if (request.status === OPEN) { + // We define any status below OPEN as OPEN equivalent + if (request.status <= OPEN) { request.status = ABORTING; } const abortableTasks = request.abortableTasks; From 8cf64620c7dd4ec7e72aa16ee2d5f15eb3420b92 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 16 Sep 2024 14:47:57 +0100 Subject: [PATCH 146/191] fix[rdt/fiber/renderer.js]: getCurrentFiber can be injected as null (#30968) In production artifacts for `18.x.x` `getCurrentFiber` can actually be injected as `null`. Updated `getComponentStack` and `onErrorOrWarning` implementations to support this. ![Screenshot 2024-09-16 at 10 52 00](https://github.com/user-attachments/assets/a0c773aa-ebbf-4fd5-95c4-cac3cc0c203f) --- packages/react-devtools-shared/src/backend/fiber/renderer.js | 4 ++-- packages/react-devtools-shared/src/backend/types.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 1ac78a49a58b6..2fd768b5d01c3 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1078,7 +1078,7 @@ export function attach( function getComponentStack( topFrame: Error, ): null | {enableOwnerStacks: boolean, componentStack: string} { - if (getCurrentFiber === undefined) { + if (getCurrentFiber == null) { // Expected this to be part of the renderer. Ignore. return null; } @@ -1130,7 +1130,7 @@ export function attach( type: 'error' | 'warn', args: $ReadOnlyArray, ): void { - if (getCurrentFiber === undefined) { + if (getCurrentFiber == null) { // Expected this to be part of the renderer. Ignore. return; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index e3c062fefaaf3..7d816d403af07 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -158,7 +158,7 @@ export type ReactRenderer = { currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef, // Only injected by React v16.9+ in DEV mode. // Enables DevTools to append owners-only component stack to error messages. - getCurrentFiber?: () => Fiber | null, + getCurrentFiber?: (() => Fiber | null) | null, // Only injected by React Flight Clients in DEV mode. // Enables DevTools to append owners-only component stack to error messages from Server Components. getCurrentComponentInfo?: () => ReactComponentInfo | null, From 0eab377a96099f0121009c8968c49d13d4e00bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 16 Sep 2024 11:09:40 -0400 Subject: [PATCH 147/191] Add enableComponentPerformanceTrack Flag (#30960) This flag will be used to gate a new timeline profiler that's integrate with the Performance Tab and the new performance.measure extensions in Chrome. It replaces the existing DevTools feature so this disables enableSchedulingProfiler when it is enabled since they can interplay in weird ways potentially. This means that experimental React now disable scheduling profiler and enables this new approach. --- .../src/__tests__/TimelineProfiler-test.js | 18 ++++++++++--- .../src/__tests__/preprocessData-test.js | 27 ++++++++++++++----- packages/shared/ReactFeatureFlags.js | 15 ++++++++--- .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 ++ 9 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index ecbe336bdcbe0..b4cc9bbc0bafc 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -18,6 +18,11 @@ import { import {ReactVersion} from '../../../../ReactVersions'; import semver from 'semver'; +let React = require('react'); +let Scheduler; +let store; +let utils; + // TODO: This is how other DevTools tests access the version but we should find // a better solution for this const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; @@ -26,11 +31,16 @@ const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; const enableSiblingPrerendering = false && semver.gte(ReactVersionTestingAgainst, '19.0.0'); +// This flag is on experimental which disables timeline profiler. +const enableComponentPerformanceTrack = + React.version.startsWith('19') && React.version.includes('experimental'); + describe('Timeline profiler', () => { - let React; - let Scheduler; - let store; - let utils; + if (enableComponentPerformanceTrack) { + test('no tests', () => {}); + // Ignore all tests. + return; + } beforeEach(() => { utils = require('./utils'); diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 06595b67ca1a0..7a77ab20eb9cf 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -16,16 +16,29 @@ import {ReactVersion} from '../../../../ReactVersions'; const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; +let React = require('react'); +let ReactDOM; +let ReactDOMClient; +let Scheduler; +let utils; +let assertLog; +let waitFor; + +// This flag is on experimental which disables timeline profiler. +const enableComponentPerformanceTrack = + React.version.startsWith('19') && React.version.includes('experimental'); + describe('Timeline profiler', () => { - let React; - let ReactDOM; - let ReactDOMClient; - let Scheduler; - let utils; - let assertLog; - let waitFor; + if (enableComponentPerformanceTrack) { + test('no tests', () => {}); + // Ignore all tests. + return; + } describe('User Timing API', () => { + if (enableComponentPerformanceTrack) { + return; + } let currentlyNotClearedMarks; let registeredMarks; let featureDetectionMarkName = null; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3331deded9af9..9935784b533d2 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -260,10 +260,6 @@ export const disableTextareaChildren = false; // Debugging and DevTools // ----------------------------------------------------------------------------- -// Adds user timing marks for e.g. state updates, suspense, and work loop stuff, -// for an experimental timeline tool. -export const enableSchedulingProfiler = __PROFILE__; - // Helps identify side effects in render-phase lifecycle hooks and setState // reducers by double invoking them in StrictLegacyMode. export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; @@ -271,6 +267,17 @@ export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; // Gather advanced timing metrics for Profiler subtrees. export const enableProfilerTimer = __PROFILE__; +// Adds performance.measure() marks using Chrome extensions to allow formatted +// Component rendering tracks to show up in the Performance tab. +// This flag will be used for both Server Component and Client Component tracks. +// All calls should also be gated on enableProfilerTimer. +export const enableComponentPerformanceTrack = __EXPERIMENTAL__; + +// Adds user timing marks for e.g. state updates, suspense, and work loop stuff, +// for an experimental timeline tool. +export const enableSchedulingProfiler: boolean = + !enableComponentPerformanceTrack && __PROFILE__; + // Record durations for commit and passive effects phases. export const enableProfilerCommitHooks = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index a6001a9559609..83df5ed548da8 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -77,6 +77,7 @@ export const enableRefAsProp = true; export const enableRenderableContext = true; export const enableRetryLaneExpiration = false; export const enableSchedulingProfiler = __PROFILE__; +export const enableComponentPerformanceTrack = false; export const enableScopeAPI = false; export const enableServerComponentLogs = true; export const enableSuspenseAvoidThisFallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 723cc06c33a27..55ab25639fea0 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -67,6 +67,7 @@ export const enableRefAsProp = true; export const enableRenderableContext = true; export const enableRetryLaneExpiration = false; export const enableSchedulingProfiler = __PROFILE__; +export const enableComponentPerformanceTrack = false; export const enableScopeAPI = false; export const enableServerComponentLogs = true; export const enableShallowPropDiffing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 3581d31831784..d12029a672f7c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -17,6 +17,7 @@ export const enableSchedulingProfiler = false; export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = __PROFILE__; export const enableProfilerNestedUpdatePhase = __PROFILE__; +export const enableComponentPerformanceTrack = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableLegacyCache = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index edf67adf18bc3..31b3a118eae5a 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -61,6 +61,7 @@ export const enableRefAsProp = true; export const enableRenderableContext = true; export const enableRetryLaneExpiration = false; export const enableSchedulingProfiler = __PROFILE__; +export const enableComponentPerformanceTrack = false; export const enableScopeAPI = false; export const enableServerComponentLogs = true; export const enableShallowPropDiffing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index e64904005e143..59850bac75099 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -17,6 +17,7 @@ export const enableSchedulingProfiler = false; export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = __PROFILE__; export const enableProfilerNestedUpdatePhase = __PROFILE__; +export const enableComponentPerformanceTrack = false; export const enableUpdaterTracking = false; export const enableCache = true; export const enableLegacyCache = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index e6025242d4a78..0caf25c155625 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -62,6 +62,8 @@ export const disableInputAttributeSyncing = false; export const enableLegacyFBSupport = true; export const enableLazyContextPropagation = true; +export const enableComponentPerformanceTrack = false; + // Logs additional User Timing API marks for use with an experimental profiling tool. export const enableSchedulingProfiler: boolean = __PROFILE__ && dynamicFeatureFlags.enableSchedulingProfiler; From ee1a403a3019dd8bffb12174d269d8c85bfab8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 16 Sep 2024 11:10:05 -0400 Subject: [PATCH 148/191] [Fiber] Move Profiler onPostCommit processing of passive effect durations to plain passive effect (#30966) We used to queue a separate third passive phase to invoke onPostCommit but this is unnecessary. We can just treat it as a plain passive effect. This means it is interleaved with other passive effects but we only need to know the duration of the things below us which is already done at this point. I also extracted the user space call to onPostCommit into ReactCommitEffects. Same as onCommit. It's now covered by runWithFiberInDEV and catches. --- .../src/ReactFiberBeginWork.js | 8 ++ .../src/ReactFiberCommitEffects.js | 53 +++++++- .../src/ReactFiberCommitWork.js | 116 +++++++----------- .../src/ReactFiberWorkLoop.js | 25 ---- 4 files changed, 106 insertions(+), 96 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c71b7044756ba..398dd8372597a 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1026,6 +1026,10 @@ function updateProfiler( workInProgress.flags |= Update; if (enableProfilerCommitHooks) { + // Schedule a passive effect for this Profiler to call onPostCommit hooks. + // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, + // because the effect is also where times bubble to parent Profilers. + workInProgress.flags |= Passive; // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; @@ -3700,6 +3704,10 @@ function attemptEarlyBailoutIfNoScheduledUpdate( } if (enableProfilerCommitHooks) { + // Schedule a passive effect for this Profiler to call onPostCommit hooks. + // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, + // because the effect is also where times bubble to parent Profilers. + workInProgress.flags |= Passive; // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index 1d9d5c65b8aa4..af5762df2110a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -881,7 +881,7 @@ function commitProfiler( commitTime: number, effectDuration: number, ) { - const {onCommit, onRender} = finishedWork.memoizedProps; + const {id, onCommit, onRender} = finishedWork.memoizedProps; let phase = current === null ? 'mount' : 'update'; if (enableProfilerNestedUpdatePhase) { @@ -892,7 +892,7 @@ function commitProfiler( if (typeof onRender === 'function') { onRender( - finishedWork.memoizedProps.id, + id, phase, finishedWork.actualDuration, finishedWork.treeBaseDuration, @@ -938,3 +938,52 @@ export function commitProfilerUpdate( } } } + +function commitProfilerPostCommitImpl( + finishedWork: Fiber, + current: Fiber | null, + commitTime: number, + passiveEffectDuration: number, +): void { + const {id, onPostCommit} = finishedWork.memoizedProps; + + let phase = current === null ? 'mount' : 'update'; + if (enableProfilerNestedUpdatePhase) { + if (isCurrentUpdateNested()) { + phase = 'nested-update'; + } + } + + if (typeof onPostCommit === 'function') { + onPostCommit(id, phase, passiveEffectDuration, commitTime); + } +} + +export function commitProfilerPostCommit( + finishedWork: Fiber, + current: Fiber | null, + commitTime: number, + passiveEffectDuration: number, +) { + try { + if (__DEV__) { + runWithFiberInDEV( + finishedWork, + commitProfilerPostCommitImpl, + finishedWork, + current, + commitTime, + passiveEffectDuration, + ); + } else { + commitProfilerPostCommitImpl( + finishedWork, + current, + commitTime, + passiveEffectDuration, + ); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } +} diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 17c104ff0fff9..bb37628c2d7a7 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -44,7 +44,6 @@ import { enablePersistedModeClonedFlag, enableProfilerTimer, enableProfilerCommitHooks, - enableProfilerNestedUpdatePhase, enableSchedulingProfiler, enableSuspenseCallback, enableScopeAPI, @@ -100,7 +99,6 @@ import { Cloned, } from './ReactFiberFlags'; import { - isCurrentUpdateNested, getCommitTime, recordLayoutEffectDuration, startLayoutEffectTimer, @@ -137,7 +135,6 @@ import { captureCommitPhaseError, resolveRetryWakeable, markCommitTimeOfFallback, - enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, addTransitionStartCallbackToPendingTransition, addTransitionProgressCallbackToPendingTransition, @@ -193,6 +190,7 @@ import { safelyDetachRef, safelyCallDestroy, commitProfilerUpdate, + commitProfilerPostCommit, commitRootCallbacks, } from './ReactFiberCommitEffects'; import { @@ -394,62 +392,6 @@ function commitBeforeMutationEffectsDeletion(deletion: Fiber) { } } -export function commitPassiveEffectDurations( - finishedRoot: FiberRoot, - finishedWork: Fiber, -): void { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - getExecutionContext() & CommitContext - ) { - // Only Profilers with work in their subtree will have an Update effect scheduled. - if ((finishedWork.flags & Update) !== NoFlags) { - switch (finishedWork.tag) { - case Profiler: { - const {passiveEffectDuration} = finishedWork.stateNode; - const {id, onPostCommit} = finishedWork.memoizedProps; - - // This value will still reflect the previous commit phase. - // It does not get reset until the start of the next commit phase. - const commitTime = getCommitTime(); - - let phase = finishedWork.alternate === null ? 'mount' : 'update'; - if (enableProfilerNestedUpdatePhase) { - if (isCurrentUpdateNested()) { - phase = 'nested-update'; - } - } - - if (typeof onPostCommit === 'function') { - onPostCommit(id, phase, passiveEffectDuration, commitTime); - } - - // Bubble times to the next nearest ancestor Profiler. - // After we process that Profiler, we'll bubble further up. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.passiveEffectDuration += passiveEffectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.passiveEffectDuration += passiveEffectDuration; - break outer; - } - parentFiber = parentFiber.return; - } - break; - } - default: - break; - } - } - } -} - function commitLayoutEffectOnFiber( finishedRoot: FiberRoot, current: Fiber | null, @@ -557,11 +499,6 @@ function commitLayoutEffectOnFiber( effectDuration, ); - // Schedule a passive effect for this Profiler to call onPostCommit hooks. - // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, - // because the effect is also where times bubble to parent Profilers. - enqueuePendingPassiveProfilerEffect(finishedWork); - // Propagate layout effect durations to the next nearest Profiler ancestor. // Do not reset these values until the next render so DevTools has a chance to read them first. let parentFiber = finishedWork.return; @@ -2475,11 +2412,6 @@ export function reappearLayoutEffects( effectDuration, ); - // Schedule a passive effect for this Profiler to call onPostCommit hooks. - // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, - // because the effect is also where times bubble to parent Profilers. - enqueuePendingPassiveProfilerEffect(finishedWork); - // Propagate layout effect durations to the next nearest Profiler ancestor. // Do not reset these values until the next render so DevTools has a chance to read them first. let parentFiber = finishedWork.return; @@ -2824,6 +2756,52 @@ function commitPassiveMountOnFiber( } break; } + case Profiler: { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + + // Only Profilers with work in their subtree will have a Passive effect scheduled. + if (flags & Passive) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + getExecutionContext() & CommitContext + ) { + const {passiveEffectDuration} = finishedWork.stateNode; + + commitProfilerPostCommit( + finishedWork, + finishedWork.alternate, + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + getCommitTime(), + passiveEffectDuration, + ); + + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + let parentFiber = finishedWork.return; + outer: while (parentFiber !== null) { + switch (parentFiber.tag) { + case HostRoot: + const root = parentFiber.stateNode; + root.passiveEffectDuration += passiveEffectDuration; + break outer; + case Profiler: + const parentStateNode = parentFiber.stateNode; + parentStateNode.passiveEffectDuration += passiveEffectDuration; + break outer; + } + parentFiber = parentFiber.return; + } + } + } + break; + } case LegacyHiddenComponent: { if (enableLegacyHidden) { recursivelyTraversePassiveMountEffects( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 3c0cafacfe202..c3f413d0ed0b9 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -189,7 +189,6 @@ import { commitBeforeMutationEffects, commitLayoutEffects, commitMutationEffects, - commitPassiveEffectDurations, commitPassiveMountEffects, commitPassiveUnmountEffects, disappearLayoutEffects, @@ -580,7 +579,6 @@ let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsLanes: Lanes = NoLanes; -let pendingPassiveProfilerEffects: Array = []; let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes; let pendingPassiveTransitions: Array | null = null; @@ -3475,19 +3473,6 @@ export function flushPassiveEffects(): boolean { return false; } -export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void { - if (enableProfilerTimer && enableProfilerCommitHooks) { - pendingPassiveProfilerEffects.push(fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } - } -} - function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) { return false; @@ -3528,16 +3513,6 @@ function flushPassiveEffectsImpl() { commitPassiveUnmountEffects(root.current); commitPassiveMountEffects(root, root.current, lanes, transitions); - // TODO: Move to commitPassiveMountEffects - if (enableProfilerTimer && enableProfilerCommitHooks) { - const profilerEffects = pendingPassiveProfilerEffects; - pendingPassiveProfilerEffects = []; - for (let i = 0; i < profilerEffects.length; i++) { - const fiber = ((profilerEffects[i]: any): Fiber); - commitPassiveEffectDurations(root, fiber); - } - } - if (__DEV__) { if (enableDebugTracing) { logPassiveEffectsStopped(); From f2df5694f2be141954f22618fd3ad035203241a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 16 Sep 2024 11:45:50 -0400 Subject: [PATCH 149/191] [Fiber] Log Component Renders to Custom Performance Track (#30967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30960 and #30966. Behind the enableComponentPerformanceTrack flag. This is the first step of performance logging. This logs the start and end time of a component render in the passive effect phase. We use the data we're already tracking on components when the Profiler component or DevTools is active in the Profiling or Dev builds. By backdating this after committing we avoid adding more overhead in the hot path. By only logging things that actually committed, we avoid the costly unwinding of an interrupted render which was hard to maintain in earlier versions. We already have the start time but we don't have the end time. That's because `actualStartTime + actualDuration` isn't enough since `actualDuration` counts the actual CPU time excluding yields and suspending in the render. Instead, we infer the end time to be the start time of the next sibling or the complete time of the whole root if there are no more siblings. We need to pass this down the passive effect tree. This will mean that any overhead and yields are attributed to this component's span. In a follow up, we'll need to start logging these yields to make it clear that this is not part of the component's self-time. In follow ups, I'll do the same for commit phases. We'll also need to log more information about the phases in the top track. We'll also need to filter out more components from the trees that we don't need to highlight like the internal Offscreen components. It also needs polish on colors etc. Currently, I place the components into separate tracks depending on which lane currently committed. That way you can see what was blocking Transitions or Suspense etc. One problem that I've hit with the new performance.measure extensions is that these tracks show up in the order they're used which is not the order of priority that we use. Even when you add fake markers they have to actually be within the performance run since otherwise the calls are noops so it's not enough to do that once. However, I think this visualization is actually not good because these trees end up so large that you can't see any other lanes once you expand one. Therefore, I think in a follow up I'll actually instead switch to a model where Components is a single track regardless of lane since we don't currently have overlap anyway. Then the description about what is actually rendering can be separate lanes. Screenshot 2024-09-15 at 10 55 55 PM Screenshot 2024-09-15 at 10 56 27 PM --- .../src/ReactFiberCommitWork.js | 74 +++++++++++++++-- .../react-reconciler/src/ReactFiberLane.js | 32 ++++++++ .../src/ReactFiberPerformanceTrack.js | 61 ++++++++++++++ .../src/ReactFiberWorkLoop.js | 60 ++++++++------ .../src/ReactProfilerTimer.js | 16 +++- .../src/__tests__/ReactSuspenseList-test.js | 10 +++ .../__tests__/ReactProfiler-test.internal.js | 81 +++++++++++++++---- 7 files changed, 285 insertions(+), 49 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberPerformanceTrack.js diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index bb37628c2d7a7..bba93bbc0da2e 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -53,6 +53,7 @@ import { enableUseEffectEventHook, enableLegacyHidden, disableLegacyMode, + enableComponentPerformanceTrack, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -102,7 +103,9 @@ import { getCommitTime, recordLayoutEffectDuration, startLayoutEffectTimer, + getCompleteTime, } from './ReactProfilerTimer'; +import {logComponentRender} from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue'; import { @@ -2648,6 +2651,9 @@ export function commitPassiveMountEffects( finishedWork, committedLanes, committedTransitions, + enableProfilerTimer && enableComponentPerformanceTrack + ? getCompleteTime() + : 0, ); } @@ -2656,17 +2662,41 @@ function recursivelyTraversePassiveMountEffects( parentFiber: Fiber, committedLanes: Lanes, committedTransitions: Array | null, + endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ) { - if (parentFiber.subtreeFlags & PassiveMask) { + if ( + parentFiber.subtreeFlags & PassiveMask || + // If this subtree rendered with profiling this commit, we need to visit it to log it. + (enableProfilerTimer && + enableComponentPerformanceTrack && + parentFiber.actualDuration !== 0 && + (parentFiber.alternate === null || + parentFiber.alternate.child !== parentFiber.child)) + ) { let child = parentFiber.child; while (child !== null) { - commitPassiveMountOnFiber( - root, - child, - committedLanes, - committedTransitions, - ); - child = child.sibling; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + const nextSibling = child.sibling; + commitPassiveMountOnFiber( + root, + child, + committedLanes, + committedTransitions, + nextSibling !== null + ? ((nextSibling.actualStartTime: any): number) + : endTime, + ); + child = nextSibling; + } else { + commitPassiveMountOnFiber( + root, + child, + committedLanes, + committedTransitions, + 0, + ); + child = child.sibling; + } } } } @@ -2676,7 +2706,25 @@ function commitPassiveMountOnFiber( finishedWork: Fiber, committedLanes: Lanes, committedTransitions: Array | null, + endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ): void { + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its + // render time. We do this after the fact in the passive effect to avoid the overhead of this + // getting in the way of the render characteristics and avoid the overhead of unwinding + // uncommitted renders. + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + ((finishedWork.actualStartTime: any): number) > 0 + ) { + logComponentRender( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + ); + } + // When updating this function, also update reconnectPassiveEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible, // or when toggling effects inside a hidden tree. @@ -2690,6 +2738,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); if (flags & Passive) { commitHookPassiveMountEffects( @@ -2705,6 +2754,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); if (flags & Passive) { if (enableCache) { @@ -2762,6 +2812,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); // Only Profilers with work in their subtree will have a Passive effect scheduled. @@ -2809,6 +2860,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); if (flags & Passive) { @@ -2834,6 +2886,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); } else { if (disableLegacyMode || finishedWork.mode & ConcurrentMode) { @@ -2858,6 +2911,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); } } @@ -2870,6 +2924,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); } else { // The effects are currently disconnected. Reconnect them, while also @@ -2901,6 +2956,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); if (flags & Passive) { // TODO: Pass `current` as argument to this function @@ -2916,6 +2972,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); if (flags & Passive) { commitTracingMarkerPassiveMountEffect(finishedWork); @@ -2930,6 +2987,7 @@ function commitPassiveMountOnFiber( finishedWork, committedLanes, committedTransitions, + endTime, ); break; } diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 4338d3af58546..f72174e208555 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -1143,3 +1143,35 @@ export function clearTransitionsForLanes(root: FiberRoot, lanes: Lane | Lanes) { lanes &= ~lane; } } + +// Used to name the Performance Track +export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string { + if ( + lanes & + (SyncHydrationLane | + SyncLane | + InputContinuousHydrationLane | + InputContinuousLane | + DefaultHydrationLane | + DefaultLane) + ) { + return 'Blocking'; + } + if (lanes & (TransitionHydrationLane | TransitionLanes)) { + return 'Transition'; + } + if (lanes & RetryLanes) { + return 'Suspense'; + } + if ( + lanes & + (SelectiveHydrationLane | + IdleHydrationLane | + IdleLane | + OffscreenLane | + DeferredLane) + ) { + return 'Idle'; + } + return 'Other'; +} diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js new file mode 100644 index 0000000000000..2058a04e47454 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -0,0 +1,61 @@ +/** + * 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. + * + * @flow + */ + +import type {Fiber} from './ReactInternalTypes'; + +import getComponentNameFromFiber from './getComponentNameFromFiber'; + +import {getGroupNameOfHighestPriorityLane} from './ReactFiberLane'; + +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + +const supportsUserTiming = + enableProfilerTimer && + typeof performance !== 'undefined' && + // $FlowFixMe[method-unbinding] + typeof performance.measure === 'function'; + +const TRACK_GROUP = 'Components ⚛'; + +// Reused to avoid thrashing the GC. +const reusableComponentDevToolDetails = { + dataType: 'track-entry', + color: 'primary', + track: 'Blocking', // Lane + trackGroup: TRACK_GROUP, +}; +const reusableComponentOptions = { + start: -0, + end: -0, + detail: { + devtools: reusableComponentDevToolDetails, + }, +}; + +export function setCurrentTrackFromLanes(lanes: number): void { + reusableComponentDevToolDetails.track = + getGroupNameOfHighestPriorityLane(lanes); +} + +export function logComponentRender( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } + if (supportsUserTiming) { + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure(name, reusableComponentOptions); + } +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c3f413d0ed0b9..dba089e81f3e0 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -44,6 +44,7 @@ import { disableDefaultPropsExceptForClasses, disableStringRefs, enableSiblingPrerendering, + enableComponentPerformanceTrack, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -221,6 +222,7 @@ import { import { markNestedUpdateScheduled, + recordCompleteTime, recordCommitTime, resetNestedUpdateFlag, startProfilerTimer, @@ -228,6 +230,7 @@ import { stopProfilerTimerIfRunningAndRecordIncompleteDuration, syncNestedUpdateFlag, } from './ReactProfilerTimer'; +import {setCurrentTrackFromLanes} from './ReactFiberPerformanceTrack'; // DEV stuff import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -1098,6 +1101,12 @@ function finishConcurrentRender( finishedWork: Fiber, lanes: Lanes, ) { + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Track when we finished the last unit of work, before we actually commit it. + // The commit can be suspended/blocked until we commit it. + recordCompleteTime(); + } + // TODO: The fact that most of these branches are identical suggests that some // of the exit statuses are not best modeled as exit statuses and should be // tracked orthogonally. @@ -1479,6 +1488,10 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { return null; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordCompleteTime(); + } + // We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. const finishedWork: Fiber = (root.current.alternate: any); @@ -2824,35 +2837,22 @@ function completeUnitOfWork(unitOfWork: Fiber): void { const returnFiber = completedWork.return; let next; - if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) { - if (__DEV__) { - next = runWithFiberInDEV( - completedWork, - completeWork, - current, - completedWork, - entangledRenderLanes, - ); - } else { - next = completeWork(current, completedWork, entangledRenderLanes); - } + startProfilerTimer(completedWork); + if (__DEV__) { + next = runWithFiberInDEV( + completedWork, + completeWork, + current, + completedWork, + entangledRenderLanes, + ); } else { - startProfilerTimer(completedWork); - if (__DEV__) { - next = runWithFiberInDEV( - completedWork, - completeWork, - current, - completedWork, - entangledRenderLanes, - ); - } else { - next = completeWork(current, completedWork, entangledRenderLanes); - } + next = completeWork(current, completedWork, entangledRenderLanes); + } + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordIncompleteDuration(completedWork); } - if (next !== null) { // Completing this fiber spawned new work. Work on that next. workInProgress = next; @@ -3104,6 +3104,10 @@ function commitRootImpl( // TODO: Delete all other places that schedule the passive effect callback // They're redundant. if ( + // If this subtree rendered with profiling this commit, we need to visit it to log it. + (enableProfilerTimer && + enableComponentPerformanceTrack && + finishedWork.actualDuration !== 0) || (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags ) { @@ -3494,6 +3498,12 @@ function flushPassiveEffectsImpl() { throw new Error('Cannot flush passive effects while already rendering.'); } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // We're about to log a lot of profiling for this commit. + // We set this once so we don't have to recompute it for every log. + setCurrentTrackFromLanes(lanes); + } + if (__DEV__) { isFlushingPassiveEffects = true; didScheduleUpdateDuringPassiveEffects = false; diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index e0c634b190001..bad2f60490d54 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -35,6 +35,7 @@ export type ProfilerTimer = { ... }; +let completeTime: number = 0; let commitTime: number = 0; let layoutEffectStartTime: number = -1; let profilerStartTime: number = -1; @@ -83,6 +84,17 @@ function syncNestedUpdateFlag(): void { } } +function getCompleteTime(): number { + return completeTime; +} + +function recordCompleteTime(): void { + if (!enableProfilerTimer) { + return; + } + completeTime = now(); +} + function getCommitTime(): number { return commitTime; } @@ -233,10 +245,12 @@ function transferActualDuration(fiber: Fiber): void { } export { + getCompleteTime, + recordCompleteTime, getCommitTime, + recordCommitTime, isCurrentUpdateNested, markNestedUpdateScheduled, - recordCommitTime, recordLayoutEffectDuration, recordPassiveEffectDuration, resetNestedUpdateFlag, diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index b4a7bcc186b6d..63c398a657b09 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -1,3 +1,13 @@ +/** + * 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. + * + * @emails react-core + * @jest-environment node + */ + let React; let ReactNoop; let Scheduler; diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index c4d72ffee7241..a19e9b1c6d161 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -164,22 +164,73 @@ describe(`onRender`, () => { // TODO: unstable_now is called by more places than just the profiler. // Rewrite this test so it's less fragile. if (gate(flags => flags.enableDeferRootSchedulingToMicrotask)) { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); + if (gate(flags => flags.enableComponentPerformanceTrack)) { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); + } else { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); + } } else { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); + if (gate(flags => flags.enableComponentPerformanceTrack)) { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); + } else { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); + } } }); From 9f4e4611ead28d34f7f598c9bd12424cf68f5781 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 16 Sep 2024 17:43:40 +0100 Subject: [PATCH 150/191] fix: add Error prefix to Error objects names (#30969) This fixes printing Error objects in Chrome DevTools. I've observed that Chrome DevTools is not source mapping and linkifying URLs, when was running this on larger apps. Chrome DevTools talks to V8 via Chrome DevTools protocol, every object has a corresponding [`RemoteObject`](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject). When Chrome DevTools sees that Error object is printed in the console, it will try to prettify it. `description` field of the corresponding `RemoteObject` for the `Error` JavaScript object is a combination of `Error` `name`, `message`, `stack` fields. This is not just a raw `stack` field, so our prefix for this field just doesn't work. [V8 is actually filtering out first line of the `stack` field, it only keeps the stack frames as a string, and then this gets prefixed by `name` and `message` fields, if they are available](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a?fbclid=IwZXh0bgNhZW0CMTEAAR1tMm5YC4jqowObad1qXFT98X4RO76CMkCGNSxZ8rVsg6k2RrdvkVFL0i4_aem_e2fRrqotKdkYIeWlJnk0RA). As an illustration, this: ``` const fakeError = new Error(''); fakeError.name = 'Stack'; fakeError.stack = 'Error Stack:' + stack; ``` will be formatted by `V8` as this `RemoteObject`: ``` { ... description: 'Stack: ...', ... } ``` Notice that there is no `Error` prefix, that was previously added. Because of this, [Chrome DevTools won't even try to symbolicate the stack](https://github.com/ChromeDevTools/devtools-frontend/blob/ee4729d2ccdf5c6715ee40e6697f5464829e3f9a/front_end/panels/console/ErrorStackParser.ts#L33-L35), because it doesn't have such prefix. --- .../react-devtools-shared/src/backend/console.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index d4e9651fa7503..61d2e490eb966 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -229,9 +229,19 @@ export function patch({ // In Chromium, only the stack property is printed but in Firefox the : // gets printed so to make the colon make sense, we name it so we print Stack: // and similarly Safari leave an expandable slot. - fakeError.name = enableOwnerStacks - ? 'Stack' - : 'Component Stack'; // This gets printed + if (__IS_CHROME__ || __IS_EDGE__) { + // Before sending the stack to Chrome DevTools for formatting, + // V8 will reconstruct this according to the template : + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a + // It has to start with ^[\w.]*Error\b to trigger stack formatting. + fakeError.name = enableOwnerStacks + ? 'Error Stack' + : 'Error Component Stack'; // This gets printed + } else { + fakeError.name = enableOwnerStacks + ? 'Stack' + : 'Component Stack'; // This gets printed + } // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it // to our own stack. From 26855e4680dedb21f2c73a069ed691822a242db1 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 16 Sep 2024 17:51:00 +0100 Subject: [PATCH 151/191] [react-native] Fix misleading crash when view config is not found (#30970) ## Summary When a view config can not be found, it currently errors with `TypeError: Cannot read property 'bubblingEventTypes' of null`. Instead invariant at the correct location and prevent further processing of the null viewConfig to improve the error logged. ## How did you test this change? Build and run RN playground app referencing an invalid native view through `requireNativeComponent`. --- .../shims/react-native/ReactNativeViewConfigRegistry.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js index 98e03187c74a5..5f53e7634afde 100644 --- a/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js +++ b/scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js @@ -93,8 +93,8 @@ export function register(name: string, callback: () => ViewConfig): string { * This configuration will be lazy-loaded from UIManager. */ export function get(name: string): ViewConfig { - let viewConfig; - if (!viewConfigs.has(name)) { + let viewConfig = viewConfigs.get(name); + if (viewConfig == null) { const callback = viewConfigCallbacks.get(name); if (typeof callback !== 'function') { invariant( @@ -109,15 +109,14 @@ export function get(name: string): ViewConfig { ); } viewConfig = callback(); + invariant(viewConfig, 'View config not found for component `%s`', name); + processEventTypes(viewConfig); viewConfigs.set(name, viewConfig); // Clear the callback after the config is set so that // we don't mask any errors during registration. viewConfigCallbacks.set(name, null); - } else { - viewConfig = viewConfigs.get(name); } - invariant(viewConfig, 'View config not found for name %s', name); return viewConfig; } From c8a7cab13f9d496d4b178ba5e95b030ca854aa20 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Mon, 16 Sep 2024 10:53:29 -0700 Subject: [PATCH 152/191] [compiler] Fix issue where second argument of all functions was considered to be a ref ghstack-source-id: 1817f3b816ab5ec013a3b1a6c8a8373a30e0b3a0 Pull Request resolved: https://github.com/facebook/react/pull/30912 --- .../babel-plugin-react-compiler/src/TypeInference/InferTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index b460124ec71f3..25bc87838ae83 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -107,7 +107,7 @@ function equation(left: Type, right: Type): TypeEquation { function* generate( func: HIRFunction, ): Generator { - if (func.env.fnType === 'Component') { + if (func.fnType === 'Component') { const [props, ref] = func.params; if (props && props.kind === 'Identifier') { yield equation(props.identifier.type, { From 1e68a0a3aed9975d2e302ccf1dff0861bf2be706 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Mon, 16 Sep 2024 10:53:32 -0700 Subject: [PATCH 153/191] [compiler] Improve handling of refs Summary: This change expands our handling of refs to build an understanding of nested refs within objects and functions that may return refs. It builds a special-purpose type system within the ref analysis that gives a very lightweight structural type to objects and array expressions (merging the types of all their members), and then propagating those types throughout the analysis (e.g., if `ref` has type `Ref`, then `{ x: ref }` and `[ref]` have type `Structural(value=Ref)` and `{x: ref}.anything` and `[ref][anything]` have type `Ref`). This allows us to support structures that contain refs, and functions that operate over them, being created and passed around during rendering without at runtime accessing a ref value. The analysis here uses a fixpoint to allow types to be fully propagated through the system, and we defend against diverging by widening the type of a variable if it could grow infinitely: so, in something like ``` let x = ref; while (condition) { x = [x] } ``` we end up giving `x` the type `Structural(value=Ref)`. ghstack-source-id: afb0b0cb014ffcf21ef4d0ede6511330fd975ec3 Pull Request resolved: https://github.com/facebook/react/pull/30902 --- .../Validation/ValidateNoRefAccesInRender.ts | 647 +++++++++++------- .../capture-ref-for-later-mutation.expect.md | 69 ++ ...tsx => capture-ref-for-later-mutation.tsx} | 0 ... error.capture-ref-for-mutation.expect.md} | 10 +- .../error.capture-ref-for-mutation.tsx | 23 + .../capture-ref-for-later-mutation.expect.md | 70 ++ .../capture-ref-for-later-mutation.tsx | 24 + 7 files changed, 579 insertions(+), 264 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.capture-ref-for-later-mutation.tsx => capture-ref-for-later-mutation.tsx} (100%) rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.capture-ref-for-later-mutation.expect.md => error.capture-ref-for-mutation.expect.md} (75%) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index 8a65b4709c174..8fee651f8d1ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -8,9 +8,11 @@ import {CompilerError, ErrorSeverity} from '../CompilerError'; import { HIRFunction, + Identifier, IdentifierId, Place, SourceLocation, + getHookKindForType, isRefValueType, isUseRefType, } from '../HIR'; @@ -42,25 +44,154 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies'; * In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref) * or based on property name alone (`foo.current` might be a ref). */ -type State = { - refs: Set; - refValues: Map; - refAccessingFunctions: Set; -}; + +type RefAccessType = {kind: 'None'} | RefAccessRefType; + +type RefAccessRefType = + | {kind: 'Ref'} + | {kind: 'RefValue'; loc?: SourceLocation} + | {kind: 'Structure'; value: null | RefAccessRefType; fn: null | RefFnType}; + +type RefFnType = {readRefEffect: boolean; returnType: RefAccessType}; + +class Env extends Map { + #changed = false; + + resetChanged(): void { + this.#changed = false; + } + + hasChanged(): boolean { + return this.#changed; + } + + override set(key: IdentifierId, value: RefAccessType): this { + const cur = this.get(key); + const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'}); + if ( + !(cur == null && widenedValue.kind === 'None') && + (cur == null || !tyEqual(cur, widenedValue)) + ) { + this.#changed = true; + } + return super.set(key, widenedValue); + } +} export function validateNoRefAccessInRender(fn: HIRFunction): void { - const state = { - refs: new Set(), - refValues: new Map(), - refAccessingFunctions: new Set(), - }; - validateNoRefAccessInRenderImpl(fn, state).unwrap(); + const env = new Env(); + validateNoRefAccessInRenderImpl(fn, env).unwrap(); +} + +function refTypeOfType(identifier: Identifier): RefAccessType { + if (isRefValueType(identifier)) { + return {kind: 'RefValue'}; + } else if (isUseRefType(identifier)) { + return {kind: 'Ref'}; + } else { + return {kind: 'None'}; + } +} + +function tyEqual(a: RefAccessType, b: RefAccessType): boolean { + if (a.kind !== b.kind) { + return false; + } + switch (a.kind) { + case 'None': + return true; + case 'Ref': + return true; + case 'RefValue': + CompilerError.invariant(b.kind === 'RefValue', { + reason: 'Expected ref value', + loc: null, + }); + return a.loc == b.loc; + case 'Structure': { + CompilerError.invariant(b.kind === 'Structure', { + reason: 'Expected structure', + loc: null, + }); + const fnTypesEqual = + (a.fn === null && b.fn === null) || + (a.fn !== null && + b.fn !== null && + a.fn.readRefEffect === b.fn.readRefEffect && + tyEqual(a.fn.returnType, b.fn.returnType)); + return ( + fnTypesEqual && + (a.value === b.value || + (a.value !== null && b.value !== null && tyEqual(a.value, b.value))) + ); + } + } +} + +function joinRefAccessTypes(...types: Array): RefAccessType { + function joinRefAccessRefTypes( + a: RefAccessRefType, + b: RefAccessRefType, + ): RefAccessRefType { + if (a.kind === 'RefValue') { + return a; + } else if (b.kind === 'RefValue') { + return b; + } else if (a.kind === 'Ref' || b.kind === 'Ref') { + return {kind: 'Ref'}; + } else { + CompilerError.invariant( + a.kind === 'Structure' && b.kind === 'Structure', + { + reason: 'Expected structure', + loc: null, + }, + ); + const fn = + a.fn === null + ? b.fn + : b.fn === null + ? a.fn + : { + readRefEffect: a.fn.readRefEffect || b.fn.readRefEffect, + returnType: joinRefAccessTypes( + a.fn.returnType, + b.fn.returnType, + ), + }; + const value = + a.value === null + ? b.value + : b.value === null + ? a.value + : joinRefAccessRefTypes(a.value, b.value); + return { + kind: 'Structure', + fn, + value, + }; + } + } + + return types.reduce( + (a, b) => { + if (a.kind === 'None') { + return b; + } else if (b.kind === 'None') { + return a; + } else { + return joinRefAccessRefTypes(a, b); + } + }, + {kind: 'None'}, + ); } function validateNoRefAccessInRenderImpl( fn: HIRFunction, - state: State, -): Result { + env: Env, +): Result { + let returnValues: Array = []; let place; for (const param of fn.params) { if (param.kind === 'Identifier') { @@ -68,293 +199,289 @@ function validateNoRefAccessInRenderImpl( } else { place = param.place; } - - if (isRefValueType(place.identifier)) { - state.refValues.set(place.identifier.id, null); - } - if (isUseRefType(place.identifier)) { - state.refs.add(place.identifier.id); - } + const type = refTypeOfType(place.identifier); + env.set(place.identifier.id, type); } - const errors = new CompilerError(); - for (const [, block] of fn.body.blocks) { - for (const phi of block.phis) { - phi.operands.forEach(operand => { - if (state.refs.has(operand.id) || isUseRefType(phi.id)) { - state.refs.add(phi.id.id); - } - const refValue = state.refValues.get(operand.id); - if (refValue !== undefined || isRefValueType(operand)) { - state.refValues.set( - phi.id.id, - refValue ?? state.refValues.get(phi.id.id) ?? null, - ); - } - if (state.refAccessingFunctions.has(operand.id)) { - state.refAccessingFunctions.add(phi.id.id); - } - }); - } - for (const instr of block.instructions) { - for (const operand of eachInstructionValueOperand(instr.value)) { - if (isRefValueType(operand.identifier)) { - CompilerError.invariant(state.refValues.has(operand.identifier.id), { - reason: 'Expected ref value to be in state', - loc: operand.loc, - }); - } - if (isUseRefType(operand.identifier)) { - CompilerError.invariant(state.refs.has(operand.identifier.id), { - reason: 'Expected ref to be in state', - loc: operand.loc, - }); - } + for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) { + env.resetChanged(); + returnValues = []; + const errors = new CompilerError(); + for (const [, block] of fn.body.blocks) { + for (const phi of block.phis) { + env.set( + phi.id.id, + joinRefAccessTypes( + ...Array(...phi.operands.values()).map( + operand => env.get(operand.id) ?? ({kind: 'None'} as const), + ), + ), + ); } - switch (instr.value.kind) { - case 'JsxExpression': - case 'JsxFragment': { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoDirectRefValueAccess(errors, operand, state); - } - break; - } - case 'ComputedLoad': - case 'PropertyLoad': { - if (typeof instr.value.property !== 'string') { - validateNoRefValueAccess(errors, state, instr.value.property); - } - if ( - state.refAccessingFunctions.has(instr.value.object.identifier.id) - ) { - state.refAccessingFunctions.add(instr.lvalue.identifier.id); - } - if (state.refs.has(instr.value.object.identifier.id)) { - /* - * Once an object contains a ref at any level, we treat it as a ref. - * If we look something up from it, that value may either be a ref - * or the ref value (or neither), so we conservatively assume it's both. - */ - state.refs.add(instr.lvalue.identifier.id); - state.refValues.set(instr.lvalue.identifier.id, instr.loc); - } - break; - } - case 'LoadContext': - case 'LoadLocal': { - if ( - state.refAccessingFunctions.has(instr.value.place.identifier.id) - ) { - state.refAccessingFunctions.add(instr.lvalue.identifier.id); - } - const refValue = state.refValues.get(instr.value.place.identifier.id); - if (refValue !== undefined) { - state.refValues.set(instr.lvalue.identifier.id, refValue); + for (const instr of block.instructions) { + switch (instr.value.kind) { + case 'JsxExpression': + case 'JsxFragment': { + for (const operand of eachInstructionValueOperand(instr.value)) { + validateNoDirectRefValueAccess(errors, operand, env); + } + break; } - if (state.refs.has(instr.value.place.identifier.id)) { - state.refs.add(instr.lvalue.identifier.id); + case 'ComputedLoad': + case 'PropertyLoad': { + if (typeof instr.value.property !== 'string') { + validateNoDirectRefValueAccess(errors, instr.value.property, env); + } + const objType = env.get(instr.value.object.identifier.id); + let lookupType: null | RefAccessType = null; + if (objType?.kind === 'Structure') { + lookupType = objType.value; + } else if (objType?.kind === 'Ref') { + lookupType = {kind: 'RefValue', loc: instr.loc}; + } + env.set( + instr.lvalue.identifier.id, + lookupType ?? refTypeOfType(instr.lvalue.identifier), + ); + break; } - break; - } - case 'StoreContext': - case 'StoreLocal': { - if ( - state.refAccessingFunctions.has(instr.value.value.identifier.id) - ) { - state.refAccessingFunctions.add( - instr.value.lvalue.place.identifier.id, + case 'LoadContext': + case 'LoadLocal': { + env.set( + instr.lvalue.identifier.id, + env.get(instr.value.place.identifier.id) ?? + refTypeOfType(instr.lvalue.identifier), ); - state.refAccessingFunctions.add(instr.lvalue.identifier.id); + break; } - const refValue = state.refValues.get(instr.value.value.identifier.id); - if ( - refValue !== undefined || - isRefValueType(instr.value.lvalue.place.identifier) - ) { - state.refValues.set( + case 'StoreContext': + case 'StoreLocal': { + env.set( instr.value.lvalue.place.identifier.id, - refValue ?? null, + env.get(instr.value.value.identifier.id) ?? + refTypeOfType(instr.value.lvalue.place.identifier), + ); + env.set( + instr.lvalue.identifier.id, + env.get(instr.value.value.identifier.id) ?? + refTypeOfType(instr.lvalue.identifier), ); - state.refValues.set(instr.lvalue.identifier.id, refValue ?? null); + break; } - if (state.refs.has(instr.value.value.identifier.id)) { - state.refs.add(instr.value.lvalue.place.identifier.id); - state.refs.add(instr.lvalue.identifier.id); + case 'Destructure': { + const objType = env.get(instr.value.value.identifier.id); + let lookupType = null; + if (objType?.kind === 'Structure') { + lookupType = objType.value; + } + env.set( + instr.lvalue.identifier.id, + lookupType ?? refTypeOfType(instr.lvalue.identifier), + ); + for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) { + env.set( + lval.identifier.id, + lookupType ?? refTypeOfType(lval.identifier), + ); + } + break; } - break; - } - case 'Destructure': { - const destructuredFunction = state.refAccessingFunctions.has( - instr.value.value.identifier.id, - ); - const destructuredRef = state.refs.has( - instr.value.value.identifier.id, - ); - for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) { - if (isUseRefType(lval.identifier)) { - state.refs.add(lval.identifier.id); + case 'ObjectMethod': + case 'FunctionExpression': { + let returnType: RefAccessType = {kind: 'None'}; + let readRefEffect = false; + const result = validateNoRefAccessInRenderImpl( + instr.value.loweredFunc.func, + env, + ); + if (result.isOk()) { + returnType = result.unwrap(); + } else if (result.isErr()) { + readRefEffect = true; } - if (destructuredRef || isRefValueType(lval.identifier)) { - state.refs.add(lval.identifier.id); - state.refValues.set(lval.identifier.id, null); + env.set(instr.lvalue.identifier.id, { + kind: 'Structure', + fn: { + readRefEffect, + returnType, + }, + value: null, + }); + break; + } + case 'MethodCall': { + if (!isEffectHook(instr.value.property.identifier)) { + for (const operand of eachInstructionValueOperand(instr.value)) { + const hookKind = getHookKindForType( + fn.env, + instr.value.property.identifier.type, + ); + if (hookKind != null) { + validateNoRefValueAccess(errors, env, operand); + } else { + validateNoRefAccess(errors, env, operand, operand.loc); + } + } } - if (destructuredFunction) { - state.refAccessingFunctions.add(lval.identifier.id); + validateNoRefValueAccess(errors, env, instr.value.receiver); + const methType = env.get(instr.value.property.identifier.id); + let returnType: RefAccessType = {kind: 'None'}; + if (methType?.kind === 'Structure' && methType.fn !== null) { + returnType = methType.fn.returnType; } + env.set(instr.lvalue.identifier.id, returnType); + break; } - break; - } - case 'ObjectMethod': - case 'FunctionExpression': { - if ( - /* - * check if the function expression accesses a ref *or* some other - * function which accesses a ref - */ - [...eachInstructionValueOperand(instr.value)].some( - operand => - state.refValues.has(operand.identifier.id) || - state.refAccessingFunctions.has(operand.identifier.id), - ) || - // check for cases where .current is accessed through an aliased ref - ([...eachInstructionValueOperand(instr.value)].some(operand => - state.refs.has(operand.identifier.id), - ) && - validateNoRefAccessInRenderImpl( - instr.value.loweredFunc.func, - state, - ).isErr()) - ) { - // This function expression unconditionally accesses a ref - state.refAccessingFunctions.add(instr.lvalue.identifier.id); + case 'CallExpression': { + const callee = instr.value.callee; + const hookKind = getHookKindForType(fn.env, callee.identifier.type); + const isUseEffect = isEffectHook(callee.identifier); + let returnType: RefAccessType = {kind: 'None'}; + if (!isUseEffect) { + // Report a more precise error when calling a local function that accesses a ref + const fnType = env.get(instr.value.callee.identifier.id); + if (fnType?.kind === 'Structure' && fnType.fn !== null) { + returnType = fnType.fn.returnType; + if (fnType.fn.readRefEffect) { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: + 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', + loc: callee.loc, + description: + callee.identifier.name !== null && + callee.identifier.name.kind === 'named' + ? `Function \`${callee.identifier.name.value}\` accesses a ref` + : null, + suggestions: null, + }); + } + } + for (const operand of eachInstructionValueOperand(instr.value)) { + if (hookKind != null) { + validateNoRefValueAccess(errors, env, operand); + } else { + validateNoRefAccess(errors, env, operand, operand.loc); + } + } + } + env.set(instr.lvalue.identifier.id, returnType); + break; } - break; - } - case 'MethodCall': { - if (!isEffectHook(instr.value.property.identifier)) { + case 'ObjectExpression': + case 'ArrayExpression': { + const types: Array = []; for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefAccess(errors, state, operand, operand.loc); + validateNoDirectRefValueAccess(errors, operand, env); + types.push(env.get(operand.identifier.id) ?? {kind: 'None'}); } - } - break; - } - case 'CallExpression': { - const callee = instr.value.callee; - const isUseEffect = isEffectHook(callee.identifier); - if (!isUseEffect) { - // Report a more precise error when calling a local function that accesses a ref - if (state.refAccessingFunctions.has(callee.identifier.id)) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: callee.loc, - description: - callee.identifier.name !== null && - callee.identifier.name.kind === 'named' - ? `Function \`${callee.identifier.name.value}\` accesses a ref` - : null, - suggestions: null, + const value = joinRefAccessTypes(...types); + if (value.kind === 'None') { + env.set(instr.lvalue.identifier.id, {kind: 'None'}); + } else { + env.set(instr.lvalue.identifier.id, { + kind: 'Structure', + value, + fn: null, }); } + break; + } + case 'PropertyDelete': + case 'PropertyStore': + case 'ComputedDelete': + case 'ComputedStore': { + validateNoRefAccess(errors, env, instr.value.object, instr.loc); for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefAccess( - errors, - state, - operand, - state.refValues.get(operand.identifier.id) ?? operand.loc, - ); + if (operand === instr.value.object) { + continue; + } + validateNoRefValueAccess(errors, env, operand); } + break; } - break; - } - case 'ObjectExpression': - case 'ArrayExpression': { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoDirectRefValueAccess(errors, operand, state); - if (state.refAccessingFunctions.has(operand.identifier.id)) { - state.refAccessingFunctions.add(instr.lvalue.identifier.id); - } - if (state.refs.has(operand.identifier.id)) { - state.refs.add(instr.lvalue.identifier.id); - } - const refValue = state.refValues.get(operand.identifier.id); - if (refValue !== undefined) { - state.refValues.set(instr.lvalue.identifier.id, refValue); + case 'StartMemoize': + case 'FinishMemoize': + break; + default: { + for (const operand of eachInstructionValueOperand(instr.value)) { + validateNoRefValueAccess(errors, env, operand); } + break; } - break; } - case 'PropertyDelete': - case 'PropertyStore': - case 'ComputedDelete': - case 'ComputedStore': { - validateNoRefAccess( - errors, - state, - instr.value.object, - state.refValues.get(instr.value.object.identifier.id) ?? instr.loc, + if (isUseRefType(instr.lvalue.identifier)) { + env.set( + instr.lvalue.identifier.id, + joinRefAccessTypes( + env.get(instr.lvalue.identifier.id) ?? {kind: 'None'}, + {kind: 'Ref'}, + ), ); - for (const operand of eachInstructionValueOperand(instr.value)) { - if (operand === instr.value.object) { - continue; - } - validateNoRefValueAccess(errors, state, operand); - } - break; } - case 'StartMemoize': - case 'FinishMemoize': - break; - default: { - for (const operand of eachInstructionValueOperand(instr.value)) { - validateNoRefValueAccess(errors, state, operand); - } - break; + if (isRefValueType(instr.lvalue.identifier)) { + env.set( + instr.lvalue.identifier.id, + joinRefAccessTypes( + env.get(instr.lvalue.identifier.id) ?? {kind: 'None'}, + {kind: 'RefValue', loc: instr.loc}, + ), + ); } } - if (isUseRefType(instr.lvalue.identifier)) { - state.refs.add(instr.lvalue.identifier.id); - } - if ( - isRefValueType(instr.lvalue.identifier) && - !state.refValues.has(instr.lvalue.identifier.id) - ) { - state.refValues.set(instr.lvalue.identifier.id, instr.loc); + for (const operand of eachTerminalOperand(block.terminal)) { + if (block.terminal.kind !== 'return') { + validateNoRefValueAccess(errors, env, operand); + } else { + // Allow functions containing refs to be returned, but not direct ref values + validateNoDirectRefValueAccess(errors, operand, env); + returnValues.push(env.get(operand.identifier.id)); + } } } - for (const operand of eachTerminalOperand(block.terminal)) { - if (block.terminal.kind !== 'return') { - validateNoRefValueAccess(errors, state, operand); - } else { - // Allow functions containing refs to be returned, but not direct ref values - validateNoDirectRefValueAccess(errors, operand, state); - } + + if (errors.hasErrors()) { + return Err(errors); } } - if (errors.hasErrors()) { - return Err(errors); - } else { - return Ok(undefined); + CompilerError.invariant(!env.hasChanged(), { + reason: 'Ref type environment did not converge', + loc: null, + }); + + return Ok( + joinRefAccessTypes( + ...returnValues.filter((env): env is RefAccessType => env !== undefined), + ), + ); +} + +function destructure( + type: RefAccessType | undefined, +): RefAccessType | undefined { + if (type?.kind === 'Structure' && type.value !== null) { + return destructure(type.value); } + return type; } function validateNoRefValueAccess( errors: CompilerError, - state: State, + env: Env, operand: Place, ): void { + const type = destructure(env.get(operand.identifier.id)); if ( - state.refValues.has(operand.identifier.id) || - state.refAccessingFunctions.has(operand.identifier.id) + type?.kind === 'RefValue' || + (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { errors.push({ severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: state.refValues.get(operand.identifier.id) ?? operand.loc, + loc: (type.kind === 'RefValue' && type.loc) || operand.loc, description: operand.identifier.name !== null && operand.identifier.name.kind === 'named' @@ -367,20 +494,21 @@ function validateNoRefValueAccess( function validateNoRefAccess( errors: CompilerError, - state: State, + env: Env, operand: Place, loc: SourceLocation, ): void { + const type = destructure(env.get(operand.identifier.id)); if ( - state.refs.has(operand.identifier.id) || - state.refValues.has(operand.identifier.id) || - state.refAccessingFunctions.has(operand.identifier.id) + type?.kind === 'Ref' || + type?.kind === 'RefValue' || + (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { errors.push({ severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: loc, + loc: (type.kind === 'RefValue' && type.loc) || loc, description: operand.identifier.name !== null && operand.identifier.name.kind === 'named' @@ -394,14 +522,15 @@ function validateNoRefAccess( function validateNoDirectRefValueAccess( errors: CompilerError, operand: Place, - state: State, + env: Env, ): void { - if (state.refValues.has(operand.identifier.id)) { + const type = destructure(env.get(operand.identifier.id)); + if (type?.kind === 'RefValue') { errors.push({ severity: ErrorSeverity.InvalidReact, reason: 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: state.refValues.get(operand.identifier.id) ?? operand.loc, + loc: type.loc ?? operand.loc, description: operand.identifier.name !== null && operand.identifier.name.kind === 'named' diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md new file mode 100644 index 0000000000000..b7371108d5867 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md @@ -0,0 +1,69 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left'), + }; + const moveRight = { + handler: handleKey('right'), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; +import { addOne } from "shared-runtime"; + +function useKeyCommand() { + const $ = _c(1); + const currentPosition = useRef(0); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const handleKey = (direction) => () => { + const position = currentPosition.current; + const nextPosition = direction === "left" ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + + const moveLeft = { handler: handleKey("left") }; + + const moveRight = { handler: handleKey("right") }; + + t0 = [moveLeft, moveRight]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + +### Eval output +(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.tsx similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md similarity index 75% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md index 52350036257d0..cff34e3449376 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-later-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md @@ -13,10 +13,10 @@ function useKeyCommand() { currentPosition.current = nextPosition; }; const moveLeft = { - handler: handleKey('left'), + handler: handleKey('left')(), }; const moveRight = { - handler: handleKey('right'), + handler: handleKey('right')(), }; return [moveLeft, moveRight]; } @@ -34,8 +34,8 @@ export const FIXTURE_ENTRYPOINT = { ``` 10 | }; 11 | const moveLeft = { -> 12 | handler: handleKey('left'), - | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) +> 12 | handler: handleKey('left')(), + | ^^^^^^^^^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) @@ -44,7 +44,7 @@ InvalidReact: This function accesses a ref value (the `current` property), which InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) 13 | }; 14 | const moveRight = { - 15 | handler: handleKey('right'), + 15 | handler: handleKey('right')(), ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.tsx new file mode 100644 index 0000000000000..41e117ed15e80 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.tsx @@ -0,0 +1,23 @@ +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left')(), + }; + const moveRight = { + handler: handleKey('right')(), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md new file mode 100644 index 0000000000000..54184be0f3f38 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md @@ -0,0 +1,70 @@ + +## Input + +```javascript +// @enableReactiveScopesInHIR:false +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left'), + }; + const moveRight = { + handler: handleKey('right'), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableReactiveScopesInHIR:false +import { useRef } from "react"; +import { addOne } from "shared-runtime"; + +function useKeyCommand() { + const $ = _c(1); + const currentPosition = useRef(0); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const handleKey = (direction) => () => { + const position = currentPosition.current; + const nextPosition = direction === "left" ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + + const moveLeft = { handler: handleKey("left") }; + + const moveRight = { handler: handleKey("right") }; + + t0 = [moveLeft, moveRight]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; + +``` + +### Eval output +(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx new file mode 100644 index 0000000000000..6f27dfe07fb33 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx @@ -0,0 +1,24 @@ +// @enableReactiveScopesInHIR:false +import {useRef} from 'react'; +import {addOne} from 'shared-runtime'; + +function useKeyCommand() { + const currentPosition = useRef(0); + const handleKey = direction => () => { + const position = currentPosition.current; + const nextPosition = direction === 'left' ? addOne(position) : position; + currentPosition.current = nextPosition; + }; + const moveLeft = { + handler: handleKey('left'), + }; + const moveRight = { + handler: handleKey('right'), + }; + return [moveLeft, moveRight]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useKeyCommand, + params: [], +}; From e78c9362c014dccaed5ff193106e44d7d072dc32 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Mon, 16 Sep 2024 10:53:34 -0700 Subject: [PATCH 154/191] [compiler] Allow all hooks to take callbacks which access refs, but ban hooks from taking direct ref value arguments Summary: This brings the behavior of ref mutation within hook callbacks into alignment with the behavior of global mutations--that is, we allow all hooks to take callbacks that may mutate a ref. This is potentially unsafe if the hook eagerly calls its callback, but the alternative is excessively limiting (and inconsistent with other enforcement). This also bans *directly* passing a ref.current value to a hook, which was previously allowed. ghstack-source-id: e66ce7123ecf4a905adab957970d0ee5d41245e0 Pull Request resolved: https://github.com/facebook/react/pull/30917 --- .../src/HIR/Globals.ts | 11 +++ .../src/HIR/ObjectShape.ts | 1 + .../Validation/ValidateNoRefAccesInRender.ts | 78 +++++++------------ .../compiler/error.hook-ref-value.expect.md | 34 ++++++++ .../fixtures/compiler/error.hook-ref-value.js | 11 +++ .../compiler/hook-ref-callback.expect.md | 54 +++++++++++++ .../fixtures/compiler/hook-ref-callback.js | 15 ++++ .../useImperativeHandle-ref-mutate.expect.md | 66 ++++++++++++++++ .../useImperativeHandle-ref-mutate.js | 19 +++++ 9 files changed, 238 insertions(+), 51 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index c923882900cc2..1128c51caeb58 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -364,6 +364,17 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Mutable, }), ], + [ + 'useImperativeHandle', + addHook(DEFAULT_SHAPES, { + positionalParams: [], + restParam: Effect.Freeze, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + hookKind: 'useImperativeHandle', + returnValueKind: ValueKind.Frozen, + }), + ], [ 'useMemo', addHook(DEFAULT_SHAPES, { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 04f85e496453a..14f809f2c4082 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -127,6 +127,7 @@ export type HookKind = | 'useMemo' | 'useCallback' | 'useTransition' + | 'useImperativeHandle' | 'Custom'; /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index 8fee651f8d1ab..5f98b0ee8e4af 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -22,7 +22,6 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; -import {isEffectHook} from './ValidateMemoizedEffectDependencies'; /** * Validates that a function does not access a ref value during render. This includes a partial check @@ -310,60 +309,37 @@ function validateNoRefAccessInRenderImpl( }); break; } - case 'MethodCall': { - if (!isEffectHook(instr.value.property.identifier)) { - for (const operand of eachInstructionValueOperand(instr.value)) { - const hookKind = getHookKindForType( - fn.env, - instr.value.property.identifier.type, - ); - if (hookKind != null) { - validateNoRefValueAccess(errors, env, operand); - } else { - validateNoRefAccess(errors, env, operand, operand.loc); - } - } - } - validateNoRefValueAccess(errors, env, instr.value.receiver); - const methType = env.get(instr.value.property.identifier.id); - let returnType: RefAccessType = {kind: 'None'}; - if (methType?.kind === 'Structure' && methType.fn !== null) { - returnType = methType.fn.returnType; - } - env.set(instr.lvalue.identifier.id, returnType); - break; - } + case 'MethodCall': case 'CallExpression': { - const callee = instr.value.callee; + const callee = + instr.value.kind === 'CallExpression' + ? instr.value.callee + : instr.value.property; const hookKind = getHookKindForType(fn.env, callee.identifier.type); - const isUseEffect = isEffectHook(callee.identifier); let returnType: RefAccessType = {kind: 'None'}; - if (!isUseEffect) { - // Report a more precise error when calling a local function that accesses a ref - const fnType = env.get(instr.value.callee.identifier.id); - if (fnType?.kind === 'Structure' && fnType.fn !== null) { - returnType = fnType.fn.returnType; - if (fnType.fn.readRefEffect) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: callee.loc, - description: - callee.identifier.name !== null && - callee.identifier.name.kind === 'named' - ? `Function \`${callee.identifier.name.value}\` accesses a ref` - : null, - suggestions: null, - }); - } + const fnType = env.get(callee.identifier.id); + if (fnType?.kind === 'Structure' && fnType.fn !== null) { + returnType = fnType.fn.returnType; + if (fnType.fn.readRefEffect) { + errors.push({ + severity: ErrorSeverity.InvalidReact, + reason: + 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', + loc: callee.loc, + description: + callee.identifier.name !== null && + callee.identifier.name.kind === 'named' + ? `Function \`${callee.identifier.name.value}\` accesses a ref` + : null, + suggestions: null, + }); } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (hookKind != null) { - validateNoRefValueAccess(errors, env, operand); - } else { - validateNoRefAccess(errors, env, operand, operand.loc); - } + } + for (const operand of eachInstructionValueOperand(instr.value)) { + if (hookKind != null) { + validateNoDirectRefValueAccess(errors, operand, env); + } else { + validateNoRefAccess(errors, env, operand, operand.loc); } } env.set(instr.lvalue.identifier.id, returnType); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md new file mode 100644 index 0000000000000..d92d918fe9f3c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +import {useEffect, useRef} from 'react'; + +function Component(props) { + const ref = useRef(); + useEffect(() => {}, [ref.current]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + + +## Error + +``` + 3 | function Component(props) { + 4 | const ref = useRef(); +> 5 | useEffect(() => {}, [ref.current]); + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) + +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) + 6 | } + 7 | + 8 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.js new file mode 100644 index 0000000000000..3276e4b4b44a9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.js @@ -0,0 +1,11 @@ +import {useEffect, useRef} from 'react'; + +function Component(props) { + const ref = useRef(); + useEffect(() => {}, [ref.current]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.expect.md new file mode 100644 index 0000000000000..3056a60a3a977 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +import {useEffect, useRef} from 'react'; + +function Component(props) { + const ref = useRef(); + useFoo(() => { + ref.current = 42; + }); +} + +function useFoo(x) {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useEffect, useRef } from "react"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + ref.current = 42; + }; + $[0] = t0; + } else { + t0 = $[0]; + } + useFoo(t0); +} + +function useFoo(x) {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.js new file mode 100644 index 0000000000000..ab496e0adf012 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.js @@ -0,0 +1,15 @@ +import {useEffect, useRef} from 'react'; + +function Component(props) { + const ref = useRef(); + useFoo(() => { + ref.current = 42; + }); +} + +function useFoo(x) {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.expect.md new file mode 100644 index 0000000000000..30d96038d43ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @flow + +import {useImperativeHandle, useRef} from 'react'; + +component Component(prop: number) { + const ref1 = useRef(null); + const ref2 = useRef(1); + useImperativeHandle(ref1, () => { + const precomputed = prop + ref2.current; + return { + foo: () => prop + ref2.current + precomputed, + }; + }, [prop]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { useImperativeHandle, useRef } from "react"; + +function Component(t0) { + const $ = _c(3); + const { prop } = t0; + const ref1 = useRef(null); + const ref2 = useRef(1); + let t1; + let t2; + if ($[0] !== prop) { + t1 = () => { + const precomputed = prop + ref2.current; + return { foo: () => prop + ref2.current + precomputed }; + }; + + t2 = [prop]; + $[0] = prop; + $[1] = t1; + $[2] = t2; + } else { + t1 = $[1]; + t2 = $[2]; + } + useImperativeHandle(ref1, t1, t2); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prop: 1 }], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.js new file mode 100644 index 0000000000000..63d090ddd8164 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.js @@ -0,0 +1,19 @@ +// @flow + +import {useImperativeHandle, useRef} from 'react'; + +component Component(prop: number) { + const ref1 = useRef(null); + const ref2 = useRef(1); + useImperativeHandle(ref1, () => { + const precomputed = prop + ref2.current; + return { + foo: () => prop + ref2.current + precomputed, + }; + }, [prop]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: 1}], +}; From d7167c35059bc6a0ad84eb34e65b3b66328d5dd8 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Mon, 16 Sep 2024 11:12:58 -0700 Subject: [PATCH 155/191] [compiler] Implement support for hoisted and recursive functions Summary: Introduces a new binding kind for functions that allows them to be hoisted. Also has the result of causing all nested function declarations to be outputted as function declarations, not as let bindings. ghstack-source-id: fa40d4909fb3d30c23691e36510ebb3c3cc41053 Pull Request resolved: https://github.com/facebook/react/pull/30922 --- .../src/HIR/BuildHIR.ts | 30 ++++--- .../src/HIR/HIR.ts | 6 +- .../src/HIR/PrintHIR.ts | 6 ++ .../ReactiveScopes/CodegenReactiveFunction.ts | 85 +++++++++---------- .../ReactiveScopes/PruneHoistedContexts.ts | 11 +++ ...ror.hoisted-function-declaration.expect.md | 29 ------- .../error.hoisted-function-declaration.js | 8 -- ...ting-simple-function-declaration.expect.md | 14 +-- .../error.todo-hoist-function-decls.expect.md | 14 +-- ...do-recursive-function-expression.expect.md | 30 ------- .../hoisted-function-declaration.expect.md | 63 ++++++++++++++ .../compiler/hoisted-function-declaration.js | 19 +++++ .../recursive-function-expression.expect.md | 53 ++++++++++++ ...on.js => recursive-function-expression.js} | 5 ++ 14 files changed, 234 insertions(+), 139 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.expect.md rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.todo-recursive-function-expression.js => recursive-function-expression.js} (67%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 7fb12d4624c10..a179224a7705f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -420,7 +420,19 @@ function lowerStatement( // Already hoisted continue; } - if (!binding.path.isVariableDeclarator()) { + + let kind: + | InstructionKind.Let + | InstructionKind.HoistedConst + | InstructionKind.HoistedLet + | InstructionKind.HoistedFunction; + if (binding.kind === 'const' || binding.kind === 'var') { + kind = InstructionKind.HoistedConst; + } else if (binding.kind === 'let') { + kind = InstructionKind.HoistedLet; + } else if (binding.path.isFunctionDeclaration()) { + kind = InstructionKind.HoistedFunction; + } else if (!binding.path.isVariableDeclarator()) { builder.errors.push({ severity: ErrorSeverity.Todo, reason: 'Unsupported declaration type for hoisting', @@ -429,11 +441,7 @@ function lowerStatement( loc: id.parentPath.node.loc ?? GeneratedSource, }); continue; - } else if ( - binding.kind !== 'const' && - binding.kind !== 'var' && - binding.kind !== 'let' - ) { + } else { builder.errors.push({ severity: ErrorSeverity.Todo, reason: 'Handle non-const declarations for hoisting', @@ -443,6 +451,7 @@ function lowerStatement( }); continue; } + const identifier = builder.resolveIdentifier(id); CompilerError.invariant(identifier.kind === 'Identifier', { reason: @@ -456,13 +465,6 @@ function lowerStatement( reactive: false, loc: id.node.loc ?? GeneratedSource, }; - const kind = - // Avoid double errors on var declarations, which we do not plan to support anyways - binding.kind === 'const' || binding.kind === 'var' - ? InstructionKind.HoistedConst - : binding.kind === 'let' - ? InstructionKind.HoistedLet - : assertExhaustive(binding.kind, 'Unexpected binding kind'); lowerValueToTemporary(builder, { kind: 'DeclareContext', lvalue: { @@ -999,7 +1001,7 @@ function lowerStatement( lowerAssignment( builder, stmt.node.loc ?? GeneratedSource, - InstructionKind.Let, + InstructionKind.Function, id, fn, 'Assignment', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 930dd79f2fd59..30408ab032b35 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -746,6 +746,9 @@ export enum InstructionKind { // hoisted const declarations HoistedLet = 'HoistedLet', + + HoistedFunction = 'HoistedFunction', + Function = 'Function', } function _staticInvariantInstructionValueHasLocation( @@ -865,7 +868,8 @@ export type InstructionValue = kind: | InstructionKind.Let | InstructionKind.HoistedConst - | InstructionKind.HoistedLet; + | InstructionKind.HoistedLet + | InstructionKind.HoistedFunction; place: Place; }; loc: SourceLocation; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index c2db20c5099a1..c88d3bf773898 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -765,6 +765,12 @@ export function printLValue(lval: LValue): string { case InstructionKind.HoistedLet: { return `HoistedLet ${lvalue}$`; } + case InstructionKind.Function: { + return `Function ${lvalue}$`; + } + case InstructionKind.HoistedFunction: { + return `HoistedFunction ${lvalue}$`; + } default: { assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 2df7b5ed1c7fd..297c7712546c9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -981,22 +981,12 @@ function codegenTerminal( suggestions: null, }); case InstructionKind.Catch: - CompilerError.invariant(false, { - reason: 'Unexpected catch variable as for..in collection', - description: null, - loc: iterableItem.loc, - suggestions: null, - }); case InstructionKind.HoistedConst: - CompilerError.invariant(false, { - reason: 'Unexpected HoistedConst variable in for..in collection', - description: null, - loc: iterableItem.loc, - suggestions: null, - }); case InstructionKind.HoistedLet: + case InstructionKind.HoistedFunction: + case InstructionKind.Function: CompilerError.invariant(false, { - reason: 'Unexpected HoistedLet variable in for..in collection', + reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`, description: null, loc: iterableItem.loc, suggestions: null, @@ -1075,30 +1065,13 @@ function codegenTerminal( varDeclKind = 'let' as const; break; case InstructionKind.Reassign: - CompilerError.invariant(false, { - reason: - 'Destructure should never be Reassign as it would be an Object/ArrayPattern', - description: null, - loc: iterableItem.loc, - suggestions: null, - }); case InstructionKind.Catch: - CompilerError.invariant(false, { - reason: 'Unexpected catch variable as for..of collection', - description: null, - loc: iterableItem.loc, - suggestions: null, - }); case InstructionKind.HoistedConst: - CompilerError.invariant(false, { - reason: 'Unexpected HoistedConst variable in for..of collection', - description: null, - loc: iterableItem.loc, - suggestions: null, - }); case InstructionKind.HoistedLet: + case InstructionKind.HoistedFunction: + case InstructionKind.Function: CompilerError.invariant(false, { - reason: 'Unexpected HoistedLet variable in for..of collection', + reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`, description: null, loc: iterableItem.loc, suggestions: null, @@ -1261,6 +1234,35 @@ function codegenInstructionNullable( t.variableDeclarator(codegenLValue(cx, lvalue), value), ]); } + case InstructionKind.Function: { + CompilerError.invariant(instr.lvalue === null, { + reason: `Function declaration cannot be referenced as an expression`, + description: null, + loc: instr.value.loc, + suggestions: null, + }); + const genLvalue = codegenLValue(cx, lvalue); + CompilerError.invariant(genLvalue.type === 'Identifier', { + reason: 'Expected an identifier as a function declaration lvalue', + description: null, + loc: instr.value.loc, + suggestions: null, + }); + CompilerError.invariant(value?.type === 'FunctionExpression', { + reason: 'Expected a function as a function declaration value', + description: null, + loc: instr.value.loc, + suggestions: null, + }); + return createFunctionDeclaration( + instr.loc, + genLvalue, + value.params, + value.body, + value.generator, + value.async, + ); + } case InstructionKind.Let: { CompilerError.invariant(instr.lvalue === null, { reason: `Const declaration cannot be referenced as an expression`, @@ -1303,19 +1305,11 @@ function codegenInstructionNullable( case InstructionKind.Catch: { return t.emptyStatement(); } - case InstructionKind.HoistedLet: { - CompilerError.invariant(false, { - reason: - 'Expected HoistedLet to have been pruned in PruneHoistedContexts', - description: null, - loc: instr.loc, - suggestions: null, - }); - } - case InstructionKind.HoistedConst: { + case InstructionKind.HoistedLet: + case InstructionKind.HoistedConst: + case InstructionKind.HoistedFunction: { CompilerError.invariant(false, { - reason: - 'Expected HoistedConsts to have been pruned in PruneHoistedContexts', + reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`, description: null, loc: instr.loc, suggestions: null, @@ -1486,6 +1480,7 @@ const createBinaryExpression = withLoc(t.binaryExpression); const createExpressionStatement = withLoc(t.expressionStatement); const _createLabelledStatement = withLoc(t.labeledStatement); const createVariableDeclaration = withLoc(t.variableDeclaration); +const createFunctionDeclaration = withLoc(t.functionDeclaration); const _createWhileStatement = withLoc(t.whileStatement); const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression); const createLogicalExpression = withLoc(t.logicalExpression); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts index 1df211afc3ae4..07b099c2ea5fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts @@ -57,6 +57,17 @@ class Visitor extends ReactiveFunctionTransform { return {kind: 'remove'}; } + if ( + instruction.value.kind === 'DeclareContext' && + instruction.value.lvalue.kind === 'HoistedFunction' + ) { + state.set( + instruction.value.lvalue.place.identifier.declarationId, + InstructionKind.Function, + ); + return {kind: 'remove'}; + } + if ( instruction.value.kind === 'StoreContext' && state.has(instruction.value.lvalue.place.identifier.declarationId) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md deleted file mode 100644 index d2e51d07256e6..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md +++ /dev/null @@ -1,29 +0,0 @@ - -## Input - -```javascript -function component(a) { - let t = {a}; - x(t); // hoisted call - function x(p) { - p.foo(); - } - return t; -} - -``` - - -## Error - -``` - 1 | function component(a) { - 2 | let t = {a}; -> 3 | x(t); // hoisted call - | ^^^^ Todo: Unsupported declaration type for hoisting. variable "x" declared with FunctionDeclaration (3:3) - 4 | function x(p) { - 5 | p.foo(); - 6 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js deleted file mode 100644 index 3b0586d4376e6..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js +++ /dev/null @@ -1,8 +0,0 @@ -function component(a) { - let t = {a}; - x(t); // hoisted call - function x(p) { - p.foo(); - } - return t; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md index 91f8e67d0c698..2045ee7901e96 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md @@ -24,13 +24,13 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 3 | return x; - 4 | } -> 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting - | ^^^^^ Todo: Unsupported declaration type for hoisting. variable "baz" declared with FunctionDeclaration (5:5) - 6 | function baz() { - 7 | return bar(); - 8 | } + 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting + 6 | function baz() { +> 7 | return bar(); + | ^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (7:7) + 8 | } + 9 | } + 10 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md index 02b2060e84e7e..b3aa848f1c745 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md @@ -16,11 +16,15 @@ function Component() { ``` 1 | function Component() { -> 2 | return get2(); - | ^^^^^^ Todo: Unsupported declaration type for hoisting. variable "get2" declared with FunctionDeclaration (2:2) - 3 | function get2() { - 4 | return 2; - 5 | } + 2 | return get2(); +> 3 | function get2() { + | ^^^^^^^^^^^^^^^^^ +> 4 | return 2; + | ^^^^^^^^^^^^^ +> 5 | } + | ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (3:5) + 6 | } + 7 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md deleted file mode 100644 index d4d09dfeae9b0..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md +++ /dev/null @@ -1,30 +0,0 @@ - -## Input - -```javascript -function Component() { - function callback(x) { - if (x == 0) { - return null; - } - return callback(x - 1); - } - return callback(10); -} - -``` - - -## Error - -``` - 4 | return null; - 5 | } -> 6 | return callback(x - 1); - | ^^^^^^^^^^^^^^^ Todo: Unsupported declaration type for hoisting. variable "callback" declared with FunctionDeclaration (6:6) - 7 | } - 8 | return callback(10); - 9 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.expect.md new file mode 100644 index 0000000000000..aa3246614ab41 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +function component(a) { + let t = {a}; + x(t); // hoisted call + function x(p) { + p.a.foo(); + } + return t; +} + +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: [ + { + foo: () => { + console.log(42); + }, + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function component(a) { + const $ = _c(2); + let t; + if ($[0] !== a) { + t = { a }; + x(t); + function x(p) { + p.a.foo(); + } + $[0] = a; + $[1] = t; + } else { + t = $[1]; + } + return t; +} + +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: [ + { + foo: () => { + console.log(42); + }, + }, + ], +}; + +``` + +### Eval output +(kind: ok) {"a":{"foo":"[[ function params=0 ]]"}} +logs: [42] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.js new file mode 100644 index 0000000000000..6fe6dc04cc97b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.js @@ -0,0 +1,19 @@ +function component(a) { + let t = {a}; + x(t); // hoisted call + function x(p) { + p.a.foo(); + } + return t; +} + +export const FIXTURE_ENTRYPOINT = { + fn: component, + params: [ + { + foo: () => { + console.log(42); + }, + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.expect.md new file mode 100644 index 0000000000000..bc46c1b85578b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +function Component() { + function callback(x) { + if (x == 0) { + return null; + } + return callback(x - 1); + } + return callback(10); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + function callback(x) { + if (x == 0) { + return null; + } + return callback(x - 1); + } + + t0 = callback(10); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.js similarity index 67% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.js index f1b95e5105761..5d50de75bae50 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.js @@ -7,3 +7,8 @@ function Component() { } return callback(10); } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; From 8152e5cd27721e792f395c0b62c8a7769a54777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 16 Sep 2024 15:00:17 -0400 Subject: [PATCH 156/191] Remove execution context check from shouldProfile (#30971) I don't know why this is here since all these callsites are within the CommitWork/CommitEffects helpers. This should help with inlining. --- .../react-reconciler/src/ReactFiberCommitEffects.js | 8 ++------ .../react-reconciler/src/ReactFiberCommitWork.js | 12 ++---------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index af5762df2110a..dde2479ba8638 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -46,9 +46,6 @@ import {getPublicInstance} from './ReactFiberConfig'; import { captureCommitPhaseError, setIsRunningInsertionEffect, - getExecutionContext, - CommitContext, - NoContext, } from './ReactFiberWorkLoop'; import { NoFlags as NoHookEffect, @@ -81,8 +78,7 @@ function shouldProfile(current: Fiber): boolean { return ( enableProfilerTimer && enableProfilerCommitHooks && - (current.mode & ProfileMode) !== NoMode && - (getExecutionContext() & CommitContext) !== NoContext + (current.mode & ProfileMode) !== NoMode ); } @@ -919,7 +915,7 @@ export function commitProfilerUpdate( commitTime: number, effectDuration: number, ) { - if (enableProfilerTimer && getExecutionContext() & CommitContext) { + if (enableProfilerTimer) { try { if (__DEV__) { runWithFiberInDEV( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index bba93bbc0da2e..bf5b9c64030ea 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -145,9 +145,6 @@ import { addMarkerProgressCallbackToPendingTransition, addMarkerIncompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, - getExecutionContext, - CommitContext, - NoContext, setIsRunningInsertionEffect, } from './ReactFiberWorkLoop'; import { @@ -233,8 +230,7 @@ function shouldProfile(current: Fiber): boolean { return ( enableProfilerTimer && enableProfilerCommitHooks && - (current.mode & ProfileMode) !== NoMode && - (getExecutionContext() & CommitContext) !== NoContext + (current.mode & ProfileMode) !== NoMode ); } @@ -2817,11 +2813,7 @@ function commitPassiveMountOnFiber( // Only Profilers with work in their subtree will have a Passive effect scheduled. if (flags & Passive) { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - getExecutionContext() & CommitContext - ) { + if (enableProfilerTimer && enableProfilerCommitHooks) { const {passiveEffectDuration} = finishedWork.stateNode; commitProfilerPostCommit( From a99d8e8d97055127a8ad7b01835d2660154689ed Mon Sep 17 00:00:00 2001 From: mofeiZ <34200447+mofeiZ@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:56:24 -0400 Subject: [PATCH 157/191] [compiler][eslint] Report bailout diagnostics with correct column # (#30977) Compiler bailout diagnostics should now highlight only the first line of the source location span. (Resubmission of #30423 which was reverted due to invalid column number.) --- .../src/rules/ReactCompilerRule.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index f95eaaae007ed..b9a1ffa440c49 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -166,9 +166,33 @@ const rule: Rule.RuleModule = { detail.loc != null && typeof detail.loc !== 'symbol' ? ` (@:${detail.loc.start.line}:${detail.loc.start.column})` : ''; + /** + * Report bailouts with a smaller span (just the first line). + * Compiler bailout lints only serve to flag that a react function + * has not been optimized by the compiler for codebases which depend + * on compiler memo heavily for perf. These lints are also often not + * actionable. + */ + let endLoc; + if (event.fnLoc.end.line === event.fnLoc.start.line) { + endLoc = event.fnLoc.end; + } else { + endLoc = { + line: event.fnLoc.start.line, + // Babel loc line numbers are 1-indexed + column: sourceCode.split( + /\r?\n|\r|\n/g, + event.fnLoc.start.line, + )[event.fnLoc.start.line - 1].length, + }; + } + const firstLineLoc = { + start: event.fnLoc.start, + end: endLoc, + }; context.report({ message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, - loc: event.fnLoc, + loc: firstLineLoc, suggest, }); } From 7b56a542987890f618eeda4e4906fbf1f1df2213 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Tue, 17 Sep 2024 11:05:59 -0700 Subject: [PATCH 158/191] [compiler][playground] create playground API in pipeline, and allow spaces in pass names Summary: 1. Minor refactor to provide a stable API for calling the compiler from the playground 2. Allows spaces in pass names without breaking the appearance of the playground by replacing spaces with   in pass tabs ghstack-source-id: 12a43ad86c16c0e21f3e6b4086d531cdefd893eb Pull Request resolved: https://github.com/facebook/react/pull/30988 --- .../apps/playground/components/Editor/EditorImpl.tsx | 12 +++--------- compiler/apps/playground/components/TabbedWindow.tsx | 7 +++++-- .../src/Entrypoint/Pipeline.ts | 11 +++++++++++ .../babel-plugin-react-compiler/src/index.ts | 1 + 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index ab85beebd5fca..5e3f3276b003f 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {parse as babelParse, ParserPlugin} from '@babel/parser'; +import {parse as babelParse} from '@babel/parser'; import * as HermesParser from 'hermes-parser'; import traverse, {NodePath} from '@babel/traverse'; import * as t from '@babel/types'; @@ -15,10 +15,8 @@ import { Effect, ErrorSeverity, parseConfigPragma, - printHIR, - printReactiveFunction, - run, ValueKind, + runPlayground, type Hook, } from 'babel-plugin-react-compiler/src'; import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment'; @@ -214,17 +212,13 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { for (const fn of parseFunctions(source, language)) { const id = withIdentifier(getFunctionIdentifier(fn)); - for (const result of run( + for (const result of runPlayground( fn, { ...config, customHooks: new Map([...COMMON_HOOKS]), }, getReactFunctionType(id), - '_c', - null, - null, - null, )) { const fnName = id.name; switch (result.kind) { diff --git a/compiler/apps/playground/components/TabbedWindow.tsx b/compiler/apps/playground/components/TabbedWindow.tsx index 1537a2817e1e0..4b01056f25bb7 100644 --- a/compiler/apps/playground/components/TabbedWindow.tsx +++ b/compiler/apps/playground/components/TabbedWindow.tsx @@ -69,6 +69,9 @@ function TabbedWindowItem({ setTabsOpen(nextState); }, [tabsOpen, name, setTabsOpen]); + // Replace spaces with non-breaking spaces + const displayName = name.replace(/ /g, '\u00A0'); + return (
{isShow ? ( @@ -80,7 +83,7 @@ function TabbedWindowItem({ className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${ hasChanged ? 'font-bold' : 'font-light' } text-secondary hover:text-link`}> - - {name} + - {displayName} {tabs.get(name) ??
No output for {name}
} @@ -94,7 +97,7 @@ function TabbedWindowItem({ className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${ hasChanged ? 'font-bold' : 'font-light' } text-secondary hover:text-link`}> - {name} + {displayName}
)} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index aef18c90c2e5e..f87d371bed6a3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -554,3 +554,14 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue { } return value; } + +export function* runPlayground( + func: NodePath< + t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression + >, + config: EnvironmentConfig, + fnType: ReactFunctionType, +): Generator { + const ast = yield* run(func, config, fnType, '_c', null, null, null); + return ast; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index aac65331a0ff2..256da2e5ed5c6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -18,6 +18,7 @@ export { compileProgram, parsePluginOptions, run, + runPlayground, OPT_OUT_DIRECTIVES, type CompilerPipelineValue, type PluginOptions, From 4549be0f846e7df5a4eaabf06369d93bd120271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 17 Sep 2024 15:12:16 -0400 Subject: [PATCH 159/191] [Fiber] Optimize enableProfilerCommitHooks by Collecting Elapsed Effect Duration in Module Scope (#30981) Stacked on #30979. The problem with the previous approach is that it recursively walked the tree up to propagate the resulting time from recording a layout effect. Instead, we keep a running count of the effect duration on the module scope. Then we reset it when entering a nested Profiler and then we add its elapsed count when we exit the Profiler. This also fixes a bug where we weren't previously including unmount times for some detached trees since they couldn't bubble up to find the profiler. --- .../src/ReactFiberBeginWork.js | 8 +- .../src/ReactFiberCommitEffects.js | 71 ++- .../src/ReactFiberCommitWork.js | 428 +++++++----------- .../react-reconciler/src/ReactFiberRoot.js | 4 +- .../src/ReactInternalTypes.js | 7 +- .../src/ReactProfilerTimer.js | 117 ++--- .../__tests__/ReactProfiler-test.internal.js | 12 +- 7 files changed, 272 insertions(+), 375 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 398dd8372597a..f4ce6e849b25e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1033,8 +1033,8 @@ function updateProfiler( // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; - stateNode.effectDuration = 0; - stateNode.passiveEffectDuration = 0; + stateNode.effectDuration = -0; + stateNode.passiveEffectDuration = -0; } } const nextProps = workInProgress.pendingProps; @@ -3711,8 +3711,8 @@ function attemptEarlyBailoutIfNoScheduledUpdate( // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; - stateNode.effectDuration = 0; - stateNode.passiveEffectDuration = 0; + stateNode.effectDuration = -0; + stateNode.passiveEffectDuration = -0; } } break; diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index dde2479ba8638..8b1d4ceb6f9bb 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -31,10 +31,8 @@ import {NoFlags} from './ReactFiberFlags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import {resolveClassComponentProps} from './ReactFiberClassComponent'; import { - recordLayoutEffectDuration, - startLayoutEffectTimer, - recordPassiveEffectDuration, - startPassiveEffectTimer, + recordEffectDuration, + startEffectTimer, isCurrentUpdateNested, } from './ReactProfilerTimer'; import {NoMode, ProfileMode} from './ReactTypeOfMode'; @@ -91,14 +89,41 @@ export function commitHookLayoutEffects( // e.g. a destroy function in one component should never override a ref set // by a create function in another component during the same commit. if (shouldProfile(finishedWork)) { - startLayoutEffectTimer(); + startEffectTimer(); commitHookEffectListMount(hookFlags, finishedWork); - recordLayoutEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } else { commitHookEffectListMount(hookFlags, finishedWork); } } +export function commitHookLayoutUnmountEffects( + finishedWork: Fiber, + nearestMountedAncestor: null | Fiber, + hookFlags: HookFlags, +) { + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if (shouldProfile(finishedWork)) { + startEffectTimer(); + commitHookEffectListUnmount( + hookFlags, + finishedWork, + nearestMountedAncestor, + ); + recordEffectDuration(finishedWork); + } else { + commitHookEffectListUnmount( + hookFlags, + finishedWork, + nearestMountedAncestor, + ); + } +} + export function commitHookEffectListMount( flags: HookFlags, finishedWork: Fiber, @@ -265,9 +290,9 @@ export function commitHookPassiveMountEffects( hookFlags: HookFlags, ) { if (shouldProfile(finishedWork)) { - startPassiveEffectTimer(); + startEffectTimer(); commitHookEffectListMount(hookFlags, finishedWork); - recordPassiveEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } else { commitHookEffectListMount(hookFlags, finishedWork); } @@ -279,13 +304,13 @@ export function commitHookPassiveUnmountEffects( hookFlags: HookFlags, ) { if (shouldProfile(finishedWork)) { - startPassiveEffectTimer(); + startEffectTimer(); commitHookEffectListUnmount( hookFlags, finishedWork, nearestMountedAncestor, ); - recordPassiveEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } else { commitHookEffectListUnmount( hookFlags, @@ -333,7 +358,7 @@ export function commitClassLayoutLifecycles( } } if (shouldProfile(finishedWork)) { - startLayoutEffectTimer(); + startEffectTimer(); if (__DEV__) { runWithFiberInDEV( finishedWork, @@ -348,7 +373,7 @@ export function commitClassLayoutLifecycles( captureCommitPhaseError(finishedWork, finishedWork.return, error); } } - recordLayoutEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } else { if (__DEV__) { runWithFiberInDEV( @@ -404,7 +429,7 @@ export function commitClassLayoutLifecycles( } } if (shouldProfile(finishedWork)) { - startLayoutEffectTimer(); + startEffectTimer(); if (__DEV__) { runWithFiberInDEV( finishedWork, @@ -426,7 +451,7 @@ export function commitClassLayoutLifecycles( captureCommitPhaseError(finishedWork, finishedWork.return, error); } } - recordLayoutEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } else { if (__DEV__) { runWithFiberInDEV( @@ -679,7 +704,7 @@ export function safelyCallComponentWillUnmount( ); instance.state = current.memoizedState; if (shouldProfile(current)) { - startLayoutEffectTimer(); + startEffectTimer(); if (__DEV__) { runWithFiberInDEV( current, @@ -695,7 +720,7 @@ export function safelyCallComponentWillUnmount( captureCommitPhaseError(current, nearestMountedAncestor, error); } } - recordLayoutEffectDuration(current); + recordEffectDuration(current); } else { if (__DEV__) { runWithFiberInDEV( @@ -736,10 +761,10 @@ function commitAttachRef(finishedWork: Fiber) { if (typeof ref === 'function') { if (shouldProfile(finishedWork)) { try { - startLayoutEffectTimer(); + startEffectTimer(); finishedWork.refCleanup = ref(instanceToUse); } finally { - recordLayoutEffectDuration(finishedWork); + recordEffectDuration(finishedWork); } } else { finishedWork.refCleanup = ref(instanceToUse); @@ -793,14 +818,14 @@ export function safelyDetachRef( try { if (shouldProfile(current)) { try { - startLayoutEffectTimer(); + startEffectTimer(); if (__DEV__) { runWithFiberInDEV(current, refCleanup); } else { refCleanup(); } } finally { - recordLayoutEffectDuration(current); + recordEffectDuration(current); } } else { if (__DEV__) { @@ -823,14 +848,14 @@ export function safelyDetachRef( try { if (shouldProfile(current)) { try { - startLayoutEffectTimer(); + startEffectTimer(); if (__DEV__) { (runWithFiberInDEV(current, ref, null): void); } else { ref(null); } } finally { - recordLayoutEffectDuration(current); + recordEffectDuration(current); } } else { if (__DEV__) { @@ -849,7 +874,7 @@ export function safelyDetachRef( } } -export function safelyCallDestroy( +function safelyCallDestroy( current: Fiber, nearestMountedAncestor: Fiber | null, destroy: () => void, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index bf5b9c64030ea..a0558ea549f76 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -44,7 +44,6 @@ import { enablePersistedModeClonedFlag, enableProfilerTimer, enableProfilerCommitHooks, - enableSchedulingProfiler, enableSuspenseCallback, enableScopeAPI, enableUpdaterTracking, @@ -101,9 +100,10 @@ import { } from './ReactFiberFlags'; import { getCommitTime, - recordLayoutEffectDuration, - startLayoutEffectTimer, getCompleteTime, + pushNestedEffectDurations, + popNestedEffectDurations, + bubbleNestedEffectDurations, } from './ReactProfilerTimer'; import {logComponentRender} from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; @@ -145,22 +145,15 @@ import { addMarkerProgressCallbackToPendingTransition, addMarkerIncompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, - setIsRunningInsertionEffect, } from './ReactFiberWorkLoop'; import { - NoFlags as NoHookEffect, HasEffect as HookHasEffect, Layout as HookLayout, Insertion as HookInsertion, Passive as HookPassive, } from './ReactHookEffectTags'; import {doesFiberContain} from './ReactFiberTreeReflection'; -import { - isDevToolsPresent, - markComponentLayoutEffectUnmountStarted, - markComponentLayoutEffectUnmountStopped, - onCommitUnmount, -} from './ReactFiberDevToolsHook'; +import {isDevToolsPresent, onCommitUnmount} from './ReactFiberDevToolsHook'; import {releaseCache, retainCache} from './ReactFiberCacheComponent'; import {clearTransitionsForLanes} from './ReactFiberLane'; import { @@ -176,6 +169,7 @@ import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop'; import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates'; import { commitHookLayoutEffects, + commitHookLayoutUnmountEffects, commitHookEffectListMount, commitHookEffectListUnmount, commitHookPassiveMountEffects, @@ -188,7 +182,6 @@ import { safelyCallComponentWillUnmount, safelyAttachRef, safelyDetachRef, - safelyCallDestroy, commitProfilerUpdate, commitProfilerPostCommit, commitRootCallbacks, @@ -226,14 +219,6 @@ let nextEffect: Fiber | null = null; let inProgressLanes: Lanes | null = null; let inProgressRoot: FiberRoot | null = null; -function shouldProfile(current: Fiber): boolean { - return ( - enableProfilerTimer && - enableProfilerCommitHooks && - (current.mode & ProfileMode) !== NoMode - ); -} - let focusedInstanceHandle: null | Fiber = null; let shouldFireAfterActiveInstanceBlur: boolean = false; @@ -434,6 +419,7 @@ function commitLayoutEffectOnFiber( break; } case HostRoot: { + const prevEffectDuration = pushNestedEffectDurations(); recursivelyTraverseLayoutEffects( finishedRoot, finishedWork, @@ -442,6 +428,10 @@ function commitLayoutEffectOnFiber( if (flags & Callback) { commitRootCallbacks(finishedWork); } + if (enableProfilerTimer && enableProfilerCommitHooks) { + finishedRoot.effectDuration += + popNestedEffectDurations(prevEffectDuration); + } break; } case HostHoistable: { @@ -481,39 +471,38 @@ function commitLayoutEffectOnFiber( break; } case Profiler: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); // TODO: Should this fire inside an offscreen tree? Or should it wait to // fire when the tree becomes visible again. if (flags & Update) { - const {effectDuration} = finishedWork.stateNode; + const prevEffectDuration = pushNestedEffectDurations(); + + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + + const profilerInstance = finishedWork.stateNode; + + if (enableProfilerTimer && enableProfilerCommitHooks) { + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + profilerInstance.effectDuration += + bubbleNestedEffectDurations(prevEffectDuration); + } commitProfilerUpdate( finishedWork, current, getCommitTime(), - effectDuration, + profilerInstance.effectDuration, + ); + } else { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, ); - - // Propagate layout effect durations to the next nearest Profiler ancestor. - // Do not reset these values until the next render so DevTools has a chance to read them first. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.effectDuration += effectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += effectDuration; - break outer; - } - parentFiber = parentFiber.return; - } } break; } @@ -1262,132 +1251,24 @@ function commitDeletionEffectsOnFiber( case ForwardRef: case MemoComponent: case SimpleMemoComponent: { - if (enableHiddenSubtreeInsertionEffectCleanup) { - // When deleting a fiber, we may need to destroy insertion or layout effects. - // Insertion effects are not destroyed on hidden, only when destroyed, so now - // we need to destroy them. Layout effects are destroyed when hidden, so - // we only need to destroy them if the tree is visible. - const updateQueue: FunctionComponentUpdateQueue | null = - (deletedFiber.updateQueue: any); - if (updateQueue !== null) { - const lastEffect = updateQueue.lastEffect; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - - let effect = firstEffect; - do { - const tag = effect.tag; - const inst = effect.inst; - const destroy = inst.destroy; - if (destroy !== undefined) { - if ((tag & HookInsertion) !== NoHookEffect) { - // TODO: add insertion effect marks and profiling. - if (__DEV__) { - setIsRunningInsertionEffect(true); - } - - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - - if (__DEV__) { - setIsRunningInsertionEffect(false); - } - } else if ( - !offscreenSubtreeWasHidden && - (tag & HookLayout) !== NoHookEffect - ) { - // Offscreen fibers already unmounted their layout effects. - // We only need to destroy layout effects for visible trees. - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStarted(deletedFiber); - } - - if (shouldProfile(deletedFiber)) { - startLayoutEffectTimer(); - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - recordLayoutEffectDuration(deletedFiber); - } else { - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - } - - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } - } - } else if (!offscreenSubtreeWasHidden) { - const updateQueue: FunctionComponentUpdateQueue | null = - (deletedFiber.updateQueue: any); - if (updateQueue !== null) { - const lastEffect = updateQueue.lastEffect; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - - let effect = firstEffect; - do { - const tag = effect.tag; - const inst = effect.inst; - const destroy = inst.destroy; - if (destroy !== undefined) { - if ((tag & HookInsertion) !== NoHookEffect) { - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - } else if ((tag & HookLayout) !== NoHookEffect) { - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStarted(deletedFiber); - } - - if (shouldProfile(deletedFiber)) { - startLayoutEffectTimer(); - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - recordLayoutEffectDuration(deletedFiber); - } else { - inst.destroy = undefined; - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - } - - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } - } + if ( + enableHiddenSubtreeInsertionEffectCleanup || + !offscreenSubtreeWasHidden + ) { + // TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings. + commitHookEffectListUnmount( + HookInsertion, + deletedFiber, + nearestMountedAncestor, + ); + } + if (!offscreenSubtreeWasHidden) { + commitHookLayoutUnmountEffects( + deletedFiber, + nearestMountedAncestor, + HookLayout, + ); } - recursivelyTraverseDeletionEffects( finishedRoot, nearestMountedAncestor, @@ -1709,27 +1590,13 @@ function commitMutationEffectsOnFiber( finishedWork, finishedWork.return, ); + // TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings. commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); - // Layout effects are destroyed during the mutation phase so that all - // destroy functions for all fibers are called before any create functions. - // This prevents sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if (shouldProfile(finishedWork)) { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - recordLayoutEffectDuration(finishedWork); - } else { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } + commitHookLayoutUnmountEffects( + finishedWork, + finishedWork.return, + HookLayout | HookHasEffect, + ); } return; } @@ -1917,6 +1784,8 @@ function commitMutationEffectsOnFiber( return; } case HostRoot: { + const prevEffectDuration = pushNestedEffectDurations(); + if (supportsResources) { prepareToCommitHoistables(); @@ -1960,6 +1829,10 @@ function commitMutationEffectsOnFiber( recursivelyResetForms(finishedWork); } + if (enableProfilerTimer && enableProfilerCommitHooks) { + root.effectDuration += popNestedEffectDurations(prevEffectDuration); + } + return; } case HostPortal: { @@ -1987,6 +1860,21 @@ function commitMutationEffectsOnFiber( } return; } + case Profiler: { + const prevEffectDuration = pushNestedEffectDurations(); + + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (enableProfilerTimer && enableProfilerCommitHooks) { + const profilerInstance = finishedWork.stateNode; + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + profilerInstance.effectDuration += + bubbleNestedEffectDurations(prevEffectDuration); + } + return; + } case SuspenseComponent: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork); @@ -2247,25 +2135,11 @@ export function disappearLayoutEffects(finishedWork: Fiber) { case MemoComponent: case SimpleMemoComponent: { // TODO (Offscreen) Check: flags & LayoutStatic - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout, - finishedWork, - finishedWork.return, - ); - } - + commitHookLayoutUnmountEffects( + finishedWork, + finishedWork.return, + HookLayout, + ); recursivelyTraverseDisappearLayoutEffects(finishedWork); break; } @@ -2395,38 +2269,37 @@ export function reappearLayoutEffects( break; } case Profiler: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); // TODO: Figure out how Profiler updates should work with Offscreen if (includeWorkInProgressEffects && flags & Update) { - const {effectDuration} = finishedWork.stateNode; + const prevEffectDuration = pushNestedEffectDurations(); + + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + + const profilerInstance = finishedWork.stateNode; + + if (enableProfilerTimer && enableProfilerCommitHooks) { + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + profilerInstance.effectDuration += + bubbleNestedEffectDurations(prevEffectDuration); + } commitProfilerUpdate( finishedWork, current, getCommitTime(), - effectDuration, + profilerInstance.effectDuration, + ); + } else { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, ); - - // Propagate layout effect durations to the next nearest Profiler ancestor. - // Do not reset these values until the next render so DevTools has a chance to read them first. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.effectDuration += effectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += effectDuration; - break outer; - } - parentFiber = parentFiber.return; - } } break; } @@ -2745,6 +2618,7 @@ function commitPassiveMountOnFiber( break; } case HostRoot: { + const prevEffectDuration = pushNestedEffectDurations(); recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -2800,48 +2674,50 @@ function commitPassiveMountOnFiber( clearTransitionsForLanes(finishedRoot, committedLanes); } } + if (enableProfilerTimer && enableProfilerCommitHooks) { + finishedRoot.passiveEffectDuration += + popNestedEffectDurations(prevEffectDuration); + } break; } case Profiler: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - endTime, - ); - // Only Profilers with work in their subtree will have a Passive effect scheduled. if (flags & Passive) { - if (enableProfilerTimer && enableProfilerCommitHooks) { - const {passiveEffectDuration} = finishedWork.stateNode; + const prevEffectDuration = pushNestedEffectDurations(); - commitProfilerPostCommit( - finishedWork, - finishedWork.alternate, - // This value will still reflect the previous commit phase. - // It does not get reset until the start of the next commit phase. - getCommitTime(), - passiveEffectDuration, - ); + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + endTime, + ); + + const profilerInstance = finishedWork.stateNode; + if (enableProfilerTimer && enableProfilerCommitHooks) { // Bubble times to the next nearest ancestor Profiler. // After we process that Profiler, we'll bubble further up. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.passiveEffectDuration += passiveEffectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.passiveEffectDuration += passiveEffectDuration; - break outer; - } - parentFiber = parentFiber.return; - } + profilerInstance.passiveEffectDuration += + bubbleNestedEffectDurations(prevEffectDuration); } + + commitProfilerPostCommit( + finishedWork, + finishedWork.alternate, + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + getCommitTime(), + profilerInstance.passiveEffectDuration, + ); + } else { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + endTime, + ); } break; } @@ -3427,6 +3303,30 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { } break; } + case HostRoot: { + const prevEffectDuration = pushNestedEffectDurations(); + recursivelyTraversePassiveUnmountEffects(finishedWork); + if (enableProfilerTimer && enableProfilerCommitHooks) { + const finishedRoot: FiberRoot = finishedWork.stateNode; + finishedRoot.passiveEffectDuration += + popNestedEffectDurations(prevEffectDuration); + } + break; + } + case Profiler: { + const prevEffectDuration = pushNestedEffectDurations(); + + recursivelyTraversePassiveUnmountEffects(finishedWork); + + if (enableProfilerTimer && enableProfilerCommitHooks) { + const profilerInstance = finishedWork.stateNode; + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + profilerInstance.passiveEffectDuration += + bubbleNestedEffectDurations(prevEffectDuration); + } + break; + } case OffscreenComponent: { const instance: OffscreenInstance = finishedWork.stateNode; const nextState: OffscreenState | null = finishedWork.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index f62a5d7158db1..176c3846d1336 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -112,8 +112,8 @@ function FiberRootNode( } if (enableProfilerTimer && enableProfilerCommitHooks) { - this.effectDuration = 0; - this.passiveEffectDuration = 0; + this.effectDuration = -0; + this.passiveEffectDuration = -0; } if (enableUpdaterTracking) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index d160aa0d228f5..1c98a9e9c7fbc 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -373,6 +373,11 @@ type TransitionTracingOnlyFiberRootProperties = { incompleteTransitions: Map, }; +type ProfilerCommitHooksOnlyFiberRootProperties = { + effectDuration: number, + passiveEffectDuration: number, +}; + // Exported FiberRoot type includes all properties, // To avoid requiring potentially error-prone :any casts throughout the project. // The types are defined separately within this file to ensure they stay in sync. @@ -381,7 +386,7 @@ export type FiberRoot = { ...SuspenseCallbackOnlyFiberRootProperties, ...UpdaterTrackingOnlyFiberRootProperties, ...TransitionTracingOnlyFiberRootProperties, - ... + ...ProfilerCommitHooksOnlyFiberRootProperties, }; type BasicStateAction = (S => S) | S; diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index bad2f60490d54..3cccc399327ba 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -14,7 +14,6 @@ import { enableProfilerNestedUpdatePhase, enableProfilerTimer, } from 'shared/ReactFeatureFlags'; -import {HostRoot, Profiler} from './ReactWorkTags'; // Intentionally not named imports because Rollup would use dynamic dispatch for // CommonJS interop named imports. @@ -35,11 +34,38 @@ export type ProfilerTimer = { ... }; -let completeTime: number = 0; -let commitTime: number = 0; -let layoutEffectStartTime: number = -1; -let profilerStartTime: number = -1; -let passiveEffectStartTime: number = -1; +let completeTime: number = -0; +let commitTime: number = -0; +let profilerStartTime: number = -1.1; +let profilerEffectDuration: number = -0; + +function pushNestedEffectDurations(): number { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return 0; + } + const prevEffectDuration = profilerEffectDuration; + profilerEffectDuration = 0; // Reset counter. + return prevEffectDuration; +} + +function popNestedEffectDurations(prevEffectDuration: number): number { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return 0; + } + const elapsedTime = profilerEffectDuration; + profilerEffectDuration = prevEffectDuration; + return elapsedTime; +} + +// Like pop but it also adds the current elapsed time to the parent scope. +function bubbleNestedEffectDurations(prevEffectDuration: number): number { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return 0; + } + const elapsedTime = profilerEffectDuration; + profilerEffectDuration += prevEffectDuration; + return elapsedTime; +} /** * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect). @@ -153,83 +179,27 @@ function stopProfilerTimerIfRunningAndRecordIncompleteDuration( } } -function recordLayoutEffectDuration(fiber: Fiber): void { - if (!enableProfilerTimer || !enableProfilerCommitHooks) { - return; - } - - if (layoutEffectStartTime >= 0) { - const elapsedTime = now() - layoutEffectStartTime; - - layoutEffectStartTime = -1; - - // Store duration on the next nearest Profiler ancestor - // Or the root (for the DevTools Profiler to read) - let parentFiber = fiber.return; - while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.effectDuration += elapsedTime; - return; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += elapsedTime; - return; - } - parentFiber = parentFiber.return; - } - } -} - -function recordPassiveEffectDuration(fiber: Fiber): void { +function recordEffectDuration(fiber: Fiber): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return; } - if (passiveEffectStartTime >= 0) { - const elapsedTime = now() - passiveEffectStartTime; + if (profilerStartTime >= 0) { + const elapsedTime = now() - profilerStartTime; - passiveEffectStartTime = -1; + profilerStartTime = -1; // Store duration on the next nearest Profiler ancestor // Or the root (for the DevTools Profiler to read) - let parentFiber = fiber.return; - while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - if (root !== null) { - root.passiveEffectDuration += elapsedTime; - } - return; - case Profiler: - const parentStateNode = parentFiber.stateNode; - if (parentStateNode !== null) { - // Detached fibers have their state node cleared out. - // In this case, the return pointer is also cleared out, - // so we won't be able to report the time spent in this Profiler's subtree. - parentStateNode.passiveEffectDuration += elapsedTime; - } - return; - } - parentFiber = parentFiber.return; - } - } -} - -function startLayoutEffectTimer(): void { - if (!enableProfilerTimer || !enableProfilerCommitHooks) { - return; + profilerEffectDuration += elapsedTime; } - layoutEffectStartTime = now(); } -function startPassiveEffectTimer(): void { +function startEffectTimer(): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return; } - passiveEffectStartTime = now(); + profilerStartTime = now(); } function transferActualDuration(fiber: Fiber): void { @@ -251,15 +221,16 @@ export { recordCommitTime, isCurrentUpdateNested, markNestedUpdateScheduled, - recordLayoutEffectDuration, - recordPassiveEffectDuration, + recordEffectDuration, resetNestedUpdateFlag, - startLayoutEffectTimer, - startPassiveEffectTimer, + startEffectTimer, startProfilerTimer, stopProfilerTimerIfRunning, stopProfilerTimerIfRunningAndRecordDuration, stopProfilerTimerIfRunningAndRecordIncompleteDuration, syncNestedUpdateFlag, transferActualDuration, + pushNestedEffectDurations, + popNestedEffectDurations, + bubbleNestedEffectDurations, }; diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index a19e9b1c6d161..0d7aead65f81d 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -1643,7 +1643,7 @@ describe(`onCommit`, () => { expect(call).toHaveLength(4); expect(call[0]).toBe('root-update'); expect(call[1]).toBe('update'); - expect(call[2]).toBe(1100); // durations + expect(call[2]).toBe(11100); // durations expect(call[3]).toBe(1124); // commit start time (before mutations or effects) }); @@ -1952,11 +1952,7 @@ describe(`onPostCommit`, () => { expect(call).toHaveLength(4); expect(call[0]).toBe('unmount-test'); expect(call[1]).toBe('update'); - // TODO (bvaughn) The duration reported below should be 10100, but is 0 - // by the time the passive effect is flushed its parent Fiber pointer is gone. - // If we refactor to preserve the unmounted Fiber tree we could fix this. - // The current implementation would require too much extra overhead to track this. - expect(call[2]).toBe(0); // durations + expect(call[2]).toBe(10100); // durations expect(call[3]).toBe(12030); // commit start time (before mutations or effects) }); @@ -2085,7 +2081,7 @@ describe(`onPostCommit`, () => { expect(call).toHaveLength(4); expect(call[0]).toBe('root-update'); expect(call[1]).toBe('update'); - expect(call[2]).toBe(1100); // durations + expect(call[2]).toBe(11100); // durations expect(call[3]).toBe(1124); // commit start time (before mutations or effects) }); @@ -2300,7 +2296,7 @@ describe(`onPostCommit`, () => { expect(call).toHaveLength(4); expect(call[0]).toBe('root'); expect(call[1]).toBe('update'); - expect(call[2]).toBe(100000000); // durations + expect(call[2]).toBe(100001000); // durations // The commit time varies because the above duration time varies expect(call[3]).toBe(11221221); // commit start time (before mutations or effects) }); From 15da9174518f18f82869767ebe2a21be2fc8bd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 17 Sep 2024 15:25:00 -0400 Subject: [PATCH 160/191] Don't read currentTransition back from internals (#30991) This code is weird. It reads back the transition that it just set from the shared internals. It's almost like it expects it to be a getter or something. This avoids that and makes it consistent with what ReactFiberHooks already does. --- packages/react/src/ReactStartTransition.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index 6bae8947e1cea..a9b30c424a07f 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -23,20 +23,17 @@ export function startTransition( options?: StartTransitionOptions, ) { const prevTransition = ReactSharedInternals.T; - const transition: BatchConfigTransition = {}; - ReactSharedInternals.T = transition; - const currentTransition = ReactSharedInternals.T; + const currentTransition: BatchConfigTransition = {}; + ReactSharedInternals.T = currentTransition; if (__DEV__) { - ReactSharedInternals.T._updatedFibers = new Set(); + currentTransition._updatedFibers = new Set(); } if (enableTransitionTracing) { if (options !== undefined && options.name !== undefined) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - ReactSharedInternals.T.name = options.name; - // $FlowFixMe[incompatible-use] found when upgrading Flow - ReactSharedInternals.T.startTime = -1; + currentTransition.name = options.name; + currentTransition.startTime = -1; } } @@ -45,7 +42,7 @@ export function startTransition( const returnValue = scope(); const onStartTransitionFinish = ReactSharedInternals.S; if (onStartTransitionFinish !== null) { - onStartTransitionFinish(transition, returnValue); + onStartTransitionFinish(currentTransition, returnValue); } if ( typeof returnValue === 'object' && From e1c20902c39d1dfe2649185622f2f21b526e2be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 17 Sep 2024 16:14:57 -0400 Subject: [PATCH 161/191] [Fiber] Log Component Effects to Performance Track (#30983) Stacked on #30981. Same as #30967 but for effects. This logs a tree of components using `performance.measure()`. In addition to the previous render phase this logs one tree for each commit phase: - Mutation Phase - Layout Effect - Passive Unmounts - Passive Mounts I currently skip the Before Mutation phase since the snapshots are so unusual it's not worth creating trees for those. The mechanism is that I reuse the timings we track for `enableProfilerCommitHooks`. I track first and last effect timestamp within each component subtree. Then on the way up do we log the entry. This means that we don't include overhead to find our way down to a component and that we don't need to add any additional overhead by reading timestamps. To ensure that the entries get ordered correctly we need to ensure that the start time of each parent is slightly before the inner one. --- .../src/ReactFiberCommitWork.js | 134 +++++++++++++++--- .../src/ReactFiberPerformanceTrack.js | 36 ++++- .../src/ReactProfilerTimer.js | 133 ++++++++--------- 3 files changed, 215 insertions(+), 88 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a0558ea549f76..4c66470342c89 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -99,13 +99,21 @@ import { Cloned, } from './ReactFiberFlags'; import { - getCommitTime, - getCompleteTime, + commitTime, + completeTime, pushNestedEffectDurations, popNestedEffectDurations, bubbleNestedEffectDurations, + resetComponentEffectTimers, + pushComponentEffectStart, + popComponentEffectStart, + componentEffectStartTime, + componentEffectEndTime, } from './ReactProfilerTimer'; -import {logComponentRender} from './ReactFiberPerformanceTrack'; +import { + logComponentRender, + logComponentEffect, +} from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue'; import { @@ -382,6 +390,8 @@ function commitLayoutEffectOnFiber( finishedWork: Fiber, committedLanes: Lanes, ): void { + const prevEffectStart = pushComponentEffectStart(); + // When updating this function, also update reappearLayoutEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible. const flags = finishedWork.flags; @@ -494,7 +504,7 @@ function commitLayoutEffectOnFiber( commitProfilerUpdate( finishedWork, current, - getCommitTime(), + commitTime, profilerInstance.effectDuration, ); } else { @@ -585,6 +595,23 @@ function commitLayoutEffectOnFiber( break; } } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 + ) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + + popComponentEffectStart(prevEffectStart); } function abortRootTransitions( @@ -1530,6 +1557,8 @@ export function commitMutationEffects( inProgressLanes = committedLanes; inProgressRoot = root; + resetComponentEffectTimers(); + commitMutationEffectsOnFiber(finishedWork, root, committedLanes); inProgressLanes = null; @@ -1570,6 +1599,8 @@ function commitMutationEffectsOnFiber( root: FiberRoot, lanes: Lanes, ) { + const prevEffectStart = pushComponentEffectStart(); + const current = finishedWork.alternate; const flags = finishedWork.flags; @@ -1598,7 +1629,7 @@ function commitMutationEffectsOnFiber( HookLayout | HookHasEffect, ); } - return; + break; } case ClassComponent: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); @@ -1617,7 +1648,7 @@ function commitMutationEffectsOnFiber( deferHiddenCallbacks(updateQueue); } } - return; + break; } case HostHoistable: { if (supportsResources) { @@ -1693,7 +1724,7 @@ function commitMutationEffectsOnFiber( ); } } - return; + break; } // Fall through } @@ -1756,7 +1787,7 @@ function commitMutationEffectsOnFiber( } } } - return; + break; } case HostText: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); @@ -1781,7 +1812,7 @@ function commitMutationEffectsOnFiber( commitHostTextUpdate(finishedWork, newText, oldText); } } - return; + break; } case HostRoot: { const prevEffectDuration = pushNestedEffectDurations(); @@ -1833,7 +1864,7 @@ function commitMutationEffectsOnFiber( root.effectDuration += popNestedEffectDurations(prevEffectDuration); } - return; + break; } case HostPortal: { if (supportsResources) { @@ -1858,7 +1889,7 @@ function commitMutationEffectsOnFiber( ); } } - return; + break; } case Profiler: { const prevEffectDuration = pushNestedEffectDurations(); @@ -1873,7 +1904,7 @@ function commitMutationEffectsOnFiber( profilerInstance.effectDuration += bubbleNestedEffectDurations(prevEffectDuration); } - return; + break; } case SuspenseComponent: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); @@ -1925,7 +1956,7 @@ function commitMutationEffectsOnFiber( attachSuspenseRetryListeners(finishedWork, retryQueue); } } - return; + break; } case OffscreenComponent: { if (flags & Ref) { @@ -2018,7 +2049,7 @@ function commitMutationEffectsOnFiber( } } } - return; + break; } case SuspenseListComponent: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); @@ -2032,7 +2063,7 @@ function commitMutationEffectsOnFiber( attachSuspenseRetryListeners(finishedWork, retryQueue); } } - return; + break; } case ScopeComponent: { if (enableScopeAPI) { @@ -2052,16 +2083,34 @@ function commitMutationEffectsOnFiber( prepareScopeUpdate(scopeInstance, finishedWork); } } - return; + break; } default: { recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork); - return; + break; } } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 + ) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + + popComponentEffectStart(prevEffectStart); } + function commitReconciliationEffects(finishedWork: Fiber) { // Placement effects (insertions, reorders) can be scheduled on any fiber // type. They needs to happen after the children effects have fired, but @@ -2106,6 +2155,8 @@ export function commitLayoutEffects( inProgressLanes = committedLanes; inProgressRoot = root; + resetComponentEffectTimers(); + const current = finishedWork.alternate; commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes); @@ -2291,7 +2342,7 @@ export function reappearLayoutEffects( commitProfilerUpdate( finishedWork, current, - getCommitTime(), + commitTime, profilerInstance.effectDuration, ); } else { @@ -2515,14 +2566,14 @@ export function commitPassiveMountEffects( committedLanes: Lanes, committedTransitions: Array | null, ): void { + resetComponentEffectTimers(); + commitPassiveMountOnFiber( root, finishedWork, committedLanes, committedTransitions, - enableProfilerTimer && enableComponentPerformanceTrack - ? getCompleteTime() - : 0, + enableProfilerTimer && enableComponentPerformanceTrack ? completeTime : 0, ); } @@ -2577,6 +2628,8 @@ function commitPassiveMountOnFiber( committedTransitions: Array | null, endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ): void { + const prevEffectStart = pushComponentEffectStart(); + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its // render time. We do this after the fact in the passive effect to avoid the overhead of this // getting in the way of the render characteristics and avoid the overhead of unwinding @@ -2707,7 +2760,7 @@ function commitPassiveMountOnFiber( finishedWork.alternate, // This value will still reflect the previous commit phase. // It does not get reset until the start of the next commit phase. - getCommitTime(), + commitTime, profilerInstance.passiveEffectDuration, ); } else { @@ -2860,6 +2913,23 @@ function commitPassiveMountOnFiber( break; } } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 + ) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + + popComponentEffectStart(prevEffectStart); } function recursivelyTraverseReconnectPassiveEffects( @@ -3131,6 +3201,7 @@ function commitAtomicPassiveEffects( } export function commitPassiveUnmountEffects(finishedWork: Fiber): void { + resetComponentEffectTimers(); commitPassiveUnmountOnFiber(finishedWork); } @@ -3289,6 +3360,8 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { } function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { + const prevEffectStart = pushComponentEffectStart(); + switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -3358,6 +3431,23 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { break; } } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + componentEffectStartTime >= 0 && + componentEffectEndTime >= 0 + ) { + logComponentEffect( + finishedWork, + componentEffectStartTime, + componentEffectEndTime, + ); + } + + popComponentEffectStart(prevEffectStart); } function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 2058a04e47454..9c6c7ff8a80d3 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -38,9 +38,24 @@ const reusableComponentOptions = { }, }; +const reusableComponentEffectDevToolDetails = { + dataType: 'track-entry', + color: 'secondary', + track: 'Blocking', // Lane + trackGroup: TRACK_GROUP, +}; +const reusableComponentEffectOptions = { + start: -0, + end: -0, + detail: { + devtools: reusableComponentEffectDevToolDetails, + }, +}; + export function setCurrentTrackFromLanes(lanes: number): void { - reusableComponentDevToolDetails.track = - getGroupNameOfHighestPriorityLane(lanes); + reusableComponentEffectDevToolDetails.track = + reusableComponentDevToolDetails.track = + getGroupNameOfHighestPriorityLane(lanes); } export function logComponentRender( @@ -59,3 +74,20 @@ export function logComponentRender( performance.measure(name, reusableComponentOptions); } } + +export function logComponentEffect( + fiber: Fiber, + startTime: number, + endTime: number, +): void { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } + if (supportsUserTiming) { + reusableComponentEffectOptions.start = startTime; + reusableComponentEffectOptions.end = endTime; + performance.measure(name, reusableComponentEffectOptions); + } +} diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index 3cccc399327ba..da450d7f28f52 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -21,25 +21,14 @@ import * as Scheduler from 'scheduler'; const {unstable_now: now} = Scheduler; -export type ProfilerTimer = { - getCommitTime(): number, - isCurrentUpdateNested(): boolean, - markNestedUpdateScheduled(): void, - recordCommitTime(): void, - startProfilerTimer(fiber: Fiber): void, - stopProfilerTimerIfRunning(fiber: Fiber): void, - stopProfilerTimerIfRunningAndRecordDuration(fiber: Fiber): void, - stopProfilerTimerIfRunningAndRecordIncompleteDuration(fiber: Fiber): void, - syncNestedUpdateFlag(): void, - ... -}; - -let completeTime: number = -0; -let commitTime: number = -0; -let profilerStartTime: number = -1.1; -let profilerEffectDuration: number = -0; - -function pushNestedEffectDurations(): number { +export let completeTime: number = -0; +export let commitTime: number = -0; +export let profilerStartTime: number = -1.1; +export let profilerEffectDuration: number = -0; +export let componentEffectStartTime: number = -1.1; +export let componentEffectEndTime: number = -1.1; + +export function pushNestedEffectDurations(): number { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return 0; } @@ -48,7 +37,7 @@ function pushNestedEffectDurations(): number { return prevEffectDuration; } -function popNestedEffectDurations(prevEffectDuration: number): number { +export function popNestedEffectDurations(prevEffectDuration: number): number { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return 0; } @@ -58,7 +47,9 @@ function popNestedEffectDurations(prevEffectDuration: number): number { } // Like pop but it also adds the current elapsed time to the parent scope. -function bubbleNestedEffectDurations(prevEffectDuration: number): number { +export function bubbleNestedEffectDurations( + prevEffectDuration: number, +): number { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return 0; } @@ -67,6 +58,39 @@ function bubbleNestedEffectDurations(prevEffectDuration: number): number { return elapsedTime; } +export function resetComponentEffectTimers(): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + componentEffectStartTime = -1.1; + componentEffectEndTime = -1.1; +} + +export function pushComponentEffectStart(): number { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return 0; + } + const prevEffectStart = componentEffectStartTime; + componentEffectStartTime = -1.1; // Track the next start. + return prevEffectStart; +} + +export function popComponentEffectStart(prevEffectStart: number): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + if (prevEffectStart < 0) { + // If the parent component didn't have a start time, we use the start + // of the child as the parent's start time. We subtrack a minimal amount of + // time to ensure that the parent's start time is before the child to ensure + // that the performance tracks line up in the right order. + componentEffectStartTime -= 0.001; + } else { + // Otherwise, we restore the previous parent's start time. + componentEffectStartTime = prevEffectStart; + } +} + /** * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect). * @@ -86,53 +110,45 @@ function bubbleNestedEffectDurations(prevEffectDuration: number): number { let currentUpdateIsNested: boolean = false; let nestedUpdateScheduled: boolean = false; -function isCurrentUpdateNested(): boolean { +export function isCurrentUpdateNested(): boolean { return currentUpdateIsNested; } -function markNestedUpdateScheduled(): void { +export function markNestedUpdateScheduled(): void { if (enableProfilerNestedUpdatePhase) { nestedUpdateScheduled = true; } } -function resetNestedUpdateFlag(): void { +export function resetNestedUpdateFlag(): void { if (enableProfilerNestedUpdatePhase) { currentUpdateIsNested = false; nestedUpdateScheduled = false; } } -function syncNestedUpdateFlag(): void { +export function syncNestedUpdateFlag(): void { if (enableProfilerNestedUpdatePhase) { currentUpdateIsNested = nestedUpdateScheduled; nestedUpdateScheduled = false; } } -function getCompleteTime(): number { - return completeTime; -} - -function recordCompleteTime(): void { +export function recordCompleteTime(): void { if (!enableProfilerTimer) { return; } completeTime = now(); } -function getCommitTime(): number { - return commitTime; -} - -function recordCommitTime(): void { +export function recordCommitTime(): void { if (!enableProfilerTimer) { return; } commitTime = now(); } -function startProfilerTimer(fiber: Fiber): void { +export function startProfilerTimer(fiber: Fiber): void { if (!enableProfilerTimer) { return; } @@ -144,14 +160,16 @@ function startProfilerTimer(fiber: Fiber): void { } } -function stopProfilerTimerIfRunning(fiber: Fiber): void { +export function stopProfilerTimerIfRunning(fiber: Fiber): void { if (!enableProfilerTimer) { return; } profilerStartTime = -1; } -function stopProfilerTimerIfRunningAndRecordDuration(fiber: Fiber): void { +export function stopProfilerTimerIfRunningAndRecordDuration( + fiber: Fiber, +): void { if (!enableProfilerTimer) { return; } @@ -164,7 +182,7 @@ function stopProfilerTimerIfRunningAndRecordDuration(fiber: Fiber): void { } } -function stopProfilerTimerIfRunningAndRecordIncompleteDuration( +export function stopProfilerTimerIfRunningAndRecordIncompleteDuration( fiber: Fiber, ): void { if (!enableProfilerTimer) { @@ -179,30 +197,38 @@ function stopProfilerTimerIfRunningAndRecordIncompleteDuration( } } -function recordEffectDuration(fiber: Fiber): void { +export function recordEffectDuration(fiber: Fiber): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return; } if (profilerStartTime >= 0) { - const elapsedTime = now() - profilerStartTime; + const endTime = now(); + const elapsedTime = endTime - profilerStartTime; profilerStartTime = -1; // Store duration on the next nearest Profiler ancestor // Or the root (for the DevTools Profiler to read) profilerEffectDuration += elapsedTime; + + // Keep track of the last end time of the effects. + componentEffectEndTime = endTime; } } -function startEffectTimer(): void { +export function startEffectTimer(): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return; } profilerStartTime = now(); + if (componentEffectStartTime < 0) { + // Keep track of the first time we start an effect as the component's effect start time. + componentEffectStartTime = profilerStartTime; + } } -function transferActualDuration(fiber: Fiber): void { +export function transferActualDuration(fiber: Fiber): void { // Transfer time spent rendering these children so we don't lose it // after we rerender. This is used as a helper in special cases // where we should count the work of multiple passes. @@ -213,24 +239,3 @@ function transferActualDuration(fiber: Fiber): void { child = child.sibling; } } - -export { - getCompleteTime, - recordCompleteTime, - getCommitTime, - recordCommitTime, - isCurrentUpdateNested, - markNestedUpdateScheduled, - recordEffectDuration, - resetNestedUpdateFlag, - startEffectTimer, - startProfilerTimer, - stopProfilerTimerIfRunning, - stopProfilerTimerIfRunningAndRecordDuration, - stopProfilerTimerIfRunningAndRecordIncompleteDuration, - syncNestedUpdateFlag, - transferActualDuration, - pushNestedEffectDurations, - popNestedEffectDurations, - bubbleNestedEffectDurations, -}; From 8dfbd16fce9077ab4e5fe85a7b86fa7c97a5ae04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 17 Sep 2024 16:36:10 -0400 Subject: [PATCH 162/191] [Fiber] Color Performance Track Entries by Self Time (#30984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30983. This colors each component entry by its self time from light to dark depending on how long it took. If it took longer than a cut off we color it red (the error color). Screenshot 2024-09-16 at 11 48 15 PM --- .../src/ReactFiberCommitWork.js | 5 ++ .../src/ReactFiberPerformanceTrack.js | 48 +++++++++++-------- .../src/ReactProfilerTimer.js | 3 ++ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 4c66470342c89..5965dbe8db0a8 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -109,6 +109,7 @@ import { popComponentEffectStart, componentEffectStartTime, componentEffectEndTime, + componentEffectDuration, } from './ReactProfilerTimer'; import { logComponentRender, @@ -608,6 +609,7 @@ function commitLayoutEffectOnFiber( finishedWork, componentEffectStartTime, componentEffectEndTime, + componentEffectDuration, ); } @@ -2105,6 +2107,7 @@ function commitMutationEffectsOnFiber( finishedWork, componentEffectStartTime, componentEffectEndTime, + componentEffectDuration, ); } @@ -2926,6 +2929,7 @@ function commitPassiveMountOnFiber( finishedWork, componentEffectStartTime, componentEffectEndTime, + componentEffectDuration, ); } @@ -3444,6 +3448,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { finishedWork, componentEffectStartTime, componentEffectEndTime, + componentEffectDuration, ); } diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 9c6c7ff8a80d3..a053ea56ffc7f 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -38,24 +38,9 @@ const reusableComponentOptions = { }, }; -const reusableComponentEffectDevToolDetails = { - dataType: 'track-entry', - color: 'secondary', - track: 'Blocking', // Lane - trackGroup: TRACK_GROUP, -}; -const reusableComponentEffectOptions = { - start: -0, - end: -0, - detail: { - devtools: reusableComponentEffectDevToolDetails, - }, -}; - export function setCurrentTrackFromLanes(lanes: number): void { - reusableComponentEffectDevToolDetails.track = - reusableComponentDevToolDetails.track = - getGroupNameOfHighestPriorityLane(lanes); + reusableComponentDevToolDetails.track = + getGroupNameOfHighestPriorityLane(lanes); } export function logComponentRender( @@ -69,6 +54,20 @@ export function logComponentRender( return; } if (supportsUserTiming) { + let selfTime: number = (fiber.actualDuration: any); + if (fiber.alternate === null || fiber.alternate.child !== fiber.child) { + for (let child = fiber.child; child !== null; child = child.sibling) { + selfTime -= (child.actualDuration: any); + } + } + reusableComponentDevToolDetails.color = + selfTime < 0.5 + ? 'primary-light' + : selfTime < 10 + ? 'primary' + : selfTime < 100 + ? 'primary-dark' + : 'error'; reusableComponentOptions.start = startTime; reusableComponentOptions.end = endTime; performance.measure(name, reusableComponentOptions); @@ -79,6 +78,7 @@ export function logComponentEffect( fiber: Fiber, startTime: number, endTime: number, + selfTime: number, ): void { const name = getComponentNameFromFiber(fiber); if (name === null) { @@ -86,8 +86,16 @@ export function logComponentEffect( return; } if (supportsUserTiming) { - reusableComponentEffectOptions.start = startTime; - reusableComponentEffectOptions.end = endTime; - performance.measure(name, reusableComponentEffectOptions); + reusableComponentDevToolDetails.color = + selfTime < 1 + ? 'secondary-light' + : selfTime < 100 + ? 'secondary' + : selfTime < 500 + ? 'secondary-dark' + : 'error'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure(name, reusableComponentOptions); } } diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index da450d7f28f52..d65ca0a7544ee 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -25,6 +25,7 @@ export let completeTime: number = -0; export let commitTime: number = -0; export let profilerStartTime: number = -1.1; export let profilerEffectDuration: number = -0; +export let componentEffectDuration: number = -0; export let componentEffectStartTime: number = -1.1; export let componentEffectEndTime: number = -1.1; @@ -72,6 +73,7 @@ export function pushComponentEffectStart(): number { } const prevEffectStart = componentEffectStartTime; componentEffectStartTime = -1.1; // Track the next start. + componentEffectDuration = -0; // Reset component level duration. return prevEffectStart; } @@ -211,6 +213,7 @@ export function recordEffectDuration(fiber: Fiber): void { // Store duration on the next nearest Profiler ancestor // Or the root (for the DevTools Profiler to read) profilerEffectDuration += elapsedTime; + componentEffectDuration += elapsedTime; // Keep track of the last end time of the effects. componentEffectEndTime = endTime; From 5dcb009760160c085496e943f76090d98528f971 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Wed, 18 Sep 2024 11:51:36 -0400 Subject: [PATCH 163/191] [compiler] Add JSX inlining optimization (#30867) This adds an `InlineJsxTransform` optimization pass, toggled by the `enableInlineJsxTransform` flag. When enabled, JSX will be transformed into React Element object literals, preventing runtime overhead during element creation. TODO: - [ ] Add conditionals to make transform PROD-only - [ ] Make the React element symbol configurable so this works with runtimes that support `react.element` or `react.transitional.element` - [ ] Look into additional optimization to pass props spread through directly if none of the properties are mutated --- .../src/Entrypoint/Pipeline.ts | 10 + .../src/HIR/BuildReactiveScopeTerminalsHIR.ts | 16 +- .../src/HIR/Environment.ts | 7 + .../src/HIR/HIRBuilder.ts | 20 + .../src/Optimization/InlineJsxTransform.ts | 402 ++++++++++++++++++ .../src/Optimization/index.ts | 1 + .../compiler/inline-jsx-transform.expect.md | 226 ++++++++++ .../fixtures/compiler/inline-jsx-transform.js | 43 ++ 8 files changed, 711 insertions(+), 14 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index f87d371bed6a3..606440b241f9b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -41,6 +41,7 @@ import { constantPropagation, deadCodeElimination, pruneMaybeThrows, + inlineJsxTransform, } from '../Optimization'; import {instructionReordering} from '../Optimization/InstructionReordering'; import { @@ -351,6 +352,15 @@ function* runWithEnvironment( }); } + if (env.config.enableInlineJsxTransform) { + inlineJsxTransform(hir); + yield log({ + kind: 'hir', + name: 'inlineJsxTransform', + value: hir, + }); + } + const reactiveFunction = buildReactiveFunction(hir); yield log({ kind: 'reactive', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts index 0999a3492b4a4..7c1fb54ea8058 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts @@ -14,6 +14,7 @@ import { ScopeId, } from './HIR'; import { + fixScopeAndIdentifierRanges, markInstructionIds, markPredecessors, reversePostorderBlocks, @@ -176,20 +177,7 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void { * Step 5: * Fix scope and identifier ranges to account for renumbered instructions */ - for (const [, block] of fn.body.blocks) { - const terminal = block.terminal; - if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') { - /* - * Scope ranges should always align to start at the 'scope' terminal - * and end at the first instruction of the fallthrough block - */ - const fallthroughBlock = fn.body.blocks.get(terminal.fallthrough)!; - const firstId = - fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; - terminal.scope.range.start = terminal.id; - terminal.scope.range.end = firstId; - } - } + fixScopeAndIdentifierRanges(fn.body); } type TerminalRewriteInfo = diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 75f3086011fd0..66270345fdf35 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -233,6 +233,13 @@ const EnvironmentConfigSchema = z.object({ */ enableOptionalDependencies: z.boolean().default(true), + /** + * Enables inlining ReactElement object literals in place of JSX + * An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime + * Currently a prod-only optimization, requiring Fast JSX dependencies + */ + enableInlineJsxTransform: z.boolean().default(false), + /* * Enable validation of hooks to partially check that the component honors the rules of hooks. * When disabled, the component is assumed to follow the rules (though the Babel plugin looks diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index c694cf310fb39..9202f2145f27e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -912,3 +912,23 @@ export function clonePlaceToTemporary(env: Environment, place: Place): Place { temp.reactive = place.reactive; return temp; } + +/** + * Fix scope and identifier ranges to account for renumbered instructions + */ +export function fixScopeAndIdentifierRanges(func: HIR): void { + for (const [, block] of func.blocks) { + const terminal = block.terminal; + if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') { + /* + * Scope ranges should always align to start at the 'scope' terminal + * and end at the first instruction of the fallthrough block + */ + const fallthroughBlock = func.blocks.get(terminal.fallthrough)!; + const firstId = + fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; + terminal.scope.range.start = terminal.id; + terminal.scope.range.end = firstId; + } + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts new file mode 100644 index 0000000000000..344159d0b6072 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -0,0 +1,402 @@ +/** + * 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. + */ + +import { + BuiltinTag, + Effect, + HIRFunction, + Instruction, + JsxAttribute, + makeInstructionId, + ObjectProperty, + Place, + SpreadPattern, +} from '../HIR'; +import { + createTemporaryPlace, + fixScopeAndIdentifierRanges, + markInstructionIds, + markPredecessors, + reversePostorderBlocks, +} from '../HIR/HIRBuilder'; + +function createSymbolProperty( + fn: HIRFunction, + instr: Instruction, + nextInstructions: Array, + propertyName: string, + symbolName: string, +): ObjectProperty { + const symbolPlace = createTemporaryPlace(fn.env, instr.value.loc); + const symbolInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...symbolPlace, effect: Effect.Mutate}, + value: { + kind: 'LoadGlobal', + binding: {kind: 'Global', name: 'Symbol'}, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(symbolInstruction); + + const symbolForPlace = createTemporaryPlace(fn.env, instr.value.loc); + const symbolForInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...symbolForPlace, effect: Effect.Read}, + value: { + kind: 'PropertyLoad', + object: {...symbolInstruction.lvalue}, + property: 'for', + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(symbolForInstruction); + + const symbolValuePlace = createTemporaryPlace(fn.env, instr.value.loc); + const symbolValueInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...symbolValuePlace, effect: Effect.Mutate}, + value: { + kind: 'Primitive', + value: symbolName, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(symbolValueInstruction); + + const $$typeofPlace = createTemporaryPlace(fn.env, instr.value.loc); + const $$typeofInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...$$typeofPlace, effect: Effect.Mutate}, + value: { + kind: 'MethodCall', + receiver: symbolInstruction.lvalue, + property: symbolForInstruction.lvalue, + args: [symbolValueInstruction.lvalue], + loc: instr.value.loc, + }, + loc: instr.loc, + }; + const $$typeofProperty: ObjectProperty = { + kind: 'ObjectProperty', + key: {name: propertyName, kind: 'string'}, + type: 'property', + place: {...$$typeofPlace, effect: Effect.Capture}, + }; + nextInstructions.push($$typeofInstruction); + return $$typeofProperty; +} + +function createTagProperty( + fn: HIRFunction, + instr: Instruction, + nextInstructions: Array, + componentTag: BuiltinTag | Place, +): ObjectProperty { + let tagProperty: ObjectProperty; + switch (componentTag.kind) { + case 'BuiltinTag': { + const tagPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc); + const tagInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...tagPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'Primitive', + value: componentTag.name, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + tagProperty = { + kind: 'ObjectProperty', + key: {name: 'type', kind: 'string'}, + type: 'property', + place: {...tagPropertyPlace, effect: Effect.Capture}, + }; + nextInstructions.push(tagInstruction); + break; + } + case 'Identifier': { + tagProperty = { + kind: 'ObjectProperty', + key: {name: 'type', kind: 'string'}, + type: 'property', + place: {...componentTag, effect: Effect.Capture}, + }; + break; + } + } + + return tagProperty; +} + +function createPropsProperties( + fn: HIRFunction, + instr: Instruction, + nextInstructions: Array, + propAttributes: Array, + children: Array | null, +): { + refProperty: ObjectProperty; + keyProperty: ObjectProperty; + propsProperty: ObjectProperty; +} { + let refProperty: ObjectProperty | undefined; + let keyProperty: ObjectProperty | undefined; + const props: Array = []; + propAttributes.forEach(prop => { + switch (prop.kind) { + case 'JsxAttribute': { + if (prop.name === 'ref') { + refProperty = { + kind: 'ObjectProperty', + key: {name: 'ref', kind: 'string'}, + type: 'property', + place: {...prop.place}, + }; + } else if (prop.name === 'key') { + keyProperty = { + kind: 'ObjectProperty', + key: {name: 'key', kind: 'string'}, + type: 'property', + place: {...prop.place}, + }; + } else { + const attributeProperty: ObjectProperty = { + kind: 'ObjectProperty', + key: {name: prop.name, kind: 'string'}, + type: 'property', + place: {...prop.place}, + }; + props.push(attributeProperty); + } + break; + } + case 'JsxSpreadAttribute': { + // TODO: Optimize spreads to pass object directly if none of its properties are mutated + props.push({ + kind: 'Spread', + place: {...prop.argument}, + }); + break; + } + } + }); + const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc); + if (children) { + let childrenPropProperty: ObjectProperty; + if (children.length === 1) { + childrenPropProperty = { + kind: 'ObjectProperty', + key: {name: 'children', kind: 'string'}, + type: 'property', + place: {...children[0], effect: Effect.Capture}, + }; + } else { + const childrenPropPropertyPlace = createTemporaryPlace( + fn.env, + instr.value.loc, + ); + + const childrenPropInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...childrenPropPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'ArrayExpression', + elements: [...children], + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(childrenPropInstruction); + childrenPropProperty = { + kind: 'ObjectProperty', + key: {name: 'children', kind: 'string'}, + type: 'property', + place: {...childrenPropPropertyPlace, effect: Effect.Capture}, + }; + } + props.push(childrenPropProperty); + } + + if (refProperty == null) { + const refPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc); + const refInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...refPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'Primitive', + value: null, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + refProperty = { + kind: 'ObjectProperty', + key: {name: 'ref', kind: 'string'}, + type: 'property', + place: {...refPropertyPlace, effect: Effect.Capture}, + }; + nextInstructions.push(refInstruction); + } + + if (keyProperty == null) { + const keyPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc); + const keyInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...keyPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'Primitive', + value: null, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + keyProperty = { + kind: 'ObjectProperty', + key: {name: 'key', kind: 'string'}, + type: 'property', + place: {...keyPropertyPlace, effect: Effect.Capture}, + }; + nextInstructions.push(keyInstruction); + } + + const propsInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...propsPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'ObjectExpression', + properties: props, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + const propsProperty: ObjectProperty = { + kind: 'ObjectProperty', + key: {name: 'props', kind: 'string'}, + type: 'property', + place: {...propsPropertyPlace, effect: Effect.Capture}, + }; + nextInstructions.push(propsInstruction); + return {refProperty, keyProperty, propsProperty}; +} + +// TODO: Make PROD only with conditional statements +export function inlineJsxTransform(fn: HIRFunction): void { + for (const [, block] of fn.body.blocks) { + let nextInstructions: Array | null = null; + for (let i = 0; i < block.instructions.length; i++) { + const instr = block.instructions[i]!; + switch (instr.value.kind) { + case 'JsxExpression': { + nextInstructions ??= block.instructions.slice(0, i); + + const {refProperty, keyProperty, propsProperty} = + createPropsProperties( + fn, + instr, + nextInstructions, + instr.value.props, + instr.value.children, + ); + const reactElementInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...instr.lvalue, effect: Effect.Store}, + value: { + kind: 'ObjectExpression', + properties: [ + createSymbolProperty( + fn, + instr, + nextInstructions, + '$$typeof', + /** + * TODO: Add this to config so we can switch between + * react.element / react.transitional.element + */ + 'react.transitional.element', + ), + createTagProperty(fn, instr, nextInstructions, instr.value.tag), + refProperty, + keyProperty, + propsProperty, + ], + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(reactElementInstruction); + + break; + } + case 'JsxFragment': { + nextInstructions ??= block.instructions.slice(0, i); + const {refProperty, keyProperty, propsProperty} = + createPropsProperties( + fn, + instr, + nextInstructions, + [], + instr.value.children, + ); + const reactElementInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...instr.lvalue, effect: Effect.Store}, + value: { + kind: 'ObjectExpression', + properties: [ + createSymbolProperty( + fn, + instr, + nextInstructions, + '$$typeof', + /** + * TODO: Add this to config so we can switch between + * react.element / react.transitional.element + */ + 'react.transitional.element', + ), + createSymbolProperty( + fn, + instr, + nextInstructions, + 'type', + 'react.fragment', + ), + refProperty, + keyProperty, + propsProperty, + ], + loc: instr.value.loc, + }, + loc: instr.loc, + }; + nextInstructions.push(reactElementInstruction); + break; + } + default: { + if (nextInstructions !== null) { + nextInstructions.push(instr); + } + } + } + } + if (nextInstructions !== null) { + block.instructions = nextInstructions; + } + } + + // Fixup the HIR to restore RPO, ensure correct predecessors, and renumber instructions. + reversePostorderBlocks(fn.body); + markPredecessors(fn.body); + markInstructionIds(fn.body); + // The renumbering instructions invalidates scope and identifier ranges + fixScopeAndIdentifierRanges(fn.body); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/index.ts index 722b05a809960..bb060b8dc285c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/index.ts @@ -8,3 +8,4 @@ export {constantPropagation} from './ConstantPropagation'; export {deadCodeElimination} from './DeadCodeElimination'; export {pruneMaybeThrows} from './PruneMaybeThrows'; +export {inlineJsxTransform} from './InlineJsxTransform'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md new file mode 100644 index 0000000000000..f399317925c64 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md @@ -0,0 +1,226 @@ + +## Input + +```javascript +// @enableInlineJsxTransform + +function Parent({children, a: _a, b: _b, c: _c, ref}) { + return
{children}
; +} + +function Child({children}) { + return <>{children}; +} + +function GrandChild({className}) { + return ( + + Hello world + + ); +} + +function ParentAndRefAndKey(props) { + const testRef = useRef(); + return ; +} + +function ParentAndChildren(props) { + return ( + + + + + + + ); +} + +const propsToSpread = {a: 'a', b: 'b', c: 'c'}; +function PropsSpread() { + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentAndChildren, + params: [{foo: 'abc'}], +}; + +``` + +## Code + +```javascript +import { c as _c2 } from "react/compiler-runtime"; // @enableInlineJsxTransform + +function Parent(t0) { + const $ = _c2(2); + const { children, ref } = t0; + let t1; + if ($[0] !== children) { + t1 = { + $$typeof: Symbol.for("react.transitional.element"), + type: "div", + ref: ref, + key: null, + props: { children: children }, + }; + $[0] = children; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +function Child(t0) { + const $ = _c2(2); + const { children } = t0; + let t1; + if ($[0] !== children) { + t1 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Symbol.for("react.fragment"), + ref: null, + key: null, + props: { children: children }, + }; + $[0] = children; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +function GrandChild(t0) { + const $ = _c2(3); + const { className } = t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { + $$typeof: Symbol.for("react.transitional.element"), + type: React.Fragment, + ref: null, + key: "fragmentKey", + props: { children: "Hello world" }, + }; + $[0] = t1; + } else { + t1 = $[0]; + } + let t2; + if ($[1] !== className) { + t2 = { + $$typeof: Symbol.for("react.transitional.element"), + type: "span", + ref: null, + key: null, + props: { className: className, children: t1 }, + }; + $[1] = className; + $[2] = t2; + } else { + t2 = $[2]; + } + return t2; +} + +function ParentAndRefAndKey(props) { + const $ = _c2(1); + const testRef = useRef(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Parent, + ref: testRef, + key: "testKey", + props: { a: "a", b: { b: "b" }, c: C }, + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +function ParentAndChildren(props) { + const $ = _c2(3); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Child, + ref: null, + key: "a", + props: {}, + }; + $[0] = t0; + } else { + t0 = $[0]; + } + let t1; + if ($[1] !== props.foo) { + t1 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Parent, + ref: null, + key: null, + props: { + children: [ + t0, + { + $$typeof: Symbol.for("react.transitional.element"), + type: Child, + ref: null, + key: "b", + props: { + children: { + $$typeof: Symbol.for("react.transitional.element"), + type: GrandChild, + ref: null, + key: null, + props: { className: props.foo }, + }, + }, + }, + ], + }, + }; + $[1] = props.foo; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +const propsToSpread = { a: "a", b: "b", c: "c" }; +function PropsSpread() { + const $ = _c2(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Test, + ref: null, + key: null, + props: { ...propsToSpread }, + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentAndChildren, + params: [{ foo: "abc" }], +}; + +``` + +### Eval output +(kind: ok)
Hello world
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js new file mode 100644 index 0000000000000..29acaf60d276f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js @@ -0,0 +1,43 @@ +// @enableInlineJsxTransform + +function Parent({children, a: _a, b: _b, c: _c, ref}) { + return
{children}
; +} + +function Child({children}) { + return <>{children}; +} + +function GrandChild({className}) { + return ( + + Hello world + + ); +} + +function ParentAndRefAndKey(props) { + const testRef = useRef(); + return ; +} + +function ParentAndChildren(props) { + return ( + + + + + + + ); +} + +const propsToSpread = {a: 'a', b: 'b', c: 'c'}; +function PropsSpread() { + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentAndChildren, + params: [{foo: 'abc'}], +}; From 5e83d9ab3b3f88853591dff43cd70ee4e5c90c5d Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 17:37:00 +0100 Subject: [PATCH 164/191] feat[react-devtools]: add settings to global hook object (#30564) Right now we are patching console 2 times: when hook is installed (before page is loaded) and when backend is connected. Because of this, even if user had `appendComponentStack` setting enabled, all emitted error and warning logs are not going to have component stacks appended. They also won't have component stacks appended retroactively when user opens browser DevTools (this is when frontend is initialized and connects to backend). This behavior adds potential race conditions with LogBox in React Native, and also unpredictable to the user, because in order to get component stacks logged you have to open browser DevTools, but by the time you do it, error or warning log was already emitted. To solve this, we are going to only patch console in the hook object, because it is guaranteed to load even before React. Settings are going to be synchronized with the hook via Bridge, and React DevTools Backend Host (React Native or browser extension shell) will be responsible for persisting these settings across the session, this is going to be implemented in a separate PR. --- .../src/backend/agent.js | 39 +++++++------------ .../src/backend/console.js | 2 +- .../src/backend/index.js | 4 ++ .../src/backend/types.js | 8 ++++ packages/react-devtools-shared/src/hook.js | 28 ++++++++++++- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index f4665e014c023..8112fdcd2584b 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -37,11 +37,9 @@ import type { RendererID, RendererInterface, ConsolePatchSettings, + DevToolsHookSettings, } from './types'; -import type { - ComponentFilter, - BrowserTheme, -} from 'react-devtools-shared/src/frontend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils'; const debug = (methodName: string, ...args: Array) => { @@ -153,6 +151,7 @@ export default class Agent extends EventEmitter<{ drawTraceUpdates: [Array], disableTraceUpdates: [], getIfHasUnsupportedRendererVersion: [], + updateHookSettings: [DevToolsHookSettings], }> { _bridge: BackendBridge; _isProfiling: boolean = false; @@ -805,30 +804,22 @@ export default class Agent extends EventEmitter<{ } }; - updateConsolePatchSettings: ({ - appendComponentStack: boolean, - breakOnConsoleErrors: boolean, - browserTheme: BrowserTheme, - hideConsoleLogsInStrictMode: boolean, - showInlineWarningsAndErrors: boolean, - }) => void = ({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - browserTheme, - }: ConsolePatchSettings) => { + updateConsolePatchSettings: ( + settings: $ReadOnly, + ) => void = settings => { + // Propagate the settings, so Backend can subscribe to it and modify hook + this.emit('updateHookSettings', { + appendComponentStack: settings.appendComponentStack, + breakOnConsoleErrors: settings.breakOnConsoleErrors, + showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, + hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, + }); + // If the frontend preferences have changed, // or in the case of React Native- if the backend is just finding out the preferences- // then reinstall the console overrides. // It's safe to call `patchConsole` multiple times. - patchConsole({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - browserTheme, - }); + patchConsole(settings); }; updateComponentFilters: (componentFilters: Array) => void = diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 61d2e490eb966..ecca4c94246c6 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -135,7 +135,7 @@ export function patch({ showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, browserTheme, -}: ConsolePatchSettings): void { +}: $ReadOnly): void { // Settings may change after we've patched the console. // Using a shared ref allows the patch function to read the latest values. consoleSettingsRef.appendComponentStack = appendComponentStack; diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 0ac7ecc468aa5..5d84bf6bb70f0 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -83,6 +83,10 @@ export function initBackend( agent.removeListener('shutdown', onAgentShutdown); }); + agent.addListener('updateHookSettings', settings => { + hook.settings = settings; + }); + return () => { subs.forEach(fn => fn()); }; diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 7d816d403af07..5c014bba8051f 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -527,6 +527,7 @@ export type DevToolsHook = { // Testing dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void, + settings?: DevToolsHookSettings, ... }; @@ -537,3 +538,10 @@ export type ConsolePatchSettings = { hideConsoleLogsInStrictMode: boolean, browserTheme: BrowserTheme, }; + +export type DevToolsHookSettings = { + appendComponentStack: boolean, + breakOnConsoleErrors: boolean, + showInlineWarningsAndErrors: boolean, + hideConsoleLogsInStrictMode: boolean, +}; diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index eb81e81d4615e..1dd1fcbd4c455 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -15,6 +15,7 @@ import type { RendererID, RendererInterface, DevToolsBackend, + DevToolsHookSettings, } from './backend/types'; import { @@ -25,7 +26,12 @@ import attachRenderer from './attachRenderer'; declare var window: any; -export function installHook(target: any): DevToolsHook | null { +export function installHook( + target: any, + maybeSettingsOrSettingsPromise?: + | DevToolsHookSettings + | Promise, +): DevToolsHook | null { if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { return null; } @@ -566,6 +572,26 @@ export function installHook(target: any): DevToolsHook | null { registerInternalModuleStop, }; + if (maybeSettingsOrSettingsPromise == null) { + // Set default settings + hook.settings = { + appendComponentStack: true, + breakOnConsoleErrors: false, + showInlineWarningsAndErrors: true, + hideConsoleLogsInStrictMode: false, + }; + } else { + Promise.resolve(maybeSettingsOrSettingsPromise) + .then(settings => { + hook.settings = settings; + }) + .catch(() => { + targetConsole.error( + "React DevTools failed to get Console Patching settings. Console won't be patched and some console features will not work.", + ); + }); + } + if (__TEST__) { hook.dangerous_setTargetConsoleForTesting = dangerous_setTargetConsoleForTesting; From b521ef8a2aaff61154e59f6d0d3791ee4dbe6395 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:02:13 +0100 Subject: [PATCH 165/191] refactor[react-devtools]: remove browserTheme from ConsolePatchSettings (#30566) Stacked on https://github.com/facebook/react/pull/30564. We are no longer using browser theme in our console patching, this was removed in unification of console patching for strict mode, we started using ansi escape symbols and forking based on browser theme is no longer required - https://github.com/facebook/react/pull/29869 The real browser theme initialization for frontend is happening at the other place and is not affected: https://github.com/facebook/react/blob/40be968257a7a10a267210670103f20dd0429ef3/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js#L117-L120 --- packages/react-devtools-core/src/cachedSettings.js | 5 ++--- .../src/main/syncSavedPreferences.js | 4 ---- .../react-devtools-shared/src/backend/console.js | 9 +-------- packages/react-devtools-shared/src/backend/types.js | 12 +++--------- .../src/devtools/views/Settings/SettingsContext.js | 2 -- 5 files changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-core/src/cachedSettings.js b/packages/react-devtools-core/src/cachedSettings.js index 18ecd696b3c7c..afe12bfdbc5ad 100644 --- a/packages/react-devtools-core/src/cachedSettings.js +++ b/packages/react-devtools-core/src/cachedSettings.js @@ -9,7 +9,7 @@ import type {ConsolePatchSettings} from 'react-devtools-shared/src/backend/types'; import {writeConsolePatchSettingsToWindow} from 'react-devtools-shared/src/backend/console'; -import {castBool, castBrowserTheme} from 'react-devtools-shared/src/utils'; +import {castBool} from 'react-devtools-shared/src/utils'; // Note: all keys should be optional in this type, because users can use newer // versions of React DevTools with older versions of React Native, and the object @@ -54,14 +54,13 @@ function parseConsolePatchSettings( breakOnConsoleErrors, showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, - browserTheme, } = parsedValue; + return { appendComponentStack: castBool(appendComponentStack) ?? true, breakOnConsoleErrors: castBool(breakOnConsoleErrors) ?? false, showInlineWarningsAndErrors: castBool(showInlineWarningsAndErrors) ?? true, hideConsoleLogsInStrictMode: castBool(hideConsoleLogsInStrictMode) ?? false, - browserTheme: castBrowserTheme(browserTheme) ?? 'dark', }; } diff --git a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js b/packages/react-devtools-extensions/src/main/syncSavedPreferences.js index 6ceed86fcd06d..f22d41eb7c904 100644 --- a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js +++ b/packages/react-devtools-extensions/src/main/syncSavedPreferences.js @@ -7,7 +7,6 @@ import { getShowInlineWarningsAndErrors, getHideConsoleLogsInStrictMode, } from 'react-devtools-shared/src/utils'; -import {getBrowserTheme} from 'react-devtools-extensions/src/utils'; // The renderer interface can't read saved component filters directly, // because they are stored in localStorage within the context of the extension. @@ -28,9 +27,6 @@ function syncSavedPreferences() { )}; window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify( getHideConsoleLogsInStrictMode(), - )}; - window.__REACT_DEVTOOLS_BROWSER_THEME__ = ${JSON.stringify( - getBrowserTheme(), )};`, ); } diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index ecca4c94246c6..0b6b19626a9bb 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -22,7 +22,7 @@ import { ANSI_STYLE_DIMMING_TEMPLATE, ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, } from 'react-devtools-shared/src/constants'; -import {castBool, castBrowserTheme} from '../utils'; +import {castBool} from '../utils'; const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn']; @@ -124,7 +124,6 @@ const consoleSettingsRef: ConsolePatchSettings = { breakOnConsoleErrors: false, showInlineWarningsAndErrors: false, hideConsoleLogsInStrictMode: false, - browserTheme: 'dark', }; // Patches console methods to append component stack for the current fiber. @@ -134,7 +133,6 @@ export function patch({ breakOnConsoleErrors, showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, - browserTheme, }: $ReadOnly): void { // Settings may change after we've patched the console. // Using a shared ref allows the patch function to read the latest values. @@ -142,7 +140,6 @@ export function patch({ consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors; consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors; consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode; - consoleSettingsRef.browserTheme = browserTheme; if ( appendComponentStack || @@ -412,15 +409,12 @@ export function patchConsoleUsingWindowValues() { const hideConsoleLogsInStrictMode = castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ?? false; - const browserTheme = - castBrowserTheme(window.__REACT_DEVTOOLS_BROWSER_THEME__) ?? 'dark'; patch({ appendComponentStack, breakOnConsoleErrors, showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, - browserTheme, }); } @@ -438,7 +432,6 @@ export function writeConsolePatchSettingsToWindow( settings.showInlineWarningsAndErrors; window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = settings.hideConsoleLogsInStrictMode; - window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme; } export function installConsoleFunctionsToWindow(): void { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 5c014bba8051f..fa9949e3ddc5d 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -31,7 +31,6 @@ import type { } from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; import type {InitBackend} from 'react-devtools-shared/src/backend'; import type {TimelineDataExport} from 'react-devtools-timeline/src/types'; -import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type {Source} from 'react-devtools-shared/src/shared/types'; import type Agent from './agent'; @@ -531,17 +530,12 @@ export type DevToolsHook = { ... }; -export type ConsolePatchSettings = { - appendComponentStack: boolean, - breakOnConsoleErrors: boolean, - showInlineWarningsAndErrors: boolean, - hideConsoleLogsInStrictMode: boolean, - browserTheme: BrowserTheme, -}; - export type DevToolsHookSettings = { appendComponentStack: boolean, breakOnConsoleErrors: boolean, showInlineWarningsAndErrors: boolean, hideConsoleLogsInStrictMode: boolean, }; + +// Will be removed together with console patching from backend/console.js to hook.js +export type ConsolePatchSettings = DevToolsHookSettings; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index a6f2c5e8c2a78..33487c2f553d6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -202,7 +202,6 @@ function SettingsContextController({ breakOnConsoleErrors, showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, - browserTheme, }); }, [ bridge, @@ -210,7 +209,6 @@ function SettingsContextController({ breakOnConsoleErrors, showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, - browserTheme, ]); useEffect(() => { From 3cac0875dcd60b8db099d8fa671c5ad1f8f0ef23 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:12:18 +0100 Subject: [PATCH 166/191] refactor[react-devtools]: move console patching to global hook (#30596) Stacked on https://github.com/facebook/react/pull/30566 and whats under it. See [this commit](https://github.com/facebook/react/pull/30596/commits/374fd737e4b0b7028afb765838db7c0e22def865). It is mostly copying code from one place to another and updating tests. With these changes, for every console method that we patch, there is going to be a single applied patch: - For `error`, `warn`, and `trace` we are patching when hook is installed. This guarantees that component stacks are going to be appended even if browser DevTools are not opened. We pay some price for it, though: if user has browser DevTools closed and if at this point some warning or error is emitted (logged), the next time user opens browser DevTools, they are going to see `hook.js` as the source frame. Unfortunately, ignore listing from source maps is not applied retroactively, and I don't know if its a bug or just a design limitations. Once browser DevTools are opened, source maps will be loaded and ignore listing will be applied for all emitted logs in the future. - For `log`, `info`, `group`, `groupCollapsed` we are only patching when React notifies React DevTools about running in StrictMode. We unpatch the methods right after it. --- packages/react-devtools-core/src/backend.js | 4 - packages/react-devtools-inline/src/backend.js | 4 - .../src/__tests__/componentStacks-test.js | 62 +- .../src/__tests__/console-test.js | 973 +++++------------- .../src/__tests__/setupTests.js | 144 ++- .../src/backend/agent.js | 10 +- .../src/backend/console.js | 418 +------- .../src/backend/fiber/renderer.js | 18 - .../src/backend/flight/renderer.js | 10 - .../src/backend/legacy/renderer.js | 6 - .../src/backend/types.js | 2 - packages/react-devtools-shared/src/bridge.js | 4 +- packages/react-devtools-shared/src/hook.js | 408 +++++--- 13 files changed, 680 insertions(+), 1383 deletions(-) diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 25001502f1c2b..d588d4f91e9f0 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -11,7 +11,6 @@ import Agent from 'react-devtools-shared/src/backend/agent'; import Bridge from 'react-devtools-shared/src/bridge'; import {installHook} from 'react-devtools-shared/src/hook'; import {initBackend} from 'react-devtools-shared/src/backend'; -import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; @@ -41,9 +40,6 @@ type ConnectOptions = { devToolsSettingsManager: ?DevToolsSettingsManager, }; -// Install a global variable to allow patching console early (during injection). -// This provides React Native developers with components stacks even if they don't run DevTools. -installConsoleFunctionsToWindow(); installHook(window); const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index e7d0485b37569..fca1535c4e5ba 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -3,7 +3,6 @@ import Agent from 'react-devtools-shared/src/backend/agent'; import Bridge from 'react-devtools-shared/src/bridge'; import {initBackend} from 'react-devtools-shared/src/backend'; -import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console'; import {installHook} from 'react-devtools-shared/src/hook'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; @@ -120,8 +119,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge { } export function initialize(contentWindow: any): void { - // Install a global variable to allow patching console early (during injection). - // This provides React Native developers with components stacks even if they don't run DevTools. - installConsoleFunctionsToWindow(); installHook(contentWindow); } diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js index b99db5c540097..54af62db44e92 100644 --- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js +++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js @@ -7,27 +7,17 @@ * @flow */ -import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils'; +import { + getVersionedRenderImplementation, + normalizeCodeLocInfo, +} from 'react-devtools-shared/src/__tests__/utils'; describe('component stack', () => { let React; let act; - let mockError; - let mockWarn; let supportsOwnerStacks; beforeEach(() => { - // Intercept native console methods before DevTools bootstraps. - // Normalize component stack locations. - mockError = jest.fn(); - mockWarn = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; - console.warn = (...args) => { - mockWarn(...args.map(normalizeCodeLocInfo)); - }; - const utils = require('./utils'); act = utils.act; @@ -54,18 +44,22 @@ describe('component stack', () => { act(() => render()); - expect(mockError).toHaveBeenCalledWith( + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ 'Test error.', '\n in Child (at **)' + '\n in Parent (at **)' + '\n in Grandparent (at **)', - ); - expect(mockWarn).toHaveBeenCalledWith( + ]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ 'Test warning.', '\n in Child (at **)' + '\n in Parent (at **)' + '\n in Grandparent (at **)', - ); + ]); }); // This test should have caught #19911 @@ -89,13 +83,15 @@ describe('component stack', () => { expect(useEffectCount).toBe(1); - expect(mockWarn).toHaveBeenCalledWith( + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ 'Warning to trigger appended component stacks.', '\n in Example (at **)', - ); + ]); }); - // @reactVersion >=18.3 + // @reactVersion >= 18.3 it('should log the current component stack with debug info from promises', () => { const Child = () => { console.error('Test error.'); @@ -117,23 +113,27 @@ describe('component stack', () => { act(() => render()); - expect(mockError).toHaveBeenCalledWith( + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ 'Test error.', supportsOwnerStacks ? '\n in Child (at **)' : '\n in Child (at **)' + - '\n in ServerComponent (at **)' + - '\n in Parent (at **)' + - '\n in Grandparent (at **)', - ); - expect(mockWarn).toHaveBeenCalledWith( + '\n in ServerComponent (at **)' + + '\n in Parent (at **)' + + '\n in Grandparent (at **)', + ]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ 'Test warning.', supportsOwnerStacks ? '\n in Child (at **)' : '\n in Child (at **)' + - '\n in ServerComponent (at **)' + - '\n in Parent (at **)' + - '\n in Grandparent (at **)', - ); + '\n in ServerComponent (at **)' + + '\n in Parent (at **)' + + '\n in Grandparent (at **)', + ]); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index 516762132e884..00d6d9712679a 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -7,52 +7,25 @@ * @flow */ -import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils'; +import { + getVersionedRenderImplementation, + normalizeCodeLocInfo, +} from 'react-devtools-shared/src/__tests__/utils'; let React; let ReactDOMClient; let act; -let fakeConsole; -let mockError; -let mockInfo; -let mockGroup; -let mockGroupCollapsed; -let mockLog; -let mockWarn; -let patchConsole; -let unpatchConsole; let rendererID; let supportsOwnerStacks = false; describe('console', () => { beforeEach(() => { - const Console = require('react-devtools-shared/src/backend/console'); - - patchConsole = Console.patch; - unpatchConsole = Console.unpatch; - - // Patch a fake console so we can verify with tests below. - // Patching the real console is too complicated, - // because Jest itself has hooks into it as does our test env setup. - mockError = jest.fn(); - mockInfo = jest.fn(); - mockGroup = jest.fn(); - mockGroupCollapsed = jest.fn(); - mockLog = jest.fn(); - mockWarn = jest.fn(); - fakeConsole = { - error: mockError, - info: mockInfo, - log: mockLog, - warn: mockWarn, - group: mockGroup, - groupCollapsed: mockGroupCollapsed, - }; + const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => { + rendererID = inject(internals); - Console.dangerous_setTargetConsoleForTesting(fakeConsole); - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.dangerous_setTargetConsoleForTesting( - fakeConsole, - ); + return rendererID; + }; React = require('react'); if ( @@ -69,137 +42,44 @@ describe('console', () => { const {render} = getVersionedRenderImplementation(); - // @reactVersion >=18.0 - it('should not patch console methods that are not explicitly overridden', () => { - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.info).toBe(mockInfo); - expect(fakeConsole.log).toBe(mockLog); - expect(fakeConsole.warn).not.toBe(mockWarn); - expect(fakeConsole.group).toBe(mockGroup); - expect(fakeConsole.groupCollapsed).toBe(mockGroupCollapsed); - }); - - // @reactVersion >=18.0 - it('should patch the console when appendComponentStack is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should patch the console when breakOnConsoleErrors is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: false, - breakOnConsoleErrors: true, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should patch the console when showInlineWarningsAndErrors is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: false, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: true, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should only patch the console once', () => { - const {error, warn} = fakeConsole; - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).toBe(error); - expect(fakeConsole.warn).toBe(warn); - }); - - // @reactVersion >=18.0 - it('should un-patch when requested', () => { - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); + // @reactVersion >= 18.0 + it('should pass through logs when there is no current fiber', () => { + expect(global.consoleLogMock).toHaveBeenCalledTimes(0); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(0); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(0); - unpatchConsole(); + console.log('log'); + console.warn('warn'); + console.error('error'); - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect(global.consoleWarnMock.mock.calls).toEqual([['warn']]); + expect(global.consoleErrorMock.mock.calls).toEqual([['error']]); }); - // @reactVersion >=18.0 - it('should pass through logs when there is no current fiber', () => { - expect(mockLog).toHaveBeenCalledTimes(0); - expect(mockWarn).toHaveBeenCalledTimes(0); - expect(mockError).toHaveBeenCalledTimes(0); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - }); - - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should not append multiple stacks', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Child = ({children}) => { - fakeConsole.warn('warn\n in Child (at fake.js:123)'); - fakeConsole.error('error', '\n in Child (at fake.js:123)'); + console.warn('warn', '\n in Child (at fake.js:123)'); + console.error('error', '\n in Child (at fake.js:123)'); return null; }; act(() => render()); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe( - 'warn\n in Child (at fake.js:123)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)'); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual(['warn', '\n in Child (at **)']); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual(['error', '\n in Child (at **)']); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged during render', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -208,36 +88,34 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from effects', () => { const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -247,60 +125,63 @@ describe('console', () => { ); const Child = ({children}) => { React.useLayoutEffect(function Child_useLayoutEffect() { - fakeConsole.error('active error'); - fakeConsole.log('active log'); - fakeConsole.warn('active warn'); + console.error('active error'); + console.log('active log'); + console.warn('active warn'); }); React.useEffect(function Child_useEffect() { - fakeConsole.error('passive error'); - fakeConsole.log('passive log'); - fakeConsole.warn('passive warn'); + console.error('passive error'); + console.log('passive log'); + console.warn('passive warn'); }); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('active log'); - expect(mockLog.mock.calls[1]).toHaveLength(1); - expect(mockLog.mock.calls[1][0]).toBe('passive log'); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('active warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([ + ['active log'], + ['passive log'], + ]); + + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'active warn', supportsOwnerStacks ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('passive warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + ]); + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'passive warn', supportsOwnerStacks ? '\n in Child_useEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('active error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'active error', supportsOwnerStacks ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('passive error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'passive error', supportsOwnerStacks ? '\n in Child_useEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from commit hooks', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -310,14 +191,14 @@ describe('console', () => { ); class Child extends React.Component { componentDidMount() { - fakeConsole.error('didMount error'); - fakeConsole.log('didMount log'); - fakeConsole.warn('didMount warn'); + console.error('didMount error'); + console.log('didMount log'); + console.warn('didMount warn'); } componentDidUpdate() { - fakeConsole.error('didUpdate error'); - fakeConsole.log('didUpdate log'); - fakeConsole.warn('didUpdate warn'); + console.error('didUpdate error'); + console.log('didUpdate log'); + console.warn('didUpdate warn'); } render() { return null; @@ -327,44 +208,47 @@ describe('console', () => { act(() => render()); act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('didMount log'); - expect(mockLog.mock.calls[1]).toHaveLength(1); - expect(mockLog.mock.calls[1][0]).toBe('didUpdate log'); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('didMount warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([ + ['didMount log'], + ['didUpdate log'], + ]); + + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'didMount warn', supportsOwnerStacks ? '\n in Child.componentDidMount (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + ]); + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'didUpdate warn', supportsOwnerStacks ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('didMount error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'didMount error', supportsOwnerStacks ? '\n in Child.componentDidMount (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('didUpdate error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'didUpdate error', supportsOwnerStacks ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from gDSFP', () => { const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -375,9 +259,9 @@ describe('console', () => { class Child extends React.Component { state = {}; static getDerivedStateFromProps() { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; } render() { @@ -387,71 +271,27 @@ describe('console', () => { act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - }); - - // @reactVersion >=18.0 - it('should append stacks after being uninstalled and reinstalled', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - - const Child = ({children}) => { - fakeConsole.warn('warn'); - fakeConsole.error('error'); - return null; - }; - - act(() => render()); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - act(() => render()); - - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( - '\n in Child (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( - '\n in Child (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should be resilient to prepareStackTrace', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; - Error.prepareStackTrace = function (error, callsites) { const stack = ['An error occurred:', error.message]; for (let i = 0; i < callsites.length; i++) { @@ -473,62 +313,66 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should correctly log Symbols', () => { + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + const Component = ({children}) => { - fakeConsole.warn('Symbol:', Symbol('')); + console.warn('Symbol:', Symbol('')); return null; }; act(() => render()); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0][0]).toBe('Symbol:'); + expect(global.consoleWarnMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Symbol:", + Symbol(), + ], + ] + `); }); it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -539,77 +383,38 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toEqual([ + + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); - - expect(mockInfo).toHaveBeenCalledTimes(2); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - expect(mockInfo.mock.calls[1]).toHaveLength(2); - expect(mockInfo.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'info', - ]); - - expect(mockGroup).toHaveBeenCalledTimes(2); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - expect(mockGroup.mock.calls[1]).toHaveLength(2); - expect(mockGroup.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'group', - ]); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2); - expect(mockGroupCollapsed.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'groupCollapsed', - ]); }); it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - console.log( - 'CALL', - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__, - ); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -621,54 +426,29 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - expect(mockInfo).toHaveBeenCalledTimes(1); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - - expect(mockGroup).toHaveBeenCalledTimes(1); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useEffect(() => { - fakeConsole.log('log effect create'); - fakeConsole.warn('warn effect create'); - fakeConsole.error('error effect create'); - fakeConsole.info('info effect create'); - fakeConsole.group('group effect create'); - fakeConsole.groupCollapsed('groupCollapsed effect create'); + console.log('log effect create'); + console.warn('warn effect create'); + console.error('error effect create'); return () => { - fakeConsole.log('log effect cleanup'); - fakeConsole.warn('warn effect cleanup'); - fakeConsole.error('error effect cleanup'); - fakeConsole.info('info effect cleanup'); - fakeConsole.group('group effect cleanup'); - fakeConsole.groupCollapsed('groupCollapsed effect cleanup'); + console.log('log effect cleanup'); + console.warn('warn effect cleanup'); + console.error('error effect cleanup'); }; }); @@ -682,61 +462,41 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls).toEqual([ + expect(global.consoleLogMock.mock.calls).toEqual([ ['log effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect create'], ]); - expect(mockWarn.mock.calls).toEqual([ + expect(global.consoleWarnMock.mock.calls).toEqual([ ['warn effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect create'], ]); - expect(mockError.mock.calls).toEqual([ + expect(global.consoleErrorMock.mock.calls).toEqual([ ['error effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect create'], ]); - expect(mockInfo.mock.calls).toEqual([ - ['info effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect create'], - ]); - expect(mockGroup.mock.calls).toEqual([ - ['group effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect create'], - ]); - expect(mockGroupCollapsed.mock.calls).toEqual([ - ['groupCollapsed effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect create'], - ]); }); it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useEffect(() => { - fakeConsole.log('log effect create'); - fakeConsole.warn('warn effect create'); - fakeConsole.error('error effect create'); - fakeConsole.info('info effect create'); - fakeConsole.group('group effect create'); - fakeConsole.groupCollapsed('groupCollapsed effect create'); + console.log('log effect create'); + console.warn('warn effect create'); + console.error('error effect create'); return () => { - fakeConsole.log('log effect cleanup'); - fakeConsole.warn('warn effect cleanup'); - fakeConsole.error('error effect cleanup'); - fakeConsole.info('info effect cleanup'); - fakeConsole.group('group effect cleanup'); - fakeConsole.groupCollapsed('groupCollapsed effect cleanup'); + console.log('log effect cleanup'); + console.warn('warn effect cleanup'); + console.error('error effect cleanup'); }; }); @@ -750,31 +510,25 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls).toEqual([['log effect create']]); - expect(mockWarn.mock.calls).toEqual([['warn effect create']]); - expect(mockError.mock.calls).toEqual([['error effect create']]); - expect(mockInfo.mock.calls).toEqual([['info effect create']]); - expect(mockGroup.mock.calls).toEqual([['group effect create']]); - expect(mockGroupCollapsed.mock.calls).toEqual([ - ['groupCollapsed effect create'], - ]); + + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useMemo(() => { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); }, []); return
; } @@ -786,78 +540,39 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toEqual([ + + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); - - expect(mockInfo).toHaveBeenCalledTimes(2); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - expect(mockInfo.mock.calls[1]).toHaveLength(2); - expect(mockInfo.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'info', - ]); - - expect(mockGroup).toHaveBeenCalledTimes(2); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - expect(mockGroup.mock.calls[1]).toHaveLength(2); - expect(mockGroup.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'group', - ]); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2); - expect(mockGroupCollapsed.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'groupCollapsed', - ]); }); it('should not double log from useMemo fns if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useMemo(() => { - console.log( - 'CALL', - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__, - ); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); }, []); return
; } @@ -870,49 +585,27 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - expect(mockInfo).toHaveBeenCalledTimes(1); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - - expect(mockGroup).toHaveBeenCalledTimes(1); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log in Strict mode initial render for extension', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; // This simulates a render that happens before React DevTools have finished // their handshake to attach the React DOM renderer functions to DevTools // In this case, we should still be able to mock the console in Strict mode - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set( - rendererID, - null, - ); + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -924,52 +617,41 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toHaveLength(2); - expect(mockLog.mock.calls[1]).toEqual([ + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); }); it('should not double log in Strict mode initial render for extension', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; // This simulates a render that happens before React DevTools have finished // their handshake to attach the React DOM renderer functions to DevTools // In this case, we should still be able to mock the console in Strict mode - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set( - rendererID, - null, - ); + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -980,22 +662,16 @@ describe('console', () => { , ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should properly dim component stacks during strict mode double log', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -1007,8 +683,8 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.warn('warn'); + console.error('error'); + console.warn('warn'); return null; }; @@ -1020,140 +696,41 @@ describe('console', () => { ), ); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(3); - expect(mockWarn.mock.calls[1][0]).toEqual( + ]); + + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', - ); - expect(mockWarn.mock.calls[1][1]).toMatch('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual( + 'warn', supportsOwnerStacks - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist. - '\n in Child (at **)\n in Parent (at **)' + ? '\n in Child (at **)\n in Parent (at **)' : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual( + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(3); - expect(mockError.mock.calls[1][0]).toEqual( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', - ); - expect(mockError.mock.calls[1][1]).toEqual('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual( + 'error', supportsOwnerStacks - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist. - '\n in Child (at **)\n in Parent (at **)' + ? '\n in Child (at **)\n in Parent (at **)' : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - }); -}); - -describe('console error', () => { - beforeEach(() => { - jest.resetModules(); - - const Console = require('react-devtools-shared/src/backend/console'); - patchConsole = Console.patch; - unpatchConsole = Console.unpatch; - - // Patch a fake console so we can verify with tests below. - // Patching the real console is too complicated, - // because Jest itself has hooks into it as does our test env setup. - mockError = jest.fn(); - mockInfo = jest.fn(); - mockGroup = jest.fn(); - mockGroupCollapsed = jest.fn(); - mockLog = jest.fn(); - mockWarn = jest.fn(); - fakeConsole = { - error: mockError, - info: mockInfo, - log: mockLog, - warn: mockWarn, - group: mockGroup, - groupCollapsed: mockGroupCollapsed, - }; - - Console.dangerous_setTargetConsoleForTesting(fakeConsole); - - const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => { - inject(internals); - - Console.registerRenderer( - () => { - throw Error('foo'); - }, - () => { - return { - enableOwnerStacks: true, - componentStack: '\n at FakeStack (fake-file)', - }; - }, - ); - }; - - React = require('react'); - ReactDOMClient = require('react-dom/client'); - - const utils = require('./utils'); - act = utils.act; - }); - - // @reactVersion >=18.0 - it('error in console log throws without interfering with logging', () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - return
; - } - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: true, - hideConsoleLogsInStrictMode: false, - }); - - expect(() => { - act(() => { - root.render(); - }); - }).toThrowError('foo'); - - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - // An error in showInlineWarningsAndErrors doesn't need to break component stacks. - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in FakeStack (at **)', - ); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in FakeStack (at **)', - ); + ]); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 79cda67f99b18..d4afd05899a56 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -13,6 +13,7 @@ import type { BackendBridge, FrontendBridge, } from 'react-devtools-shared/src/bridge'; + const {getTestFlags} = require('../../../../scripts/jest/TestFlags'); // Argument is serialized when passed from jest-cli script through to setupTests. @@ -103,61 +104,36 @@ global.gate = fn => { return fn(flags); }; -beforeEach(() => { - global.mockClipboardCopy = jest.fn(); - - // Test environment doesn't support document methods like execCommand() - // Also once the backend components below have been required, - // it's too late for a test to mock the clipboard-js modules. - jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy})); - - // These files should be required (and re-required) before each test, - // rather than imported at the head of the module. - // That's because we reset modules between tests, - // which disconnects the DevTool's cache from the current dispatcher ref. - const Agent = require('react-devtools-shared/src/backend/agent').default; - const {initBackend} = require('react-devtools-shared/src/backend'); - const Bridge = require('react-devtools-shared/src/bridge').default; - const Store = require('react-devtools-shared/src/devtools/store').default; - const {installHook} = require('react-devtools-shared/src/hook'); - const { - getDefaultComponentFilters, - setSavedComponentFilters, - } = require('react-devtools-shared/src/utils'); +function shouldIgnoreConsoleErrorOrWarn(args) { + let firstArg = args[0]; + if ( + firstArg !== null && + typeof firstArg === 'object' && + String(firstArg).indexOf('Error: Uncaught [') === 0 + ) { + firstArg = String(firstArg); + } else if (typeof firstArg !== 'string') { + return false; + } - // Fake timers let us flush Bridge operations between setup and assertions. - jest.useFakeTimers(); + return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => { + return firstArg.indexOf(errorOrWarningMessage) !== -1; + }); +} - // We use fake timers heavily in tests but the bridge batching now uses microtasks. - global.devtoolsJestTestScheduler = callback => { - setTimeout(callback, 0); - }; +function patchConsoleForTestingBeforeHookInstallation() { + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + const originalConsoleLog = console.log; - // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array. - global._ignoredErrorOrWarningMessages = [ - 'react-test-renderer is deprecated.', - ]; - function shouldIgnoreConsoleErrorOrWarn(args) { - let firstArg = args[0]; - if ( - firstArg !== null && - typeof firstArg === 'object' && - String(firstArg).indexOf('Error: Uncaught [') === 0 - ) { - firstArg = String(firstArg); - } else if (typeof firstArg !== 'string') { - return false; - } - const shouldFilter = global._ignoredErrorOrWarningMessages.some( - errorOrWarningMessage => { - return firstArg.indexOf(errorOrWarningMessage) !== -1; - }, - ); + const consoleErrorMock = jest.fn(); + const consoleWarnMock = jest.fn(); + const consoleLogMock = jest.fn(); - return shouldFilter; - } + global.consoleErrorMock = consoleErrorMock; + global.consoleWarnMock = consoleWarnMock; + global.consoleLogMock = consoleLogMock; - const originalConsoleError = console.error; console.error = (...args) => { let firstArg = args[0]; if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) { @@ -184,17 +160,68 @@ beforeEach(() => { // Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored return; } + + consoleErrorMock(...args); originalConsoleError.apply(console, args); }; - const originalConsoleWarn = console.warn; console.warn = (...args) => { if (shouldIgnoreConsoleErrorOrWarn(args)) { // Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output. // Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored return; } + + consoleWarnMock(...args); originalConsoleWarn.apply(console, args); }; + console.log = (...args) => { + consoleLogMock(...args); + originalConsoleLog.apply(console, args); + }; +} + +function unpatchConsoleAfterTesting() { + delete global.consoleErrorMock; + delete global.consoleWarnMock; + delete global.consoleLogMock; +} + +beforeEach(() => { + patchConsoleForTestingBeforeHookInstallation(); + + global.mockClipboardCopy = jest.fn(); + + // Test environment doesn't support document methods like execCommand() + // Also once the backend components below have been required, + // it's too late for a test to mock the clipboard-js modules. + jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy})); + + // These files should be required (and re-required) before each test, + // rather than imported at the head of the module. + // That's because we reset modules between tests, + // which disconnects the DevTool's cache from the current dispatcher ref. + const Agent = require('react-devtools-shared/src/backend/agent').default; + const {initBackend} = require('react-devtools-shared/src/backend'); + const Bridge = require('react-devtools-shared/src/bridge').default; + const Store = require('react-devtools-shared/src/devtools/store').default; + const {installHook} = require('react-devtools-shared/src/hook'); + const { + getDefaultComponentFilters, + setSavedComponentFilters, + } = require('react-devtools-shared/src/utils'); + + // Fake timers let us flush Bridge operations between setup and assertions. + jest.useFakeTimers(); + + // We use fake timers heavily in tests but the bridge batching now uses microtasks. + global.devtoolsJestTestScheduler = callback => { + setTimeout(callback, 0); + }; + + // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array. + global._ignoredErrorOrWarningMessages = [ + 'react-test-renderer is deprecated.', + ]; // Initialize filters to a known good state. setSavedComponentFilters(getDefaultComponentFilters()); @@ -203,7 +230,12 @@ beforeEach(() => { // Also initialize inline warnings so that we can test them. global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true; - installHook(global); + installHook(global, { + appendComponentStack: true, + breakOnConsoleErrors: false, + showInlineWarningsAndErrors: true, + hideConsoleLogsInStrictMode: false, + }); const bridgeListeners = []; const bridge = new Bridge({ @@ -221,14 +253,12 @@ beforeEach(() => { }, }); - const agent = new Agent(((bridge: any): BackendBridge)); + const store = new Store(((bridge: any): FrontendBridge)); + const agent = new Agent(((bridge: any): BackendBridge)); const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__; - initBackend(hook, agent, global); - const store = new Store(((bridge: any): FrontendBridge)); - global.agent = agent; global.bridge = bridge; global.store = store; @@ -243,8 +273,10 @@ beforeEach(() => { } global.fetch = mockFetch; }); + afterEach(() => { delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; + unpatchConsoleAfterTesting(); // It's important to reset modules between test runs; // Without this, ReactDOM won't re-inject itself into the new hook. diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 8112fdcd2584b..814c6c1c40e67 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -24,7 +24,6 @@ import { initialize as setupTraceUpdates, toggleEnabled as setTraceUpdatesEnabled, } from './views/TraceUpdates'; -import {patch as patchConsole} from './console'; import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; @@ -36,7 +35,6 @@ import type { PathMatch, RendererID, RendererInterface, - ConsolePatchSettings, DevToolsHookSettings, } from './types'; import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; @@ -805,7 +803,7 @@ export default class Agent extends EventEmitter<{ }; updateConsolePatchSettings: ( - settings: $ReadOnly, + settings: $ReadOnly, ) => void = settings => { // Propagate the settings, so Backend can subscribe to it and modify hook this.emit('updateHookSettings', { @@ -814,12 +812,6 @@ export default class Agent extends EventEmitter<{ showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, }); - - // If the frontend preferences have changed, - // or in the case of React Native- if the backend is just finding out the preferences- - // then reinstall the console overrides. - // It's safe to call `patchConsole` multiple times. - patchConsole(settings); }; updateComponentFilters: (componentFilters: Array) => void = diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 0b6b19626a9bb..9e61285b50fe4 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -7,416 +7,7 @@ * @flow */ -import type { - ConsolePatchSettings, - OnErrorOrWarning, - GetComponentStack, -} from './types'; - -import { - formatConsoleArguments, - formatWithStyles, -} from 'react-devtools-shared/src/backend/utils'; -import { - FIREFOX_CONSOLE_DIMMING_COLOR, - ANSI_STYLE_DIMMING_TEMPLATE, - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, -} from 'react-devtools-shared/src/constants'; -import {castBool} from '../utils'; - -const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn']; - -// React's custom built component stack strings match "\s{4}in" -// Chrome's prefix matches "\s{4}at" -const PREFIX_REGEX = /\s{4}(in|at)\s{1}/; -// Firefox and Safari have no prefix ("") -// but we can fallback to looking for location info (e.g. "foo.js:12:345") -const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/; - -export function isStringComponentStack(text: string): boolean { - return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text); -} - -const STYLE_DIRECTIVE_REGEX = /^%c/; - -// This function tells whether or not the arguments for a console -// method has been overridden by the patchForStrictMode function. -// If it has we'll need to do some special formatting of the arguments -// so the console color stays consistent -function isStrictModeOverride(args: Array): boolean { - if (__IS_FIREFOX__) { - return ( - args.length >= 2 && - STYLE_DIRECTIVE_REGEX.test(args[0]) && - args[1] === FIREFOX_CONSOLE_DIMMING_COLOR - ); - } else { - return args.length >= 2 && args[0] === ANSI_STYLE_DIMMING_TEMPLATE; - } -} - -// We add a suffix to some frames that older versions of React didn't do. -// To compare if it's equivalent we strip out the suffix to see if they're -// still equivalent. Similarly, we sometimes use [] and sometimes () so we -// strip them to for the comparison. -const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; -function areStackTracesEqual(a: string, b: string): boolean { - return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); -} - -function restorePotentiallyModifiedArgs(args: Array): Array { - // If the arguments don't have any styles applied, then just copy - if (!isStrictModeOverride(args)) { - return args.slice(); - } - - if (__IS_FIREFOX__) { - // Filter out %c from the start of the first argument and color as a second argument - return [args[0].slice(2)].concat(args.slice(2)); - } else { - // Filter out the `\x1b...%s\x1b` template - return args.slice(1); - } -} - -const injectedRenderers: Array<{ - onErrorOrWarning: ?OnErrorOrWarning, - getComponentStack: ?GetComponentStack, -}> = []; - -let targetConsole: Object = console; -let targetConsoleMethods: {[string]: $FlowFixMe} = {}; -for (const method in console) { - // $FlowFixMe[invalid-computed-prop] - targetConsoleMethods[method] = console[method]; -} - -let unpatchFn: null | (() => void) = null; - -// Enables e.g. Jest tests to inject a mock console object. -export function dangerous_setTargetConsoleForTesting( - targetConsoleForTesting: Object, -): void { - targetConsole = targetConsoleForTesting; - - targetConsoleMethods = ({}: {[string]: $FlowFixMe}); - for (const method in targetConsole) { - // $FlowFixMe[invalid-computed-prop] - targetConsoleMethods[method] = console[method]; - } -} - -// v16 renderers should use this method to inject internals necessary to generate a component stack. -// These internals will be used if the console is patched. -// Injecting them separately allows the console to easily be patched or un-patched later (at runtime). -export function registerRenderer( - onErrorOrWarning?: OnErrorOrWarning, - getComponentStack?: GetComponentStack, -): void { - injectedRenderers.push({ - onErrorOrWarning, - getComponentStack, - }); -} - -const consoleSettingsRef: ConsolePatchSettings = { - appendComponentStack: false, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - hideConsoleLogsInStrictMode: false, -}; - -// Patches console methods to append component stack for the current fiber. -// Call unpatch() to remove the injected behavior. -export function patch({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, -}: $ReadOnly): void { - // Settings may change after we've patched the console. - // Using a shared ref allows the patch function to read the latest values. - consoleSettingsRef.appendComponentStack = appendComponentStack; - consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors; - consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors; - consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode; - - if ( - appendComponentStack || - breakOnConsoleErrors || - showInlineWarningsAndErrors - ) { - if (unpatchFn !== null) { - // Don't patch twice. - return; - } - - const originalConsoleMethods: {[string]: $FlowFixMe} = {}; - - unpatchFn = () => { - for (const method in originalConsoleMethods) { - try { - targetConsole[method] = originalConsoleMethods[method]; - } catch (error) {} - } - }; - - OVERRIDE_CONSOLE_METHODS.forEach(method => { - try { - const originalMethod = (originalConsoleMethods[method] = targetConsole[ - method - ].__REACT_DEVTOOLS_ORIGINAL_METHOD__ - ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__ - : targetConsole[method]); - - // $FlowFixMe[missing-local-annot] - const overrideMethod = (...args) => { - let alreadyHasComponentStack = false; - if (method !== 'log' && consoleSettingsRef.appendComponentStack) { - const lastArg = args.length > 0 ? args[args.length - 1] : null; - alreadyHasComponentStack = - typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack. - } - - const shouldShowInlineWarningsAndErrors = - consoleSettingsRef.showInlineWarningsAndErrors && - (method === 'error' || method === 'warn'); - - // Search for the first renderer that has a current Fiber. - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?) - for (let i = 0; i < injectedRenderers.length; i++) { - const renderer = injectedRenderers[i]; - const {getComponentStack, onErrorOrWarning} = renderer; - try { - if (shouldShowInlineWarningsAndErrors) { - // patch() is called by two places: (1) the hook and (2) the renderer backend. - // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning. - if (onErrorOrWarning != null) { - onErrorOrWarning( - ((method: any): 'error' | 'warn'), - // Restore and copy args before we mutate them (e.g. adding the component stack) - restorePotentiallyModifiedArgs(args), - ); - } - } - } catch (error) { - // Don't let a DevTools or React internal error interfere with logging. - setTimeout(() => { - throw error; - }, 0); - } - try { - if ( - consoleSettingsRef.appendComponentStack && - getComponentStack != null - ) { - // This needs to be directly in the wrapper so we can pop exactly one frame. - const topFrame = Error('react-stack-top-frame'); - const match = getComponentStack(topFrame); - if (match !== null) { - const {enableOwnerStacks, componentStack} = match; - // Empty string means we have a match but no component stack. - // We don't need to look in other renderers but we also don't add anything. - if (componentStack !== '') { - // Create a fake Error so that when we print it we get native source maps. Every - // browser will print the .stack property of the error and then parse it back for source - // mapping. Rather than print the internal slot. So it doesn't matter that the internal - // slot doesn't line up. - const fakeError = new Error(''); - // In Chromium, only the stack property is printed but in Firefox the : - // gets printed so to make the colon make sense, we name it so we print Stack: - // and similarly Safari leave an expandable slot. - if (__IS_CHROME__ || __IS_EDGE__) { - // Before sending the stack to Chrome DevTools for formatting, - // V8 will reconstruct this according to the template : - // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a - // It has to start with ^[\w.]*Error\b to trigger stack formatting. - fakeError.name = enableOwnerStacks - ? 'Error Stack' - : 'Error Component Stack'; // This gets printed - } else { - fakeError.name = enableOwnerStacks - ? 'Stack' - : 'Component Stack'; // This gets printed - } - // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack - // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it - // to our own stack. - fakeError.stack = - __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__ - ? (enableOwnerStacks - ? 'Error Stack:' - : 'Error Component Stack:') + componentStack - : componentStack; - - if (alreadyHasComponentStack) { - // Only modify the component stack if it matches what we would've added anyway. - // Otherwise we assume it was a non-React stack. - if (isStrictModeOverride(args)) { - // We do nothing to Strict Mode overrides that already has a stack - // because we have already lost some context for how to format it - // since we've already merged the stack into the log at this point. - } else if ( - areStackTracesEqual( - args[args.length - 1], - componentStack, - ) - ) { - const firstArg = args[0]; - if ( - args.length > 1 && - typeof firstArg === 'string' && - firstArg.endsWith('%s') - ) { - args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param - } - args[args.length - 1] = fakeError; - } - } else { - args.push(fakeError); - if (isStrictModeOverride(args)) { - if (__IS_FIREFOX__) { - args[0] = `${args[0]} %o`; - } else { - args[0] = - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK; - } - } - } - } - // Don't add stacks from other renderers. - break; - } - } - } catch (error) { - // Don't let a DevTools or React internal error interfere with logging. - setTimeout(() => { - throw error; - }, 0); - } - } - - if (consoleSettingsRef.breakOnConsoleErrors) { - // --- Welcome to debugging with React DevTools --- - // This debugger statement means that you've enabled the "break on warnings" feature. - // Use the browser's Call Stack panel to step out of this override function- - // to where the original warning or error was logged. - // eslint-disable-next-line no-debugger - debugger; - } - - originalMethod(...args); - }; - - overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod; - originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod; - - targetConsole[method] = overrideMethod; - } catch (error) {} - }); - } else { - unpatch(); - } -} - -// Removed component stack patch from console methods. -export function unpatch(): void { - if (unpatchFn !== null) { - unpatchFn(); - unpatchFn = null; - } -} - -let unpatchForStrictModeFn: null | (() => void) = null; - -// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode -export function patchForStrictMode() { - const overrideConsoleMethods = [ - 'error', - 'group', - 'groupCollapsed', - 'info', - 'log', - 'trace', - 'warn', - ]; - - if (unpatchForStrictModeFn !== null) { - // Don't patch twice. - return; - } - - const originalConsoleMethods: {[string]: $FlowFixMe} = {}; - - unpatchForStrictModeFn = () => { - for (const method in originalConsoleMethods) { - try { - targetConsole[method] = originalConsoleMethods[method]; - } catch (error) {} - } - }; - - overrideConsoleMethods.forEach(method => { - try { - const originalMethod = (originalConsoleMethods[method] = targetConsole[ - method - ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ - ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ - : targetConsole[method]); - - // $FlowFixMe[missing-local-annot] - const overrideMethod = (...args) => { - if (!consoleSettingsRef.hideConsoleLogsInStrictMode) { - // Dim the text color of the double logs if we're not hiding them. - if (__IS_FIREFOX__) { - originalMethod( - ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR), - ); - } else { - originalMethod( - ANSI_STYLE_DIMMING_TEMPLATE, - ...formatConsoleArguments(...args), - ); - } - } - }; - - overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ = - originalMethod; - originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ = - overrideMethod; - - targetConsole[method] = overrideMethod; - } catch (error) {} - }); -} - -// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode -export function unpatchForStrictMode(): void { - if (unpatchForStrictModeFn !== null) { - unpatchForStrictModeFn(); - unpatchForStrictModeFn = null; - } -} - -export function patchConsoleUsingWindowValues() { - const appendComponentStack = - castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true; - const breakOnConsoleErrors = - castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false; - const showInlineWarningsAndErrors = - castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ?? true; - const hideConsoleLogsInStrictMode = - castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ?? - false; - - patch({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - }); -} +import type {ConsolePatchSettings} from './types'; // After receiving cached console patch settings from React Native, we set them on window. // When the console is initially patched (in renderer.js and hook.js), these values are read. @@ -433,10 +24,3 @@ export function writeConsolePatchSettingsToWindow( window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = settings.hideConsoleLogsInStrictMode; } - -export function installConsoleFunctionsToWindow(): void { - window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = { - patchConsoleUsingWindowValues, - registerRendererWithConsole: registerRenderer, - }; -} diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 2fd768b5d01c3..b8636c557ea25 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -71,12 +71,6 @@ import { TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../../constants'; import {inspectHooksOfFiber} from 'react-debug-tools'; -import { - patchConsoleUsingWindowValues, - registerRenderer as registerRendererWithConsole, - patchForStrictMode as patchConsoleForStrictMode, - unpatchForStrictMode as unpatchConsoleForStrictMode, -} from '../console'; import { CONCURRENT_MODE_NUMBER, CONCURRENT_MODE_SYMBOL_STRING, @@ -1198,16 +1192,6 @@ export function attach( needsToFlushComponentLogs = true; } - // Patching the console enables DevTools to do a few useful things: - // * Append component stacks to warnings and error messages - // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber) - registerRendererWithConsole(onErrorOrWarning, getComponentStack); - - // The renderer interface can't read these preferences directly, - // because it is stored in localStorage within the context of the extension. - // It relies on the extension to pass the preference through via the global. - patchConsoleUsingWindowValues(); - function debug( name: string, instance: DevToolsInstance, @@ -5788,7 +5772,6 @@ export function attach( hasElementWithId, inspectElement, logElementToConsole, - patchConsoleForStrictMode, getComponentStack, getElementAttributeByPath, getElementSourceFunctionById, @@ -5803,7 +5786,6 @@ export function attach( startProfiling, stopProfiling, storeAsGlobal, - unpatchConsoleForStrictMode, updateComponentFilters, getEnvironmentNames, }; diff --git a/packages/react-devtools-shared/src/backend/flight/renderer.js b/packages/react-devtools-shared/src/backend/flight/renderer.js index 065dc81a071a7..3d8befd4215e7 100644 --- a/packages/react-devtools-shared/src/backend/flight/renderer.js +++ b/packages/react-devtools-shared/src/backend/flight/renderer.js @@ -19,11 +19,6 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils'; -import { - patchConsoleUsingWindowValues, - registerRenderer as registerRendererWithConsole, -} from '../console'; - function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean { // If this ReactComponentInfo supports native console.createTask then we are already running // inside a native async stack trace if it's active - meaning the DevTools is open. @@ -145,9 +140,6 @@ export function attach( // The changes will be flushed later when we commit this tree to Fiber. } - patchConsoleUsingWindowValues(); - registerRendererWithConsole(onErrorOrWarning, getComponentStack); - return { cleanup() {}, clearErrorsAndWarnings() {}, @@ -205,7 +197,6 @@ export function attach( }; }, logElementToConsole() {}, - patchConsoleForStrictMode() {}, getElementAttributeByPath() {}, getElementSourceFunctionById() {}, onErrorOrWarning, @@ -219,7 +210,6 @@ export function attach( startProfiling() {}, stopProfiling() {}, storeAsGlobal() {}, - unpatchConsoleForStrictMode() {}, updateComponentFilters() {}, getEnvironmentNames() { return []; diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 828d05fc27f81..8e26dcae445ee 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -1103,10 +1103,6 @@ export function attach( // Not implemented } - function patchConsoleForStrictMode() {} - - function unpatchConsoleForStrictMode() {} - function hasElementWithId(id: number): boolean { return idToInternalInstanceMap.has(id); } @@ -1141,7 +1137,6 @@ export function attach( overrideSuspense, overrideValueAtPath, renamePath, - patchConsoleForStrictMode, getElementAttributeByPath, getElementSourceFunctionById, renderer, @@ -1150,7 +1145,6 @@ export function attach( startProfiling, stopProfiling, storeAsGlobal, - unpatchConsoleForStrictMode, updateComponentFilters, getEnvironmentNames, }; diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index fa9949e3ddc5d..bdc93a19baa8b 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -404,7 +404,6 @@ export type RendererInterface = { path: Array, value: any, ) => void, - patchConsoleForStrictMode: () => void, getElementAttributeByPath: ( id: number, path: Array, @@ -427,7 +426,6 @@ export type RendererInterface = { path: Array, count: number, ) => void, - unpatchConsoleForStrictMode: () => void, updateComponentFilters: (componentFilters: Array) => void, getEnvironmentNames: () => Array, diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 65a52b571a680..156319b366d71 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -15,7 +15,7 @@ import type { OwnersList, ProfilingDataBackend, RendererID, - ConsolePatchSettings, + DevToolsHookSettings, } from 'react-devtools-shared/src/backend/types'; import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types'; @@ -241,7 +241,7 @@ type FrontendEvents = { storeAsGlobal: [StoreAsGlobalParams], updateComponentFilters: [Array], getEnvironmentNames: [], - updateConsolePatchSettings: [ConsolePatchSettings], + updateConsolePatchSettings: [DevToolsHookSettings], viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 1dd1fcbd4c455..ac56d13d6ab4e 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -21,10 +21,31 @@ import type { import { FIREFOX_CONSOLE_DIMMING_COLOR, ANSI_STYLE_DIMMING_TEMPLATE, + ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, } from 'react-devtools-shared/src/constants'; import attachRenderer from './attachRenderer'; -declare var window: any; +// React's custom built component stack strings match "\s{4}in" +// Chrome's prefix matches "\s{4}at" +const PREFIX_REGEX = /\s{4}(in|at)\s{1}/; +// Firefox and Safari have no prefix ("") +// but we can fallback to looking for location info (e.g. "foo.js:12:345") +const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/; + +function isStringComponentStack(text: string): boolean { + return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text); +} + +// We add a suffix to some frames that older versions of React didn't do. +// To compare if it's equivalent we strip out the suffix to see if they're +// still equivalent. Similarly, we sometimes use [] and sometimes () so we +// strip them to for the comparison. +const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; +function areStackTracesEqual(a: string, b: string): boolean { + return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); +} + +const targetConsole: Object = console; export function installHook( target: any, @@ -36,25 +57,6 @@ export function installHook( return null; } - let targetConsole: Object = console; - let targetConsoleMethods: {[string]: $FlowFixMe} = {}; - for (const method in console) { - // $FlowFixMe[invalid-computed-prop] - targetConsoleMethods[method] = console[method]; - } - - function dangerous_setTargetConsoleForTesting( - targetConsoleForTesting: Object, - ): void { - targetConsole = targetConsoleForTesting; - - targetConsoleMethods = ({}: {[string]: $FlowFixMe}); - for (const method in targetConsole) { - // $FlowFixMe[invalid-computed-prop] - targetConsoleMethods[method] = console[method]; - } - } - function detectReactBuildType(renderer: ReactRenderer) { try { if (typeof renderer.version === 'string') { @@ -189,10 +191,7 @@ export function installHook( } // NOTE: KEEP IN SYNC with src/backend/utils.js - function formatWithStyles( - inputArgs: $ReadOnlyArray, - style?: string, - ): $ReadOnlyArray { + function formatWithStyles(inputArgs: Array, style?: string): Array { if ( inputArgs === undefined || inputArgs === null || @@ -285,85 +284,6 @@ export function installHook( return [template, ...args]; } - let unpatchFn = null; - - // NOTE: KEEP IN SYNC with src/backend/console.js:patchForStrictMode - // This function hides or dims console logs during the initial double renderer - // in Strict Mode. We need this function because during initial render, - // React and DevTools are connecting and the renderer interface isn't avaiable - // and we want to be able to have consistent logging behavior for double logs - // during the initial renderer. - function patchConsoleForInitialCommitInStrictMode( - hideConsoleLogsInStrictMode: boolean, - ) { - const overrideConsoleMethods = [ - 'error', - 'group', - 'groupCollapsed', - 'info', - 'log', - 'trace', - 'warn', - ]; - - if (unpatchFn !== null) { - // Don't patch twice. - return; - } - - const originalConsoleMethods: {[string]: $FlowFixMe} = {}; - - unpatchFn = () => { - for (const method in originalConsoleMethods) { - try { - targetConsole[method] = originalConsoleMethods[method]; - } catch (error) {} - } - }; - - overrideConsoleMethods.forEach(method => { - try { - const originalMethod = (originalConsoleMethods[method] = targetConsole[ - method - ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ - ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ - : targetConsole[method]); - - const overrideMethod = (...args: $ReadOnlyArray) => { - // Dim the text color of the double logs if we're not hiding them. - if (!hideConsoleLogsInStrictMode) { - // Firefox doesn't support ANSI escape sequences - if (__IS_FIREFOX__) { - originalMethod( - ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR), - ); - } else { - originalMethod( - ANSI_STYLE_DIMMING_TEMPLATE, - ...formatConsoleArguments(...args), - ); - } - } - }; - - overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ = - originalMethod; - originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ = - overrideMethod; - - targetConsole[method] = overrideMethod; - } catch (error) {} - }); - } - - // NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode - function unpatchConsoleForInitialCommitInStrictMode() { - if (unpatchFn !== null) { - unpatchFn(); - unpatchFn = null; - } - } - let uidCounter = 0; function inject(renderer: ReactRenderer): number { const id = ++uidCounter; @@ -469,28 +389,85 @@ export function installHook( } } - function setStrictMode(rendererID: RendererID, isStrictMode: any) { - const rendererInterface = rendererInterfaces.get(rendererID); - if (rendererInterface != null) { - if (isStrictMode) { - rendererInterface.patchConsoleForStrictMode(); - } else { - rendererInterface.unpatchConsoleForStrictMode(); - } + let isRunningDuringStrictModeInvocation = false; + function setStrictMode(rendererID: RendererID, isStrictMode: boolean) { + isRunningDuringStrictModeInvocation = isStrictMode; + + if (isStrictMode) { + patchConsoleForStrictMode(); } else { - // This should only happen during initial commit in the extension before DevTools - // finishes its handshake with the injected renderer - if (isStrictMode) { - const hideConsoleLogsInStrictMode = - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true; - - patchConsoleForInitialCommitInStrictMode(hideConsoleLogsInStrictMode); - } else { - unpatchConsoleForInitialCommitInStrictMode(); - } + unpatchConsoleForStrictMode(); + } + } + + const unpatchConsoleCallbacks = []; + // For StrictMode we patch console once we are running in StrictMode and unpatch right after it + // So patching could happen multiple times during the runtime + // Notice how we don't patch error or warn methods, because they are already patched in patchConsoleForErrorsAndWarnings + // This will only happen once, when hook is installed + function patchConsoleForStrictMode() { + // Don't patch console in case settings were not injected + if (!hook.settings) { + return; + } + + // Don't patch twice + if (unpatchConsoleCallbacks.length > 0) { + return; + } + + // At this point 'error', 'warn', and 'trace' methods are already patched + // by React DevTools hook to append component stacks and other possible features. + const consoleMethodsToOverrideForStrictMode = [ + 'group', + 'groupCollapsed', + 'info', + 'log', + ]; + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const method of consoleMethodsToOverrideForStrictMode) { + const originalMethod = targetConsole[method]; + const overrideMethod: (...args: Array) => void = ( + ...args: any[] + ) => { + const settings = hook.settings; + // Something unexpected happened, fallback to just printing the console message. + if (settings == null) { + originalMethod(...args); + return; + } + + if (settings.hideConsoleLogsInStrictMode) { + return; + } + + // Dim the text color of the double logs if we're not hiding them. + // Firefox doesn't support ANSI escape sequences + if (__IS_FIREFOX__) { + originalMethod( + ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR), + ); + } else { + originalMethod( + ANSI_STYLE_DIMMING_TEMPLATE, + ...formatConsoleArguments(...args), + ); + } + }; + + targetConsole[method] = overrideMethod; + unpatchConsoleCallbacks.push(() => { + targetConsole[method] = originalMethod; + }); } } + function unpatchConsoleForStrictMode() { + unpatchConsoleCallbacks.forEach(callback => callback()); + unpatchConsoleCallbacks.length = 0; + } + type StackFrameString = string; const openModuleRangesStack: Array = []; @@ -526,6 +503,188 @@ export function installHook( } } + // For Errors and Warnings we only patch console once + function patchConsoleForErrorsAndWarnings() { + // Don't patch console in case settings were not injected + if (!hook.settings) { + return; + } + + const consoleMethodsToOverrideForErrorsAndWarnings = [ + 'error', + 'trace', + 'warn', + ]; + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const method of consoleMethodsToOverrideForErrorsAndWarnings) { + const originalMethod = targetConsole[method]; + const overrideMethod: (...args: Array) => void = (...args) => { + const settings = hook.settings; + // Something unexpected happened, fallback to just printing the console message. + if (settings == null) { + originalMethod(...args); + return; + } + + if ( + isRunningDuringStrictModeInvocation && + settings.hideConsoleLogsInStrictMode + ) { + return; + } + + let injectedComponentStackAsFakeError = false; + let alreadyHasComponentStack = false; + if (settings.appendComponentStack) { + const lastArg = args.length > 0 ? args[args.length - 1] : null; + alreadyHasComponentStack = + typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack. + } + + const shouldShowInlineWarningsAndErrors = + settings.showInlineWarningsAndErrors && + (method === 'error' || method === 'warn'); + + // Search for the first renderer that has a current Fiber. + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?) + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const rendererInterface of hook.rendererInterfaces.values()) { + const {onErrorOrWarning, getComponentStack} = rendererInterface; + try { + if (shouldShowInlineWarningsAndErrors) { + // patch() is called by two places: (1) the hook and (2) the renderer backend. + // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning. + if (onErrorOrWarning != null) { + onErrorOrWarning( + ((method: any): 'error' | 'warn'), + args.slice(), + ); + } + } + } catch (error) { + // Don't let a DevTools or React internal error interfere with logging. + setTimeout(() => { + throw error; + }, 0); + } + + try { + if (settings.appendComponentStack && getComponentStack != null) { + // This needs to be directly in the wrapper so we can pop exactly one frame. + const topFrame = Error('react-stack-top-frame'); + const match = getComponentStack(topFrame); + if (match !== null) { + const {enableOwnerStacks, componentStack} = match; + // Empty string means we have a match but no component stack. + // We don't need to look in other renderers but we also don't add anything. + if (componentStack !== '') { + // Create a fake Error so that when we print it we get native source maps. Every + // browser will print the .stack property of the error and then parse it back for source + // mapping. Rather than print the internal slot. So it doesn't matter that the internal + // slot doesn't line up. + const fakeError = new Error(''); + // In Chromium, only the stack property is printed but in Firefox the : + // gets printed so to make the colon make sense, we name it so we print Stack: + // and similarly Safari leave an expandable slot. + if (__IS_CHROME__ || __IS_EDGE__) { + // Before sending the stack to Chrome DevTools for formatting, + // V8 will reconstruct this according to the template : + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a + // It has to start with ^[\w.]*Error\b to trigger stack formatting. + fakeError.name = enableOwnerStacks + ? 'Error Stack' + : 'Error Component Stack'; // This gets printed + } else { + fakeError.name = enableOwnerStacks + ? 'Stack' + : 'Component Stack'; // This gets printed + } + // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack + // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it + // to our own stack. + fakeError.stack = + __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__ + ? (enableOwnerStacks + ? 'Error Stack:' + : 'Error Component Stack:') + componentStack + : componentStack; + + if (alreadyHasComponentStack) { + // Only modify the component stack if it matches what we would've added anyway. + // Otherwise we assume it was a non-React stack. + if ( + areStackTracesEqual(args[args.length - 1], componentStack) + ) { + const firstArg = args[0]; + if ( + args.length > 1 && + typeof firstArg === 'string' && + firstArg.endsWith('%s') + ) { + args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param + } + args[args.length - 1] = fakeError; + injectedComponentStackAsFakeError = true; + } + } else { + args.push(fakeError); + injectedComponentStackAsFakeError = true; + } + } + + // Don't add stacks from other renderers. + break; + } + } + } catch (error) { + // Don't let a DevTools or React internal error interfere with logging. + setTimeout(() => { + throw error; + }, 0); + } + } + + if (settings.breakOnConsoleErrors) { + // --- Welcome to debugging with React DevTools --- + // This debugger statement means that you've enabled the "break on warnings" feature. + // Use the browser's Call Stack panel to step out of this override function + // to where the original warning or error was logged. + // eslint-disable-next-line no-debugger + debugger; + } + + if (isRunningDuringStrictModeInvocation) { + // Dim the text color of the double logs if we're not hiding them. + // Firefox doesn't support ANSI escape sequences + if (__IS_FIREFOX__) { + const argsWithCSSStyles = formatWithStyles( + args, + FIREFOX_CONSOLE_DIMMING_COLOR, + ); + + if (injectedComponentStackAsFakeError) { + argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`; + } + + originalMethod(...argsWithCSSStyles); + } else { + originalMethod( + injectedComponentStackAsFakeError + ? ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK + : ANSI_STYLE_DIMMING_TEMPLATE, + ...formatConsoleArguments(...args), + ); + } + } else { + originalMethod(...args); + } + }; + + targetConsole[method] = overrideMethod; + } + } + // TODO: More meaningful names for "rendererInterfaces" and "renderers". const fiberRoots: {[RendererID]: Set} = {}; const rendererInterfaces = new Map(); @@ -580,10 +739,12 @@ export function installHook( showInlineWarningsAndErrors: true, hideConsoleLogsInStrictMode: false, }; + patchConsoleForErrorsAndWarnings(); } else { Promise.resolve(maybeSettingsOrSettingsPromise) .then(settings => { hook.settings = settings; + patchConsoleForErrorsAndWarnings(); }) .catch(() => { targetConsole.error( @@ -592,11 +753,6 @@ export function installHook( }); } - if (__TEST__) { - hook.dangerous_setTargetConsoleForTesting = - dangerous_setTargetConsoleForTesting; - } - Object.defineProperty( target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', From fce46066571e7bf3ab6ce5bfe5fd3a615e098421 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:16:20 +0100 Subject: [PATCH 167/191] chore[react-devtools]: extract some utils into separate modules to unify implementations (#30597) Stacked on https://github.com/facebook/react/pull/30596. See [this commit](https://github.com/facebook/react/pull/30597/commits/4ba5e784bbfdcd69021e2d84c75ffe26fcb698f4). Moving `formatWithStyles` and `formatConsoleArguments` to its own modules, so that we can finally have a single implementation for these and stop inlining them in RDT global hook object. --- .../backend/utils/formatConsoleArguments.js | 72 ++++++++++ .../src/backend/utils/formatWithStyles.js | 67 ++++++++++ .../src/backend/{utils.js => utils/index.js} | 124 +----------------- packages/react-devtools-shared/src/hook.js | 103 +-------------- 4 files changed, 150 insertions(+), 216 deletions(-) create mode 100644 packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js create mode 100644 packages/react-devtools-shared/src/backend/utils/formatWithStyles.js rename packages/react-devtools-shared/src/backend/{utils.js => utils/index.js} (72%) diff --git a/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js b/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js new file mode 100644 index 0000000000000..a2d303e543ac0 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow + */ + +// Do not add / import anything to this file. +// This function could be used from multiple places, including hook. + +// Skips CSS and object arguments, inlines other in the first argument as a template string +export default function formatConsoleArguments( + maybeMessage: any, + ...inputArgs: $ReadOnlyArray +): $ReadOnlyArray { + if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { + return [maybeMessage, ...inputArgs]; + } + + const args = inputArgs.slice(); + + let template = ''; + let argumentsPointer = 0; + for (let i = 0; i < maybeMessage.length; ++i) { + const currentChar = maybeMessage[i]; + if (currentChar !== '%') { + template += currentChar; + continue; + } + + const nextChar = maybeMessage[i + 1]; + ++i; + + // Only keep CSS and objects, inline other arguments + switch (nextChar) { + case 'c': + case 'O': + case 'o': { + ++argumentsPointer; + template += `%${nextChar}`; + + break; + } + case 'd': + case 'i': { + const [arg] = args.splice(argumentsPointer, 1); + template += parseInt(arg, 10).toString(); + + break; + } + case 'f': { + const [arg] = args.splice(argumentsPointer, 1); + template += parseFloat(arg).toString(); + + break; + } + case 's': { + const [arg] = args.splice(argumentsPointer, 1); + template += arg.toString(); + + break; + } + + default: + template += `%${nextChar}`; + } + } + + return [template, ...args]; +} diff --git a/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js b/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js new file mode 100644 index 0000000000000..b258141e353f0 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js @@ -0,0 +1,67 @@ +/** + * 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. + * + * @flow + */ + +// Do not add / import anything to this file. +// This function could be used from multiple places, including hook. + +// Formats an array of args with a style for console methods, using +// the following algorithm: +// 1. The first param is a string that contains %c +// - Bail out and return the args without modifying the styles. +// We don't want to affect styles that the developer deliberately set. +// 2. The first param is a string that doesn't contain %c but contains +// string formatting +// - [`%c${args[0]}`, style, ...args.slice(1)] +// - Note: we assume that the string formatting that the developer uses +// is correct. +// 3. The first param is a string that doesn't contain string formatting +// OR is not a string +// - Create a formatting string where: +// boolean, string, symbol -> %s +// number -> %f OR %i depending on if it's an int or float +// default -> %o +export default function formatWithStyles( + inputArgs: $ReadOnlyArray, + style?: string, +): $ReadOnlyArray { + if ( + inputArgs === undefined || + inputArgs === null || + inputArgs.length === 0 || + // Matches any of %c but not %%c + (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) || + style === undefined + ) { + return inputArgs; + } + + // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f) + const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g; + if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) { + return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)]; + } else { + const firstArg = inputArgs.reduce((formatStr, elem, i) => { + if (i > 0) { + formatStr += ' '; + } + switch (typeof elem) { + case 'string': + case 'boolean': + case 'symbol': + return (formatStr += '%s'); + case 'number': + const formatting = Number.isInteger(elem) ? '%i' : '%f'; + return (formatStr += formatting); + default: + return (formatStr += '%o'); + } + }, '%c'); + return [firstArg, style, ...inputArgs]; + } +} diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils/index.js similarity index 72% rename from packages/react-devtools-shared/src/backend/utils.js rename to packages/react-devtools-shared/src/backend/utils/index.js index e763bdd759759..1e7934af9835b 100644 --- a/packages/react-devtools-shared/src/backend/utils.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -9,12 +9,15 @@ */ import {compareVersions} from 'compare-versions'; -import {dehydrate} from '../hydration'; +import {dehydrate} from 'react-devtools-shared/src/hydration'; import isArray from 'shared/isArray'; import type {Source} from 'react-devtools-shared/src/shared/types'; import type {DehydratedData} from 'react-devtools-shared/src/frontend/types'; +export {default as formatWithStyles} from './formatWithStyles'; +export {default as formatConsoleArguments} from './formatConsoleArguments'; + // TODO: update this to the first React version that has a corresponding DevTools backend const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9'; export function hasAssignedBackend(version?: string): boolean { @@ -164,125 +167,6 @@ export function serializeToString(data: any): string { ); } -// NOTE: KEEP IN SYNC with src/hook.js -// Formats an array of args with a style for console methods, using -// the following algorithm: -// 1. The first param is a string that contains %c -// - Bail out and return the args without modifying the styles. -// We don't want to affect styles that the developer deliberately set. -// 2. The first param is a string that doesn't contain %c but contains -// string formatting -// - [`%c${args[0]}`, style, ...args.slice(1)] -// - Note: we assume that the string formatting that the developer uses -// is correct. -// 3. The first param is a string that doesn't contain string formatting -// OR is not a string -// - Create a formatting string where: -// boolean, string, symbol -> %s -// number -> %f OR %i depending on if it's an int or float -// default -> %o -export function formatWithStyles( - inputArgs: $ReadOnlyArray, - style?: string, -): $ReadOnlyArray { - if ( - inputArgs === undefined || - inputArgs === null || - inputArgs.length === 0 || - // Matches any of %c but not %%c - (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) || - style === undefined - ) { - return inputArgs; - } - - // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f) - const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g; - if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) { - return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)]; - } else { - const firstArg = inputArgs.reduce((formatStr, elem, i) => { - if (i > 0) { - formatStr += ' '; - } - switch (typeof elem) { - case 'string': - case 'boolean': - case 'symbol': - return (formatStr += '%s'); - case 'number': - const formatting = Number.isInteger(elem) ? '%i' : '%f'; - return (formatStr += formatting); - default: - return (formatStr += '%o'); - } - }, '%c'); - return [firstArg, style, ...inputArgs]; - } -} - -// NOTE: KEEP IN SYNC with src/hook.js -// Skips CSS and object arguments, inlines other in the first argument as a template string -export function formatConsoleArguments( - maybeMessage: any, - ...inputArgs: $ReadOnlyArray -): $ReadOnlyArray { - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { - return [maybeMessage, ...inputArgs]; - } - - const args = inputArgs.slice(); - - let template = ''; - let argumentsPointer = 0; - for (let i = 0; i < maybeMessage.length; ++i) { - const currentChar = maybeMessage[i]; - if (currentChar !== '%') { - template += currentChar; - continue; - } - - const nextChar = maybeMessage[i + 1]; - ++i; - - // Only keep CSS and objects, inline other arguments - switch (nextChar) { - case 'c': - case 'O': - case 'o': { - ++argumentsPointer; - template += `%${nextChar}`; - - break; - } - case 'd': - case 'i': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseInt(arg, 10).toString(); - - break; - } - case 'f': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseFloat(arg).toString(); - - break; - } - case 's': { - const [arg] = args.splice(argumentsPointer, 1); - template += arg.toString(); - - break; - } - - default: - template += `%${nextChar}`; - } - } - - return [template, ...args]; -} - // based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1 // based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions // Implements s, d, i and f placeholders diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index ac56d13d6ab4e..2f0698b8e1353 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -24,6 +24,8 @@ import { ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, } from 'react-devtools-shared/src/constants'; import attachRenderer from './attachRenderer'; +import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments'; +import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles'; // React's custom built component stack strings match "\s{4}in" // Chrome's prefix matches "\s{4}at" @@ -190,100 +192,6 @@ export function installHook( } catch (err) {} } - // NOTE: KEEP IN SYNC with src/backend/utils.js - function formatWithStyles(inputArgs: Array, style?: string): Array { - if ( - inputArgs === undefined || - inputArgs === null || - inputArgs.length === 0 || - // Matches any of %c but not %%c - (typeof inputArgs[0] === 'string' && - inputArgs[0].match(/([^%]|^)(%c)/g)) || - style === undefined - ) { - return inputArgs; - } - - // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f) - const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g; - if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) { - return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)]; - } else { - const firstArg = inputArgs.reduce((formatStr, elem, i) => { - if (i > 0) { - formatStr += ' '; - } - switch (typeof elem) { - case 'string': - case 'boolean': - case 'symbol': - return (formatStr += '%s'); - case 'number': - const formatting = Number.isInteger(elem) ? '%i' : '%f'; - return (formatStr += formatting); - default: - return (formatStr += '%o'); - } - }, '%c'); - return [firstArg, style, ...inputArgs]; - } - } - // NOTE: KEEP IN SYNC with src/backend/utils.js - function formatConsoleArguments( - maybeMessage: any, - ...inputArgs: $ReadOnlyArray - ): $ReadOnlyArray { - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { - return [maybeMessage, ...inputArgs]; - } - - const args = inputArgs.slice(); - - let template = ''; - let argumentsPointer = 0; - for (let i = 0; i < maybeMessage.length; ++i) { - const currentChar = maybeMessage[i]; - if (currentChar !== '%') { - template += currentChar; - continue; - } - - const nextChar = maybeMessage[i + 1]; - ++i; - - // Only keep CSS and objects, inline other arguments - switch (nextChar) { - case 'c': - case 'O': - case 'o': { - ++argumentsPointer; - template += `%${nextChar}`; - - break; - } - case 'd': - case 'i': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseInt(arg, 10).toString(); - - break; - } - case 'f': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseFloat(arg).toString(); - - break; - } - case 's': { - const [arg] = args.splice(argumentsPointer, 1); - template += arg.toString(); - } - } - } - - return [template, ...args]; - } - let uidCounter = 0; function inject(renderer: ReactRenderer): number { const id = ++uidCounter; @@ -658,13 +566,16 @@ export function installHook( // Dim the text color of the double logs if we're not hiding them. // Firefox doesn't support ANSI escape sequences if (__IS_FIREFOX__) { - const argsWithCSSStyles = formatWithStyles( + let argsWithCSSStyles = formatWithStyles( args, FIREFOX_CONSOLE_DIMMING_COLOR, ); if (injectedComponentStackAsFakeError) { - argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`; + argsWithCSSStyles = [ + `${argsWithCSSStyles[0]} %o`, + ...argsWithCSSStyles.slice(1), + ]; } originalMethod(...argsWithCSSStyles); From e33acfd67f0003272a9aec7a0725d19a429f2460 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:19:01 +0100 Subject: [PATCH 168/191] refactor[react-devtools]: propagate settings from global hook object to frontend (#30610) Stacked on https://github.com/facebook/react/pull/30597 and whats under it. See [this commit](https://github.com/facebook/react/pull/30610/commits/59b4efa72377bf62f5ec8c0e32e56902cf73fbd7). With this change, the initial values for console patching settings are propagated from hook (which is the source of truth now, because of https://github.com/facebook/react/pull/30596) to the UI. Instead of reading from `localStorage` the frontend is now requesting it from the hook. This happens when settings modal is rendered, and wrapped in a transition. Also, this is happening even if settings modal is not opened yet, so we have enough time to fetch this data without displaying loader or similar UI. --- packages/react-devtools-core/src/backend.js | 4 +- .../react-devtools-core/src/cachedSettings.js | 2 +- .../src/backend/agent.js | 37 ++++++++----- .../src/backend/index.js | 7 +++ packages/react-devtools-shared/src/bridge.js | 6 +- .../src/devtools/store.js | 25 +++++++++ .../views/Settings/DebuggingSettings.js | 47 ++++++++++++---- .../views/Settings/SettingsContext.js | 55 ------------------- .../devtools/views/Settings/SettingsModal.js | 17 +++--- .../views/Settings/SettingsModalContext.js | 42 +++++++++++--- packages/react-devtools-shared/src/hook.js | 2 + 11 files changed, 145 insertions(+), 99 deletions(-) diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index d588d4f91e9f0..6949d6019145a 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -162,7 +162,7 @@ export function connectToDevTools(options: ?ConnectOptions) { ); if (devToolsSettingsManager != null && bridge != null) { - bridge.addListener('updateConsolePatchSettings', consolePatchSettings => + bridge.addListener('updateHookSettings', consolePatchSettings => cacheConsolePatchSettings( devToolsSettingsManager, consolePatchSettings, @@ -368,7 +368,7 @@ export function connectWithCustomMessagingProtocol({ ); if (settingsManager != null) { - bridge.addListener('updateConsolePatchSettings', consolePatchSettings => + bridge.addListener('updateHookSettings', consolePatchSettings => cacheConsolePatchSettings(settingsManager, consolePatchSettings), ); } diff --git a/packages/react-devtools-core/src/cachedSettings.js b/packages/react-devtools-core/src/cachedSettings.js index afe12bfdbc5ad..5c603447e8d88 100644 --- a/packages/react-devtools-core/src/cachedSettings.js +++ b/packages/react-devtools-core/src/cachedSettings.js @@ -66,7 +66,7 @@ function parseConsolePatchSettings( export function cacheConsolePatchSettings( devToolsSettingsManager: DevToolsSettingsManager, - value: ConsolePatchSettings, + value: $ReadOnly, ): void { if (devToolsSettingsManager.setConsolePatchSettings == null) { return; diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 814c6c1c40e67..277b743f3f6a4 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -150,6 +150,7 @@ export default class Agent extends EventEmitter<{ disableTraceUpdates: [], getIfHasUnsupportedRendererVersion: [], updateHookSettings: [DevToolsHookSettings], + getHookSettings: [], }> { _bridge: BackendBridge; _isProfiling: boolean = false; @@ -213,10 +214,10 @@ export default class Agent extends EventEmitter<{ this.syncSelectionFromBuiltinElementsPanel, ); bridge.addListener('shutdown', this.shutdown); - bridge.addListener( - 'updateConsolePatchSettings', - this.updateConsolePatchSettings, - ); + + bridge.addListener('updateHookSettings', this.updateHookSettings); + bridge.addListener('getHookSettings', this.getHookSettings); + bridge.addListener('updateComponentFilters', this.updateComponentFilters); bridge.addListener('getEnvironmentNames', this.getEnvironmentNames); bridge.addListener( @@ -802,18 +803,26 @@ export default class Agent extends EventEmitter<{ } }; - updateConsolePatchSettings: ( - settings: $ReadOnly, - ) => void = settings => { - // Propagate the settings, so Backend can subscribe to it and modify hook - this.emit('updateHookSettings', { - appendComponentStack: settings.appendComponentStack, - breakOnConsoleErrors: settings.breakOnConsoleErrors, - showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, - }); + updateHookSettings: (settings: $ReadOnly) => void = + settings => { + // Propagate the settings, so Backend can subscribe to it and modify hook + this.emit('updateHookSettings', { + appendComponentStack: settings.appendComponentStack, + breakOnConsoleErrors: settings.breakOnConsoleErrors, + showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, + hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, + }); + }; + + getHookSettings: () => void = () => { + this.emit('getHookSettings'); }; + onHookSettings: (settings: $ReadOnly) => void = + settings => { + this._bridge.send('hookSettings', settings); + }; + updateComponentFilters: (componentFilters: Array) => void = componentFilters => { for (const rendererIDString in this._rendererInterfaces) { diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 5d84bf6bb70f0..5893424b394c8 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -54,6 +54,7 @@ export function initBackend( hook.sub('fastRefreshScheduled', agent.onFastRefreshScheduled), hook.sub('operations', agent.onHookOperations), hook.sub('traceUpdates', agent.onTraceUpdates), + hook.sub('settingsInitialized', agent.onHookSettings), // TODO Add additional subscriptions required for profiling mode ]; @@ -87,6 +88,12 @@ export function initBackend( hook.settings = settings; }); + agent.addListener('getHookSettings', () => { + if (hook.settings != null) { + agent.onHookSettings(hook.settings); + } + }); + return () => { subs.forEach(fn => fn()); }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 156319b366d71..4751f70cfaefb 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -207,6 +207,8 @@ export type BackendEvents = { {isSupported: boolean, validAttributes: ?$ReadOnlyArray}, ], NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload], + + hookSettings: [$ReadOnly], }; type FrontendEvents = { @@ -241,7 +243,7 @@ type FrontendEvents = { storeAsGlobal: [StoreAsGlobalParams], updateComponentFilters: [Array], getEnvironmentNames: [], - updateConsolePatchSettings: [DevToolsHookSettings], + updateHookSettings: [$ReadOnly], viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], @@ -267,6 +269,8 @@ type FrontendEvents = { resumeElementPolling: [], pauseElementPolling: [], + + getHookSettings: [], }; class Bridge< diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index d351306a44546..1798e0952b8c2 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -49,6 +49,7 @@ import type { BridgeProtocol, } from 'react-devtools-shared/src/bridge'; import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError'; +import type {DevToolsHookSettings} from '../backend/types'; const debug = (methodName: string, ...args: Array) => { if (__DEBUG__) { @@ -94,6 +95,7 @@ export default class Store extends EventEmitter<{ collapseNodesByDefault: [], componentFilters: [], error: [Error], + hookSettings: [$ReadOnly], mutated: [[Array, Map]], recordChangeDescriptions: [], roots: [], @@ -192,6 +194,7 @@ export default class Store extends EventEmitter<{ _weightAcrossRoots: number = 0; _shouldCheckBridgeProtocolCompatibility: boolean = false; + _hookSettings: $ReadOnly | null = null; constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -270,6 +273,7 @@ export default class Store extends EventEmitter<{ bridge.addListener('backendVersion', this.onBridgeBackendVersion); bridge.addListener('saveToClipboard', this.onSaveToClipboard); + bridge.addListener('hookSettings', this.onHookSettings); bridge.addListener('backendInitialized', this.onBackendInitialized); } @@ -1501,8 +1505,29 @@ export default class Store extends EventEmitter<{ this._bridge.send('getBackendVersion'); this._bridge.send('getIfHasUnsupportedRendererVersion'); + this._bridge.send('getHookSettings'); // Warm up cached hook settings }; + getHookSettings: () => void = () => { + if (this._hookSettings != null) { + this.emit('hookSettings', this._hookSettings); + } else { + this._bridge.send('getHookSettings'); + } + }; + + updateHookSettings: (settings: $ReadOnly) => void = + settings => { + this._hookSettings = settings; + this._bridge.send('updateHookSettings', settings); + }; + + onHookSettings: (settings: $ReadOnly) => void = + settings => { + this._hookSettings = settings; + this.emit('hookSettings', settings); + }; + // The Store should never throw an Error without also emitting an event. // Otherwise Store errors will be invisible to users, // but the downstream errors they cause will be reported as bugs. diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js index b28ca67c0bcb4..8bde14e62e606 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js @@ -8,22 +8,49 @@ */ import * as React from 'react'; -import {useContext} from 'react'; -import {SettingsContext} from './SettingsContext'; +import {use, useState, useEffect} from 'react'; + +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type Store from 'react-devtools-shared/src/devtools/store'; import styles from './SettingsShared.css'; -export default function DebuggingSettings(_: {}): React.Node { - const { +type Props = { + hookSettings: Promise<$ReadOnly>, + store: Store, +}; + +export default function DebuggingSettings({ + hookSettings, + store, +}: Props): React.Node { + const usedHookSettings = use(hookSettings); + + const [appendComponentStack, setAppendComponentStack] = useState( + usedHookSettings.appendComponentStack, + ); + const [breakOnConsoleErrors, setBreakOnConsoleErrors] = useState( + usedHookSettings.breakOnConsoleErrors, + ); + const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] = + useState(usedHookSettings.hideConsoleLogsInStrictMode); + const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] = + useState(usedHookSettings.showInlineWarningsAndErrors); + + useEffect(() => { + store.updateHookSettings({ + appendComponentStack, + breakOnConsoleErrors, + showInlineWarningsAndErrors, + hideConsoleLogsInStrictMode, + }); + }, [ + store, appendComponentStack, breakOnConsoleErrors, - hideConsoleLogsInStrictMode, - setAppendComponentStack, - setBreakOnConsoleErrors, - setShowInlineWarningsAndErrors, showInlineWarningsAndErrors, - setHideConsoleLogsInStrictMode, - } = useContext(SettingsContext); + hideConsoleLogsInStrictMode, + ]); return (
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 33487c2f553d6..17514d94648ac 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -20,11 +20,7 @@ import { import { LOCAL_STORAGE_BROWSER_THEME, LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY, - LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY, LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE, } from 'react-devtools-shared/src/constants'; import { COMFORTABLE_LINE_HEIGHT, @@ -118,30 +114,10 @@ function SettingsContextController({ LOCAL_STORAGE_BROWSER_THEME, 'auto', ); - const [appendComponentStack, setAppendComponentStack] = - useLocalStorageWithLog( - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY, - true, - ); - const [breakOnConsoleErrors, setBreakOnConsoleErrors] = - useLocalStorageWithLog( - LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, - false, - ); const [parseHookNames, setParseHookNames] = useLocalStorageWithLog( LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY, false, ); - const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] = - useLocalStorageWithLog( - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE, - false, - ); - const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] = - useLocalStorageWithLog( - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, - true, - ); const [traceUpdatesEnabled, setTraceUpdatesEnabled] = useLocalStorageWithLog( LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, @@ -196,64 +172,33 @@ function SettingsContextController({ } }, [browserTheme, theme, documentElements]); - useEffect(() => { - bridge.send('updateConsolePatchSettings', { - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - }); - }, [ - bridge, - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - ]); - useEffect(() => { bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled); }, [bridge, traceUpdatesEnabled]); const value = useMemo( () => ({ - appendComponentStack, - breakOnConsoleErrors, displayDensity, lineHeight: displayDensity === 'compact' ? COMPACT_LINE_HEIGHT : COMFORTABLE_LINE_HEIGHT, parseHookNames, - setAppendComponentStack, - setBreakOnConsoleErrors, setDisplayDensity, setParseHookNames, setTheme, setTraceUpdatesEnabled, - setShowInlineWarningsAndErrors, - showInlineWarningsAndErrors, - setHideConsoleLogsInStrictMode, - hideConsoleLogsInStrictMode, theme, browserTheme, traceUpdatesEnabled, }), [ - appendComponentStack, - breakOnConsoleErrors, displayDensity, parseHookNames, - setAppendComponentStack, - setBreakOnConsoleErrors, setDisplayDensity, setParseHookNames, setTheme, setTraceUpdatesEnabled, - setShowInlineWarningsAndErrors, - showInlineWarningsAndErrors, - setHideConsoleLogsInStrictMode, - hideConsoleLogsInStrictMode, theme, browserTheme, traceUpdatesEnabled, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js index 24884098a0bfc..f6652ada3a860 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js @@ -26,9 +26,11 @@ import ProfilerSettings from './ProfilerSettings'; import styles from './SettingsModal.css'; -type TabID = 'general' | 'components' | 'profiler'; +import type Store from 'react-devtools-shared/src/devtools/store'; -export default function SettingsModal(_: {}): React.Node { +type TabID = 'general' | 'debugging' | 'components' | 'profiler'; + +export default function SettingsModal(): React.Node { const {isModalShowing, setIsModalShowing} = useContext(SettingsModalContext); const store = useContext(StoreContext); const {profilerStore} = store; @@ -54,11 +56,13 @@ export default function SettingsModal(_: {}): React.Node { return null; } - return ; + return ; } -function SettingsModalImpl(_: {}) { - const {setIsModalShowing, environmentNames} = +type ImplProps = {store: Store}; + +function SettingsModalImpl({store}: ImplProps) { + const {setIsModalShowing, environmentNames, hookSettings} = useContext(SettingsModalContext); const dismissModal = useCallback( () => setIsModalShowing(false), @@ -84,9 +88,8 @@ function SettingsModalImpl(_: {}) { case 'components': view = ; break; - // $FlowFixMe[incompatible-type] is this missing in TabID? case 'debugging': - view = ; + view = ; break; case 'general': view = ; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js index 55336a9d650b0..98e491f44b6eb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js @@ -18,8 +18,11 @@ import { startTransition, } from 'react'; -import {BridgeContext} from '../context'; +import {BridgeContext, StoreContext} from '../context'; + import type {FrontendBridge} from '../../../bridge'; +import type {DevToolsHookSettings} from '../../../backend/types'; +import type Store from '../../store'; export type DisplayDensity = 'comfortable' | 'compact'; export type Theme = 'auto' | 'light' | 'dark'; @@ -28,6 +31,7 @@ type Context = { isModalShowing: boolean, setIsModalShowing: (value: boolean) => void, environmentNames: null | Promise>, + hookSettings: null | Promise<$ReadOnly>, }; const SettingsModalContext: ReactContext = createContext( @@ -46,27 +50,47 @@ function fetchEnvironmentNames(bridge: FrontendBridge): Promise> { }); } +function fetchHookSettings( + store: Store, +): Promise<$ReadOnly> { + return new Promise(resolve => { + function onHookSettings(settings: $ReadOnly) { + store.removeListener('hookSettings', onHookSettings); + resolve(settings); + } + + store.addListener('hookSettings', onHookSettings); + store.getHookSettings(); + }); +} + function SettingsModalContextController({ children, }: { children: React$Node, }): React.Node { const bridge = useContext(BridgeContext); + const store = useContext(StoreContext); - const setIsModalShowing: boolean => void = useCallback((value: boolean) => { - startTransition(() => { - setContext({ - isModalShowing: value, - setIsModalShowing, - environmentNames: value ? fetchEnvironmentNames(bridge) : null, + const setIsModalShowing: boolean => void = useCallback( + (value: boolean) => { + startTransition(() => { + setContext({ + isModalShowing: value, + setIsModalShowing, + environmentNames: value ? fetchEnvironmentNames(bridge) : null, + hookSettings: value ? fetchHookSettings(store) : null, + }); }); - }); - }); + }, + [bridge, store], + ); const [currentContext, setContext] = useState({ isModalShowing: false, setIsModalShowing, environmentNames: null, + hookSettings: null, }); return ( diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 2f0698b8e1353..1916a8c93822c 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -655,6 +655,8 @@ export function installHook( Promise.resolve(maybeSettingsOrSettingsPromise) .then(settings => { hook.settings = settings; + hook.emit('settingsInitialized', settings); + patchConsoleForErrorsAndWarnings(); }) .catch(() => { From f37c7bc6539b4da38f7080b5486eb00bdb2c3237 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:26:39 +0100 Subject: [PATCH 169/191] feat[react-devtools/extension]: use chrome.storage to persist settings across sessions (#30636) Stacked on https://github.com/facebook/react/pull/30610 and whats under it. See [last commit](https://github.com/facebook/react/pull/30636/commits/248ddba18608e1bb5ef14c823085a7ff9d7a54a3). Now, we are using [`chrome.storage`](https://developer.chrome.com/docs/extensions/reference/api/storage) to persist settings for the browser extension across different sessions. Once settings are updated from the UI, the `Store` will emit `settingsUpdated` event, and we are going to persist them via `chrome.storage.local.set` in `main/index.js`. When hook is being injected, we are going to pass a `Promise`, which is going to be resolved after the settings are read from the storage via `chrome.storage.local.get` in `hookSettingsInjector.js`. --- .../chrome/manifest.json | 1 + .../edge/manifest.json | 1 + .../firefox/manifest.json | 1 + .../dynamicallyInjectContentScripts.js | 7 ++++ .../contentScripts/hookSettingsInjector.js | 42 +++++++++++++++++++ .../src/contentScripts/installHook.js | 39 +++++++++++++++-- .../src/main/index.js | 10 ++--- .../src/main/syncSavedPreferences.js | 34 --------------- .../webpack.config.js | 1 + .../src/backend/agent.js | 9 +--- .../src/backend/types.js | 2 +- .../src/devtools/store.js | 3 ++ 12 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js delete mode 100644 packages/react-devtools-extensions/src/main/syncSavedPreferences.js diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index e61ebd1e57ed0..1ab43194f2620 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -42,6 +42,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 48a56c7400ce4..bd03dea08efb3 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -42,6 +42,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 930c1ab11083e..8a5a272fb4500 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -49,6 +49,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index 9398d71a54e7c..f1a3598a519ca 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -25,6 +25,13 @@ const contentScriptsToInject = [ runAt: 'document_start', world: chrome.scripting.ExecutionWorld.MAIN, }, + { + id: '@react-devtools/hook-settings-injector', + js: ['build/hookSettingsInjector.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + }, ]; async function dynamicallyInjectContentScripts() { diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js new file mode 100644 index 0000000000000..e26108edac7e6 --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -0,0 +1,42 @@ +/* global chrome */ + +// We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN +// This is the only purpose of this script - to send persisted settings to installHook.js content script + +async function messageListener(event: MessageEvent) { + if (event.source !== window) { + return; + } + + if (event.data.source === 'react-devtools-hook-installer') { + if (event.data.payload.handshake) { + const settings = await chrome.storage.local.get(); + // If storage was empty (first installation), define default settings + if (typeof settings.appendComponentStack !== 'boolean') { + settings.appendComponentStack = true; + } + if (typeof settings.breakOnConsoleErrors !== 'boolean') { + settings.breakOnConsoleErrors = false; + } + if (typeof settings.showInlineWarningsAndErrors !== 'boolean') { + settings.showInlineWarningsAndErrors = true; + } + if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') { + settings.hideConsoleLogsInStrictMode = false; + } + + window.postMessage({ + source: 'react-devtools-hook-settings-injector', + payload: {settings}, + }); + + window.removeEventListener('message', messageListener); + } + } +} + +window.addEventListener('message', messageListener); +window.postMessage({ + source: 'react-devtools-hook-settings-injector', + payload: {handshake: true}, +}); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index ff7e041627f0e..b7b96ed24714b 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -1,10 +1,43 @@ import {installHook} from 'react-devtools-shared/src/hook'; -// avoid double execution +let resolveHookSettingsInjection; + +function messageListener(event: MessageEvent) { + if (event.source !== window) { + return; + } + + if (event.data.source === 'react-devtools-hook-settings-injector') { + // In case handshake message was sent prior to hookSettingsInjector execution + // We can't guarantee order + if (event.data.payload.handshake) { + window.postMessage({ + source: 'react-devtools-hook-installer', + payload: {handshake: true}, + }); + } else if (event.data.payload.settings) { + window.removeEventListener('message', messageListener); + resolveHookSettingsInjection(event.data.payload.settings); + } + } +} + +// Avoid double execution if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { - installHook(window); + const hookSettingsPromise = new Promise(resolve => { + resolveHookSettingsInjection = resolve; + }); + + window.addEventListener('message', messageListener); + window.postMessage({ + source: 'react-devtools-hook-installer', + payload: {handshake: true}, + }); + + // Can't delay hook installation, inject settings lazily + installHook(window, hookSettingsPromise); - // detect react + // Detect React window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on( 'renderer', function ({reactBuildType}) { diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 36931e42194a4..a2758567138c8 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -27,7 +27,6 @@ import {startReactPolling} from './reactPolling'; import cloneStyleTags from './cloneStyleTags'; import fetchFileWithCaching from './fetchFileWithCaching'; import injectBackendManager from './injectBackendManager'; -import syncSavedPreferences from './syncSavedPreferences'; import registerEventsLogger from './registerEventsLogger'; import getProfilingFlags from './getProfilingFlags'; import debounce from './debounce'; @@ -103,6 +102,10 @@ function createBridgeAndStore() { supportsClickToInspect: true, }); + store.addListener('settingsUpdated', settings => { + chrome.storage.local.set(settings); + }); + if (!isProfiling) { // We previously stored this in performCleanup function store.profilerStore.profilingData = profilingData; @@ -393,10 +396,6 @@ let root = null; let port = null; -// Re-initialize saved filters on navigation, -// since global values stored on window get reset in this case. -chrome.devtools.network.onNavigated.addListener(syncSavedPreferences); - // In case when multiple navigation events emitted in a short period of time // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results // into subscribing to the same events from Bridge and window multiple times @@ -426,5 +425,4 @@ if (__IS_FIREFOX__) { connectExtensionPort(); -syncSavedPreferences(); mountReactDevToolsWhenReactHasLoaded(); diff --git a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js b/packages/react-devtools-extensions/src/main/syncSavedPreferences.js deleted file mode 100644 index f22d41eb7c904..0000000000000 --- a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js +++ /dev/null @@ -1,34 +0,0 @@ -/* global chrome */ - -import { - getAppendComponentStack, - getBreakOnConsoleErrors, - getSavedComponentFilters, - getShowInlineWarningsAndErrors, - getHideConsoleLogsInStrictMode, -} from 'react-devtools-shared/src/utils'; - -// The renderer interface can't read saved component filters directly, -// because they are stored in localStorage within the context of the extension. -// Instead it relies on the extension to pass filters through. -function syncSavedPreferences() { - chrome.devtools.inspectedWindow.eval( - `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( - getAppendComponentStack(), - )}; - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify( - getBreakOnConsoleErrors(), - )}; - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( - getSavedComponentFilters(), - )}; - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify( - getShowInlineWarningsAndErrors(), - )}; - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify( - getHideConsoleLogsInStrictMode(), - )};`, - ); -} - -export default syncSavedPreferences; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index ddbb4356f658c..e6d40a1f20ad2 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -56,6 +56,7 @@ module.exports = { proxy: './src/contentScripts/proxy.js', prepareInjection: './src/contentScripts/prepareInjection.js', installHook: './src/contentScripts/installHook.js', + hookSettingsInjector: './src/contentScripts/hookSettingsInjector.js', }, output: { path: __dirname + '/build', diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 277b743f3f6a4..e55e8a6d41e2b 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -149,7 +149,7 @@ export default class Agent extends EventEmitter<{ drawTraceUpdates: [Array], disableTraceUpdates: [], getIfHasUnsupportedRendererVersion: [], - updateHookSettings: [DevToolsHookSettings], + updateHookSettings: [$ReadOnly], getHookSettings: [], }> { _bridge: BackendBridge; @@ -806,12 +806,7 @@ export default class Agent extends EventEmitter<{ updateHookSettings: (settings: $ReadOnly) => void = settings => { // Propagate the settings, so Backend can subscribe to it and modify hook - this.emit('updateHookSettings', { - appendComponentStack: settings.appendComponentStack, - breakOnConsoleErrors: settings.breakOnConsoleErrors, - showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, - }); + this.emit('updateHookSettings', settings); }; getHookSettings: () => void = () => { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index bdc93a19baa8b..4065b536afeeb 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -524,7 +524,7 @@ export type DevToolsHook = { // Testing dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void, - settings?: DevToolsHookSettings, + settings?: $ReadOnly, ... }; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 1798e0952b8c2..b54907338b372 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{ componentFilters: [], error: [Error], hookSettings: [$ReadOnly], + settingsUpdated: [$ReadOnly], mutated: [[Array, Map]], recordChangeDescriptions: [], roots: [], @@ -1519,7 +1520,9 @@ export default class Store extends EventEmitter<{ updateHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; + this._bridge.send('updateHookSettings', settings); + this.emit('settingsUpdated', settings); }; onHookSettings: (settings: $ReadOnly) => void = From f2c57a31e9953b3889c56f68e129e67afca15d0e Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 18:30:32 +0100 Subject: [PATCH 170/191] chore: remove settings manager from react-devtools-core (#30986) Stacked on https://github.com/facebook/react/pull/30636. See [this commit](https://github.com/facebook/react/pull/30986/commits/20cec76c44f77e74b3a85225fecab5a431cd986f). This has been only used for React Native and will be replaced by another approach (initialization via `installHook` call) in the next PR. --- packages/react-devtools-core/src/backend.js | 44 ----------- .../react-devtools-core/src/cachedSettings.js | 75 ------------------- .../src/backend/console.js | 26 ------- .../src/backend/types.js | 3 - 4 files changed, 148 deletions(-) delete mode 100644 packages/react-devtools-core/src/cachedSettings.js delete mode 100644 packages/react-devtools-shared/src/backend/console.js diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 6949d6019145a..84f537e875d00 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -14,11 +14,6 @@ import {initBackend} from 'react-devtools-shared/src/backend'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; -import { - initializeUsingCachedSettings, - cacheConsolePatchSettings, - type DevToolsSettingsManager, -} from './cachedSettings'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type { @@ -37,7 +32,6 @@ type ConnectOptions = { retryConnectionDelay?: number, isAppActive?: () => boolean, websocket?: ?WebSocket, - devToolsSettingsManager: ?DevToolsSettingsManager, }; installHook(window); @@ -72,7 +66,6 @@ export function connectToDevTools(options: ?ConnectOptions) { resolveRNStyle = (null: $FlowFixMe), retryConnectionDelay = 2000, isAppActive = () => true, - devToolsSettingsManager, } = options || {}; const protocol = useHttps ? 'wss' : 'ws'; @@ -88,16 +81,6 @@ export function connectToDevTools(options: ?ConnectOptions) { } } - if (devToolsSettingsManager != null) { - try { - initializeUsingCachedSettings(devToolsSettingsManager); - } catch (e) { - // If we call a method on devToolsSettingsManager that throws, or if - // is invalid data read out, don't throw and don't interrupt initialization - console.error(e); - } - } - if (!isAppActive()) { // If the app is in background, maybe retry later. // Don't actually attempt to connect until we're in foreground. @@ -161,15 +144,6 @@ export function connectToDevTools(options: ?ConnectOptions) { }, ); - if (devToolsSettingsManager != null && bridge != null) { - bridge.addListener('updateHookSettings', consolePatchSettings => - cacheConsolePatchSettings( - devToolsSettingsManager, - consolePatchSettings, - ), - ); - } - // The renderer interface doesn't read saved component filters directly, // because they are generally stored in localStorage within the context of the extension. // Because of this it relies on the extension to pass filters. @@ -314,7 +288,6 @@ type ConnectWithCustomMessagingOptions = { onSubscribe: (cb: Function) => void, onUnsubscribe: (cb: Function) => void, onMessage: (event: string, payload: any) => void, - settingsManager: ?DevToolsSettingsManager, nativeStyleEditorValidAttributes?: $ReadOnlyArray, resolveRNStyle?: ResolveNativeStyle, }; @@ -323,7 +296,6 @@ export function connectWithCustomMessagingProtocol({ onSubscribe, onUnsubscribe, onMessage, - settingsManager, nativeStyleEditorValidAttributes, resolveRNStyle, }: ConnectWithCustomMessagingOptions): Function { @@ -332,16 +304,6 @@ export function connectWithCustomMessagingProtocol({ return; } - if (settingsManager != null) { - try { - initializeUsingCachedSettings(settingsManager); - } catch (e) { - // If we call a method on devToolsSettingsManager that throws, or if - // is invalid data read out, don't throw and don't interrupt initialization - console.error(e); - } - } - const wall: Wall = { listen(fn: Function) { onSubscribe(fn); @@ -367,12 +329,6 @@ export function connectWithCustomMessagingProtocol({ }, ); - if (settingsManager != null) { - bridge.addListener('updateHookSettings', consolePatchSettings => - cacheConsolePatchSettings(settingsManager, consolePatchSettings), - ); - } - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { bridge.send('overrideComponentFilters', savedComponentFilters); } diff --git a/packages/react-devtools-core/src/cachedSettings.js b/packages/react-devtools-core/src/cachedSettings.js deleted file mode 100644 index 5c603447e8d88..0000000000000 --- a/packages/react-devtools-core/src/cachedSettings.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {ConsolePatchSettings} from 'react-devtools-shared/src/backend/types'; -import {writeConsolePatchSettingsToWindow} from 'react-devtools-shared/src/backend/console'; -import {castBool} from 'react-devtools-shared/src/utils'; - -// Note: all keys should be optional in this type, because users can use newer -// versions of React DevTools with older versions of React Native, and the object -// provided by React Native may not include all of this type's fields. -export type DevToolsSettingsManager = { - getConsolePatchSettings: ?() => string, - setConsolePatchSettings: ?(key: string) => void, -}; - -export function initializeUsingCachedSettings( - devToolsSettingsManager: DevToolsSettingsManager, -) { - initializeConsolePatchSettings(devToolsSettingsManager); -} - -function initializeConsolePatchSettings( - devToolsSettingsManager: DevToolsSettingsManager, -) { - if (devToolsSettingsManager.getConsolePatchSettings == null) { - return; - } - const consolePatchSettingsString = - devToolsSettingsManager.getConsolePatchSettings(); - if (consolePatchSettingsString == null) { - return; - } - const parsedConsolePatchSettings = parseConsolePatchSettings( - consolePatchSettingsString, - ); - if (parsedConsolePatchSettings == null) { - return; - } - writeConsolePatchSettingsToWindow(parsedConsolePatchSettings); -} - -function parseConsolePatchSettings( - consolePatchSettingsString: string, -): ?ConsolePatchSettings { - const parsedValue = JSON.parse(consolePatchSettingsString ?? '{}'); - const { - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - } = parsedValue; - - return { - appendComponentStack: castBool(appendComponentStack) ?? true, - breakOnConsoleErrors: castBool(breakOnConsoleErrors) ?? false, - showInlineWarningsAndErrors: castBool(showInlineWarningsAndErrors) ?? true, - hideConsoleLogsInStrictMode: castBool(hideConsoleLogsInStrictMode) ?? false, - }; -} - -export function cacheConsolePatchSettings( - devToolsSettingsManager: DevToolsSettingsManager, - value: $ReadOnly, -): void { - if (devToolsSettingsManager.setConsolePatchSettings == null) { - return; - } - devToolsSettingsManager.setConsolePatchSettings(JSON.stringify(value)); -} diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js deleted file mode 100644 index 9e61285b50fe4..0000000000000 --- a/packages/react-devtools-shared/src/backend/console.js +++ /dev/null @@ -1,26 +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. - * - * @flow - */ - -import type {ConsolePatchSettings} from './types'; - -// After receiving cached console patch settings from React Native, we set them on window. -// When the console is initially patched (in renderer.js and hook.js), these values are read. -// The browser extension (etc.) sets these values on window, but through another method. -export function writeConsolePatchSettingsToWindow( - settings: ConsolePatchSettings, -): void { - window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = - settings.appendComponentStack; - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = - settings.breakOnConsoleErrors; - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = - settings.showInlineWarningsAndErrors; - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = - settings.hideConsoleLogsInStrictMode; -} diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 4065b536afeeb..c3110dc517fdc 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -534,6 +534,3 @@ export type DevToolsHookSettings = { showInlineWarningsAndErrors: boolean, hideConsoleLogsInStrictMode: boolean, }; - -// Will be removed together with console patching from backend/console.js to hook.js -export type ConsolePatchSettings = DevToolsHookSettings; From 09d82835993b16cd5dc8350c03627f9573354a25 Mon Sep 17 00:00:00 2001 From: mofeiZ <34200447+mofeiZ@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:39:04 -0400 Subject: [PATCH 171/191] [ez] Rewrite optional chaining and nullish coalescing syntax (#30982) Rewrite `containerInfo?.ownerDocument?.defaultView ?? window` to instead use a ternary. This changes the compilation output (see [bundle changes from #30951](https://github.com/facebook/react/commit/d65fb06955e9f32e6a40d1c7177d77893dff95b9)). ```js // compilation of containerInfo?.ownerDocument?.defaultView ?? window var $jscomp$optchain$tmpm1756096108$1, $jscomp$nullish$tmp0; containerInfo = null != ($jscomp$nullish$tmp0 = null == containerInfo ? void 0 : null == ($jscomp$optchain$tmpm1756096108$1 = containerInfo.ownerDocument) ? void 0 : $jscomp$optchain$tmpm1756096108$1.defaultView) ? $jscomp$nullish$tmp0 : window; // compilation of ternary expression containerInfo = null != containerInfo && null != containerInfo.ownerDocument && null != containerInfo.ownerDocument.defaultView ? containerInfo.ownerDocument.defaultView : window; ``` This also reduces the number of no-op bundle syncs for Meta. Note that Closure compiler's `jscomp$optchain$tmp` identifiers change when we rebuild (likely due to version number changes). See [workflow](https://github.com/facebook/react/actions/runs/10891164281/job/30221518374) for a PR that was synced despite making no changes to the runtime. --- .../react-dom-bindings/src/client/ReactInputSelection.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-dom-bindings/src/client/ReactInputSelection.js b/packages/react-dom-bindings/src/client/ReactInputSelection.js index 0f3dfe11cd9f9..36cdc554f0f44 100644 --- a/packages/react-dom-bindings/src/client/ReactInputSelection.js +++ b/packages/react-dom-bindings/src/client/ReactInputSelection.js @@ -57,7 +57,12 @@ function isSameOriginFrame(iframe) { } function getActiveElementDeep(containerInfo) { - let win = containerInfo?.ownerDocument?.defaultView ?? window; + let win = + containerInfo != null && + containerInfo.ownerDocument != null && + containerInfo.ownerDocument.defaultView != null + ? containerInfo.ownerDocument.defaultView + : window; let element = getActiveElement(win.document); while (element instanceof win.HTMLIFrameElement) { if (isSameOriginFrame(element)) { From e72127a4ec6f91288e9008711215068823100599 Mon Sep 17 00:00:00 2001 From: Timothy Yung Date: Wed, 18 Sep 2024 14:44:55 -0700 Subject: [PATCH 172/191] Build `react-dom` in `builds/facebook-fbsource` (#30711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Builds `react-dom` for React Native so that it also populates the `builds/facebook-fbsource` branch. **NOTE:** For Meta employees, D61354219 is the internal integration. ## How did you test this change? ``` $ yarn build … $ ls build/facebook-react-native/react-dom/cjs ReactDOM-dev.js ReactDOM-prod.js ReactDOM-profiling.js ``` --- .../workflows/runtime_commit_artifacts.yml | 1 + scripts/rollup/bundles.js | 48 ++++++++++++++++++- scripts/rollup/packaging.js | 1 + 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index f47eb3ff360e9..ed0c59caf883e 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -125,6 +125,7 @@ jobs: mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/ mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/ + mv build/facebook-react-native/react/dom/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/ mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/ mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/ diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 081b91b9d0b80..26a88fb00811d 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -183,6 +183,7 @@ const bundles = [ wrapWithModuleBoundaries: true, externals: ['react'], }, + /******* React DOM Client *******/ { bundleTypes: [NODE_DEV, NODE_PROD], @@ -204,7 +205,8 @@ const bundles = [ wrapWithModuleBoundaries: true, externals: ['react', 'react-dom'], }, - /******* React DOM FB *******/ + + /******* React DOM (www) *******/ { bundleTypes: [FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING], moduleType: RENDERER, @@ -215,6 +217,50 @@ const bundles = [ externals: ['react'], }, + /******* React DOM (fbsource) *******/ + { + bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING], + moduleType: RENDERER, + entry: 'react-dom', + global: 'ReactDOM', + minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, + externals: ['react', 'ReactNativeInternalFeatureFlags'], + }, + + /******* React DOM Client (fbsource) *******/ + { + bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING], + moduleType: RENDERER, + entry: 'react-dom/client', + global: 'ReactDOMClient', + minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'ReactNativeInternalFeatureFlags'], + }, + + /******* React DOM Profiling (fbsource) *******/ + { + bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING], + moduleType: RENDERER, + entry: 'react-dom/profiling', + global: 'ReactDOMProfiling', + minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: true, + externals: ['react', 'react-dom', 'ReactNativeInternalFeatureFlags'], + }, + + /******* React DOM Test Utils (fbsource) *******/ + { + moduleType: RENDERER_UTILS, + bundleTypes: [RN_FB_DEV, RN_FB_PROD, RN_FB_PROFILING], + entry: 'react-dom/test-utils', + global: 'ReactDOMTestUtils', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'ReactNativeInternalFeatureFlags'], + }, + /******* React DOM React Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/rollup/packaging.js b/scripts/rollup/packaging.js index 7c433fc2dd330..bc83cb8789eb8 100644 --- a/scripts/rollup/packaging.js +++ b/scripts/rollup/packaging.js @@ -76,6 +76,7 @@ function getBundleOutputPath(bundle, bundleType, filename, packageName) { switch (packageName) { case 'scheduler': case 'react': + case 'react-dom': case 'react-is': case 'react-test-renderer': return `build/facebook-react-native/${packageName}/cjs/${filename}`; From a86afe8e560f452a9df5ceb4893d9423e5840800 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 19 Sep 2024 13:55:08 +0100 Subject: [PATCH 173/191] feat: expose installHook with settings argument from react-devtools-core/backend (#30987) Stacked on https://github.com/facebook/react/pull/30986. Previously, we would call `installHook` at a top level of the JavaScript module. Because of this, having `require` statement for `react-devtools-core` package was enough to initialize the React DevTools global hook on the `window`. Now, the Hook can actually receive an argument - initial user settings for console patching. We expose this as a function `initialize`, which can be used by third parties (including React Native) to provide the persisted settings. The README was also updated to reflect the changes. --- packages/react-devtools-core/README.md | 47 +++++++++++++-------- packages/react-devtools-core/src/backend.js | 38 ++++++++++++++--- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md index b0f0e98191b35..f3487fefba75e 100644 --- a/packages/react-devtools-core/README.md +++ b/packages/react-devtools-core/README.md @@ -14,35 +14,46 @@ If you are building a non-browser-based React renderer, you can use the backend ```js if (process.env.NODE_ENV !== 'production') { - const { connectToDevTools } = require("react-devtools-core"); + const { initialize, connectToDevTools } = require("react-devtools-core"); + initialize(settings); // Must be called before packages like react or react-native are imported - connectToDevTools({ - ...config - }); + connectToDevTools({...config}); } ``` > **NOTE** that this API (`connectToDevTools`) must be (1) run in the same context as React and (2) must be called before React packages are imported (e.g. `react`, `react-dom`, `react-native`). +### `initialize` arguments +| Argument | Description | +|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `settings` | Optional. If not specified, or received as null, then default settings are used. Can be plain object or a Promise that resolves with the [plain settings object](#Settings). If Promise rejects, the console will not be patched and some console features from React DevTools will not work. | + +#### `Settings` +| Spec | Default value | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|
{
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean
}
|
{
appendComponentStack: true,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false
}
| + ### `connectToDevTools` options -| Prop | Default | Description | -|---|---|---| -| `host` | `"localhost"` | Socket connection to frontend should use this host. | -| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. | -| `port` | `8097` | Socket connection to frontend should use this port. | -| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. | -| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection | -| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). | -| `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. | +| Prop | Default | Description | +|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------| +| `host` | `"localhost"` | Socket connection to frontend should use this host. | +| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. | +| `port` | `8097` | Socket connection to frontend should use this port. | +| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. | +| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection | +| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). | +| `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. | +| `onSettingsUpdated` | | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user settings. | | ### `connectWithCustomMessagingProtocol` options -| Prop | Description | -|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). | -| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). | -| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. | +| Prop | Description | +|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). | +| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). | +| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. | +| `onSettingsUpdated` | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user settings. | Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook. diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 84f537e875d00..1f2055832a3dd 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -20,7 +20,10 @@ import type { ComponentFilter, Wall, } from 'react-devtools-shared/src/frontend/types'; -import type {DevToolsHook} from 'react-devtools-shared/src/backend/types'; +import type { + DevToolsHook, + DevToolsHookSettings, +} from 'react-devtools-shared/src/backend/types'; import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; type ConnectOptions = { @@ -32,12 +35,9 @@ type ConnectOptions = { retryConnectionDelay?: number, isAppActive?: () => boolean, websocket?: ?WebSocket, + onSettingsUpdated?: (settings: $ReadOnly) => void, }; -installHook(window); - -const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - let savedComponentFilters: Array = getDefaultComponentFilters(); @@ -52,11 +52,21 @@ function debug(methodName: string, ...args: Array) { } } +export function initialize( + maybeSettingsOrSettingsPromise?: + | DevToolsHookSettings + | Promise, +) { + installHook(window, maybeSettingsOrSettingsPromise); +} + export function connectToDevTools(options: ?ConnectOptions) { + const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). return; } + const { host = 'localhost', nativeStyleEditorValidAttributes, @@ -66,6 +76,7 @@ export function connectToDevTools(options: ?ConnectOptions) { resolveRNStyle = (null: $FlowFixMe), retryConnectionDelay = 2000, isAppActive = () => true, + onSettingsUpdated, } = options || {}; const protocol = useHttps ? 'wss' : 'ws'; @@ -160,7 +171,14 @@ export function connectToDevTools(options: ?ConnectOptions) { // TODO (npm-packages) Warn if "isBackendStorageAPISupported" // $FlowFixMe[incompatible-call] found when upgrading Flow const agent = new Agent(bridge); + if (onSettingsUpdated != null) { + agent.addListener('updateHookSettings', onSettingsUpdated); + } agent.addListener('shutdown', () => { + if (onSettingsUpdated != null) { + agent.removeListener('updateHookSettings', onSettingsUpdated); + } + // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down, // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here. hook.emit('shutdown'); @@ -290,6 +308,7 @@ type ConnectWithCustomMessagingOptions = { onMessage: (event: string, payload: any) => void, nativeStyleEditorValidAttributes?: $ReadOnlyArray, resolveRNStyle?: ResolveNativeStyle, + onSettingsUpdated?: (settings: $ReadOnly) => void, }; export function connectWithCustomMessagingProtocol({ @@ -298,7 +317,9 @@ export function connectWithCustomMessagingProtocol({ onMessage, nativeStyleEditorValidAttributes, resolveRNStyle, + onSettingsUpdated, }: ConnectWithCustomMessagingOptions): Function { + const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). return; @@ -334,7 +355,14 @@ export function connectWithCustomMessagingProtocol({ } const agent = new Agent(bridge); + if (onSettingsUpdated != null) { + agent.addListener('updateHookSettings', onSettingsUpdated); + } agent.addListener('shutdown', () => { + if (onSettingsUpdated != null) { + agent.removeListener('updateHookSettings', onSettingsUpdated); + } + // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down, // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here. hook.emit('shutdown'); From d5e955d3c0c1fa2494de0ab33be9cd90c65aff1e Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Thu, 19 Sep 2024 10:07:29 -0400 Subject: [PATCH 174/191] [compiler] Pass through unmodified props spread when inlining jsx (#30995) If JSX receives a props spread without additional attributes (besides `ref` and `key`), we can pass the spread object as a property directly to avoid the extra object copy. ``` // {props: propsToSpread} // {props: {...propsToSpread, a: "z"}} ``` --- .../src/Optimization/InlineJsxTransform.ts | 63 ++++++++---- .../compiler/inline-jsx-transform.expect.md | 97 ++++++++++++------- .../fixtures/compiler/inline-jsx-transform.js | 11 ++- 3 files changed, 115 insertions(+), 56 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts index 344159d0b6072..0fe516da977e3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -23,6 +23,7 @@ import { markPredecessors, reversePostorderBlocks, } from '../HIR/HIRBuilder'; +import {CompilerError} from '..'; function createSymbolProperty( fn: HIRFunction, @@ -151,6 +152,16 @@ function createPropsProperties( let refProperty: ObjectProperty | undefined; let keyProperty: ObjectProperty | undefined; const props: Array = []; + const jsxAttributesWithoutKeyAndRef = propAttributes.filter( + p => p.kind === 'JsxAttribute' && p.name !== 'key' && p.name !== 'ref', + ); + const jsxSpreadAttributes = propAttributes.filter( + p => p.kind === 'JsxSpreadAttribute', + ); + const spreadPropsOnly = + jsxAttributesWithoutKeyAndRef.length === 0 && + jsxSpreadAttributes.length === 1; + propAttributes.forEach(prop => { switch (prop.kind) { case 'JsxAttribute': { @@ -180,7 +191,6 @@ function createPropsProperties( break; } case 'JsxSpreadAttribute': { - // TODO: Optimize spreads to pass object directly if none of its properties are mutated props.push({ kind: 'Spread', place: {...prop.argument}, @@ -189,6 +199,7 @@ function createPropsProperties( } } }); + const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc); if (children) { let childrenPropProperty: ObjectProperty; @@ -268,23 +279,39 @@ function createPropsProperties( nextInstructions.push(keyInstruction); } - const propsInstruction: Instruction = { - id: makeInstructionId(0), - lvalue: {...propsPropertyPlace, effect: Effect.Mutate}, - value: { - kind: 'ObjectExpression', - properties: props, - loc: instr.value.loc, - }, - loc: instr.loc, - }; - const propsProperty: ObjectProperty = { - kind: 'ObjectProperty', - key: {name: 'props', kind: 'string'}, - type: 'property', - place: {...propsPropertyPlace, effect: Effect.Capture}, - }; - nextInstructions.push(propsInstruction); + let propsProperty: ObjectProperty; + if (spreadPropsOnly) { + const spreadProp = jsxSpreadAttributes[0]; + CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', { + reason: 'Spread prop attribute must be of kind JSXSpreadAttribute', + loc: instr.loc, + }); + propsProperty = { + kind: 'ObjectProperty', + key: {name: 'props', kind: 'string'}, + type: 'property', + place: {...spreadProp.argument, effect: Effect.Mutate}, + }; + } else { + const propsInstruction: Instruction = { + id: makeInstructionId(0), + lvalue: {...propsPropertyPlace, effect: Effect.Mutate}, + value: { + kind: 'ObjectExpression', + properties: props, + loc: instr.value.loc, + }, + loc: instr.loc, + }; + propsProperty = { + kind: 'ObjectProperty', + key: {name: 'props', kind: 'string'}, + type: 'property', + place: {...propsPropertyPlace, effect: Effect.Capture}, + }; + nextInstructions.push(propsInstruction); + } + return {refProperty, keyProperty, propsProperty}; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md index f399317925c64..6dd899d5c7408 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md @@ -28,9 +28,9 @@ function ParentAndRefAndKey(props) { function ParentAndChildren(props) { return ( - + - + ); @@ -38,7 +38,12 @@ function ParentAndChildren(props) { const propsToSpread = {a: 'a', b: 'b', c: 'c'}; function PropsSpread() { - return ; + return ( + <> + + + + ); } export const FIXTURE_ENTRYPOINT = { @@ -146,54 +151,59 @@ function ParentAndRefAndKey(props) { } function ParentAndChildren(props) { - const $ = _c2(3); + const $ = _c2(7); let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + if ($[0] !== props) { t0 = { $$typeof: Symbol.for("react.transitional.element"), type: Child, ref: null, key: "a", - props: {}, + props: props, }; - $[0] = t0; + $[0] = props; + $[1] = t0; } else { - t0 = $[0]; + t0 = $[1]; } let t1; - if ($[1] !== props.foo) { + if ($[2] !== props) { t1 = { $$typeof: Symbol.for("react.transitional.element"), - type: Parent, + type: Child, ref: null, - key: null, + key: "b", props: { - children: [ - t0, - { - $$typeof: Symbol.for("react.transitional.element"), - type: Child, - ref: null, - key: "b", - props: { - children: { - $$typeof: Symbol.for("react.transitional.element"), - type: GrandChild, - ref: null, - key: null, - props: { className: props.foo }, - }, - }, - }, - ], + children: { + $$typeof: Symbol.for("react.transitional.element"), + type: GrandChild, + ref: null, + key: null, + props: { className: props.foo, ...props }, + }, }, }; - $[1] = props.foo; - $[2] = t1; + $[2] = props; + $[3] = t1; } else { - t1 = $[2]; + t1 = $[3]; } - return t1; + let t2; + if ($[4] !== t0 || $[5] !== t1) { + t2 = { + $$typeof: Symbol.for("react.transitional.element"), + type: Parent, + ref: null, + key: null, + props: { children: [t0, t1] }, + }; + $[4] = t0; + $[5] = t1; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; } const propsToSpread = { a: "a", b: "b", c: "c" }; @@ -203,10 +213,27 @@ function PropsSpread() { if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = { $$typeof: Symbol.for("react.transitional.element"), - type: Test, + type: Symbol.for("react.fragment"), ref: null, key: null, - props: { ...propsToSpread }, + props: { + children: [ + { + $$typeof: Symbol.for("react.transitional.element"), + type: Test, + ref: null, + key: null, + props: propsToSpread, + }, + { + $$typeof: Symbol.for("react.transitional.element"), + type: Test, + ref: null, + key: null, + props: { ...propsToSpread, a: "z" }, + }, + ], + }, }; $[0] = t0; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js index 29acaf60d276f..4a9d53b6f4b96 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js @@ -24,9 +24,9 @@ function ParentAndRefAndKey(props) { function ParentAndChildren(props) { return ( - + - + ); @@ -34,7 +34,12 @@ function ParentAndChildren(props) { const propsToSpread = {a: 'a', b: 'b', c: 'c'}; function PropsSpread() { - return ; + return ( + <> + + + + ); } export const FIXTURE_ENTRYPOINT = { From 632f88df11329c9ad66781ddd75b27df6f8effb9 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Thu, 19 Sep 2024 10:34:24 -0400 Subject: [PATCH 175/191] [compiler] Allow ReactElement symbol to be configured when inlining jsx (#30996) Based on https://github.com/facebook/react/pull/30995 ([rendered diff](https://github.com/jackpope/react/compare/inline-jsx-2...jackpope:react:inline-jsx-3?expand=1)) ____ Some apps still use `react.element` symbols. Not only do we want to test there but we also want to be able to upgrade those sites to `react.transitional.element` without blocking on the compiler (we can change the symbol feature flag and compiler config at the same time). The compiler runtime uses `react.transitional.element`, so the snap fixture will fail if we change the default here. However I confirmed that commenting out the fixture entrypoint and running snap with `react.element` will update the fixture symbols as expected. --- .../src/Entrypoint/Pipeline.ts | 4 ++-- .../src/HIR/Environment.ts | 11 +++++++++- .../src/Optimization/InlineJsxTransform.ts | 21 ++++++++----------- compiler/packages/snap/src/compiler.ts | 7 +++++++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 606440b241f9b..900e4f4908494 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -352,8 +352,8 @@ function* runWithEnvironment( }); } - if (env.config.enableInlineJsxTransform) { - inlineJsxTransform(hir); + if (env.config.inlineJsxTransform) { + inlineJsxTransform(hir, env.config.inlineJsxTransform); yield log({ kind: 'hir', name: 'inlineJsxTransform', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 66270345fdf35..50905bc581dc1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -50,6 +50,13 @@ import { import {Scope as BabelScope} from '@babel/traverse'; import {TypeSchema} from './TypeSchema'; +export const ReactElementSymbolSchema = z.object({ + elementSymbol: z.union([ + z.literal('react.element'), + z.literal('react.transitional.element'), + ]), +}); + export const ExternalFunctionSchema = z.object({ // Source for the imported module that exports the `importSpecifierName` functions source: z.string(), @@ -237,8 +244,10 @@ const EnvironmentConfigSchema = z.object({ * Enables inlining ReactElement object literals in place of JSX * An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime * Currently a prod-only optimization, requiring Fast JSX dependencies + * + * The symbol configuration is set for backwards compatability with pre-React 19 transforms */ - enableInlineJsxTransform: z.boolean().default(false), + inlineJsxTransform: ReactElementSymbolSchema.nullish(), /* * Enable validation of hooks to partially check that the component honors the rules of hooks. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts index 0fe516da977e3..396e6ad1be8dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -23,7 +23,7 @@ import { markPredecessors, reversePostorderBlocks, } from '../HIR/HIRBuilder'; -import {CompilerError} from '..'; +import {CompilerError, EnvironmentConfig} from '..'; function createSymbolProperty( fn: HIRFunction, @@ -316,7 +316,12 @@ function createPropsProperties( } // TODO: Make PROD only with conditional statements -export function inlineJsxTransform(fn: HIRFunction): void { +export function inlineJsxTransform( + fn: HIRFunction, + inlineJsxTransformConfig: NonNullable< + EnvironmentConfig['inlineJsxTransform'] + >, +): void { for (const [, block] of fn.body.blocks) { let nextInstructions: Array | null = null; for (let i = 0; i < block.instructions.length; i++) { @@ -344,11 +349,7 @@ export function inlineJsxTransform(fn: HIRFunction): void { instr, nextInstructions, '$$typeof', - /** - * TODO: Add this to config so we can switch between - * react.element / react.transitional.element - */ - 'react.transitional.element', + inlineJsxTransformConfig.elementSymbol, ), createTagProperty(fn, instr, nextInstructions, instr.value.tag), refProperty, @@ -384,11 +385,7 @@ export function inlineJsxTransform(fn: HIRFunction): void { instr, nextInstructions, '$$typeof', - /** - * TODO: Add this to config so we can switch between - * react.element / react.transitional.element - */ - 'react.transitional.element', + inlineJsxTransformConfig.elementSymbol, ), createSymbolProperty( fn, diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 417a657d28012..bbb6aeded750c 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -21,6 +21,7 @@ import type { } from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR'; import type { + EnvironmentConfig, Macro, MacroMethod, parseConfigPragma as ParseConfigPragma, @@ -201,6 +202,11 @@ function makePluginOptions( }; } + let inlineJsxTransform: EnvironmentConfig['inlineJsxTransform'] = null; + if (firstLine.includes('@enableInlineJsxTransform')) { + inlineJsxTransform = {elementSymbol: 'react.transitional.element'}; + } + let logs: Array<{filename: string | null; event: LoggerEvent}> = []; let logger: Logger | null = null; if (firstLine.includes('@logger')) { @@ -230,6 +236,7 @@ function makePluginOptions( enableChangeDetectionForDebugging, lowerContextAccess, validateBlocklistedImports, + inlineJsxTransform, }, compilationMode, logger, From c21ce4a39667c4094208bc35dc86fc9f49fceec6 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 19 Sep 2024 15:44:34 +0100 Subject: [PATCH 176/191] feat: display message if user ended up opening hook script (#31000) In https://github.com/facebook/react/pull/30596 we've moved console patching to the global hook. Generally speaking, the patching happens even before React is loaded on the page. If browser DevTools were opened after when `console.error` or `console.warn` were called, the source script will be `hook.js`, because of the patching. ![devtools-opened-after-the-message](https://github.com/user-attachments/assets/3d3dbc16-96b8-4234-b061-57b21b60cf2e) This is because ignore listing is not applied retroactively by Chrome DevTools. If you had it open before console calls, Hook script would be correctly filtered out from the stack: ![devtools-opened-before-the-message](https://github.com/user-attachments/assets/3e99cb22-97b0-4b49-9a76-f7bc948e6452) I had hopes that the fix for https://issues.chromium.org/issues/345248263 will also apply ignore listing retroactively, but looks like we need to open a separate feature request for the Chrome DevTools team. With these changes, if user attempts to open `hook.js` script, they are going to see this message: ![Screenshot 2024-09-19 at 11 30 59](https://github.com/user-attachments/assets/5850b74c-329f-4fbe-a3dd-33f9ac717ee9) --- .../webpack.config.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index e6d40a1f20ad2..51b8f4e2105e3 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -154,6 +154,62 @@ module.exports = { ); }, }), + { + apply(compiler) { + if (__DEV__) { + return; + } + + const {RawSource} = compiler.webpack.sources; + compiler.hooks.compilation.tap( + 'CustomContentForHookScriptPlugin', + compilation => { + compilation.hooks.processAssets.tap( + { + name: 'CustomContentForHookScriptPlugin', + stage: Webpack.Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, + additionalAssets: true, + }, + assets => { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const [name, asset] of Object.entries(assets)) { + if (name !== 'installHook.js.map') { + continue; + } + + const mapContent = asset.source().toString(); + if (!mapContent) { + continue; + } + + const map = JSON.parse(mapContent); + map.sourcesContent = map.sources.map(sourceName => { + if (!sourceName.endsWith('/hook.js')) { + return null; + } + + return ( + '/*\n' + + ' * This script is from React DevTools.\n' + + " * You're likely here because you thought it sent an error or warning to the console.\n" + + ' * React DevTools patches the console to support features like appending component stacks, \n' + + ' * so this file appears as a source. However, the console call actually came from another script.\n' + + " * To remove this script from stack traces, open your browser's DevTools (to enable source mapping) before these console calls happen.\n" + + ' */' + ); + }); + + compilation.updateAsset( + name, + new RawSource(JSON.stringify(map)), + ); + } + }, + ); + }, + ); + }, + }, ], module: { defaultRules: [ From e740d4b14b27b4c7a21f67d20a4526a57d6729a7 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 19 Sep 2024 15:47:25 +0100 Subject: [PATCH 177/191] chore: remove using local storage for persisting console settings on the frontend (#31002) After https://github.com/facebook/react/pull/30636 and https://github.com/facebook/react/pull/30986 we no longer store settings on the Frontend side via `localStorage`. This PR removes all occurrences of it from `react-devtools-core/standalone` and `react-devtools-inline`. --- .../react-devtools-core/src/standalone.js | 20 +------ .../react-devtools-inline/src/frontend.js | 12 +---- packages/react-devtools-shared/src/bridge.js | 4 -- .../react-devtools-shared/src/constants.js | 8 --- .../views/Settings/SettingsModalContext.js | 1 - packages/react-devtools-shared/src/utils.js | 52 ------------------- 6 files changed, 2 insertions(+), 95 deletions(-) diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 22aea529dd8e3..bfd05cf5227ee 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -12,13 +12,7 @@ import {flushSync} from 'react-dom'; import {createRoot} from 'react-dom/client'; import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; -import { - getAppendComponentStack, - getBreakOnConsoleErrors, - getSavedComponentFilters, - getShowInlineWarningsAndErrors, - getHideConsoleLogsInStrictMode, -} from 'react-devtools-shared/src/utils'; +import {getSavedComponentFilters} from 'react-devtools-shared/src/utils'; import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger'; import {Server} from 'ws'; import {join} from 'path'; @@ -368,20 +362,8 @@ function startServer( // Because of this it relies on the extension to pass filters, so include them wth the response here. // This will ensure that saved filters are shared across different web pages. const savedPreferencesString = ` - window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( - getAppendComponentStack(), - )}; - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify( - getBreakOnConsoleErrors(), - )}; window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( getSavedComponentFilters(), - )}; - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify( - getShowInlineWarningsAndErrors(), - )}; - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify( - getHideConsoleLogsInStrictMode(), )};`; response.end( diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 9031f6ffc7bd7..f96d29ac50745 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -5,13 +5,7 @@ import {forwardRef} from 'react'; import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; -import { - getAppendComponentStack, - getBreakOnConsoleErrors, - getSavedComponentFilters, - getShowInlineWarningsAndErrors, - getHideConsoleLogsInStrictMode, -} from 'react-devtools-shared/src/utils'; +import {getSavedComponentFilters} from 'react-devtools-shared/src/utils'; import type {Wall} from 'react-devtools-shared/src/frontend/types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; @@ -76,11 +70,7 @@ export function initialize( frontendBridge.removeListener('getSavedPreferences', onGetSavedPreferences); const data = { - appendComponentStack: getAppendComponentStack(), - breakOnConsoleErrors: getBreakOnConsoleErrors(), componentFilters: getSavedComponentFilters(), - showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(), - hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(), }; // The renderer interface can't read saved preferences directly, diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 4751f70cfaefb..fcf9b2d21e88d 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -170,11 +170,7 @@ type NativeStyleEditor_SetValueParams = { }; type SavedPreferencesParams = { - appendComponentStack: boolean, - breakOnConsoleErrors: boolean, componentFilters: Array, - showInlineWarningsAndErrors: boolean, - hideConsoleLogsInStrictMode: boolean, }; export type BackendEvents = { diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 52d39f2d90b07..6893610b46d8f 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -43,17 +43,9 @@ export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY = 'React::DevTools::recordChangeDescriptions'; export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile'; -export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS = - 'React::DevTools::breakOnConsoleErrors'; export const LOCAL_STORAGE_BROWSER_THEME = 'React::DevTools::theme'; -export const LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY = - 'React::DevTools::appendComponentStack'; -export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY = - 'React::DevTools::showInlineWarningsAndErrors'; export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY = 'React::DevTools::traceUpdatesEnabled'; -export const LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE = - 'React::DevTools::hideConsoleLogsInStrictMode'; export const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js index 98e491f44b6eb..f8f76053fc129 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js @@ -24,7 +24,6 @@ import type {FrontendBridge} from '../../../bridge'; import type {DevToolsHookSettings} from '../../../backend/types'; import type Store from '../../store'; -export type DisplayDensity = 'comfortable' | 'compact'; export type Theme = 'auto' | 'light' | 'dark'; type Context = { diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 9b927c19219fb..e24414812c5b3 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -36,10 +36,6 @@ import { TREE_OPERATION_UPDATE_TREE_BASE_DURATION, LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_OPEN_IN_EDITOR_URL, - LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY, - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE, } from './constants'; import { ComponentFilterElementType, @@ -61,7 +57,6 @@ import isArray from './isArray'; import type { ComponentFilter, ElementType, - BrowserTheme, SerializedElement as SerializedElementFrontend, LRUCache, } from 'react-devtools-shared/src/frontend/types'; @@ -374,53 +369,6 @@ export function filterOutLocationComponentFilters( return componentFilters.filter(f => f.type !== ComponentFilterLocation); } -function parseBool(s: ?string): ?boolean { - if (s === 'true') { - return true; - } - if (s === 'false') { - return false; - } -} - -export function castBool(v: any): ?boolean { - if (v === true || v === false) { - return v; - } -} - -export function castBrowserTheme(v: any): ?BrowserTheme { - if (v === 'light' || v === 'dark' || v === 'auto') { - return v; - } -} - -export function getAppendComponentStack(): boolean { - const raw = localStorageGetItem( - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY, - ); - return parseBool(raw) ?? true; -} - -export function getBreakOnConsoleErrors(): boolean { - const raw = localStorageGetItem(LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS); - return parseBool(raw) ?? false; -} - -export function getHideConsoleLogsInStrictMode(): boolean { - const raw = localStorageGetItem( - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE, - ); - return parseBool(raw) ?? false; -} - -export function getShowInlineWarningsAndErrors(): boolean { - const raw = localStorageGetItem( - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY, - ); - return parseBool(raw) ?? true; -} - export function getDefaultOpenInEditorURL(): string { return typeof process.env.EDITOR_URL === 'string' ? process.env.EDITOR_URL From babde5d1826365bfb794abdb7de4bd21f7b02356 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 19 Sep 2024 13:42:49 -0400 Subject: [PATCH 178/191] [lint] Add no-optional-chaining (#31003) ## Overview Adds a lint rule to prevent optional chaining to catch issues like https://github.com/facebook/react/pull/30982 until we support optional chaining without a bundle impact. --- .eslintrc.js | 4 +++- package.json | 1 + packages/react-debug-tools/src/ReactDebugHooks.js | 5 +++-- yarn.lock | 10 +++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ed134392a9727..3366a38517c93 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { 'babel', 'ft-flow', 'jest', + 'es', 'no-for-of-loops', 'no-function-declare-after-return', 'react', @@ -47,7 +48,7 @@ module.exports = { 'ft-flow/no-unused-expressions': ERROR, // 'ft-flow/no-weak-types': WARNING, // 'ft-flow/require-valid-file-annotation': ERROR, - + 'es/no-optional-chaining': ERROR, 'no-cond-assign': OFF, 'no-constant-condition': OFF, 'no-control-regex': OFF, @@ -435,6 +436,7 @@ module.exports = { 'packages/react-dom/src/test-utils/*.js', ], rules: { + 'es/no-optional-chaining': OFF, 'react-internal/no-production-logging': OFF, 'react-internal/warning-args': OFF, 'react-internal/safe-string-coercion': [ diff --git a/package.json b/package.json index f840fc278e7be..cae8499738d22 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "eslint": "^7.7.0", "eslint-config-prettier": "^6.9.0", "eslint-plugin-babel": "^5.3.0", + "eslint-plugin-es": "^4.1.0", "eslint-plugin-eslint-plugin": "^3.5.3", "eslint-plugin-ft-flow": "^2.0.3", "eslint-plugin-jest": "28.4.0", diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 55a1454142235..44d684003861b 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -567,8 +567,9 @@ function useMemoCache(size: number): Array { return []; } - // $FlowFixMe[incompatible-use]: updateQueue is mixed - const memoCache = fiber.updateQueue?.memoCache; + const memoCache = + // $FlowFixMe[incompatible-use]: updateQueue is mixed + fiber.updateQueue != null ? fiber.updateQueue.memoCache : null; if (memoCache == null) { return []; } diff --git a/yarn.lock b/yarn.lock index bf4f7825f6997..b23f0d92aebfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7262,6 +7262,14 @@ eslint-plugin-babel@^5.3.0: dependencies: eslint-rule-composer "^0.3.0" +eslint-plugin-es@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9" + integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ== + dependencies: + eslint-utils "^2.0.0" + regexpp "^3.0.0" + eslint-plugin-eslint-plugin@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-3.5.3.tgz#6cac958e944b820962a4cf9e65cc32a2e0415eaf" @@ -13971,7 +13979,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^3.1.0: +regexpp@^3.0.0, regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== From e4953922a99b5477c3bcf98cdaa2b13ac0a81f0d Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Thu, 19 Sep 2024 18:04:06 -0400 Subject: [PATCH 179/191] Update react-native/react-dom build directory (#31006) Commit artifact actions are breaking after https://github.com/facebook/react/pull/30711 See: https://github.com/facebook/react/actions/runs/10930658977/job/30344033974 > mv: cannot stat 'build/facebook-react-native/react/dom/': No such file or directory After build, the new artifacts are in `/react-dom/cjs`, not `/react/dom/` ``` $> yarn build $> ls build/facebook-react-native/react/ # ... no dom $> ls build/facebook-react-native/react-dom/cjs ``` --- .github/workflows/runtime_commit_artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index ed0c59caf883e..2baabaf59f5c5 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -125,7 +125,7 @@ jobs: mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/ mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/ - mv build/facebook-react-native/react/dom/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/ + mv build/facebook-react-native/react-dom/cjs $BASE_FOLDER/RKJSModules/vendor/react/react-dom/ mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/ mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/ From ae75d5a3f5cf14a5031ee251376b1adf88c32813 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 20 Sep 2024 10:00:02 -0700 Subject: [PATCH 180/191] [Fizz] Include componentStack at the root when aborting (#31011) When aborting we currently don't produce a componentStack when aborting the shell. This is likely just an oversight and this change updates this behavior to be consistent with what we do when there is a boundary --- packages/react-server/src/ReactFizzServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index d0e6eb852c08f..0f863d24b9c57 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -3870,8 +3870,9 @@ function abortTask(task: Task, request: Request, error: mixed): void { segment.status = ABORTED; } + const errorInfo = getThrownInfo(task.componentStack); + if (boundary === null) { - const errorInfo: ThrownInfo = {}; if (request.status !== CLOSING && request.status !== CLOSED) { const replay: null | ReplaySet = task.replay; if (replay === null) { @@ -3957,7 +3958,6 @@ function abortTask(task: Task, request: Request, error: mixed): void { boundary.pendingTasks--; // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which // boundary the message is referring to - const errorInfo = getThrownInfo(task.componentStack); const trackedPostpones = request.trackedPostpones; if (boundary.status !== CLIENT_RENDERED) { if (enableHalt) { From d4688dfaafe51a4cb6e3c51fc2330662cb4e2296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 20 Sep 2024 14:27:12 -0400 Subject: [PATCH 181/191] [Fiber] Track Event Time, startTransition Time and setState Time (#31008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This tracks the current window.event.timeStamp the first time we setState or call startTransition. For either the blocking track or transition track. We can use this to show how long we were blocked by other events or overhead from when the user interacted until we got called into React. Then we track the time we start awaiting a Promise returned from startTransition. We can use this track how long we waited on an Action to complete before setState was called. Then finally we track when setState was called so we can track how long we were blocked by other word before we could actually start rendering. For a Transition this might be blocked by Blocking React render work. We only log these once a subsequent render actually happened. If no render was actually scheduled, then we don't log these. E.g. if an isomorphic Action doesn't call startTransition there's no render so we don't log it. We only log the first event/update/transition even if multiple are batched into it later. If multiple Actions are entangled they're all treated as one until an update happens. If no update happens and all entangled actions finish, we clear the transition so that the next time a new sequence starts we can log it. We also clamp these (start the track later) if they were scheduled within a render/commit. Since we share a single track we don't want to create overlapping tracks. The purpose of this is not to show every event/action that happens but to show a prelude to how long we were blocked before a render started. So you can follow the first event to commit. Screenshot 2024-09-20 at 1 59 58 AM I still need to add the rendering/suspended phases to the timeline which why this screenshot has a gap. Screenshot 2024-09-20 at 12 50 27 AM In this case it's a Form Action which started a render into the form which then suspended on the action. The action then caused a refresh, which interrupts with its own update that's blocked before rendering. Suspended roots like this is interesting because we could in theory start working on a different root in the meantime which makes this timeline less linear. --- packages/react-art/src/ReactFiberConfigART.js | 8 ++ .../src/client/ReactFiberConfigDOM.js | 10 ++ .../src/ReactFiberConfigFabric.js | 8 ++ .../src/ReactFiberConfigNative.js | 8 ++ .../src/createReactNoop.js | 8 ++ .../src/ReactFiberAsyncAction.js | 52 +++++--- .../src/ReactFiberClassComponent.js | 4 + .../react-reconciler/src/ReactFiberHooks.js | 69 ++++++++-- .../react-reconciler/src/ReactFiberLane.js | 17 +++ .../src/ReactFiberPerformanceTrack.js | 65 ++++++++++ .../src/ReactFiberReconciler.js | 2 + .../src/ReactFiberTransition.js | 8 ++ .../src/ReactFiberWorkLoop.js | 70 +++++++++++ .../src/ReactProfilerTimer.js | 119 ++++++++++++++++++ .../ReactFiberHostContext-test.internal.js | 6 + .../src/forks/ReactFiberConfig.custom.js | 2 + .../src/ReactFiberConfigTestHost.js | 7 ++ .../__tests__/ReactProfiler-test.internal.js | 6 + 18 files changed, 443 insertions(+), 26 deletions(-) diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 816353e0ced2f..33bbe055724d9 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -363,6 +363,14 @@ export function resolveUpdatePriority(): EventPriority { return currentUpdatePriority || DefaultEventPriority; } +export function resolveEventType(): null | string { + return null; +} + +export function resolveEventTimeStamp(): number { + return -1.1; +} + export function shouldAttemptEagerTransition() { return false; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 0e39f988f813c..d46f61035b8d4 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -606,6 +606,16 @@ export function shouldAttemptEagerTransition(): boolean { return false; } +export function resolveEventType(): null | string { + const event = window.event; + return event ? event.type : null; +} + +export function resolveEventTimeStamp(): number { + const event = window.event; + return event ? event.timeStamp : -1.1; +} + export const isPrimaryRenderer = true; export const warnsIfNotActing = true; // This initialization code may run even on server environments diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 80215d11ec14c..10f7c6ecd84a3 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -372,6 +372,14 @@ export function resolveUpdatePriority(): EventPriority { return DefaultEventPriority; } +export function resolveEventType(): null | string { + return null; +} + +export function resolveEventTimeStamp(): number { + return -1.1; +} + export function shouldAttemptEagerTransition(): boolean { return false; } diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 0a95e0f818cdb..1dc8c627b1ccb 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -288,6 +288,14 @@ export function resolveUpdatePriority(): EventPriority { return DefaultEventPriority; } +export function resolveEventType(): null | string { + return null; +} + +export function resolveEventTimeStamp(): number { + return -1.1; +} + export function shouldAttemptEagerTransition(): boolean { return false; } diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 859bab499b6f3..55e0fd24c9b9b 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -533,6 +533,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return currentEventPriority; }, + resolveEventType(): null | string { + return null; + }, + + resolveEventTimeStamp(): number { + return -1.1; + }, + shouldAttemptEagerTransition(): boolean { return false; }, diff --git a/packages/react-reconciler/src/ReactFiberAsyncAction.js b/packages/react-reconciler/src/ReactFiberAsyncAction.js index ec53c7d80346c..4d210d02ac827 100644 --- a/packages/react-reconciler/src/ReactFiberAsyncAction.js +++ b/packages/react-reconciler/src/ReactFiberAsyncAction.js @@ -17,6 +17,14 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent'; import {requestTransitionLane} from './ReactFiberRootScheduler'; import {NoLane} from './ReactFiberLane'; +import { + hasScheduledTransitionWork, + clearAsyncTransitionTimer, +} from './ReactProfilerTimer'; +import { + enableComponentPerformanceTrack, + enableProfilerTimer, +} from 'shared/ReactFeatureFlags'; // If there are multiple, concurrent async actions, they are entangled. All // transition updates that occur while the async action is still in progress @@ -64,24 +72,34 @@ export function entangleAsyncAction( } function pingEngtangledActionScope() { - if ( - currentEntangledListeners !== null && - --currentEntangledPendingCount === 0 - ) { - // All the actions have finished. Close the entangled async action scope - // and notify all the listeners. - if (currentEntangledActionThenable !== null) { - const fulfilledThenable: FulfilledThenable = - (currentEntangledActionThenable: any); - fulfilledThenable.status = 'fulfilled'; + if (--currentEntangledPendingCount === 0) { + if (enableProfilerTimer && enableComponentPerformanceTrack) { + if (!hasScheduledTransitionWork()) { + // If we have received no updates since we started the entangled Actions + // that means it didn't lead to a Transition being rendered. We need to + // clear the timer so that if we start another entangled sequence we use + // the next start timer instead of appearing like we were blocked the + // whole time. We currently don't log a track for Actions that don't + // render a Transition. + clearAsyncTransitionTimer(); + } } - const listeners = currentEntangledListeners; - currentEntangledListeners = null; - currentEntangledLane = NoLane; - currentEntangledActionThenable = null; - for (let i = 0; i < listeners.length; i++) { - const listener = listeners[i]; - listener(); + if (currentEntangledListeners !== null) { + // All the actions have finished. Close the entangled async action scope + // and notify all the listeners. + if (currentEntangledActionThenable !== null) { + const fulfilledThenable: FulfilledThenable = + (currentEntangledActionThenable: any); + fulfilledThenable.status = 'fulfilled'; + } + const listeners = currentEntangledListeners; + currentEntangledListeners = null; + currentEntangledLane = NoLane; + currentEntangledActionThenable = null; + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + listener(); + } } } } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index f1419d1aad047..79c516eacf117 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -72,6 +72,7 @@ import { markStateUpdateScheduled, setIsStrictModeForDevtools, } from './ReactFiberDevToolsHook'; +import {startUpdateTimerByLane} from './ReactProfilerTimer'; const fakeInternalInstance = {}; @@ -194,6 +195,7 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, fiber, lane); entangleTransitions(root, fiber, lane); } @@ -228,6 +230,7 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, fiber, lane); entangleTransitions(root, fiber, lane); } @@ -262,6 +265,7 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, fiber, lane); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 59d2f0f025610..77d3e985011de 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -131,6 +131,7 @@ import { markStateUpdateScheduled, setIsStrictModeForDevtools, } from './ReactFiberDevToolsHook'; +import {startUpdateTimerByLane} from './ReactProfilerTimer'; import {createCache} from './ReactFiberCacheComponent'; import { createUpdate as createLegacyQueueUpdate, @@ -3019,7 +3020,12 @@ function startTransition( dispatchOptimisticSetState(fiber, false, queue, pendingState); } else { ReactSharedInternals.T = null; - dispatchSetState(fiber, queue, pendingState); + dispatchSetStateInternal( + fiber, + queue, + pendingState, + requestUpdateLane(fiber), + ); ReactSharedInternals.T = currentTransition; } @@ -3062,13 +3068,28 @@ function startTransition( thenable, finishedState, ); - dispatchSetState(fiber, queue, (thenableForFinishedState: any)); + dispatchSetStateInternal( + fiber, + queue, + (thenableForFinishedState: any), + requestUpdateLane(fiber), + ); } else { - dispatchSetState(fiber, queue, finishedState); + dispatchSetStateInternal( + fiber, + queue, + finishedState, + requestUpdateLane(fiber), + ); } } else { // Async actions are not enabled. - dispatchSetState(fiber, queue, finishedState); + dispatchSetStateInternal( + fiber, + queue, + finishedState, + requestUpdateLane(fiber), + ); callback(); } } catch (error) { @@ -3081,7 +3102,12 @@ function startTransition( status: 'rejected', reason: error, }; - dispatchSetState(fiber, queue, rejectedThenable); + dispatchSetStateInternal( + fiber, + queue, + rejectedThenable, + requestUpdateLane(fiber), + ); } else { // The error rethrowing behavior is only enabled when the async actions // feature is on, even for sync actions. @@ -3253,7 +3279,12 @@ export function requestFormReset(formFiber: Fiber) { const newResetState = {}; const resetStateHook: Hook = (stateHook.next: any); const resetStateQueue = resetStateHook.queue; - dispatchSetState(formFiber, resetStateQueue, newResetState); + dispatchSetStateInternal( + formFiber, + resetStateQueue, + newResetState, + requestUpdateLane(formFiber), + ); } function mountTransition(): [ @@ -3385,6 +3416,7 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T): void { const refreshUpdate = createLegacyQueueUpdate(lane); const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, provider, lane); entangleLegacyQueueTransitions(root, provider, lane); } @@ -3450,6 +3482,7 @@ function dispatchReducerAction( } else { const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, fiber, lane); entangleTransitionUpdate(root, queue, lane); } @@ -3474,7 +3507,24 @@ function dispatchSetState( } const lane = requestUpdateLane(fiber); + const didScheduleUpdate = dispatchSetStateInternal( + fiber, + queue, + action, + lane, + ); + if (didScheduleUpdate) { + startUpdateTimerByLane(lane); + } + markUpdateInDevTools(fiber, lane, action); +} +function dispatchSetStateInternal( + fiber: Fiber, + queue: UpdateQueue, + action: A, + lane: Lane, +): boolean { const update: Update = { lane, revertLane: NoLane, @@ -3518,7 +3568,7 @@ function dispatchSetState( // time the reducer has changed. // TODO: Do we still need to entangle transitions in this case? enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update); - return; + return false; } } catch (error) { // Suppress the error. It will throw again in the render phase. @@ -3534,10 +3584,10 @@ function dispatchSetState( if (root !== null) { scheduleUpdateOnFiber(root, fiber, lane); entangleTransitionUpdate(root, queue, lane); + return true; } } - - markUpdateInDevTools(fiber, lane, action); + return false; } function dispatchOptimisticSetState( @@ -3619,6 +3669,7 @@ function dispatchOptimisticSetState( // will never be attempted before the optimistic update. This currently // holds because the optimistic update is always synchronous. If we ever // change that, we'll need to account for this. + startUpdateTimerByLane(SyncLane); scheduleUpdateOnFiber(root, fiber, SyncLane); // Optimistic updates are always synchronous, so we don't need to call // entangleTransitionUpdate here. diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index f72174e208555..b8c051def4eef 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -592,6 +592,10 @@ export function includesSyncLane(lanes: Lanes): boolean { return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes; } +export function isSyncLane(lanes: Lanes): boolean { + return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes; +} + export function includesNonIdleWork(lanes: Lanes): boolean { return (lanes & NonIdleLanes) !== NoLanes; } @@ -608,6 +612,10 @@ export function includesOnlyTransitions(lanes: Lanes): boolean { return (lanes & TransitionLanes) === lanes; } +export function includesTransitionLane(lanes: Lanes): boolean { + return (lanes & TransitionLanes) !== NoLanes; +} + export function includesBlockingLane(lanes: Lanes): boolean { const SyncDefaultLanes = InputContinuousHydrationLane | @@ -623,6 +631,15 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean { return (lanes & root.expiredLanes) !== NoLanes; } +export function isBlockingLane(lane: Lane): boolean { + const SyncDefaultLanes = + InputContinuousHydrationLane | + InputContinuousLane | + DefaultHydrationLane | + DefaultLane; + return (lane & SyncDefaultLanes) !== NoLanes; +} + export function isTransitionLane(lane: Lane): boolean { return (lane & TransitionLanes) !== NoLanes; } diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index a053ea56ffc7f..94a7ec458fc57 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -99,3 +99,68 @@ export function logComponentEffect( performance.measure(name, reusableComponentOptions); } } + +export function logBlockingStart( + updateTime: number, + eventTime: number, + eventType: null | string, + renderStartTime: number, +): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.track = 'Blocking'; + if (eventTime > 0 && eventType !== null) { + // Log the time from the event timeStamp until we called setState. + reusableComponentDevToolDetails.color = 'secondary-dark'; + reusableComponentOptions.start = eventTime; + reusableComponentOptions.end = + updateTime > 0 ? updateTime : renderStartTime; + performance.measure(eventType, reusableComponentOptions); + } + if (updateTime > 0) { + // Log the time from when we called setState until we started rendering. + reusableComponentDevToolDetails.color = 'primary-light'; + reusableComponentOptions.start = updateTime; + reusableComponentOptions.end = renderStartTime; + performance.measure('Blocked', reusableComponentOptions); + } + } +} + +export function logTransitionStart( + startTime: number, + updateTime: number, + eventTime: number, + eventType: null | string, + renderStartTime: number, +): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.track = 'Transition'; + if (eventTime > 0 && eventType !== null) { + // Log the time from the event timeStamp until we started a transition. + reusableComponentDevToolDetails.color = 'secondary-dark'; + reusableComponentOptions.start = eventTime; + reusableComponentOptions.end = + startTime > 0 + ? startTime + : updateTime > 0 + ? updateTime + : renderStartTime; + performance.measure(eventType, reusableComponentOptions); + } + if (startTime > 0) { + // Log the time from when we started an async transition until we called setState or started rendering. + reusableComponentDevToolDetails.color = 'primary-dark'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = + updateTime > 0 ? updateTime : renderStartTime; + performance.measure('Action', reusableComponentOptions); + } + if (updateTime > 0) { + // Log the time from when we called setState until we started rendering. + reusableComponentDevToolDetails.color = 'primary-light'; + reusableComponentOptions.start = updateTime; + reusableComponentOptions.end = renderStartTime; + performance.measure('Blocked', reusableComponentOptions); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 9a200b4e4feb4..94f1397eb129e 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -61,6 +61,7 @@ import { onScheduleRoot, injectProfilingHooks, } from './ReactFiberDevToolsHook'; +import {startUpdateTimerByLane} from './ReactProfilerTimer'; import { requestUpdateLane, scheduleUpdateOnFiber, @@ -433,6 +434,7 @@ function updateContainerImpl( const root = enqueueUpdate(rootFiber, update, lane); if (root !== null) { + startUpdateTimerByLane(lane); scheduleUpdateOnFiber(root, rootFiber, lane); entangleTransitions(root, rootFiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberTransition.js b/packages/react-reconciler/src/ReactFiberTransition.js index c378beee30d3b..8ddd7083363a8 100644 --- a/packages/react-reconciler/src/ReactFiberTransition.js +++ b/packages/react-reconciler/src/ReactFiberTransition.js @@ -35,6 +35,7 @@ import { import ReactSharedInternals from 'shared/ReactSharedInternals'; import {entangleAsyncAction} from './ReactFiberAsyncAction'; +import {startAsyncTransitionTimer} from './ReactProfilerTimer'; export const NoTransition = null; @@ -69,6 +70,13 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler( returnValue !== null && typeof returnValue.then === 'function' ) { + // If we're going to wait on some async work before scheduling an update. + // We mark the time so we can later log how long we were blocked on the Action. + // Ideally, we'd include the sync part of the action too but since that starts + // in isomorphic code it currently leads to tricky layering. We'd have to pass + // in performance.now() to this callback but we sometimes use a polyfill. + startAsyncTransitionTimer(); + // This is an async action const thenable: Thenable = (returnValue: any); entangleAsyncAction(transition, thenable); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index dba089e81f3e0..b576309dc84ab 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -68,6 +68,10 @@ import { logRenderStarted, logRenderStopped, } from './DebugTracing'; +import { + logBlockingStart, + logTransitionStart, +} from './ReactFiberPerformanceTrack'; import { resetAfterCommit, @@ -145,6 +149,7 @@ import { includesOnlyRetries, includesOnlyTransitions, includesBlockingLane, + includesTransitionLane, includesExpiredLane, getNextLanes, getEntangledLanes, @@ -221,7 +226,20 @@ import { } from './ReactFiberConcurrentUpdates'; import { + blockingUpdateTime, + blockingEventTime, + blockingEventType, + transitionStartTime, + transitionUpdateTime, + transitionEventTime, + transitionEventType, + clearBlockingTimers, + clearTransitionTimers, + clampBlockingTimers, + clampTransitionTimers, markNestedUpdateScheduled, + renderStartTime, + recordRenderTime, recordCompleteTime, recordCommitTime, resetNestedUpdateFlag, @@ -1698,7 +1716,48 @@ function resetWorkInProgressStack() { workInProgress = null; } +function finalizeRender(lanes: Lanes, finalizationTime: number): void { + if (enableProfilerTimer && enableComponentPerformanceTrack) { + if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + clampBlockingTimers(finalizationTime); + } + if (includesTransitionLane(lanes)) { + clampTransitionTimers(finalizationTime); + } + } +} + function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Starting a new render. Log the end of any previous renders and the + // blocked time before the render started. + recordRenderTime(); + // If this was a restart, e.g. due to an interrupting update, then there's no space + // in the track to log the cause since we'll have rendered all the way up until the + // restart so we need to clamp that. + finalizeRender(workInProgressRootRenderLanes, renderStartTime); + + if (includesSyncLane(lanes) || includesBlockingLane(lanes)) { + logBlockingStart( + blockingUpdateTime, + blockingEventTime, + blockingEventType, + renderStartTime, + ); + clearBlockingTimers(); + } + if (includesTransitionLane(lanes)) { + logTransitionStart( + transitionStartTime, + transitionUpdateTime, + transitionEventTime, + transitionEventType, + renderStartTime, + ); + clearTransitionTimers(); + } + } + root.finishedWork = null; root.finishedLanes = NoLanes; @@ -2240,6 +2299,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } workInProgressTransitions = getTransitionsForLanes(root, lanes); + resetRenderTimer(); prepareFreshStack(root, lanes); } else { @@ -3358,6 +3418,12 @@ function commitRootImpl( nestedUpdateCount = 0; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + if (!rootDidHavePassiveEffects) { + finalizeRender(lanes, now()); + } + } + // If layout work was scheduled, flush it now. flushSyncWorkOnAllRoots(); @@ -3539,6 +3605,10 @@ function flushPassiveEffectsImpl() { executionContext = prevExecutionContext; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + finalizeRender(lanes, now()); + } + flushSyncWorkOnAllRoots(); if (enableTransitionTracing) { diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d65ca0a7544ee..cf3133524813e 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -9,10 +9,16 @@ import type {Fiber} from './ReactInternalTypes'; +import type {Lane} from './ReactFiberLane'; +import {isTransitionLane, isBlockingLane, isSyncLane} from './ReactFiberLane'; + +import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig'; + import { enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerTimer, + enableComponentPerformanceTrack, } from 'shared/ReactFeatureFlags'; // Intentionally not named imports because Rollup would use dynamic dispatch for @@ -21,6 +27,7 @@ import * as Scheduler from 'scheduler'; const {unstable_now: now} = Scheduler; +export let renderStartTime: number = -0; export let completeTime: number = -0; export let commitTime: number = -0; export let profilerStartTime: number = -1.1; @@ -29,6 +36,111 @@ export let componentEffectDuration: number = -0; export let componentEffectStartTime: number = -1.1; export let componentEffectEndTime: number = -1.1; +export let blockingUpdateTime: number = -1.1; // First sync setState scheduled. +export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState. +export let blockingEventType: null | string = null; // Event type of the first setState. +// TODO: This should really be one per Transition lane. +export let transitionStartTime: number = -1.1; // First startTransition call before setState. +export let transitionUpdateTime: number = -1.1; // First transition setState scheduled. +export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition. +export let transitionEventType: null | string = null; // Event type of the first transition. + +export function startUpdateTimerByLane(lane: Lane): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + if (isSyncLane(lane) || isBlockingLane(lane)) { + if (blockingUpdateTime < 0) { + blockingUpdateTime = now(); + blockingEventTime = resolveEventTimeStamp(); + blockingEventType = resolveEventType(); + } + } else if (isTransitionLane(lane)) { + if (transitionUpdateTime < 0) { + transitionUpdateTime = now(); + if (transitionStartTime < 0) { + transitionEventTime = resolveEventTimeStamp(); + transitionEventType = resolveEventType(); + } + } + } +} + +export function clearBlockingTimers(): void { + blockingUpdateTime = -1.1; +} + +export function startAsyncTransitionTimer(): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + if (transitionStartTime < 0 && transitionUpdateTime < 0) { + transitionStartTime = now(); + transitionEventTime = resolveEventTimeStamp(); + transitionEventType = resolveEventType(); + } +} + +export function hasScheduledTransitionWork(): boolean { + // If we have setState on a transition or scheduled useActionState update. + return transitionUpdateTime > -1; +} + +// We use this marker to indicate that we have scheduled a render to be performed +// but it's not an explicit state update. +const ACTION_STATE_MARKER = -0.5; + +export function startActionStateUpdate(): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + if (transitionUpdateTime < 0) { + transitionUpdateTime = ACTION_STATE_MARKER; + } +} + +export function clearAsyncTransitionTimer(): void { + transitionStartTime = -1.1; +} + +export function clearTransitionTimers(): void { + transitionStartTime = -1.1; + transitionUpdateTime = -1.1; +} + +export function clampBlockingTimers(finalTime: number): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + // If we had new updates come in while we were still rendering or committing, we don't want + // those update times to create overlapping tracks in the performance timeline so we clamp + // them to the end of the commit phase. + if (blockingUpdateTime >= 0 && blockingUpdateTime < finalTime) { + blockingUpdateTime = finalTime; + } + if (blockingEventTime >= 0 && blockingEventTime < finalTime) { + blockingEventTime = finalTime; + } +} + +export function clampTransitionTimers(finalTime: number): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + // If we had new updates come in while we were still rendering or committing, we don't want + // those update times to create overlapping tracks in the performance timeline so we clamp + // them to the end of the commit phase. + if (transitionStartTime >= 0 && transitionStartTime < finalTime) { + transitionStartTime = finalTime; + } + if (transitionUpdateTime >= 0 && transitionUpdateTime < finalTime) { + transitionUpdateTime = finalTime; + } + if (transitionEventTime >= 0 && transitionEventTime < finalTime) { + transitionEventTime = finalTime; + } +} + export function pushNestedEffectDurations(): number { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return 0; @@ -136,6 +248,13 @@ export function syncNestedUpdateFlag(): void { } } +export function recordRenderTime(): void { + if (!enableProfilerTimer || !enableComponentPerformanceTrack) { + return; + } + renderStartTime = now(); +} + export function recordCompleteTime(): void { if (!enableProfilerTimer) { return; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 753c2d849b19a..60726514474fb 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -83,6 +83,12 @@ describe('ReactFiberHostContext', () => { } return DefaultEventPriority; }, + resolveEventType: function () { + return null; + }, + resolveEventTimeStamp: function () { + return -1.1; + }, shouldAttemptEagerTransition() { return false; }, diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 0826f9d95ee0d..1f525d9a05c52 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -73,6 +73,8 @@ export const getInstanceFromScope = $$$config.getInstanceFromScope; export const setCurrentUpdatePriority = $$$config.setCurrentUpdatePriority; export const getCurrentUpdatePriority = $$$config.getCurrentUpdatePriority; export const resolveUpdatePriority = $$$config.resolveUpdatePriority; +export const resolveEventType = $$$config.resolveEventType; +export const resolveEventTimeStamp = $$$config.resolveEventTimeStamp; export const shouldAttemptEagerTransition = $$$config.shouldAttemptEagerTransition; export const detachDeletedInstance = $$$config.detachDeletedInstance; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 9d62e8a19ccf2..cb38a985eb800 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -224,6 +224,13 @@ export function resolveUpdatePriority(): EventPriority { } return DefaultEventPriority; } +export function resolveEventType(): null | string { + return null; +} + +export function resolveEventTimeStamp(): number { + return -1.1; +} export function shouldAttemptEagerTransition(): boolean { return false; } diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 0d7aead65f81d..14aacca63a7ca 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -178,6 +178,9 @@ describe(`onRender`, () => { 'read current time', 'read current time', 'read current time', + 'read current time', + 'read current time', + 'read current time', ]); } else { assertLog([ @@ -212,6 +215,9 @@ describe(`onRender`, () => { 'read current time', 'read current time', 'read current time', + 'read current time', + 'read current time', + 'read current time', ]); } else { assertLog([ From 5d19e1c8d1a6c0b5cd7532d43b707191eaf105b7 Mon Sep 17 00:00:00 2001 From: Edmond Chui <1967998+EdmondChuiHW@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:25:44 +0100 Subject: [PATCH 182/191] Fix: profiling crashes #30661 #28838 (#31024) ## Summary Profiling fails sometimes because `onProfilingStatus` is called repeatedly on some occasions, e.g. multiple calls to `getProfilingStatus`. Subsequent calls should be a no-op if the profiling status hasn't changed. Reported via #30661 #28838. > [!TIP] > Hide whitespace changes on this PR screenshot showing the UI controls for hiding
whitespace changes on GitHub ## How did you test this change? Tested as part of Fusebox implementation of reload-to-profile. https://github.com/facebook/react/pull/31021?#discussion_r1770589753 --- .../src/devtools/ProfilerStore.js | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/ProfilerStore.js b/packages/react-devtools-shared/src/devtools/ProfilerStore.js index 46b2edcab4d32..a55487ec19a37 100644 --- a/packages/react-devtools-shared/src/devtools/ProfilerStore.js +++ b/packages/react-devtools-shared/src/devtools/ProfilerStore.js @@ -289,6 +289,10 @@ export default class ProfilerStore extends EventEmitter<{ }; onProfilingStatus: (isProfiling: boolean) => void = isProfiling => { + if (this._isProfiling === isProfiling) { + return; + } + if (isProfiling) { this._dataBackends.splice(0); this._dataFrontend = null; @@ -315,36 +319,34 @@ export default class ProfilerStore extends EventEmitter<{ }); } - if (this._isProfiling !== isProfiling) { - this._isProfiling = isProfiling; + this._isProfiling = isProfiling; - // Invalidate suspense cache if profiling data is being (re-)recorded. - // Note that we clear again, in case any views read from the cache while profiling. - // (That would have resolved a now-stale value without any profiling data.) - this._cache.invalidate(); + // Invalidate suspense cache if profiling data is being (re-)recorded. + // Note that we clear again, in case any views read from the cache while profiling. + // (That would have resolved a now-stale value without any profiling data.) + this._cache.invalidate(); - this.emit('isProfiling'); + this.emit('isProfiling'); - // If we've just finished a profiling session, we need to fetch data stored in each renderer interface - // and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI. - // During this time, DevTools UI should probably not be interactive. - if (!isProfiling) { - this._dataBackends.splice(0); - this._rendererQueue.clear(); + // If we've just finished a profiling session, we need to fetch data stored in each renderer interface + // and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI. + // During this time, DevTools UI should probably not be interactive. + if (!isProfiling) { + this._dataBackends.splice(0); + this._rendererQueue.clear(); - // Only request data from renderers that actually logged it. - // This avoids unnecessary bridge requests and also avoids edge case mixed renderer bugs. - // (e.g. when v15 and v16 are both present) - this._rendererIDsThatReportedProfilingData.forEach(rendererID => { - if (!this._rendererQueue.has(rendererID)) { - this._rendererQueue.add(rendererID); + // Only request data from renderers that actually logged it. + // This avoids unnecessary bridge requests and also avoids edge case mixed renderer bugs. + // (e.g. when v15 and v16 are both present) + this._rendererIDsThatReportedProfilingData.forEach(rendererID => { + if (!this._rendererQueue.has(rendererID)) { + this._rendererQueue.add(rendererID); - this._bridge.send('getProfilingData', {rendererID}); - } - }); + this._bridge.send('getProfilingData', {rendererID}); + } + }); - this.emit('isProcessingData'); - } + this.emit('isProcessingData'); } }; } From 4e9540e3c2a8f9ae56318b967939c99b3a815190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 23 Sep 2024 14:09:48 -0400 Subject: [PATCH 183/191] [Fiber] Log the Render/Commit phases and the gaps in between (#31016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A slight behavior change here too is that I now mark the start of the commit phase before the BeforeMutationEffect phase. This affects `` too. The named sequences are as follows: Render -> Suspended or Throttled -> Commit -> Waiting for Paint -> Remaining Effects The Suspended phase is only logged if we delay the Commit due to CSS / images. The Throttled phase is only logged if we delay the commit due to the Suspense throttling timer. Screenshot 2024-09-20 at 9 14 23 PM I don't yet log render phases that don't complete. I think I also need to special case renders that or don't commit after being suspended. --- .../src/ReactFiberCommitEffects.js | 22 ++-- .../src/ReactFiberCommitWork.js | 12 +- .../src/ReactFiberPerformanceTrack.js | 65 ++++++++++ .../src/ReactFiberWorkLoop.js | 115 ++++++++++++++---- .../src/ReactProfilerTimer.js | 12 +- .../__tests__/ReactProfiler-test.internal.js | 2 + 6 files changed, 184 insertions(+), 44 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index 8b1d4ceb6f9bb..4d10c9ee6faff 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -899,7 +899,7 @@ function safelyCallDestroy( function commitProfiler( finishedWork: Fiber, current: Fiber | null, - commitTime: number, + commitStartTime: number, effectDuration: number, ) { const {id, onCommit, onRender} = finishedWork.memoizedProps; @@ -918,7 +918,7 @@ function commitProfiler( finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, - commitTime, + commitStartTime, ); } @@ -928,7 +928,7 @@ function commitProfiler( finishedWork.memoizedProps.id, phase, effectDuration, - commitTime, + commitStartTime, ); } } @@ -937,7 +937,7 @@ function commitProfiler( export function commitProfilerUpdate( finishedWork: Fiber, current: Fiber | null, - commitTime: number, + commitStartTime: number, effectDuration: number, ) { if (enableProfilerTimer) { @@ -948,11 +948,11 @@ export function commitProfilerUpdate( commitProfiler, finishedWork, current, - commitTime, + commitStartTime, effectDuration, ); } else { - commitProfiler(finishedWork, current, commitTime, effectDuration); + commitProfiler(finishedWork, current, commitStartTime, effectDuration); } } catch (error) { captureCommitPhaseError(finishedWork, finishedWork.return, error); @@ -963,7 +963,7 @@ export function commitProfilerUpdate( function commitProfilerPostCommitImpl( finishedWork: Fiber, current: Fiber | null, - commitTime: number, + commitStartTime: number, passiveEffectDuration: number, ): void { const {id, onPostCommit} = finishedWork.memoizedProps; @@ -976,14 +976,14 @@ function commitProfilerPostCommitImpl( } if (typeof onPostCommit === 'function') { - onPostCommit(id, phase, passiveEffectDuration, commitTime); + onPostCommit(id, phase, passiveEffectDuration, commitStartTime); } } export function commitProfilerPostCommit( finishedWork: Fiber, current: Fiber | null, - commitTime: number, + commitStartTime: number, passiveEffectDuration: number, ) { try { @@ -993,14 +993,14 @@ export function commitProfilerPostCommit( commitProfilerPostCommitImpl, finishedWork, current, - commitTime, + commitStartTime, passiveEffectDuration, ); } else { commitProfilerPostCommitImpl( finishedWork, current, - commitTime, + commitStartTime, passiveEffectDuration, ); } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 5965dbe8db0a8..89aa35d760b26 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -99,8 +99,7 @@ import { Cloned, } from './ReactFiberFlags'; import { - commitTime, - completeTime, + commitStartTime, pushNestedEffectDurations, popNestedEffectDurations, bubbleNestedEffectDurations, @@ -505,7 +504,7 @@ function commitLayoutEffectOnFiber( commitProfilerUpdate( finishedWork, current, - commitTime, + commitStartTime, profilerInstance.effectDuration, ); } else { @@ -2345,7 +2344,7 @@ export function reappearLayoutEffects( commitProfilerUpdate( finishedWork, current, - commitTime, + commitStartTime, profilerInstance.effectDuration, ); } else { @@ -2568,6 +2567,7 @@ export function commitPassiveMountEffects( finishedWork: Fiber, committedLanes: Lanes, committedTransitions: Array | null, + renderEndTime: number, // Profiling-only ): void { resetComponentEffectTimers(); @@ -2576,7 +2576,7 @@ export function commitPassiveMountEffects( finishedWork, committedLanes, committedTransitions, - enableProfilerTimer && enableComponentPerformanceTrack ? completeTime : 0, + enableProfilerTimer && enableComponentPerformanceTrack ? renderEndTime : 0, ); } @@ -2763,7 +2763,7 @@ function commitPassiveMountOnFiber( finishedWork.alternate, // This value will still reflect the previous commit phase. // It does not get reset until the start of the next commit phase. - commitTime, + commitStartTime, profilerInstance.passiveEffectDuration, ); } else { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 94a7ec458fc57..3d52dcf4e1073 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -164,3 +164,68 @@ export function logTransitionStart( } } } + +export function logRenderPhase(startTime: number, endTime: number): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'primary-dark'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Render', reusableComponentOptions); + } +} + +export function logSuspenseThrottlePhase( + startTime: number, + endTime: number, +): void { + // This was inside a throttled Suspense boundary commit. + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'secondary-light'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Throttled', reusableComponentOptions); + } +} + +export function logSuspendedCommitPhase( + startTime: number, + endTime: number, +): void { + // This means the commit was suspended on CSS or images. + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'secondary-light'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Suspended', reusableComponentOptions); + } +} + +export function logCommitPhase(startTime: number, endTime: number): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'secondary-dark'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Commit', reusableComponentOptions); + } +} + +export function logPaintYieldPhase(startTime: number, endTime: number): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'secondary-light'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Waiting for Paint', reusableComponentOptions); + } +} + +export function logPassiveCommitPhase( + startTime: number, + endTime: number, +): void { + if (supportsUserTiming) { + reusableComponentDevToolDetails.color = 'secondary-dark'; + reusableComponentOptions.start = startTime; + reusableComponentOptions.end = endTime; + performance.measure('Remaining Effects', reusableComponentOptions); + } +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index b576309dc84ab..c13b9c65a65a4 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -71,6 +71,12 @@ import { import { logBlockingStart, logTransitionStart, + logRenderPhase, + logSuspenseThrottlePhase, + logSuspendedCommitPhase, + logCommitPhase, + logPaintYieldPhase, + logPassiveCommitPhase, } from './ReactFiberPerformanceTrack'; import { @@ -239,9 +245,11 @@ import { clampTransitionTimers, markNestedUpdateScheduled, renderStartTime, + commitStartTime, + commitEndTime, recordRenderTime, - recordCompleteTime, recordCommitTime, + recordCommitEndTime, resetNestedUpdateFlag, startProfilerTimer, stopProfilerTimerIfRunningAndRecordDuration, @@ -601,6 +609,7 @@ let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsLanes: Lanes = NoLanes; let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes; +let pendingPassiveEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; // Use these to prevent an infinite loop of nested updates @@ -1119,10 +1128,11 @@ function finishConcurrentRender( finishedWork: Fiber, lanes: Lanes, ) { + let renderEndTime = 0; if (enableProfilerTimer && enableComponentPerformanceTrack) { // Track when we finished the last unit of work, before we actually commit it. // The commit can be suspended/blocked until we commit it. - recordCompleteTime(); + renderEndTime = now(); } // TODO: The fact that most of these branches are identical suggests that some @@ -1182,6 +1192,9 @@ function finishConcurrentRender( workInProgressDeferredLane, workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, + IMMEDIATE_COMMIT, + renderStartTime, + renderEndTime, ); } else { if ( @@ -1227,6 +1240,9 @@ function finishConcurrentRender( workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, workInProgressRootDidSkipSuspendedSiblings, + THROTTLED_COMMIT, + renderStartTime, + renderEndTime, ), msUntilTimeout, ); @@ -1244,6 +1260,9 @@ function finishConcurrentRender( workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, workInProgressRootDidSkipSuspendedSiblings, + IMMEDIATE_COMMIT, + renderStartTime, + renderEndTime, ); } } @@ -1259,6 +1278,9 @@ function commitRootWhenReady( updatedLanes: Lanes, suspendedRetryLanes: Lanes, didSkipSuspendedSiblings: boolean, + suspendedCommitReason: SuspendedCommitReason, // Profiling-only + completedRenderStartTime: number, // Profiling-only + completedRenderEndTime: number, // Profiling-only ) { // TODO: Combine retry throttling with Suspensey commits. Right now they run // one after the other. @@ -1299,6 +1321,7 @@ function commitRootWhenReady( spawnedLane, updatedLanes, suspendedRetryLanes, + SUSPENDED_COMMIT, ), ); markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings); @@ -1315,6 +1338,9 @@ function commitRootWhenReady( spawnedLane, updatedLanes, suspendedRetryLanes, + suspendedCommitReason, + completedRenderStartTime, + completedRenderEndTime, ); } @@ -1506,8 +1532,9 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { return null; } + let renderEndTime = 0; if (enableProfilerTimer && enableComponentPerformanceTrack) { - recordCompleteTime(); + renderEndTime = now(); } // We now have a consistent tree. Because this is a sync render, we @@ -1523,6 +1550,9 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { workInProgressDeferredLane, workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, + IMMEDIATE_COMMIT, + renderStartTime, + renderEndTime, ); // Before exiting, make sure there's a callback scheduled for the next @@ -3016,6 +3046,11 @@ function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void { workInProgress = null; } +type SuspendedCommitReason = 0 | 1 | 2; +const IMMEDIATE_COMMIT = 0; +const SUSPENDED_COMMIT = 1; +const THROTTLED_COMMIT = 2; + function commitRoot( root: FiberRoot, recoverableErrors: null | Array>, @@ -3024,6 +3059,9 @@ function commitRoot( spawnedLane: Lane, updatedLanes: Lanes, suspendedRetryLanes: Lanes, + suspendedCommitReason: SuspendedCommitReason, // Profiling-only + completedRenderStartTime: number, // Profiling-only + completedRenderEndTime: number, // Profiling-only ) { // TODO: This no longer makes any sense. We already wrap the mutation and // layout phases. Should be able to remove. @@ -3041,6 +3079,9 @@ function commitRoot( spawnedLane, updatedLanes, suspendedRetryLanes, + suspendedCommitReason, + completedRenderStartTime, + completedRenderEndTime, ); } finally { ReactSharedInternals.T = prevTransition; @@ -3059,6 +3100,9 @@ function commitRootImpl( spawnedLane: Lane, updatedLanes: Lanes, suspendedRetryLanes: Lanes, + suspendedCommitReason: SuspendedCommitReason, // Profiling-only + completedRenderStartTime: number, // Profiling-only + completedRenderEndTime: number, // Profiling-only ) { do { // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which @@ -3078,6 +3122,12 @@ function commitRootImpl( const finishedWork = root.finishedWork; const lanes = root.finishedLanes; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Log the previous render phase once we commit. I.e. we weren't interrupted. + setCurrentTrackFromLanes(lanes); + logRenderPhase(completedRenderStartTime, completedRenderEndTime); + } + if (__DEV__) { if (enableDebugTracing) { logCommitStarted(lanes); @@ -3174,6 +3224,7 @@ function commitRootImpl( if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; pendingPassiveEffectsRemainingLanes = remainingLanes; + pendingPassiveEffectsRenderEndTime = completedRenderEndTime; // workInProgressTransitions might be overwritten, so we want // to store it in pendingPassiveTransitions until they get processed // We need to pass this through as an argument to commitRoot @@ -3182,7 +3233,7 @@ function commitRootImpl( // with setTimeout pendingPassiveTransitions = transitions; scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); + flushPassiveEffects(true); // This render triggered passive effects: release the root cache pool // *after* passive effects fire to avoid freeing a cache pool that may // be referenced by a node in the tree (HostRoot, Cache boundary etc) @@ -3191,6 +3242,19 @@ function commitRootImpl( } } + if (enableProfilerTimer) { + // Mark the current commit time to be shared by all Profilers in this + // batch. This enables them to be grouped later. + recordCommitTime(); + if (enableComponentPerformanceTrack) { + if (suspendedCommitReason === SUSPENDED_COMMIT) { + logSuspendedCommitPhase(completedRenderEndTime, commitStartTime); + } else if (suspendedCommitReason === THROTTLED_COMMIT) { + logSuspenseThrottlePhase(completedRenderEndTime, commitStartTime); + } + } + } + // Check if there are any effects in the whole tree. // TODO: This is left over from the effect list implementation, where we had // to check for the existence of `firstEffect` to satisfy Flow. I think the @@ -3226,12 +3290,6 @@ function commitRootImpl( finishedWork, ); - if (enableProfilerTimer) { - // Mark the current commit time to be shared by all Profilers in this - // batch. This enables them to be grouped later. - recordCommitTime(); - } - // The next phase is the mutation phase, where we mutate the host tree. commitMutationEffects(root, finishedWork, lanes); @@ -3282,12 +3340,11 @@ function commitRootImpl( } else { // No effects. root.current = finishedWork; - // Measure these anyway so the flamegraph explicitly shows that there were - // no effects. - // TODO: Maybe there's a better way to report this. - if (enableProfilerTimer) { - recordCommitTime(); - } + } + + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordCommitEndTime(); + logCommitPhase(commitStartTime, commitEndTime); } const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; @@ -3504,7 +3561,7 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) { } } -export function flushPassiveEffects(): boolean { +export function flushPassiveEffects(wasDelayedCommit?: boolean): boolean { // Returns whether passive effects were flushed. // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should // probably just combine the two functions. I believe they were only separate @@ -3529,7 +3586,7 @@ export function flushPassiveEffects(): boolean { try { setCurrentUpdatePriority(priority); ReactSharedInternals.T = null; - return flushPassiveEffectsImpl(); + return flushPassiveEffectsImpl(wasDelayedCommit); } finally { setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; @@ -3543,7 +3600,7 @@ export function flushPassiveEffects(): boolean { return false; } -function flushPassiveEffectsImpl() { +function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { if (rootWithPendingPassiveEffects === null) { return false; } @@ -3579,6 +3636,12 @@ function flushPassiveEffectsImpl() { } } + let passiveEffectStartTime = 0; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + passiveEffectStartTime = now(); + logPaintYieldPhase(commitEndTime, passiveEffectStartTime); + } + if (enableSchedulingProfiler) { markPassiveEffectsStarted(lanes); } @@ -3587,7 +3650,13 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; commitPassiveUnmountEffects(root.current); - commitPassiveMountEffects(root, root.current, lanes, transitions); + commitPassiveMountEffects( + root, + root.current, + lanes, + transitions, + pendingPassiveEffectsRenderEndTime, + ); if (__DEV__) { if (enableDebugTracing) { @@ -3606,7 +3675,11 @@ function flushPassiveEffectsImpl() { executionContext = prevExecutionContext; if (enableProfilerTimer && enableComponentPerformanceTrack) { - finalizeRender(lanes, now()); + const passiveEffectsEndTime = now(); + if (wasDelayedCommit) { + logPassiveCommitPhase(passiveEffectStartTime, passiveEffectsEndTime); + } + finalizeRender(lanes, passiveEffectsEndTime); } flushSyncWorkOnAllRoots(); diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index cf3133524813e..0df3427fe86b2 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -28,8 +28,8 @@ import * as Scheduler from 'scheduler'; const {unstable_now: now} = Scheduler; export let renderStartTime: number = -0; -export let completeTime: number = -0; -export let commitTime: number = -0; +export let commitStartTime: number = -0; +export let commitEndTime: number = -0; export let profilerStartTime: number = -1.1; export let profilerEffectDuration: number = -0; export let componentEffectDuration: number = -0; @@ -255,18 +255,18 @@ export function recordRenderTime(): void { renderStartTime = now(); } -export function recordCompleteTime(): void { +export function recordCommitTime(): void { if (!enableProfilerTimer) { return; } - completeTime = now(); + commitStartTime = now(); } -export function recordCommitTime(): void { +export function recordCommitEndTime(): void { if (!enableProfilerTimer) { return; } - commitTime = now(); + commitEndTime = now(); } export function startProfilerTimer(fiber: Fiber): void { diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 14aacca63a7ca..522c3a11cf29d 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -181,6 +181,7 @@ describe(`onRender`, () => { 'read current time', 'read current time', 'read current time', + 'read current time', ]); } else { assertLog([ @@ -218,6 +219,7 @@ describe(`onRender`, () => { 'read current time', 'read current time', 'read current time', + 'read current time', ]); } else { assertLog([ From 79bcf6eb23cd781bedbfccfe8d1507d18fd2c623 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Mon, 23 Sep 2024 14:58:05 -0400 Subject: [PATCH 184/191] Fix missing trailing / in commit artifacts workflow The trailing / was being omitted, so instead of moving the cjs directory itself, it would move only its contents instead. This broke some internal path assumptions. Additionally, updates the step to create the react-dom directory prior to moving. ghstack-source-id: b6eedb0c88cd3aa3a786a3d3d280ede5ee81a063 Pull Request resolved: https://github.com/facebook/react/pull/31026 --- .github/workflows/runtime_commit_artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 2baabaf59f5c5..30906cc3ea930 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -118,14 +118,14 @@ jobs: run: | BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js' mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/ - mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-is,react-test-renderer}/ + mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-dom,react-is,react-test-renderer}/ # Move React Native renderer mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/ mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/ - mv build/facebook-react-native/react-dom/cjs $BASE_FOLDER/RKJSModules/vendor/react/react-dom/ + mv build/facebook-react-native/react-dom/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/ mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/ mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/ From 5b19dc0f06e92d3ed0aa93be3c5bbe2298da5df6 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Mon, 23 Sep 2024 15:31:10 -0400 Subject: [PATCH 185/191] Allow forcing a build in artifacts workflow dispatch Sometimes it is useful to bypass the revision check when we need to make changes to the runtime_commit_artifacts script. The `force` input can be passed via the GitHub UI for manual runs of the workflow. ghstack-source-id: cf9e32c01a565d86980277115f41e3e116adf376 Pull Request resolved: https://github.com/facebook/react/pull/31027 --- .../workflows/runtime_commit_artifacts.yml | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 30906cc3ea930..ac312531ac6fa 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -11,6 +11,11 @@ on: commit_sha: required: false type: string + force: + description: 'Force a commit to the builds/... branches' + required: true + default: false + type: boolean env: TZ: /usr/share/zoneinfo/America/Los_Angeles @@ -196,6 +201,7 @@ jobs: grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g" grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted" - name: Check for changes + if: !inputs.force id: check_should_commit run: | echo "Full git status" @@ -213,7 +219,7 @@ jobs: echo "should_commit=false" >> "$GITHUB_OUTPUT" fi - name: Re-apply version changes - if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '' + if: inputs.force || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '') env: CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }} CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }} @@ -230,12 +236,12 @@ jobs: grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g" grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied" - name: Will commit these changes - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' run: | echo ":" git status -u - name: Commit changes to branch - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: | @@ -272,6 +278,7 @@ jobs: grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g" grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted" - name: Check for changes + if: !inputs.force id: check_should_commit run: | echo "Full git status" @@ -290,7 +297,7 @@ jobs: echo "should_commit=false" >> "$GITHUB_OUTPUT" fi - name: Re-apply version changes - if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '' + if: inputs.force || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '') env: CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }} LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }} @@ -300,12 +307,12 @@ jobs: grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g" grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied" - name: Add files for signing - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' run: | echo ":" git add . - name: Signing files - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' uses: actions/github-script@v7 with: script: | @@ -388,12 +395,12 @@ jobs: console.error('Error signing files:', e); } - name: Will commit these changes - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' run: | git add . git status - name: Commit changes to branch - if: steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: | From 4708fb92c24bbc769acbc075de6105590fd29edc Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Mon, 23 Sep 2024 17:36:53 -0400 Subject: [PATCH 186/191] Fix runtime_commit_artifacts workflow I messed up the yml syntax and also realized that our script doesn't currently handle renames or deletes, so I fixed that ghstack-source-id: 7d481a951abaabd1a2985c8959d8acb7103ed12e Pull Request resolved: https://github.com/facebook/react/pull/31028 --- .../workflows/runtime_commit_artifacts.yml | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index ac312531ac6fa..9710fa4eed9ff 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -171,7 +171,7 @@ jobs: commit_www_artifacts: needs: download_artifacts - if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }} + if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -201,7 +201,7 @@ jobs: grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g" grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted" - name: Check for changes - if: !inputs.force + if: inputs.force != true id: check_should_commit run: | echo "Full git status" @@ -219,7 +219,7 @@ jobs: echo "should_commit=false" >> "$GITHUB_OUTPUT" fi - name: Re-apply version changes - if: inputs.force || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '') + if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '') env: CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }} CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }} @@ -236,12 +236,12 @@ jobs: grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g" grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied" - name: Will commit these changes - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' run: | echo ":" git status -u - name: Commit changes to branch - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: | @@ -255,7 +255,7 @@ jobs: commit_fbsource_artifacts: needs: download_artifacts - if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }} + if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -278,7 +278,7 @@ jobs: grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g" grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted" - name: Check for changes - if: !inputs.force + if: inputs.force != 'true' id: check_should_commit run: | echo "Full git status" @@ -297,7 +297,7 @@ jobs: echo "should_commit=false" >> "$GITHUB_OUTPUT" fi - name: Re-apply version changes - if: inputs.force || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '') + if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '') env: CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }} LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }} @@ -307,12 +307,12 @@ jobs: grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g" grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied" - name: Add files for signing - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' run: | echo ":" git add . - name: Signing files - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' uses: actions/github-script@v7 with: script: | @@ -366,8 +366,9 @@ jobs: console.log('Signing files in directory:', directory); try { const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'}); + console.log(result); - // Parse the git status output to get file paths + // Parse the git status output to get file paths! const files = result.split('\n').filter(file => file.endsWith('.js')); if (files.length === 0) { @@ -376,7 +377,14 @@ jobs: ); } else { files.forEach(line => { - const file = line.slice(3).trim(); + let file = null; + if (line.startsWith('D ')) { + return; + } else if (line.startsWith('R ')) { + file = line.slice(line.indexOf('->') + 3); + } else { + file = line.slice(3).trim(); + } if (file) { console.log(' Signing file:', file); const originalContents = fs.readFileSync(file, 'utf8'); @@ -395,12 +403,12 @@ jobs: console.error('Error signing files:', e); } - name: Will commit these changes - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' run: | git add . git status - name: Commit changes to branch - if: inputs.force || steps.check_should_commit.outputs.should_commit == 'true' + if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true' uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: | From 04bd67a4906d387ecdb8cbc798144dec2db811a5 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Tue, 24 Sep 2024 08:34:53 +0200 Subject: [PATCH 187/191] Resolve references to deduped owner objects (#30549) This is a follow-up from #30528 to not only handle props (the critical change), but also the owner ~and stack~ of a referenced element. ~Handling stacks here is rather academic because the Flight Server currently does not deduplicate owner stacks. And if they are really identical, we should probably just dedupe the whole element.~ EDIT: Removed from the PR. Handling owner objects on the other hand is an actual requirement as reported in https://github.com/vercel/next.js/issues/69545. This problem only affects the stable release channel, as the absence of owner stacks allows for the specific kind of shared owner deduping as demonstrated in the unit test. --- .../react-client/src/ReactFlightClient.js | 27 +++-- .../__tests__/ReactFlightDOMBrowser-test.js | 113 ++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 3 + 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 597536c961333..83c509dab46a8 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -937,26 +937,35 @@ function waitForReference( } value = value[path[i]]; } - parentObject[key] = map(response, value); + const mappedValue = map(response, value); + parentObject[key] = mappedValue; // If this is the root object for a model reference, where `handler.value` // is a stale `null`, the resolved value can be used directly. if (key === '' && handler.value === null) { - handler.value = parentObject[key]; + handler.value = mappedValue; } - // If the parent object is an unparsed React element tuple and its outlined - // props have now been resolved, we also need to update the props of the - // parsed element object (i.e. handler.value). + // If the parent object is an unparsed React element tuple, we also need to + // update the props and owner of the parsed element object (i.e. + // handler.value). if ( parentObject[0] === REACT_ELEMENT_TYPE && - key === '3' && typeof handler.value === 'object' && handler.value !== null && - handler.value.$$typeof === REACT_ELEMENT_TYPE && - handler.value.props === null + handler.value.$$typeof === REACT_ELEMENT_TYPE ) { - handler.value.props = parentObject[key]; + const element: any = handler.value; + switch (key) { + case '3': + element.props = mappedValue; + break; + case '4': + if (__DEV__) { + element._owner = mappedValue; + } + break; + } } handler.deps--; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index fa1e65862564e..0408a63fc512f 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -630,6 +630,119 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('
'); }); + it('should handle references to deduped owner objects', async () => { + // This is replicating React components as generated by @svgr/webpack: + let path1a: React.ReactNode; + let path1b: React.ReactNode; + let path2: React.ReactNode; + + function Svg1() { + return ReactServer.createElement( + 'svg', + {id: '1'}, + path1a || (path1a = ReactServer.createElement('path', {})), + path1b || (path1b = ReactServer.createElement('path', {})), + ); + } + + function Svg2() { + return ReactServer.createElement( + 'svg', + {id: '2'}, + path2 || (path2 = ReactServer.createElement('path', {})), + ); + } + + function Server() { + return ReactServer.createElement( + ReactServer.Fragment, + {}, + ReactServer.createElement(Svg1), + ReactServer.createElement(Svg2), + ); + } + + let stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + function ClientRoot({response}) { + return use(response); + } + + let response = ReactServerDOMClient.createFromReadableStream(stream); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + const expectedHtml = + ''; + + expect(container.innerHTML).toBe(expectedHtml); + + // Render a second time: + + // Assigning the path elements to variables in module scope (here simulated + // with the test's function scope), and rendering a second time, prevents + // the owner of the path elements (i.e. Svg1/Svg2) to be deduped. The owner + // of the path in Svg1 is fully inlined. The owner of the owner of the path + // in Svg2 is Server, which is deduped and replaced with a reference to the + // owner of the owner of the path in Svg1. This nested owner is actually + // Server from the previous render pass, which is kinda broken and libraries + // probably shouldn't generate code like this. This reference can only be + // resolved properly if owners are specifically handled when resolving + // outlined models. + + stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + response = ReactServerDOMClient.createFromReadableStream(stream); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe(expectedHtml); + + if (__DEV__) { + const resolvedPath1b = await response.value[0].props.children[1]._payload; + + expect(resolvedPath1b._owner).toEqual( + expect.objectContaining({ + name: 'Svg1', + env: 'Server', + key: null, + owner: expect.objectContaining({ + name: 'Server', + env: 'Server', + key: null, + owner: null, + }), + }), + ); + + const resolvedPath2 = response.value[1].props.children; + + expect(resolvedPath2._owner).toEqual( + expect.objectContaining({ + name: 'Svg2', + env: 'Server', + key: null, + owner: expect.objectContaining({ + name: 'Server', + env: 'Server', + key: null, + owner: null, + }), + }), + ); + } + }); + it('should progressively reveal server components', async () => { let reportedErrors = []; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f0e632c5499b0..5d79482de0186 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2665,6 +2665,9 @@ function renderModelDestructive( case '3': propertyName = 'props'; break; + case '4': + propertyName = '_owner'; + break; } } writtenObjects.set(value, parentReference + ':' + propertyName); From fc4a33eaa9c935ac860ab6043b95d55540068571 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 24 Sep 2024 17:49:19 +0100 Subject: [PATCH 188/191] fix: consider alternate as a key for componentLogsEntry when inspecting raw fiber instance (#31009) Related - https://github.com/facebook/react/pull/30899. Looks like this was missed. We actually do this when we record errors and warnings before sending them via Bridge: https://github.com/facebook/react/blob/e4953922a99b5477c3bcf98cdaa2b13ac0a81f0d/packages/react-devtools-shared/src/backend/fiber/renderer.js#L2169-L2173 So, what is happening in the end, errors or warnings are displayed in the Tree, but when user clicks on the component, nothing is shown, because `fiberToComponentLogsMap` has only `alternate` as a key. --- .../react-devtools-shared/src/backend/fiber/renderer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index b8636c557ea25..d93e713911561 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1029,6 +1029,10 @@ export function attach( if (devtoolsInstance.kind === FIBER_INSTANCE) { const fiber = devtoolsInstance.data; componentLogsEntry = fiberToComponentLogsMap.get(fiber); + + if (componentLogsEntry === undefined && fiber.alternate !== null) { + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); + } } else { const componentInfo = devtoolsInstance.data; componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); @@ -4248,7 +4252,10 @@ export function attach( source = getSourceForFiberInstance(fiberInstance); } - const componentLogsEntry = fiberToComponentLogsMap.get(fiber); + let componentLogsEntry = fiberToComponentLogsMap.get(fiber); + if (componentLogsEntry === undefined && fiber.alternate !== null) { + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); + } return { id: fiberInstance.id, From a15bbe14751287cb7ac124ff88f694d0883f3ac6 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 24 Sep 2024 19:51:21 +0100 Subject: [PATCH 189/191] refactor: data source for errors and warnings tracking is now in Store (#31010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on https://github.com/facebook/react/pull/31009. 1. Instead of keeping `showInlineWarningsAndErrors` in `Settings` context (which was removed in https://github.com/facebook/react/pull/30610), `Store` will now have a boolean flag, which controls if the UI should be displaying information about errors and warnings. 2. The errors and warnings counters in the Tree view are now counting only unique errors. This makes more sense, because it is part of the Elements Tree view, so ideally it should be showing number of components with errors and number of components of warnings. Consider this example: 2.1. Warning for element `A` was emitted once and warning for element `B` was emitted twice. 2.2. With previous implementation, we would show `3 ⚠️`, because in total there were 3 warnings in total. If user tries to iterate through these, it will only take 2 steps to do the full cycle, because there are only 2 elements with warnings (with one having same warning, which was emitted twice). 2.3 With current implementation, we would show `2 ⚠️`. Inspecting the element with doubled warning will still show the warning counter (2) before the warning message. With these changes, the feature correctly works. https://fburl.com/a7fw92m4 --- .../src/__tests__/store-test.js | 4 +- .../__tests__/storeComponentFilters-test.js | 16 ++--- .../src/devtools/store.js | 68 ++++++++++++++---- .../src/devtools/views/Components/Element.js | 6 +- .../InspectedElementErrorsAndWarningsTree.js | 8 +-- .../src/devtools/views/Components/Tree.js | 72 +++++++++---------- .../views/Settings/DebuggingSettings.js | 4 ++ .../views/Settings/SettingsContext.js | 14 +--- 8 files changed, 109 insertions(+), 83 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 92e7fc6586111..93c7048b2bc5b 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -2148,8 +2148,8 @@ describe('Store', () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); }); // Regression test for https://github.com/facebook/react/issues/23202 diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 432fb1771b6ff..26cd51383297f 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -420,8 +420,8 @@ describe('Store component filters', () => { }); expect(store).toMatchInlineSnapshot(``); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -460,8 +460,8 @@ describe('Store component filters', () => { ]), ); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -510,8 +510,8 @@ describe('Store component filters', () => { }); expect(store).toMatchInlineSnapshot(``); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -550,8 +550,8 @@ describe('Store component filters', () => { ]), ); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index b54907338b372..b1544126d8d6e 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -114,8 +114,8 @@ export default class Store extends EventEmitter<{ _bridge: FrontendBridge; // Computed whenever _errorsAndWarnings Map changes. - _cachedErrorCount: number = 0; - _cachedWarningCount: number = 0; + _cachedComponentWithErrorCount: number = 0; + _cachedComponentWithWarningCount: number = 0; _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null; // Should new nodes be collapsed by default when added to the tree? @@ -196,6 +196,7 @@ export default class Store extends EventEmitter<{ _shouldCheckBridgeProtocolCompatibility: boolean = false; _hookSettings: $ReadOnly | null = null; + _shouldShowWarningsAndErrors: boolean = false; constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -383,8 +384,24 @@ export default class Store extends EventEmitter<{ return this._bridgeProtocol; } - get errorCount(): number { - return this._cachedErrorCount; + get componentWithErrorCount(): number { + if (!this._shouldShowWarningsAndErrors) { + return 0; + } + + return this._cachedComponentWithErrorCount; + } + + get componentWithWarningCount(): number { + if (!this._shouldShowWarningsAndErrors) { + return 0; + } + + return this._cachedComponentWithWarningCount; + } + + get displayingErrorsAndWarningsEnabled(): boolean { + return this._shouldShowWarningsAndErrors; } get hasOwnerMetadata(): boolean { @@ -480,10 +497,6 @@ export default class Store extends EventEmitter<{ return this._unsupportedRendererVersionDetected; } - get warningCount(): number { - return this._cachedWarningCount; - } - containsElement(id: number): boolean { return this._idToElement.has(id); } @@ -581,7 +594,11 @@ export default class Store extends EventEmitter<{ } // Returns a tuple of [id, index] - getElementsWithErrorsAndWarnings(): Array<{id: number, index: number}> { + getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples { + if (!this._shouldShowWarningsAndErrors) { + return []; + } + if (this._cachedErrorAndWarningTuples !== null) { return this._cachedErrorAndWarningTuples; } @@ -615,6 +632,10 @@ export default class Store extends EventEmitter<{ errorCount: number, warningCount: number, } { + if (!this._shouldShowWarningsAndErrors) { + return {errorCount: 0, warningCount: 0}; + } + return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0}; } @@ -1325,16 +1346,21 @@ export default class Store extends EventEmitter<{ this._cachedErrorAndWarningTuples = null; if (haveErrorsOrWarningsChanged) { - let errorCount = 0; - let warningCount = 0; + let componentWithErrorCount = 0; + let componentWithWarningCount = 0; this._errorsAndWarnings.forEach(entry => { - errorCount += entry.errorCount; - warningCount += entry.warningCount; + if (entry.errorCount > 0) { + componentWithErrorCount++; + } + + if (entry.warningCount > 0) { + componentWithWarningCount++; + } }); - this._cachedErrorCount = errorCount; - this._cachedWarningCount = warningCount; + this._cachedComponentWithErrorCount = componentWithErrorCount; + this._cachedComponentWithWarningCount = componentWithWarningCount; } if (haveRootsChanged) { @@ -1528,9 +1554,21 @@ export default class Store extends EventEmitter<{ onHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; + + this.setShouldShowWarningsAndErrors(settings.showInlineWarningsAndErrors); this.emit('hookSettings', settings); }; + setShouldShowWarningsAndErrors(status: boolean): void { + const previousStatus = this._shouldShowWarningsAndErrors; + this._shouldShowWarningsAndErrors = status; + + if (previousStatus !== status) { + // Propagate to subscribers, although tree state has not changed + this.emit('mutated', [[], new Map()]); + } + } + // The Store should never throw an Error without also emitting an event. // Otherwise Store errors will be invisible to users, // but the downstream errors they cause will be reported as bugs. diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index f5c30d2d2b0d6..48bfbe90906ff 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -12,7 +12,6 @@ import {Fragment, useContext, useMemo, useState} from 'react'; import Store from 'react-devtools-shared/src/devtools/store'; import ButtonIcon from '../ButtonIcon'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; -import {SettingsContext} from '../Settings/SettingsContext'; import {StoreContext} from '../context'; import {useSubscription} from '../hooks'; import {logEvent} from 'react-devtools-shared/src/Logger'; @@ -37,7 +36,6 @@ export default function Element({data, index, style}: Props): React.Node { const {ownerFlatTree, ownerID, selectedElementID} = useContext(TreeStateContext); const dispatch = useContext(TreeDispatcherContext); - const {showInlineWarningsAndErrors} = React.useContext(SettingsContext); const element = ownerFlatTree !== null @@ -181,7 +179,7 @@ export default function Element({data, index, style}: Props): React.Node { className={styles.BadgesBlock} /> - {showInlineWarningsAndErrors && errorCount > 0 && ( + {errorCount > 0 && ( )} - {showInlineWarningsAndErrors && warningCount > 0 && ( + {warningCount > 0 && ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 136baa1205de2..db1bf9a98c135 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -73,7 +73,7 @@ export default function Tree(props: Props): React.Node { const [treeFocused, setTreeFocused] = useState(false); - const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext); + const {lineHeight} = useContext(SettingsContext); // Make sure a newly selected element is visible in the list. // This is helpful for things like the owners list and search. @@ -325,8 +325,8 @@ export default function Tree(props: Props): React.Node { const errorsOrWarningsSubscription = useMemo( () => ({ getCurrentValue: () => ({ - errors: store.errorCount, - warnings: store.warningCount, + errors: store.componentWithErrorCount, + warnings: store.componentWithWarningCount, }), subscribe: (callback: Function) => { store.addListener('mutated', callback); @@ -370,40 +370,38 @@ export default function Tree(props: Props): React.Node { }> {ownerID !== null ? : } - {showInlineWarningsAndErrors && - ownerID === null && - (errors > 0 || warnings > 0) && ( - -
- {errors > 0 && ( -
- - {errors} -
- )} - {warnings > 0 && ( -
- - {warnings} -
- )} - - - - - )} + {ownerID === null && (errors > 0 || warnings > 0) && ( + +
+ {errors > 0 && ( +
+ + {errors} +
+ )} + {warnings > 0 && ( +
+ + {warnings} +
+ )} + + + + + )} {!hideSettings && (
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js index 8bde14e62e606..c48cdb58e3e4c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js @@ -37,6 +37,10 @@ export default function DebuggingSettings({ const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] = useState(usedHookSettings.showInlineWarningsAndErrors); + useEffect(() => { + store.setShouldShowWarningsAndErrors(showInlineWarningsAndErrors); + }, [showInlineWarningsAndErrors]); + useEffect(() => { store.updateHookSettings({ appendComponentStack, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 17514d94648ac..196ea806f6aac 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -43,21 +43,9 @@ type Context = { // Specified as a separate prop so it can trigger a re-render of FixedSizeList. lineHeight: number, - appendComponentStack: boolean, - setAppendComponentStack: (value: boolean) => void, - - breakOnConsoleErrors: boolean, - setBreakOnConsoleErrors: (value: boolean) => void, - parseHookNames: boolean, setParseHookNames: (value: boolean) => void, - hideConsoleLogsInStrictMode: boolean, - setHideConsoleLogsInStrictMode: (value: boolean) => void, - - showInlineWarningsAndErrors: boolean, - setShowInlineWarningsAndErrors: (value: boolean) => void, - theme: Theme, setTheme(value: Theme): void, @@ -176,7 +164,7 @@ function SettingsContextController({ bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled); }, [bridge, traceUpdatesEnabled]); - const value = useMemo( + const value: Context = useMemo( () => ({ displayDensity, lineHeight: From d2e9b9b4dc22639e2c51fb34e9388b9971ee3e27 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 25 Sep 2024 14:38:34 +0100 Subject: [PATCH 190/191] React DevTools 5.3.1 -> 6.0.0 (#31058) Full list of changes: * refactor: data source for errors and warnings tracking is now in Store ([hoxyq](https://github.com/hoxyq) in [#31010](https://github.com/facebook/react/pull/31010)) * fix: consider alternate as a key for componentLogsEntry when inspecting raw fiber instance ([hoxyq](https://github.com/hoxyq) in [#31009](https://github.com/facebook/react/pull/31009)) * Fix: profiling crashes #30661 #28838 ([EdmondChuiHW](https://github.com/EdmondChuiHW) in [#31024](https://github.com/facebook/react/pull/31024)) * chore: remove using local storage for persisting console settings on the frontend ([hoxyq](https://github.com/hoxyq) in [#31002](https://github.com/facebook/react/pull/31002)) * feat: display message if user ended up opening hook script ([hoxyq](https://github.com/hoxyq) in [#31000](https://github.com/facebook/react/pull/31000)) * feat: expose installHook with settings argument from react-devtools-core/backend ([hoxyq](https://github.com/hoxyq) in [#30987](https://github.com/facebook/react/pull/30987)) * chore: remove settings manager from react-devtools-core ([hoxyq](https://github.com/hoxyq) in [#30986](https://github.com/facebook/react/pull/30986)) * feat[react-devtools/extension]: use chrome.storage to persist settings across sessions ([hoxyq](https://github.com/hoxyq) in [#30636](https://github.com/facebook/react/pull/30636)) * refactor[react-devtools]: propagate settings from global hook object to frontend ([hoxyq](https://github.com/hoxyq) in [#30610](https://github.com/facebook/react/pull/30610)) * chore[react-devtools]: extract some utils into separate modules to unify implementations ([hoxyq](https://github.com/hoxyq) in [#30597](https://github.com/facebook/react/pull/30597)) * refactor[react-devtools]: move console patching to global hook ([hoxyq](https://github.com/hoxyq) in [#30596](https://github.com/facebook/react/pull/30596)) * refactor[react-devtools]: remove browserTheme from ConsolePatchSettings ([hoxyq](https://github.com/hoxyq) in [#30566](https://github.com/facebook/react/pull/30566)) * feat[react-devtools]: add settings to global hook object ([hoxyq](https://github.com/hoxyq) in [#30564](https://github.com/facebook/react/pull/30564)) * fix: add Error prefix to Error objects names ([hoxyq](https://github.com/hoxyq) in [#30969](https://github.com/facebook/react/pull/30969)) * Add enableComponentPerformanceTrack Flag ([sebmarkbage](https://github.com/sebmarkbage) in [#30960](https://github.com/facebook/react/pull/30960)) * fix[rdt/fiber/renderer.js]: getCurrentFiber can be injected as null ([hoxyq](https://github.com/hoxyq) in [#30968](https://github.com/facebook/react/pull/30968)) * disable `enableSiblingPrerendering` in experimental channel ([gnoff](https://github.com/gnoff) in [#30952](https://github.com/facebook/react/pull/30952)) * refactor[react-devtools]: initialize renderer interface early ([hoxyq](https://github.com/hoxyq) in [#30946](https://github.com/facebook/react/pull/30946)) * Start prerendering Suspense retries immediately ([acdlite](https://github.com/acdlite) in [#30934](https://github.com/facebook/react/pull/30934)) * refactor[Agent/Store]: Store to send messages only after Agent is initialized ([hoxyq](https://github.com/hoxyq) in [#30945](https://github.com/facebook/react/pull/30945)) * refactor[RendererInterface]: expose onErrorOrWarning and getComponentStack ([hoxyq](https://github.com/hoxyq) in [#30931](https://github.com/facebook/react/pull/30931)) * Implement getComponentStack and onErrorOrWarning for replayed Flight logs ([sebmarkbage](https://github.com/sebmarkbage) in [#30930](https://github.com/facebook/react/pull/30930)) * Use Unicode Atom Symbol instead of Atom Emoji ([sebmarkbage](https://github.com/sebmarkbage) in [#30832](https://github.com/facebook/react/pull/30832)) * Improve Layering Between Console and Renderer ([sebmarkbage](https://github.com/sebmarkbage) in [#30925](https://github.com/facebook/react/pull/30925)) * Add Map for Server Component Logs ([sebmarkbage](https://github.com/sebmarkbage) in [#30905](https://github.com/facebook/react/pull/30905)) * Delete fiberToFiberInstanceMap ([sebmarkbage](https://github.com/sebmarkbage) in [#30900](https://github.com/facebook/react/pull/30900)) * Add Flight Renderer ([sebmarkbage](https://github.com/sebmarkbage) in [#30906](https://github.com/facebook/react/pull/30906)) * Refactor Error / Warning Count Tracking ([sebmarkbage](https://github.com/sebmarkbage) in [#30899](https://github.com/facebook/react/pull/30899)) * [flow] Upgrade Flow to 0.245.2 ([SamChou19815](https://github.com/SamChou19815) in [#30919](https://github.com/facebook/react/pull/30919)) * Separate RDT Fusebox into single-panel entry points ([huntie](https://github.com/huntie) in [#30708](https://github.com/facebook/react/pull/30708)) * Build Updater List from the Commit instead of Map ([sebmarkbage](https://github.com/sebmarkbage) in [#30897](https://github.com/facebook/react/pull/30897)) * Simplify Context Change Tracking in Profiler ([sebmarkbage](https://github.com/sebmarkbage) in [#30896](https://github.com/facebook/react/pull/30896)) * Remove use of .alternate in root and recordProfilingDurations ([sebmarkbage](https://github.com/sebmarkbage) in [#30895](https://github.com/facebook/react/pull/30895)) * Handle reordered contexts in Profiler ([sebmarkbage](https://github.com/sebmarkbage) in [#30887](https://github.com/facebook/react/pull/30887)) * Refactor Forcing Fallback / Error of Suspense / Error Boundaries ([sebmarkbage](https://github.com/sebmarkbage) in [#30870](https://github.com/facebook/react/pull/30870)) * Avoid getFiberIDUnsafe in debug() Helper ([sebmarkbage](https://github.com/sebmarkbage) in [#30878](https://github.com/facebook/react/pull/30878)) * Include some Filtered Fiber Instances ([sebmarkbage](https://github.com/sebmarkbage) in [#30865](https://github.com/facebook/react/pull/30865)) * Track root instances in a root Map ([sebmarkbage](https://github.com/sebmarkbage) in [#30875](https://github.com/facebook/react/pull/30875)) * Track all public HostInstances in a Map ([sebmarkbage](https://github.com/sebmarkbage) in [#30831](https://github.com/facebook/react/pull/30831)) * Support VirtualInstances in findAllCurrentHostInstances ([sebmarkbage](https://github.com/sebmarkbage) in [#30853](https://github.com/facebook/react/pull/30853)) * Add Filtering of Environment Names ([sebmarkbage](https://github.com/sebmarkbage) in [#30850](https://github.com/facebook/react/pull/30850)) * Support secondary environment name when it changes ([sebmarkbage](https://github.com/sebmarkbage) in [#30842](https://github.com/facebook/react/pull/30842)) * Increase max payload for websocket in standalone app ([runeb](https://github.com/runeb) in [#30848](https://github.com/facebook/react/pull/30848)) * Filter Server Components ([sebmarkbage](https://github.com/sebmarkbage) in [#30839](https://github.com/facebook/react/pull/30839)) * Track virtual instances on the tracked path for selections ([sebmarkbage](https://github.com/sebmarkbage) in [#30802](https://github.com/facebook/react/pull/30802)) * Remove displayName from inspected data ([sebmarkbage](https://github.com/sebmarkbage) in [#30841](https://github.com/facebook/react/pull/30841)) * chore[react-devtools/hook]: remove unused native values ([hoxyq](https://github.com/hoxyq) in [#30827](https://github.com/facebook/react/pull/30827)) * chore[react-devtools/extensions]: remove unused storage permission ([hoxyq](https://github.com/hoxyq) in [#30826](https://github.com/facebook/react/pull/30826)) * fix[react-devtools/extensions]: fixed tabs API calls and displaying restricted access popup ([hoxyq](https://github.com/hoxyq) in [#30825](https://github.com/facebook/react/pull/30825)) * feat[react-devtools]: support Manifest v3 for Firefox extension ([hoxyq](https://github.com/hoxyq) in [#30824](https://github.com/facebook/react/pull/30824)) * Reconcile Fibers Against Previous Children Instances ([sebmarkbage](https://github.com/sebmarkbage) in [#30822](https://github.com/facebook/react/pull/30822)) * Remove findCurrentFiberUsingSlowPathByFiberInstance ([sebmarkbage](https://github.com/sebmarkbage) in [#30818](https://github.com/facebook/react/pull/30818)) * Track Tree Base Duration of Virtual Instances ([sebmarkbage](https://github.com/sebmarkbage) in [#30817](https://github.com/facebook/react/pull/30817)) * Use Owner Stacks to Implement View Source of a Server Component ([sebmarkbage](https://github.com/sebmarkbage) in [#30798](https://github.com/facebook/react/pull/30798)) * Make function inspection instant ([sebmarkbage](https://github.com/sebmarkbage) in [#30786](https://github.com/facebook/react/pull/30786)) * Make Functions Clickable to Jump to Definition ([sebmarkbage](https://github.com/sebmarkbage) in [#30769](https://github.com/facebook/react/pull/30769)) * Support REACT_LEGACY_ELEMENT_TYPE for formatting JSX ([sebmarkbage](https://github.com/sebmarkbage) in [#30779](https://github.com/facebook/react/pull/30779)) * Find owners from the parent path that matches the Fiber or ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in [#30717](https://github.com/facebook/react/pull/30717)) * [Flight/DevTools] Pass the Server Component's "key" as Part of the ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in [#30703](https://github.com/facebook/react/pull/30703)) * Hide props section if it is null ([sebmarkbage](https://github.com/sebmarkbage) in [#30696](https://github.com/facebook/react/pull/30696)) * Support Server Components in Tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30684](https://github.com/facebook/react/pull/30684)) * fix[react-devtools/InspectedElement]: fixed border stylings when some of the panels are not rendered ([hoxyq](https://github.com/hoxyq) in [#30676](https://github.com/facebook/react/pull/30676)) * Compute new reordered child set from the instance tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30668](https://github.com/facebook/react/pull/30668)) * Unmount instance by walking the instance tree instead of the fiber tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30665](https://github.com/facebook/react/pull/30665)) * Further Refactoring of Unmounts ([sebmarkbage](https://github.com/sebmarkbage) in [#30658](https://github.com/facebook/react/pull/30658)) * Remove lodash.throttle ([sebmarkbage](https://github.com/sebmarkbage) in [#30657](https://github.com/facebook/react/pull/30657)) * Unmount by walking previous nodes no longer in the new tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30644](https://github.com/facebook/react/pull/30644)) * Build up DevTools Instance Shadow Tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30625](https://github.com/facebook/react/pull/30625)) * chore[packages/react-devtools]: remove unused index.js ([hoxyq](https://github.com/hoxyq) in [#30579](https://github.com/facebook/react/pull/30579)) * Track DOM nodes to Fiber map for HostHoistable Resources ([sebmarkbage](https://github.com/sebmarkbage) in [#30590](https://github.com/facebook/react/pull/30590)) * Rename mountFiberRecursively/updateFiberRecursively ([sebmarkbage](https://github.com/sebmarkbage) in [#30586](https://github.com/facebook/react/pull/30586)) * Allow Highlighting/Inspect HostSingletons/Hoistables and Resources ([sebmarkbage](https://github.com/sebmarkbage) in [#30584](https://github.com/facebook/react/pull/30584)) * chore[react-devtools]: add global for native and use it to fork backend implementation ([hoxyq](https://github.com/hoxyq) in [#30533](https://github.com/facebook/react/pull/30533)) * Enable pointEvents while scrolling ([sebmarkbage](https://github.com/sebmarkbage) in [#30560](https://github.com/facebook/react/pull/30560)) * Make Element Inspection Feel Snappy ([sebmarkbage](https://github.com/sebmarkbage) in [#30555](https://github.com/facebook/react/pull/30555)) * Track the parent DevToolsInstance while mounting a tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30542](https://github.com/facebook/react/pull/30542)) * Add DevToolsInstance to Store Stateful Information ([sebmarkbage](https://github.com/sebmarkbage) in [#30517](https://github.com/facebook/react/pull/30517)) * Implement "best renderer" by taking the inner most matched node ([sebmarkbage](https://github.com/sebmarkbage) in [#30494](https://github.com/facebook/react/pull/30494)) * Rename NativeElement to HostInstance in the Bridge ([sebmarkbage](https://github.com/sebmarkbage) in [#30491](https://github.com/facebook/react/pull/30491)) * Rename Fiber to Element in the Bridge Protocol and RendererInterface ([sebmarkbage](https://github.com/sebmarkbage) in [#30490](https://github.com/facebook/react/pull/30490)) * Stop filtering owner stacks ([sebmarkbage](https://github.com/sebmarkbage) in [#30438](https://github.com/facebook/react/pull/30438)) * [Fiber] Call life-cycles with a react-stack-bottom-frame stack frame ([sebmarkbage](https://github.com/sebmarkbage) in [#30429](https://github.com/facebook/react/pull/30429)) * [Flight] Prefix owner stacks added to the console.log with the current stack ([sebmarkbage](https://github.com/sebmarkbage) in [#30427](https://github.com/facebook/react/pull/30427)) * [BE] switch to hermes parser for prettier ([kassens](https://github.com/kassens) in [#30421](https://github.com/facebook/react/pull/30421)) * Implement Owner Stacks ([sebmarkbage](https://github.com/sebmarkbage) in [#30417](https://github.com/facebook/react/pull/30417)) * [BE] upgrade prettier to 3.3.3 ([kassens](https://github.com/kassens) in [#30420](https://github.com/facebook/react/pull/30420)) * [ci] Add yarn_test_build job to gh actions * [Fizz] Refactor Component Stack Nodes ([sebmarkbage](https://github.com/sebmarkbage) in [#30298](https://github.com/facebook/react/pull/30298)) * Print component stacks as error objects to get source mapping ([sebmarkbage](https://github.com/sebmarkbage) in [#30289](https://github.com/facebook/react/pull/30289)) * Upgrade flow to 0.235.0 ([kassens](https://github.com/kassens) in [#30118](https://github.com/facebook/react/pull/30118)) * fix: path handling in react devtools ([Jack-Works](https://github.com/Jack-Works) in [#29199](https://github.com/facebook/react/pull/29199)) --- packages/react-devtools-core/package.json | 2 +- .../chrome/manifest.json | 4 +-- .../edge/manifest.json | 4 +-- .../firefox/manifest.json | 2 +- packages/react-devtools-inline/package.json | 2 +- packages/react-devtools-timeline/package.json | 2 +- packages/react-devtools/CHANGELOG.md | 32 +++++++++++++++++++ packages/react-devtools/package.json | 4 +-- 8 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 4c7f54775f6ba..23167fa377847 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "5.3.1", + "version": "6.0.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 1ab43194f2620..f0209aaa73eed 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "5.3.1", - "version_name": "5.3.1", + "version": "6.0.0", + "version_name": "6.0.0", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index bd03dea08efb3..ca4d56b4453e9 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "5.3.1", - "version_name": "5.3.1", + "version": "6.0.0", + "version_name": "6.0.0", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 8a5a272fb4500..52f89104b4f80 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "5.3.1", + "version": "6.0.0", "browser_specific_settings": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index ead4c18380047..f0eadaf7e6d9e 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "5.3.1", + "version": "6.0.0", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json index b9a8aab901d54..883aad785f6b2 100644 --- a/packages/react-devtools-timeline/package.json +++ b/packages/react-devtools-timeline/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-timeline", - "version": "5.3.1", + "version": "6.0.0", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index bfebd3ea0a705..aab41edf090f3 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -4,6 +4,38 @@ --- +### 6.0.0 +September 25, 2024 + +#### Features +* Support Server Components in Tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30684](https://github.com/facebook/react/pull/30684)) +* feat: expose installHook with settings argument from react-devtools-core/backend ([hoxyq](https://github.com/hoxyq) in [#30987](https://github.com/facebook/react/pull/30987)) +* Add Filtering of Environment Names ([sebmarkbage](https://github.com/sebmarkbage) in [#30850](https://github.com/facebook/react/pull/30850)) +* Support secondary environment name when it changes ([sebmarkbage](https://github.com/sebmarkbage) in [#30842](https://github.com/facebook/react/pull/30842)) +* Filter Server Components ([sebmarkbage](https://github.com/sebmarkbage) in [#30839](https://github.com/facebook/react/pull/30839)) +* Make function inspection instant ([sebmarkbage](https://github.com/sebmarkbage) in [#30786](https://github.com/facebook/react/pull/30786)) +* Make Functions Clickable to Jump to Definition ([sebmarkbage](https://github.com/sebmarkbage) in [#30769](https://github.com/facebook/react/pull/30769)) +* [Flight/DevTools] Pass the Server Component's "key" as Part of the ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in [#30703](https://github.com/facebook/react/pull/30703)) +* Make Element Inspection Feel Snappy ([sebmarkbage](https://github.com/sebmarkbage) in [#30555](https://github.com/facebook/react/pull/30555)) +* Print component stacks as error objects to get source mapping ([sebmarkbage](https://github.com/sebmarkbage) in [#30289](https://github.com/facebook/react/pull/30289)) + +#### Bugfixes +* Fix: profiling crashes #30661 #28838 ([EdmondChuiHW](https://github.com/EdmondChuiHW) in [#31024](https://github.com/facebook/react/pull/31024)) +* chore: remove settings manager from react-devtools-core ([hoxyq](https://github.com/hoxyq) in [#30986](https://github.com/facebook/react/pull/30986)) +* fix[react-devtools/extensions]: fixed tabs API calls and displaying restricted access popup ([hoxyq](https://github.com/hoxyq) in [#30825](https://github.com/facebook/react/pull/30825)) +* fix[react-devtools/InspectedElement]: fixed border stylings when some of the panels are not rendered ([hoxyq](https://github.com/hoxyq) in [#30676](https://github.com/facebook/react/pull/30676)) +* fix: path handling in react devtools ([Jack-Works](https://github.com/Jack-Works) in [#29199](https://github.com/facebook/react/pull/29199)) + +#### Other +* feat: display message if user ended up opening hook script ([hoxyq](https://github.com/hoxyq) in [#31000](https://github.com/facebook/react/pull/31000)) +* refactor[react-devtools]: move console patching to global hook ([hoxyq](https://github.com/hoxyq) in [#30596](https://github.com/facebook/react/pull/30596)) +* refactor[react-devtools]: initialize renderer interface early ([hoxyq](https://github.com/hoxyq) in [#30946](https://github.com/facebook/react/pull/30946)) +* Use Unicode Atom Symbol instead of Atom Emoji ([sebmarkbage](https://github.com/sebmarkbage) in [#30832](https://github.com/facebook/react/pull/30832)) +* feat[react-devtools]: support Manifest v3 for Firefox extension ([hoxyq](https://github.com/hoxyq) in [#30824](https://github.com/facebook/react/pull/30824)) +* Enable pointEvents while scrolling ([sebmarkbage](https://github.com/sebmarkbage) in [#30560](https://github.com/facebook/react/pull/30560)) + +--- + ### 5.3.1 July 3, 2024 diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index c9db18aca46a1..0972fd8c94e7a 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "5.3.1", + "version": "6.0.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -26,7 +26,7 @@ "electron": "^23.1.2", "internal-ip": "^6.2.0", "minimist": "^1.2.3", - "react-devtools-core": "5.3.1", + "react-devtools-core": "6.0.0", "update-notifier": "^2.1.0" } } From f9ebd85a196948be17efdd6774b4d0464b3b1f53 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Wed, 25 Sep 2024 11:50:41 -0400 Subject: [PATCH 191/191] Increase nested update limit to 100 (#31061) We're seeing the limit hit in some tests after enabling sibling prerendering. Let's bump the limit so we can run more tests and gather more signal on the changes. When we understand the scope of the problem we can determine whether we need to change how the updates are counted in prerenders and/or fix specific areas of product code. --- packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js | 2 +- packages/react-dom/src/__tests__/ReactUpdates-test.js | 2 +- packages/react-reconciler/src/ReactFiberWorkLoop.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js index 26f56a938c551..57f2acfa53465 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js @@ -1427,7 +1427,7 @@ describe('ReactLegacyUpdates', () => { } } - let limit = 55; + let limit = 105; await expect(async () => { await act(() => { ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index faf4b29551350..247a53531c659 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1542,7 +1542,7 @@ describe('ReactUpdates', () => { } } - let limit = 55; + let limit = 105; const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c13b9c65a65a4..a0df584423e13 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -613,13 +613,13 @@ let pendingPassiveEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; // Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 50; +const NESTED_UPDATE_LIMIT = 100; let nestedUpdateCount: number = 0; let rootWithNestedUpdates: FiberRoot | null = null; let isFlushingPassiveEffects = false; let didScheduleUpdateDuringPassiveEffects = false; -const NESTED_PASSIVE_UPDATE_LIMIT = 50; +const NESTED_PASSIVE_UPDATE_LIMIT = 100; let nestedPassiveUpdateCount: number = 0; let rootWithPassiveNestedUpdates: FiberRoot | null = null;