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 c732e164101d4..96cce887d8655 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -37,6 +37,10 @@ const PanicThresholdOptionsSchema = z.enum([ ]); export type PanicThresholdOptions = z.infer; +const DynamicGatingOptionsSchema = z.object({ + source: z.string(), +}); +export type DynamicGatingOptions = z.infer; export type PluginOptions = { environment: EnvironmentConfig; @@ -65,6 +69,28 @@ export type PluginOptions = { */ gating: ExternalFunction | null; + /** + * If specified, this enables dynamic gating which matches `use memo if(...)` + * directives. + * + * Example usage: + * ```js + * // @dynamicGating:{"source":"myModule"} + * export function MyComponent() { + * 'use memo if(isEnabled)'; + * return
...
; + * } + * ``` + * This will emit: + * ```js + * import {isEnabled} from 'myModule'; + * export const MyComponent = isEnabled() + * ? + * : ; + * ``` + */ + dynamicGating: DynamicGatingOptions | null; + panicThreshold: PanicThresholdOptions; /* @@ -244,6 +270,7 @@ export const defaultOptions: PluginOptions = { logger: null, gating: null, noEmit: false, + dynamicGating: null, eslintSuppressionRules: null, flowSuppressions: true, ignoreUseNoForget: false, @@ -292,6 +319,25 @@ export function parsePluginOptions(obj: unknown): PluginOptions { } break; } + case 'dynamicGating': { + if (value == null) { + parsedOptions[key] = null; + } else { + const result = DynamicGatingOptionsSchema.safeParse(value); + if (result.success) { + parsedOptions[key] = result.data; + } else { + CompilerError.throwInvalidConfig({ + reason: + 'Could not parse dynamic gating. Update React Compiler config to fix the error', + description: `${fromZodError(result.error)}`, + loc: null, + suggestions: null, + }); + } + } + break; + } default: { 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 64abc110ea12f..cb57bd2c49d89 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -12,7 +12,7 @@ import { CompilerErrorDetail, ErrorSeverity, } from '../CompilerError'; -import {ReactFunctionType} from '../HIR/Environment'; +import {ExternalFunction, ReactFunctionType} from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; import {isComponentDeclaration} from '../Utils/ComponentDeclaration'; import {isHookDeclaration} from '../Utils/HookDeclaration'; @@ -31,6 +31,7 @@ import { suppressionsToCompilerError, } from './Suppression'; import {GeneratedSource} from '../HIR'; +import {Err, Ok, Result} from '../Utils/Result'; export type CompilerPass = { opts: PluginOptions; @@ -40,15 +41,24 @@ export type CompilerPass = { }; export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']); export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']); +const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$'); -export function findDirectiveEnablingMemoization( +export function tryFindDirectiveEnablingMemoization( directives: Array, -): t.Directive | null { - return ( - directives.find(directive => - OPT_IN_DIRECTIVES.has(directive.value.value), - ) ?? null + opts: PluginOptions, +): Result { + const optIn = directives.find(directive => + OPT_IN_DIRECTIVES.has(directive.value.value), ); + if (optIn != null) { + return Ok(optIn); + } + const dynamicGating = findDirectivesDynamicGating(directives, opts); + if (dynamicGating.isOk()) { + return Ok(dynamicGating.unwrap()?.directive ?? null); + } else { + return Err(dynamicGating.unwrapErr()); + } } export function findDirectiveDisablingMemoization( @@ -60,6 +70,64 @@ export function findDirectiveDisablingMemoization( ) ?? null ); } +function findDirectivesDynamicGating( + directives: Array, + opts: PluginOptions, +): Result< + { + gating: ExternalFunction; + directive: t.Directive; + } | null, + CompilerError +> { + if (opts.dynamicGating === null) { + return Ok(null); + } + const errors = new CompilerError(); + const result: Array<{directive: t.Directive; match: string}> = []; + + for (const directive of directives) { + const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value); + if (maybeMatch != null && maybeMatch[1] != null) { + if (t.isValidIdentifier(maybeMatch[1])) { + result.push({directive, match: maybeMatch[1]}); + } else { + errors.push({ + reason: `Dynamic gating directive is not a valid JavaScript identifier`, + description: `Found '${directive.value.value}'`, + severity: ErrorSeverity.InvalidReact, + loc: directive.loc ?? null, + suggestions: null, + }); + } + } + } + if (errors.hasErrors()) { + return Err(errors); + } else if (result.length > 1) { + const error = new CompilerError(); + error.push({ + reason: `Multiple dynamic gating directives found`, + description: `Expected a single directive but found [${result + .map(r => r.directive.value.value) + .join(', ')}]`, + severity: ErrorSeverity.InvalidReact, + loc: result[0].directive.loc ?? null, + suggestions: null, + }); + return Err(error); + } else if (result.length === 1) { + return Ok({ + gating: { + source: opts.dynamicGating.source, + importSpecifierName: result[0].match, + }, + directive: result[0].directive, + }); + } else { + return Ok(null); + } +} function isCriticalError(err: unknown): boolean { return !(err instanceof CompilerError) || err.isCritical(); @@ -477,12 +545,32 @@ function processFn( fnType: ReactFunctionType, programContext: ProgramContext, ): null | CodegenFunction { - let directives; + let directives: { + optIn: t.Directive | null; + optOut: t.Directive | null; + }; if (fn.node.body.type !== 'BlockStatement') { - directives = {optIn: null, optOut: null}; + directives = { + optIn: null, + optOut: null, + }; } else { + const optIn = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + programContext.opts, + ); + if (optIn.isErr()) { + /** + * If parsing opt-in directive fails, it's most likely that React Compiler + * was not tested or rolled out on this function. In that case, we handle + * the error and fall back to the safest option which is to not optimize + * the function. + */ + handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null); + return null; + } directives = { - optIn: findDirectiveEnablingMemoization(fn.node.body.directives), + optIn: optIn.unwrapOr(null), optOut: findDirectiveDisablingMemoization(fn.node.body.directives), }; } @@ -659,25 +747,31 @@ function applyCompiledFunctions( pass: CompilerPass, programContext: ProgramContext, ): void { - const referencedBeforeDeclared = - pass.opts.gating != null - ? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns) - : null; + let referencedBeforeDeclared = null; for (const result of compiledFns) { const {kind, originalFn, compiledFn} = result; const transformedFn = createNewFunctionNode(originalFn, compiledFn); programContext.alreadyCompiled.add(transformedFn); - if (referencedBeforeDeclared != null && kind === 'original') { - CompilerError.invariant(pass.opts.gating != null, { - reason: "Expected 'gating' import to be present", - loc: null, - }); + let dynamicGating: ExternalFunction | null = null; + if (originalFn.node.body.type === 'BlockStatement') { + const result = findDirectivesDynamicGating( + originalFn.node.body.directives, + pass.opts, + ); + if (result.isOk()) { + dynamicGating = result.unwrap()?.gating ?? null; + } + } + const functionGating = dynamicGating ?? pass.opts.gating; + if (kind === 'original' && functionGating != null) { + referencedBeforeDeclared ??= + getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns); insertGatedFunctionDeclaration( originalFn, transformedFn, programContext, - pass.opts.gating, + functionGating, referencedBeforeDeclared.has(result), ); } else { @@ -733,8 +827,13 @@ function getReactFunctionType( ): ReactFunctionType | null { const hookPattern = pass.opts.environment.hookPattern; if (fn.node.body.type === 'BlockStatement') { - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) + const optInDirectives = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + pass.opts, + ); + if (optInDirectives.unwrapOr(null) != null) { 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/gating/dynamic-gating-annotation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md new file mode 100644 index 0000000000000..364239e4e3a2d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" +const Foo = getTrue() + ? function Foo() { + "use memo if(getTrue)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getTrue)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### 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/gating/dynamic-gating-annotation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js new file mode 100644 index 0000000000000..c30b30fe6f5f1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-annotation.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @compilationMode:"annotation" + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md new file mode 100644 index 0000000000000..dc3cc2b98de9a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import {useMemo} from 'react'; +import {identity} from 'shared-runtime'; + +function Foo({value}) { + 'use memo if(getTrue)'; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{value: 1}], + sequentialRenders: [{value: 1}, {value: 2}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import { useMemo } from "react"; +import { identity } from "shared-runtime"; + +function Foo({ value }) { + "use memo if(getTrue)"; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ value: 1 }], + sequentialRenders: [{ value: 1 }, { value: 2 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":206},"end":{"line":16,"column":1,"index":433},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"reason":"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","description":"The inferred dependency was `value`, but the source dependencies were []. Inferred dependency not present in source","severity":"CannotPreserveMemoization","suggestions":null,"loc":{"start":{"line":9,"column":31,"index":288},"end":{"line":9,"column":52,"index":309},"filename":"dynamic-gating-bailout-nopanic.ts"}}} +``` + +### Eval output +(kind: ok)
initial value 1
current value 1
+
initial value 1
current value 2
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js new file mode 100644 index 0000000000000..ceddbefdd1b72 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-bailout-nopanic.js @@ -0,0 +1,22 @@ +// @dynamicGating:{"source":"shared-runtime"} @validatePreserveExistingMemoizationGuarantees @panicThreshold:"none" @loggerTestOnly + +import {useMemo} from 'react'; +import {identity} from 'shared-runtime'; + +function Foo({value}) { + 'use memo if(getTrue)'; + + const initialValue = useMemo(() => identity(value), []); + return ( + <> +
initial value {initialValue}
+
current value {value}
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{value: 1}], + sequentialRenders: [{value: 1}, {value: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md new file mode 100644 index 0000000000000..7d95b54317d48 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getFalse } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} +const Foo = getFalse() + ? function Foo() { + "use memo if(getFalse)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getFalse)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### 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/gating/dynamic-gating-disabled.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js new file mode 100644 index 0000000000000..be29f10568754 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-disabled.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md new file mode 100644 index 0000000000000..272c5a57143bf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getTrue } from "shared-runtime"; // @dynamicGating:{"source":"shared-runtime"} +const Foo = getTrue() + ? function Foo() { + "use memo if(getTrue)"; + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
hello world
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; + } + : function Foo() { + "use memo if(getTrue)"; + return
hello world
; + }; + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### 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/gating/dynamic-gating-enabled.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js new file mode 100644 index 0000000000000..9280e25d116fe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-enabled.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md new file mode 100644 index 0000000000000..c8c91910b02bc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + "use memo if(true)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### 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/gating/dynamic-gating-invalid-identifier-nopanic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js new file mode 100644 index 0000000000000..4d0d9c3bb86f5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-identifier-nopanic.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md new file mode 100644 index 0000000000000..327adbe792ed3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + 'use memo if(getTrue)'; + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + "use memo if(getTrue)"; + "use memo if(getFalse)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":3,"column":0,"index":86},"end":{"line":7,"column":1,"index":190},"filename":"dynamic-gating-invalid-multiple.ts"},"detail":{"reason":"Multiple dynamic gating directives found","description":"Expected a single directive but found [use memo if(getTrue), use memo if(getFalse)]","severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":2,"index":105},"end":{"line":4,"column":25,"index":128},"filename":"dynamic-gating-invalid-multiple.ts"}}} +``` + +### 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/gating/dynamic-gating-invalid-multiple.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js new file mode 100644 index 0000000000000..867ac8ee34b1b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-invalid-multiple.js @@ -0,0 +1,12 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @loggerTestOnly + +function Foo() { + 'use memo if(getTrue)'; + 'use memo if(getFalse)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md new file mode 100644 index 0000000000000..81ebd6dd9fad9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + "use memo if(getTrue)"; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### 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/gating/dynamic-gating-noemit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js new file mode 100644 index 0000000000000..97cf777a552df --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/dynamic-gating-noemit.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} @noEmit + +function Foo() { + 'use memo if(getTrue)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md new file mode 100644 index 0000000000000..7f9f608383bdd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function ReactiveVariable({propVal}) { + 'use memo if(invalid identifier)'; + const arr = [propVal]; + useEffect(() => print(arr)); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ReactiveVariable, + params: [{}], +}; + +``` + + +## Error + +``` + 6 | 'use memo if(invalid identifier)'; + 7 | const arr = [propVal]; +> 8 | useEffect(() => print(arr)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (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/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js new file mode 100644 index 0000000000000..7d5b74acc7960 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier-nopanic-required-feature.js @@ -0,0 +1,14 @@ +// @dynamicGating:{"source":"shared-runtime"} @panicThreshold:"none" @inferEffectDependencies +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function ReactiveVariable({propVal}) { + 'use memo if(invalid identifier)'; + const arr = [propVal]; + useEffect(() => print(arr)); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ReactiveVariable, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md new file mode 100644 index 0000000000000..c824afd680618 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + + +## Error + +``` + 2 | + 3 | function Foo() { +> 4 | 'use memo if(true)'; + | ^^^^^^^^^^^^^^^^^^^^ InvalidReact: Dynamic gating directive is not a valid JavaScript identifier. Found 'use memo if(true)' (4:4) + 5 | return
hello world
; + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js new file mode 100644 index 0000000000000..c400554497235 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/gating/error.dynamic-gating-invalid-identifier.js @@ -0,0 +1,11 @@ +// @dynamicGating:{"source":"shared-runtime"} + +function Foo() { + 'use memo if(true)'; + return
hello world
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md new file mode 100644 index 0000000000000..ec5ef238b7828 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none" + +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + 'use memo if(getTrue)'; + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; + +``` + + +## Error + +``` + 10 | 'use memo if(getTrue)'; + 11 | const arr = []; +> 12 | useEffectWrapper(() => arr.push(foo)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (12:12) + 13 | arr.push(2); + 14 | return arr; + 15 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js new file mode 100644 index 0000000000000..4d1ceb92b78a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-dynamic-gating.js @@ -0,0 +1,21 @@ +// @dynamicGating:{"source":"shared-runtime"} @inferEffectDependencies @panicThreshold:"none" + +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + 'use memo if(getTrue)'; + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md new file mode 100644 index 0000000000000..e071e37cb99d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @gating @inferEffectDependencies @panicThreshold:"none" +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; + +``` + + +## Error + +``` + 8 | function Component({foo}) { + 9 | const arr = []; +> 10 | useEffectWrapper(() => arr.push(foo)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics. (10:10) + 11 | arr.push(2); + 12 | return arr; + 13 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js new file mode 100644 index 0000000000000..651b24074f2bc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-gating.js @@ -0,0 +1,19 @@ +// @gating @inferEffectDependencies @panicThreshold:"none" +import useEffectWrapper from 'useEffectWrapper'; + +/** + * TODO: run the non-forget enabled version through the effect inference + * pipeline. + */ +function Component({foo}) { + const arr = []; + useEffectWrapper(() => arr.push(foo)); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], + sequentialRenders: [{foo: 1}, {foo: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index 086e010fea581..cbae672e50674 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -20,7 +20,7 @@ export { OPT_OUT_DIRECTIVES, OPT_IN_DIRECTIVES, ProgramContext, - findDirectiveEnablingMemoization, + tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization, findDirectiveDisablingMemoization, type CompilerPipelineValue, type Logger, diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 1b8648f4ff031..569d31cbd4da1 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -128,6 +128,14 @@ export function getNull(): null { return null; } +export function getTrue(): true { + return true; +} + +export function getFalse(): false { + return false; +} + export function calculateExpensiveNumber(x: number): number { return x; } diff --git a/packages/internal-test-utils/internalAct.js b/packages/internal-test-utils/internalAct.js index 752725eeb842f..1a420bcf203b4 100644 --- a/packages/internal-test-utils/internalAct.js +++ b/packages/internal-test-utils/internalAct.js @@ -138,6 +138,7 @@ export async function act(scope: () => Thenable): Thenable { // 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.runAllTicks(); j.runOnlyPendingTimers(); // If a committing a fallback triggers another update, it might not // get scheduled until a microtask. So wait one more time. @@ -194,6 +195,39 @@ export async function act(scope: () => Thenable): Thenable { } } +async function waitForTasksAndTimers(error: Error) { + 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.runAllTicks(); + 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); +} + 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. @@ -233,37 +267,17 @@ export async function serverAct(scope: () => Thenable): Thenable { } 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); + const promise = scope(); + // $FlowFixMe[prop-missing] + if (promise && typeof promise.catch === 'function') { + // $FlowFixMe[incompatible-use] + promise.catch(() => {}); // Handle below + } + // See if we need to do some work to unblock the promise first. + await waitForTasksAndTimers(error); + const result = await promise; + // Then wait to flush the result. + await waitForTasksAndTimers(error); if (thrownErrors.length > 0) { // Rethrow any errors logged by the global error handling. diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 4a3d5b00bf25a..317d13f6c5366 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -3073,19 +3073,19 @@ export function updateFragmentInstanceFiber( } export function commitNewChildToFragmentInstance( - childElement: Instance, + childInstance: Instance, fragmentInstance: FragmentInstanceType, ): void { const eventListeners = fragmentInstance._eventListeners; if (eventListeners !== null) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; - childElement.addEventListener(type, listener, optionsOrUseCapture); + childInstance.addEventListener(type, listener, optionsOrUseCapture); } } if (fragmentInstance._observers !== null) { fragmentInstance._observers.forEach(observer => { - observer.observe(childElement); + observer.observe(childInstance); }); } } diff --git a/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js b/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js index 67e7fff249855..66477bf80a96c 100644 --- a/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js +++ b/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js @@ -22,18 +22,18 @@ let ReactDOMServer; let Scheduler; let assertLog; let container; -let act; +let serverAct; describe('ReactClassComponentPropResolutionFizz', () => { beforeEach(() => { jest.resetModules(); Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + patchMessageChannel(); React = require('react'); ReactDOMServer = require('react-dom/server.browser'); assertLog = require('internal-test-utils').assertLog; + serverAct = require('internal-test-utils').serverAct; container = document.createElement('div'); document.body.appendChild(container); }); @@ -42,17 +42,6 @@ describe('ReactClassComponentPropResolutionFizz', () => { document.body.removeChild(container); }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - async function readIntoContainer(stream) { const reader = stream.getReader(); let result = ''; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js index cceef34e03d66..529e3be3132f5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js @@ -21,6 +21,7 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; let act; +let serverAct; let assertLog; let waitForPaint; let container; @@ -35,8 +36,9 @@ describe('ReactDOMFizzForm', () => { beforeEach(() => { jest.resetModules(); Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); + patchMessageChannel(); act = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; React = require('react'); ReactDOMServer = require('react-dom/server.browser'); ReactDOMClient = require('react-dom/client'); @@ -52,17 +54,6 @@ describe('ReactDOMFizzForm', () => { document.body.removeChild(container); }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - async function readIntoContainer(stream) { const reader = stream.getReader(); let result = ''; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index c7f52b1c68899..651b24a572957 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -18,6 +18,7 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; let act; +let serverAct; let container; let React; let ReactDOMServer; @@ -25,20 +26,19 @@ let ReactDOMClient; let useFormStatus; let useOptimistic; let useActionState; -let Scheduler; let assertConsoleErrorDev; describe('ReactDOMFizzForm', () => { beforeEach(() => { jest.resetModules(); - Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); + patchMessageChannel(); React = require('react'); ReactDOMServer = require('react-dom/server.browser'); ReactDOMClient = require('react-dom/client'); useFormStatus = require('react-dom').useFormStatus; useOptimistic = require('react').useOptimistic; act = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; container = document.createElement('div'); @@ -55,14 +55,6 @@ describe('ReactDOMFizzForm', () => { document.body.removeChild(container); }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - }); - return maybePromise; - } - function submit(submitter) { const form = submitter.form || submitter; if (!submitter.form) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index 7c0aa14c2607d..1755544a5d35d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -19,33 +19,20 @@ global.TextEncoder = require('util').TextEncoder; let React; let ReactDOMFizzServer; let Suspense; -let Scheduler; -let act; +let serverAct; describe('ReactDOMFizzServerBrowser', () => { beforeEach(() => { jest.resetModules(); - Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + patchMessageChannel(); + serverAct = require('internal-test-utils').serverAct; React = require('react'); ReactDOMFizzServer = require('react-dom/server.browser'); Suspense = React.Suspense; }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - const theError = new Error('This is an error'); function Throw() { throw theError; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 55a89bc525a3c..6fc68f84ab7a8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -29,9 +29,9 @@ let ReactDOM; let ReactDOMFizzServer; let ReactDOMFizzStatic; let Suspense; +let SuspenseList; let container; -let Scheduler; -let act; +let serverAct; describe('ReactDOMFizzStaticBrowser', () => { beforeEach(() => { @@ -41,15 +41,15 @@ describe('ReactDOMFizzStaticBrowser', () => { // We need the mocked version of setTimeout inside the document. window.setTimeout = setTimeout; - Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + patchMessageChannel(); + serverAct = require('internal-test-utils').serverAct; React = require('react'); ReactDOM = require('react-dom'); ReactDOMFizzServer = require('react-dom/server.browser'); ReactDOMFizzStatic = require('react-dom/static.browser'); Suspense = React.Suspense; + SuspenseList = React.unstable_SuspenseList; container = document.createElement('div'); document.body.appendChild(container); }); @@ -61,17 +61,6 @@ describe('ReactDOMFizzStaticBrowser', () => { document.body.removeChild(container); }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - const theError = new Error('This is an error'); function Throw() { throw theError; @@ -2242,4 +2231,85 @@ describe('ReactDOMFizzStaticBrowser', () => { , ); }); + + // @gate enableHalt && enableSuspenseList + it('can resume a partially prerendered SuspenseList', async () => { + const errors = []; + + let resolveA; + const promiseA = new Promise(r => (resolveA = r)); + let resolveB; + const promiseB = new Promise(r => (resolveB = r)); + + async function ComponentA() { + await promiseA; + return 'A'; + } + + async function ComponentB() { + await promiseB; + return 'B'; + } + + function App() { + return ( +
+ + + + + + + + C + +
+ ); + } + + const controller = new AbortController(); + const pendingResult = serverAct(() => + ReactDOMFizzStatic.prerender(, { + signal: controller.signal, + onError(x) { + errors.push(x.message); + }, + }), + ); + + await serverAct(() => { + controller.abort(); + }); + + const prerendered = await pendingResult; + const postponedState = JSON.stringify(prerendered.postponed); + + await readIntoContainer(prerendered.prelude); + expect(getVisibleChildren(container)).toEqual( +
+ {'Loading A'} + {'Loading B'} + {'C' /* TODO: This should not be resolved. */} +
, + ); + + expect(prerendered.postponed).not.toBe(null); + + await resolveA(); + await resolveB(); + + const dynamic = await serverAct(() => + ReactDOMFizzServer.resume(, JSON.parse(postponedState)), + ); + + await readIntoContainer(dynamic); + + expect(getVisibleChildren(container)).toEqual( +
+ {'A'} + {'B'} + {'C'} +
, + ); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js index baa65c806c0ba..8bb1c7c861075 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js @@ -27,15 +27,15 @@ let ReactDOMFizzServer; let ReactDOMFizzStatic; let Suspense; let container; -let Scheduler; let act; +let serverAct; describe('ReactDOMFizzStaticFloat', () => { beforeEach(() => { jest.resetModules(); - Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); + patchMessageChannel(); act = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; React = require('react'); ReactDOM = require('react-dom'); @@ -52,17 +52,6 @@ describe('ReactDOMFizzStaticFloat', () => { document.body.removeChild(container); }); - async function serverAct(callback) { - let maybePromise; - await act(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - async function readIntoContainer(stream) { const reader = stream.getReader(); let result = ''; diff --git a/packages/react-dom/src/test-utils/FizzTestUtils.js b/packages/react-dom/src/test-utils/FizzTestUtils.js index 12c768e1a0008..4d8b6cadbabf0 100644 --- a/packages/react-dom/src/test-utils/FizzTestUtils.js +++ b/packages/react-dom/src/test-utils/FizzTestUtils.js @@ -167,7 +167,10 @@ function getVisibleChildren(element: Element): React$Node { } props[attributes[i].name] = attributes[i].value; } - props.children = getVisibleChildren(node); + const nestedChildren = getVisibleChildren(node); + if (nestedChildren !== undefined) { + props.children = nestedChildren; + } children.push( require('react').createElement(node.tagName.toLowerCase(), props), ); diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 7a06f157e668f..9a661ee7414b3 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -695,12 +695,17 @@ export function updateFragmentInstanceFiber( } export function commitNewChildToFragmentInstance( - child: Fiber, + childInstance: Instance, fragmentInstance: FragmentInstanceType, ): void { + const publicInstance = getPublicInstance(childInstance); if (fragmentInstance._observers !== null) { + if (publicInstance == null) { + throw new Error('Expected to find a host node. This is a bug in React.'); + } fragmentInstance._observers.forEach(observer => { - observeChild(child, observer); + // $FlowFixMe[incompatible-call] Element types are behind a flag in RN + observer.observe(publicInstance); }); } } diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js index 725b8d9de694f..ac4bcf36b00b1 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js @@ -80,4 +80,46 @@ describe('Fabric FragmentRefs', () => { expect(fragmentRef && fragmentRef._fragmentFiber).toBeTruthy(); }); + + describe('observers', () => { + // @gate enableFragmentRefs + it('observes children, newly added children', async () => { + let logs = []; + const observer = { + observe: entry => { + // Here we reference internals because we don't need to mock the native observer + // We only need to test that each child node is observed on insertion + logs.push(entry.__internalInstanceHandle.pendingProps.nativeID); + }, + }; + function Test({showB}) { + const fragmentRef = React.useRef(null); + React.useEffect(() => { + fragmentRef.current.observeUsing(observer); + const lastRefValue = fragmentRef.current; + return () => { + lastRefValue.unobserveUsing(observer); + }; + }, []); + return ( + + + + {showB && } + + + ); + } + + await act(() => { + ReactFabric.render(, 11, null, true); + }); + expect(logs).toEqual(['A']); + logs = []; + await act(() => { + ReactFabric.render(, 11, null, true); + }); + expect(logs).toEqual(['B']); + }); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index 023133f2e9781..5c7ccf3987872 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -255,8 +255,16 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) { export function commitNewChildToFragmentInstances( fiber: Fiber, - parentFragmentInstances: Array, + parentFragmentInstances: null | Array, ): void { + if ( + fiber.tag !== HostComponent || + // Only run fragment insertion effects for initial insertions + fiber.alternate !== null || + parentFragmentInstances === null + ) { + return; + } for (let i = 0; i < parentFragmentInstances.length; i++) { const fragmentInstance = parentFragmentInstances[i]; commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance); @@ -384,14 +392,7 @@ function insertOrAppendPlacementNodeIntoContainer( } else { appendChildToContainer(parent, stateNode); } - // TODO: Enable HostText for RN - if ( - enableFragmentRefs && - tag === HostComponent && - // Only run fragment insertion effects for initial insertions - node.alternate === null && - parentFragmentInstances !== null - ) { + if (enableFragmentRefs) { commitNewChildToFragmentInstances(node, parentFragmentInstances); } trackHostMutation(); @@ -449,14 +450,7 @@ function insertOrAppendPlacementNode( } else { appendChild(parent, stateNode); } - // TODO: Enable HostText for RN - if ( - enableFragmentRefs && - tag === HostComponent && - // Only run fragment insertion effects for initial insertions - node.alternate === null && - parentFragmentInstances !== null - ) { + if (enableFragmentRefs) { commitNewChildToFragmentInstances(node, parentFragmentInstances); } trackHostMutation(); @@ -494,10 +488,6 @@ function insertOrAppendPlacementNode( } function commitPlacement(finishedWork: Fiber): void { - if (!supportsMutation) { - return; - } - // Recursively insert all host nodes into the parent. let hostParentFiber; let parentFragmentInstances = null; @@ -517,6 +507,17 @@ function commitPlacement(finishedWork: Fiber): void { } parentFiber = parentFiber.return; } + + if (!supportsMutation) { + if (enableFragmentRefs) { + commitImmutablePlacementNodeToFragmentInstances( + finishedWork, + parentFragmentInstances, + ); + } + return; + } + if (hostParentFiber == null) { throw new Error( 'Expected to find a host parent. This error is likely caused by a bug ' + @@ -581,6 +582,41 @@ function commitPlacement(finishedWork: Fiber): void { } } +function commitImmutablePlacementNodeToFragmentInstances( + finishedWork: Fiber, + parentFragmentInstances: null | Array, +): void { + if (!enableFragmentRefs) { + return; + } + const isHost = finishedWork.tag === HostComponent; + if (isHost) { + commitNewChildToFragmentInstances(finishedWork, parentFragmentInstances); + return; + } else if (finishedWork.tag === HostPortal) { + // If the insertion itself is a portal, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + return; + } + + const child = finishedWork.child; + if (child !== null) { + commitImmutablePlacementNodeToFragmentInstances( + child, + parentFragmentInstances, + ); + let sibling = child.sibling; + while (sibling !== null) { + commitImmutablePlacementNodeToFragmentInstances( + sibling, + parentFragmentInstances, + ); + sibling = sibling.sibling; + } + } +} + export function commitHostPlacement(finishedWork: Fiber) { try { if (__DEV__) { 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 d4fc59b826208..bd16976bb649c 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -18,6 +18,7 @@ global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; let act; +let serverAct; let use; let clientExports; let clientExportsESM; @@ -28,8 +29,6 @@ let ReactDOMClient; let ReactServerDOMServer; let ReactServerDOMClient; let Suspense; -let ReactServerScheduler; -let reactServerAct; let ErrorBoundary; describe('ReactFlightTurbopackDOM', () => { @@ -39,9 +38,8 @@ describe('ReactFlightTurbopackDOM', () => { // condition jest.resetModules(); - ReactServerScheduler = require('scheduler'); - patchSetImmediate(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + patchSetImmediate(); + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react-server-dom-turbopack/server', () => @@ -84,17 +82,6 @@ describe('ReactFlightTurbopackDOM', () => { }; }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - function getTestStream() { const writable = new Stream.PassThrough(); const readable = new ReadableStream({ 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 d90e1189d508e..7530ac9101d2b 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js @@ -21,7 +21,7 @@ let React; let ReactServerDOMServer; let ReactServerDOMClient; let ReactServerScheduler; -let reactServerAct; +let serverAct; describe('ReactFlightTurbopackDOMBrowser', () => { beforeEach(() => { @@ -29,7 +29,7 @@ describe('ReactFlightTurbopackDOMBrowser', () => { ReactServerScheduler = require('scheduler'); patchMessageChannel(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -46,17 +46,6 @@ describe('ReactFlightTurbopackDOMBrowser', () => { ReactServerDOMClient = require('react-server-dom-turbopack/client'); }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - it('should resolve HTML using W3C streams', async () => { function Text({children}) { return {children}; 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 a7b6ff75d57ba..6b6ac31b123ad 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js @@ -21,16 +21,14 @@ let ReactServerDOMServer; let ReactServerDOMClient; let Stream; let use; -let ReactServerScheduler; -let reactServerAct; +let serverAct; describe('ReactFlightTurbopackDOMNode', () => { beforeEach(() => { jest.resetModules(); - ReactServerScheduler = require('scheduler'); - patchSetImmediate(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + patchSetImmediate(); + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -59,17 +57,6 @@ describe('ReactFlightTurbopackDOMNode', () => { use = React.use; }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - function readResult(stream) { return new Promise((resolve, reject) => { let buffer = ''; 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 05a6a227c2b50..0b5f3b60164fc 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -19,6 +19,7 @@ global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; let act; +let serverAct; let use; let clientExports; let clientExportsESM; @@ -37,8 +38,6 @@ let ReactDOMStaticServer; let Suspense; let ErrorBoundary; let JSDOM; -let ReactServerScheduler; -let reactServerAct; let assertConsoleErrorDev; describe('ReactFlightDOM', () => { @@ -50,9 +49,8 @@ describe('ReactFlightDOM', () => { JSDOM = require('jsdom').JSDOM; - ReactServerScheduler = require('scheduler'); - patchSetImmediate(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + patchSetImmediate(); + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -111,17 +109,6 @@ describe('ReactFlightDOM', () => { }; }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - async function readInto( container: Document | HTMLElement, stream: ReadableStream, 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 9d668ea3c3e01..32ad8c7a5b21a 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -24,6 +24,7 @@ let serverExports; let webpackMap; let webpackServerMap; let act; +let serverAct; let React; let ReactDOM; let ReactDOMClient; @@ -35,9 +36,7 @@ let Suspense; let use; let ReactServer; let ReactServerDOM; -let Scheduler; let ReactServerScheduler; -let reactServerAct; let assertConsoleErrorDev; describe('ReactFlightDOMBrowser', () => { @@ -46,7 +45,7 @@ describe('ReactFlightDOMBrowser', () => { ReactServerScheduler = require('scheduler'); patchMessageChannel(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution @@ -73,8 +72,7 @@ describe('ReactFlightDOMBrowser', () => { __unmockReact(); jest.resetModules(); - Scheduler = require('scheduler'); - patchMessageChannel(Scheduler); + patchMessageChannel(); ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); @@ -86,17 +84,6 @@ describe('ReactFlightDOMBrowser', () => { use = React.use; }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - function makeDelayedText(Model) { let error, _resolve, _reject; let promise = new Promise((resolve, reject) => { 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 a4418b701716a..43e0aaa7e7fc3 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -36,7 +36,7 @@ let ReactServerDOMServer; let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; -let reactServerAct; +let serverAct; let assertConsoleErrorDev; function normalizeCodeLocInfo(str) { @@ -66,7 +66,7 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); - reactServerAct = require('internal-test-utils').serverAct; + serverAct = require('internal-test-utils').serverAct; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; @@ -106,17 +106,6 @@ 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. 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 43e36348327ff..381d6a434ba2e 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -26,8 +26,7 @@ let ReactServerDOMStaticServer; let ReactServerDOMClient; let Stream; let use; -let ReactServerScheduler; -let reactServerAct; +let serverAct; // We test pass-through without encoding strings but it should work without it too. const streamOptions = { @@ -38,9 +37,8 @@ describe('ReactFlightDOMNode', () => { beforeEach(() => { jest.resetModules(); - ReactServerScheduler = require('scheduler'); - patchSetImmediate(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + patchSetImmediate(); + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -76,17 +74,6 @@ describe('ReactFlightDOMNode', () => { use = React.use; }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - function readResult(stream) { return new Promise((resolve, reject) => { let buffer = ''; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 88d1b993c3c7f..6e113556206f4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -23,7 +23,7 @@ let React; let ReactServerDOMServer; let ReactServerDOMClient; let ReactServerScheduler; -let reactServerAct; +let serverAct; describe('ReactFlightDOMReply', () => { beforeEach(() => { @@ -31,7 +31,7 @@ describe('ReactFlightDOMReply', () => { ReactServerScheduler = require('scheduler'); patchMessageChannel(ReactServerScheduler); - reactServerAct = require('internal-test-utils').act; + serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -48,17 +48,6 @@ describe('ReactFlightDOMReply', () => { ReactServerDOMClient = require('react-server-dom-webpack/client'); }); - async function serverAct(callback) { - let maybePromise; - await reactServerAct(() => { - maybePromise = callback(); - if (maybePromise && typeof maybePromise.catch === 'function') { - maybePromise.catch(() => {}); - } - }); - return maybePromise; - } - // This method should exist on File but is not implemented in JSDOM async function arrayBuffer(file) { return new Promise((resolve, reject) => { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 1ea4568e1e3b1..f3c70b66c064d 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -4938,6 +4938,13 @@ function finishedTask( // preparation work during the work phase rather than the when flushing. preparePreamble(request); } + } else if (boundary.status === POSTPONED) { + const boundaryRow = boundary.row; + if (boundaryRow !== null) { + if (--boundaryRow.pendingTasks === 0) { + finishSuspenseListRow(request, boundaryRow); + } + } } } else { if (segment !== null && segment.parentFlushed) { diff --git a/scripts/jest/patchMessageChannel.js b/scripts/jest/patchMessageChannel.js index bbcc6690c529c..62716358a845a 100644 --- a/scripts/jest/patchMessageChannel.js +++ b/scripts/jest/patchMessageChannel.js @@ -1,6 +1,6 @@ 'use strict'; -export function patchMessageChannel(Scheduler) { +export function patchMessageChannel() { global.MessageChannel = class { constructor() { const port1 = { @@ -11,18 +11,9 @@ export function patchMessageChannel(Scheduler) { this.port2 = { postMessage(msg) { - if (Scheduler) { - Scheduler.unstable_scheduleCallback( - Scheduler.unstable_NormalPriority, - () => { - port1.onmessage(msg); - } - ); - } else { - throw new Error( - 'MessageChannel patch was used without providing a Scheduler implementation. This is useful for tests that require this class to exist but are not actually utilizing the MessageChannel class. However it appears some test is trying to use this class so you should pass a Scheduler implemenation to the patch method' - ); - } + setTimeout(() => { + port1.onmessage(msg); + }, 0); }, }; } diff --git a/scripts/jest/patchSetImmediate.js b/scripts/jest/patchSetImmediate.js index 831314c664510..b225f1caf62c6 100644 --- a/scripts/jest/patchSetImmediate.js +++ b/scripts/jest/patchSetImmediate.js @@ -1,13 +1,7 @@ 'use strict'; -export function patchSetImmediate(Scheduler) { - if (!Scheduler) { - throw new Error( - 'setImmediate patch was used without providing a Scheduler implementation. If you are patching setImmediate you must provide a Scheduler.' - ); - } - +export function patchSetImmediate() { global.setImmediate = cb => { - Scheduler.unstable_scheduleCallback(Scheduler.unstable_NormalPriority, cb); + setTimeout(cb, 0); }; }