From 23b2d8514f13f109b980b0a1f4f3aab906ad51d0 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 4 Mar 2026 13:39:16 +0100 Subject: [PATCH 1/4] [DevTools] Don't connect to pages that are being prerendered (#35958) --- flow-typed/environments/dom.js | 2 ++ .../src/contentScripts/proxy.js | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/flow-typed/environments/dom.js b/flow-typed/environments/dom.js index 331e73f89148..0ea2d2730a06 100644 --- a/flow-typed/environments/dom.js +++ b/flow-typed/environments/dom.js @@ -1415,6 +1415,8 @@ declare class Document extends Node { links: HTMLCollection; media: string; open(url?: string, name?: string, features?: string, replace?: boolean): any; + /** @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering} */ + prerendering: boolean; readyState: string; referrer: string; scripts: HTMLCollection; diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index a4e7dd68241c..f2b02463600b 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -10,7 +10,7 @@ 'use strict'; -function injectProxy({target}: {target: any}) { +function injectProxy() { // Firefox's behaviour for injecting this content script can be unpredictable // While navigating the history, some content scripts might not be re-injected and still be alive if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) { @@ -32,9 +32,23 @@ function injectProxy({target}: {target: any}) { } } +function handlePageShow() { + if (document.prerendering) { + // React DevTools can't handle multiple documents being connected to the same extension port. + // However, browsers are firing pageshow events while prerendering (https://issues.chromium.org/issues/489633225). + // We need to wait until prerendering is finished before injecting the proxy. + // In browsers with pagereveal support, listening to pagereveal would be sufficient. + // Waiting for prerenderingchange is a workaround to support browsers that + // have speculationrules but not pagereveal. + document.addEventListener('prerenderingchange', injectProxy, {once: true}); + } else { + injectProxy(); + } +} + window.addEventListener('pagereveal', injectProxy); // For backwards compat with browsers not implementing `pagereveal` which is a fairly new event. -window.addEventListener('pageshow', injectProxy); +window.addEventListener('pageshow', handlePageShow); window.addEventListener('pagehide', function ({target}) { if (target !== window.document) { From ee4699f5a1833bd12701d21556f36376d6ebad8b Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 4 Mar 2026 13:41:58 +0100 Subject: [PATCH 2/4] [noop] Fail tests on unasserted recoverable errors (#35948) --- .../react-noop-renderer/src/createReactNoop.js | 6 +++--- ...eactIncrementalErrorHandling-test.internal.js | 16 ++++++++++++++++ .../ReactIncrementalErrorReplay-test.js | 6 ++++++ .../src/__tests__/useMemoCache-test.js | 13 +++++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 79d1e1a8c9bc..99be1aae122a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -1151,9 +1151,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } } - function onRecoverableError(error) { - // TODO: Turn this on once tests are fixed - // console.error(error); + function onRecoverableError(error: mixed): void { + // eslint-disable-next-line react-internal/warning-args, react-internal/no-production-logging -- renderer is only used for testing. + console.error(error); } function onDefaultTransitionIndicator(): void | (() => void) {} diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index f81a83379f8d..193ffa33889f 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -287,6 +287,10 @@ describe('ReactIncrementalErrorHandling', () => { 'commit', 'commit', ]); + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); expect(ReactNoop).toMatchRenderedOutput( , ); @@ -339,6 +343,10 @@ describe('ReactIncrementalErrorHandling', () => { 'commit', 'commit', ]); + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); // This should not include the offscreen content expect(ReactNoop).toMatchRenderedOutput( <> @@ -1786,6 +1794,10 @@ describe('ReactIncrementalErrorHandling', () => { }); // Should finish without throwing. + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); expect(root).toMatchRenderedOutput('Everything is fine.'); }); @@ -1832,6 +1844,10 @@ describe('ReactIncrementalErrorHandling', () => { }); // Should render the final state without throwing the error. assertLog(['Everything is fine.']); + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); expect(root).toMatchRenderedOutput('Everything is fine.'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.js index ed4317d95706..038fee759d21 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.js @@ -12,6 +12,7 @@ let React; let ReactNoop; +let assertConsoleErrorDev; let waitForAll; let waitForThrow; @@ -22,6 +23,7 @@ describe('ReactIncrementalErrorReplay', () => { ReactNoop = require('react-noop-renderer'); const InternalTestUtils = require('internal-test-utils'); + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; waitForAll = InternalTestUtils.waitForAll; waitForThrow = InternalTestUtils.waitForThrow; }); @@ -50,5 +52,9 @@ describe('ReactIncrementalErrorReplay', () => { } ReactNoop.render(); await waitForAll([]); + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); }); }); diff --git a/packages/react-reconciler/src/__tests__/useMemoCache-test.js b/packages/react-reconciler/src/__tests__/useMemoCache-test.js index c07d3a6d9960..13c483a6308a 100644 --- a/packages/react-reconciler/src/__tests__/useMemoCache-test.js +++ b/packages/react-reconciler/src/__tests__/useMemoCache-test.js @@ -12,6 +12,7 @@ let React; let ReactNoop; let Scheduler; let act; +let assertConsoleErrorDev; let assertLog; let useMemo; let useState; @@ -26,8 +27,10 @@ describe('useMemoCache()', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); - act = require('internal-test-utils').act; - assertLog = require('internal-test-utils').assertLog; + const InternalTestUtils = require('internal-test-utils'); + act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; + assertLog = InternalTestUtils.assertLog; useMemo = React.useMemo; useMemoCache = require('react/compiler-runtime').c; useState = React.useState; @@ -256,8 +259,6 @@ describe('useMemoCache()', () => { return `${data.text} (n=${props.n})`; }); - spyOnDev(console, 'error'); - const root = ReactNoop.createRoot(); await act(() => { root.render( @@ -274,6 +275,10 @@ describe('useMemoCache()', () => { // this triggers a throw. setN(1); }); + assertConsoleErrorDev([ + 'Error: There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.' + + '\n in ', + ]); expect(root).toMatchRenderedOutput('Count 0 (n=1)'); expect(Text).toBeCalledTimes(2); expect(data).toBe(data0); From 5e4279134dbc29116407a5ec515e74e2c0ae5018 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 4 Mar 2026 13:52:11 +0100 Subject: [PATCH 3/4] [noop] Typecheck `react-noop-renderer` against host config and renderer API (#35944) --- packages/react-client/flight.js | 7 + .../src/forks/ReactFlightClientConfig.noop.js | 57 ++++ .../src/ReactFiberConfigNoop.js | 33 ++ packages/react-noop-renderer/src/ReactNoop.js | 1 + .../src/ReactNoopFlightClient.js | 16 +- .../src/ReactNoopFlightServer.js | 11 +- .../src/ReactNoopPersistent.js | 1 + .../src/ReactNoopServer.js | 14 +- .../src/createReactNoop.js | 165 +++++++--- packages/react-reconciler/index.js | 7 + .../src/forks/ReactFiberConfig.noop.js | 282 ++++++++++++++++++ packages/react-server/flight.js | 7 + packages/react-server/index.js | 7 + .../src/forks/ReactFizzConfig.noop.js | 108 +++++++ .../src/forks/ReactFlightServerConfig.noop.js | 47 +++ .../src/forks/ReactServerStreamConfig.noop.js | 48 +++ scripts/flow/config/flowconfig | 3 - scripts/jest/setupHostConfigs.js | 6 + scripts/jest/setupTests.persistent.js | 4 - scripts/jest/setupTests.xplat.js | 4 - scripts/shared/inlinedHostConfigs.js | 19 ++ 21 files changed, 782 insertions(+), 65 deletions(-) create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.noop.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoop.js create mode 100644 packages/react-reconciler/src/forks/ReactFiberConfig.noop.js create mode 100644 packages/react-server/src/forks/ReactFizzConfig.noop.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.noop.js create mode 100644 packages/react-server/src/forks/ReactServerStreamConfig.noop.js diff --git a/packages/react-client/flight.js b/packages/react-client/flight.js index 9089ae80c70f..6ff3072445c9 100644 --- a/packages/react-client/flight.js +++ b/packages/react-client/flight.js @@ -7,4 +7,11 @@ * @flow */ +import typeof * as FlightClientAPI from './src/ReactFlightClient'; +import typeof * as HostConfig from './src/ReactFlightClientConfig'; + export * from './src/ReactFlightClient'; + +// At build time, this module is wrapped as a factory function ($$$reconciler). +// Consumers pass a host config object and get back the Flight client API. +declare export default (hostConfig: HostConfig) => FlightClientAPI; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.noop.js b/packages/react-client/src/forks/ReactFlightClientConfig.noop.js new file mode 100644 index 000000000000..911cf063d0eb --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.noop.js @@ -0,0 +1,57 @@ +/** + * 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 + */ + +// This is a host config that's used for the internal `react-noop-renderer` package. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-server` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-server` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* renderer code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +declare const $$$config: $FlowFixMe; + +export opaque type ModuleLoading = mixed; +export opaque type ServerConsumerModuleMap = mixed; +export opaque type ServerManifest = mixed; +export opaque type ServerReferenceId = string; +export opaque type ClientReferenceMetadata = mixed; +export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars +export const resolveClientReference = $$$config.resolveClientReference; +export const resolveServerReference = $$$config.resolveServerReference; +export const preloadModule = $$$config.preloadModule; +export const requireModule = $$$config.requireModule; +export const getModuleDebugInfo = $$$config.getModuleDebugInfo; +export const dispatchHint = $$$config.dispatchHint; +export const prepareDestinationForModule = + $$$config.prepareDestinationForModule; +export const usedWithSSR = true; + +export opaque type Source = mixed; + +export opaque type StringDecoder = mixed; + +export const createStringDecoder = $$$config.createStringDecoder; +export const readPartialStringChunk = $$$config.readPartialStringChunk; +export const readFinalStringChunk = $$$config.readFinalStringChunk; + +export const bindToConsole = $$$config.bindToConsole; + +export const rendererVersion = $$$config.rendererVersion; +export const rendererPackageName = $$$config.rendererPackageName; + +export const checkEvalAvailabilityOnceDev = + $$$config.checkEvalAvailabilityOnceDev; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoop.js b/packages/react-noop-renderer/src/ReactFiberConfigNoop.js new file mode 100644 index 000000000000..fd8aa0bbc191 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoop.js @@ -0,0 +1,33 @@ +/** + * 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 type HostContext = Object; + +export type TextInstance = { + text: string, + id: number, + parent: number, + hidden: boolean, + context: HostContext, +}; + +export type Instance = { + type: string, + id: number, + parent: number, + children: Array, + text: string | null, + prop: any, + hidden: boolean, + context: HostContext, +}; + +export type PublicInstance = Instance; + +export type TransitionStatus = mixed; diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index df55810493cc..56486fddc3b3 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -53,6 +53,7 @@ export const { getRoot, // TODO: Remove this after callers migrate to alternatives. unstable_runWithPriority, + // $FlowFixMe[signature-verification-failure] } = createReactNoop( ReactFiberReconciler, // reconciler true, // useMutation diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 4699b149e85f..cf7e4168bd89 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -14,6 +14,7 @@ * environment. */ +import type {Thenable} from 'shared/ReactTypes'; import type {FindSourceMapURLCallback} from 'react-client/flight'; import {readModule} from 'react-noop-renderer/flight-modules'; @@ -25,6 +26,7 @@ type Source = Array; const decoderOptions = {stream: true}; const {createResponse, createStreamState, processBinaryChunk, getRoot, close} = + // $FlowFixMe[prop-missing] ReactFlightClient({ createStringDecoder() { return new TextDecoder(); @@ -44,6 +46,7 @@ const {createResponse, createStreamState, processBinaryChunk, getRoot, close} = return readModule(idx); }, bindToConsole(methodName, args, badgeName) { + // $FlowFixMe[incompatible-call] return Function.prototype.bind.apply( // eslint-disable-next-line react-internal/no-production-logging console[methodName], @@ -61,8 +64,10 @@ type ReadOptions = {| function read(source: Source, options: ReadOptions): Thenable { const response = createResponse( + // $FlowFixMe[incompatible-call] source, null, + // $FlowFixMe[incompatible-call] null, undefined, undefined, @@ -73,12 +78,19 @@ function read(source: Source, options: ReadOptions): Thenable { true, undefined, __DEV__ && options !== undefined && options.debugChannel !== undefined - ? options.debugChannel.onMessage + ? // $FlowFixMe[incompatible-call] + options.debugChannel.onMessage : undefined, ); const streamState = createStreamState(response, source); for (let i = 0; i < source.length; i++) { - processBinaryChunk(response, streamState, source[i], 0); + processBinaryChunk( + response, + streamState, + source[i], + // $FlowFixMe[extra-arg] + 0, + ); } if (options !== undefined && options.close) { close(response); diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 752652259768..a10edd2b7737 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -20,10 +20,11 @@ import {saveModule} from 'react-noop-renderer/flight-modules'; import ReactFlightServer from 'react-server/flight'; -type Destination = Array; +type Destination = Array; const textEncoder = new TextEncoder(); +// $FlowFixMe[prop-missing] const ReactNoopFlightServer = ReactFlightServer({ scheduleMicrotask(callback: () => void) { callback(); @@ -81,6 +82,7 @@ function render(model: ReactClientValue, options?: Options): Destination { const bundlerConfig = undefined; const request = ReactNoopFlightServer.createRequest( model, + // $FlowFixMe[incompatible-call] bundlerConfig, options ? options.onError : undefined, options ? options.identifierPrefix : undefined, @@ -88,6 +90,7 @@ function render(model: ReactClientValue, options?: Options): Destination { options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + // $FlowFixMe[incompatible-call] __DEV__ && options && options.debugChannel !== undefined, ); const signal = options ? options.signal : undefined; @@ -108,7 +111,11 @@ function render(model: ReactClientValue, options?: Options): Destination { }; } ReactNoopFlightServer.startWork(request); - ReactNoopFlightServer.startFlowing(request, destination); + ReactNoopFlightServer.startFlowing( + request, + // $FlowFixMe[incompatible-call] + destination, + ); return destination; } diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js index 040114b86b8a..83ca00ac9ba9 100644 --- a/packages/react-noop-renderer/src/ReactNoopPersistent.js +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -55,6 +55,7 @@ export const { // TODO: Remove this once callers migrate to alternatives. // This should only be used by React internals. unstable_runWithPriority, + // $FlowFixMe[signature-verification-failure] } = createReactNoop( ReactFiberReconciler, // reconciler false, // useMutation diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 913e72d7fc4f..93f66b27b306 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -40,12 +40,12 @@ type SuspenseInstance = { }; type Placeholder = { - parent: Instance | SuspenseInstance, + parent: Instance | Segment | SuspenseInstance, index: number, }; type Segment = { - children: null | Instance | TextInstance | SuspenseInstance, + children: Array, }; type Destination = { @@ -79,6 +79,7 @@ function write(destination: Destination, buffer: Uint8Array): void { stack.push(instance); } +// $FlowFixMe[prop-missing] const ReactNoopServer = ReactFizzServer({ scheduleMicrotask(callback: () => void) { callback(); @@ -175,6 +176,7 @@ const ReactNoopServer = ReactFizzServer({ destination: Destination, renderState: RenderState, id: number, + // $FlowFixMe[incompatible-return] ): boolean { const parent = destination.stack[destination.stack.length - 1]; destination.placeholders.set(id, { @@ -258,7 +260,7 @@ const ReactNoopServer = ReactFizzServer({ formatContext: null, id: number, ): boolean { - const segment = { + const segment: Segment = { children: [], }; destination.segments.set(id, segment); @@ -314,6 +316,7 @@ const ReactNoopServer = ReactFizzServer({ renderState: RenderState, boundary: SuspenseInstance, ): boolean { + // $FlowFixMe[prop-missing] boundary.status = 'client-render'; return true; }, @@ -357,6 +360,7 @@ type Options = { }; function render(children: React$Element, options?: Options): Destination { + // $FlowFixMe[prop-missing] const destination: Destination = { root: null, placeholders: new Map(), @@ -368,8 +372,11 @@ function render(children: React$Element, options?: Options): Destination { }; const request = ReactNoopServer.createRequest( children, + // $FlowFixMe[incompatible-call] null, + // $FlowFixMe[incompatible-call] null, + // $FlowFixMe[incompatible-call] null, options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, @@ -377,6 +384,7 @@ function render(children: React$Element, options?: Options): Destination { options ? options.onShellReady : undefined, ); ReactNoopServer.startWork(request); + // $FlowFixMe[incompatible-call] ReactNoopServer.startFlowing(request, destination); return destination; } diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 99be1aae122a..2980ea8e25bc 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -23,6 +23,14 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; +import typeof * as HostConfig from 'react-reconciler/src/ReactFiberConfig'; +import typeof * as ReconcilerAPI from 'react-reconciler/src/ReactFiberReconciler'; +import type { + HostContext, + Instance, + PublicInstance, + TextInstance, +} from './ReactFiberConfigNoop'; import * as Scheduler from 'scheduler/unstable_mock'; import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -58,28 +66,19 @@ type Props = { src?: string, ... }; -type Instance = { - type: string, - id: number, - parent: number, - children: Array, - text: string | null, - prop: any, - hidden: boolean, - context: HostContext, -}; -type TextInstance = { - text: string, - id: number, - parent: number, - hidden: boolean, - context: HostContext, -}; -type HostContext = Object; type CreateRootOptions = { unstable_transitionCallbacks?: TransitionTracingCallbacks, - onUncaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void, - onCaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void, + onUncaughtError?: ( + error: mixed, + errorInfo: {+componentStack: ?string}, + ) => void, + onCaughtError?: ( + error: mixed, + errorInfo: { + +componentStack: ?string, + +errorBoundary?: ?component(...props: any), + }, + ) => void, onDefaultTransitionIndicator?: () => void | (() => void), ... }; @@ -108,7 +107,11 @@ if (__DEV__) { Object.freeze(NO_CONTEXT); } -function createReactNoop(reconciler: Function, useMutation: boolean) { +// $FlowFixMe[signature-verification-failure] +function createReactNoop( + reconciler: (hostConfig: HostConfig) => ReconcilerAPI, + useMutation: boolean, +): any { let instanceCounter = 0; let hostUpdateCounter = 0; let hostCloneCounter = 0; @@ -118,9 +121,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { child: Instance | TextInstance, ): void { const prevParent = child.parent; + // $FlowFixMe[prop-missing] if (prevParent !== -1 && prevParent !== parentInstance.id) { throw new Error('Reparenting is not allowed'); } + // $FlowFixMe[prop-missing] child.parent = parentInstance.id; const index = parentInstance.children.indexOf(child); if (index !== -1) { @@ -265,6 +270,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }; if (type === 'suspensey-thing' && typeof newProps.src === 'string') { + // $FlowFixMe[prop-missing] clone.src = newProps.src; } @@ -285,6 +291,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { enumerable: false, }); hostCloneCounter++; + // $FlowFixMe[incompatible-return] return clone; } @@ -299,7 +306,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { ); } - function computeText(rawText, hostContext) { + function computeText(rawText: string, hostContext: HostContext) { return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText; } @@ -333,6 +340,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // Attach a listener to the suspensey thing and create a subscription // object that uses reference counting to track when all the suspensey // things have loaded. + // $FlowFixMe const record = suspenseyThingCache.get(src); if (record === undefined) { throw new Error('Could not find record for key.'); @@ -344,8 +352,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // Stash the subscription on the record. In `resolveSuspenseyThing`, // we'll use this fire the commit once all the things have loaded. if (record.subscriptions === null) { + // $FlowFixMe[incompatible-use] record.subscriptions = []; } + // $FlowFixMe[incompatible-use] record.subscriptions.push(state); } } else { @@ -361,7 +371,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { timeoutOffset: number, ): ((commit: () => mixed) => () => void) | null { if (state.pendingCount > 0) { - return (commit: () => void) => { + return (commit: () => mixed) => { + // $FlowFixMe[incompatible-type] state.commit = commit; const cancelCommit = () => { state.commit = null; @@ -392,8 +403,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return NO_CONTEXT; }, - getPublicInstance(instance) { - return instance; + getPublicInstance(instance: Instance): PublicInstance { + return (instance: any); }, createInstance( @@ -413,7 +424,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { checkPropStringCoercion(props.children, 'children'); } } - const inst = { + const inst: Instance = { id: instanceCounter++, type: type, children: [], @@ -428,6 +439,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }; if (type === 'suspensey-thing' && typeof props.src === 'string') { + // $FlowFixMe[prop-missing] inst.src = props.src; } @@ -445,10 +457,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { value: inst.context, enumerable: false, }); + // $FlowFixMe[prop-missing] Object.defineProperty(inst, 'fiber', { value: internalInstanceHandle, enumerable: false, }); + // $FlowFixMe[incompatible-return] return inst; }, @@ -511,19 +525,19 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { throw new Error('Not yet implemented.'); }, - createFragmentInstance(fragmentFiber) { + createFragmentInstance(fragmentFiber: mixed) { return null; }, - updateFragmentInstanceFiber(fragmentFiber, fragmentInstance) { + updateFragmentInstanceFiber(fragmentFiber: mixed, fragmentInstance: mixed) { // Noop }, - commitNewChildToFragmentInstance(child, fragmentInstance) { + commitNewChildToFragmentInstance(child: mixed, fragmentInstance: mixed) { // Noop }, - deleteChildFromFragmentInstance(child, fragmentInstance) { + deleteChildFromFragmentInstance(child: mixed, fragmentInstance: mixed) { // Noop }, @@ -536,7 +550,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise !== 'undefined' - ? callback => + ? (callback: () => void) => Promise.resolve(null) .then(callback) .catch(error => { @@ -610,7 +624,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // no-op }, - requestPostPaintCallback(callback) { + requestPostPaintCallback(callback: (time: number) => void) { const endTime = Scheduler.unstable_now(); callback(endTime); }, @@ -661,19 +675,23 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } + // $FlowFixMe const record = suspenseyThingCache.get(props.src); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'pending', subscriptions: null, }; + // $FlowFixMe suspenseyThingCache.set(props.src, newRecord); + // $FlowFixMe[prop-missing] const onLoadStart = props.onLoadStart; if (typeof onLoadStart === 'function') { onLoadStart(); } return false; } else { + // $FlowFixMe[prop-missing] return record.status === 'fulfilled'; } }, @@ -713,7 +731,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { resetFormInstance(form: Instance) {}, - bindToConsole(methodName, args, badgeName) { + bindToConsole(methodName: $FlowFixMe, args: Array, badgeName: string) { + // $FlowFixMe[incompatible-call] return Function.prototype.bind.apply( // eslint-disable-next-line react-internal/no-production-logging console[methodName], @@ -722,8 +741,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }, }; - const hostConfig = useMutation - ? { + const hostConfig: HostConfig = useMutation + ? // $FlowFixMe[prop-missing] + { ...sharedHostConfig, supportsMutation: true, @@ -747,6 +767,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { instance.hidden = !!newProps.hidden; if (type === 'suspensey-thing' && typeof newProps.src === 'string') { + // $FlowFixMe[prop-missing] instance.src = newProps.src; } @@ -907,7 +928,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { instance.text = null; }, } - : { + : // $FlowFixMe[prop-missing] + { ...sharedHostConfig, supportsMutation: false, supportsPersistence: true, @@ -987,8 +1009,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { const NoopRenderer = reconciler(hostConfig); - const rootContainers = new Map(); - const roots = new Map(); + const rootContainers = new Map(); + const roots = new Map(); const DEFAULT_ROOT_ID = ''; let currentUpdatePriority = NoEventPriority; @@ -1002,6 +1024,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { let currentEventPriority = DefaultEventPriority; + // $FlowFixMe[missing-local-annot] function createJSXElementForTestComparison(type, props) { if (__DEV__) { const element = { @@ -1012,6 +1035,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { _owner: null, _store: __DEV__ ? {} : undefined, }; + // $FlowFixMe[prop-missing] Object.defineProperty(element, 'ref', { enumerable: false, value: null, @@ -1028,6 +1052,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } } + // $FlowFixMe function childToJSX(child, text) { if (text !== null) { return text; @@ -1066,6 +1091,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (instance.hidden) { props.hidden = true; } + // $FlowFixMe[prop-missing] if (instance.src) { props.src = instance.src; } @@ -1082,6 +1108,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return textInstance.text; } + // $FlowFixMe[missing-local-annot] function getChildren(root) { if (root) { return root.children; @@ -1090,6 +1117,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } } + // $FlowFixMe[missing-local-annot] function getPendingChildren(root) { if (root) { return root.children; @@ -1098,6 +1126,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } } + // $FlowFixMe[missing-local-annot] function getChildrenAsJSX(root) { const children = childToJSX(getChildren(root), null); if (children === null) { @@ -1109,6 +1138,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return children; } + // $FlowFixMe[missing-local-annot] function getPendingChildrenAsJSX(root) { const children = childToJSX(getChildren(root), null); if (children === null) { @@ -1139,6 +1169,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (fn) { return fn(); } else { + // $FlowFixMe[incompatible-return] return undefined; } } finally { @@ -1159,6 +1190,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { let idCounter = 0; + // $FlowFixMe const ReactNoop = { _Scheduler: Scheduler, @@ -1199,12 +1231,19 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) { let root = roots.get(rootID); if (!root) { - const container = {rootID: rootID, pendingChildren: [], children: []}; + const container: Container = { + rootID: rootID, + pendingChildren: [], + children: [], + }; + // $FlowFixMe[incompatible-call] rootContainers.set(rootID, container); root = NoopRenderer.createContainer( + // $FlowFixMe[incompatible-call] -- Discovered when typechecking noop-renderer container, tag, null, + // $FlowFixMe[incompatible-call] -- Discovered when typechecking noop-renderer null, false, '', @@ -1221,15 +1260,17 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // TODO: Replace ReactNoop.render with createRoot + root.render createRoot(options?: CreateRootOptions) { - const container = { + const container: Container = { rootID: '' + idCounter++, pendingChildren: [], children: [], }; const fiberRoot = NoopRenderer.createContainer( + // $FlowFixMe[incompatible-call] container, ConcurrentRoot, null, + // $FlowFixMe[incompatible-call] null, false, '', @@ -1272,9 +1313,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { children: [], }; const fiberRoot = NoopRenderer.createContainer( + // $FlowFixMe[incompatible-call] -- TODO: Discovered when typechecking noop-renderer container, LegacyRoot, null, + // $FlowFixMe[incompatible-call] -- TODO: Discovered when typechecking noop-renderer null, false, '', @@ -1309,11 +1352,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return getPendingChildrenAsJSX(container); }, + // $FlowFixMe[missing-local-annot] getSuspenseyThingStatus(src): string | null { if (suspenseyThingCache === null) { return null; } else { const record = suspenseyThingCache.get(src); + // $FlowFixMe[prop-missing] return record === undefined ? null : record.status; } }, @@ -1322,18 +1367,24 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } + // $FlowFixMe[incompatible-call] const record = suspenseyThingCache.get(key); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'fulfilled', subscriptions: null, }; + // $FlowFixMe suspenseyThingCache.set(key, newRecord); } else { + // $FlowFixMe[prop-missing] if (record.status === 'pending') { + // $FlowFixMe[incompatible-use] record.status = 'fulfilled'; + // $FlowFixMe[prop-missing] const subscriptions = record.subscriptions; if (subscriptions !== null) { + // $FlowFixMe[incompatible-use] record.subscriptions = null; for (let i = 0; i < subscriptions.length; i++) { const subscription = subscriptions[i]; @@ -1411,11 +1462,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return component; } if (__DEV__) { + // $FlowFixMe[incompatible-return] return NoopRenderer.findHostInstanceWithWarning( component, 'findInstance', ); } + // $FlowFixMe[incompatible-return] return NoopRenderer.findHostInstance(component); }, @@ -1474,6 +1527,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { discreteUpdates: NoopRenderer.discreteUpdates, + // $FlowFixMe[incompatible-return] idleUpdates(fn: () => T): T { const prevEventPriority = currentEventPriority; currentEventPriority = IdleEventPriority; @@ -1497,14 +1551,16 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return; } - const bufferedLog = []; - function log(...args) { + const bufferedLog: string[] = []; + // $FlowFixMe[missing-local-annot] + function log(...args: string[]) { + // $FlowFixMe[incompatible-call] bufferedLog.push(...args, '\n'); } function logHostInstances( children: Array, - depth, + depth: number, ) { for (let i = 0; i < children.length; i++) { const child = children[i]; @@ -1512,17 +1568,23 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (typeof child.text === 'string') { log(indent + '- ' + child.text); } else { + // $FlowFixMe[unsafe-addition] log(indent + '- ' + child.type + '#' + child.id); - logHostInstances(child.children, depth + 1); + + logHostInstances( + // $FlowFixMe[incompatible-call] + child.children, + depth + 1, + ); } } } - function logContainer(container: Container, depth) { + function logContainer(container: Container, depth: number) { log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); logHostInstances(container.children, depth + 1); } - function logUpdateQueue(updateQueue: UpdateQueue, depth) { + function logUpdateQueue(updateQueue: UpdateQueue, depth: number) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); const first = updateQueue.firstBaseUpdate; const update = first; @@ -1530,6 +1592,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { do { log( ' '.repeat(depth + 1) + '~', + // $FlowFixMe '[' + update.expirationTime + ']', ); } while (update !== null); @@ -1543,6 +1606,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { do { log( ' '.repeat(depth + 1) + '~', + // $FlowFixMe '[' + pendingUpdate.expirationTime + ']', ); } while (pendingUpdate !== null && pendingUpdate !== firstPending); @@ -1550,19 +1614,26 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { } } + // $FlowFixMe[missing-local-annot] function logFiber(fiber: Fiber, depth) { log( ' '.repeat(depth) + '- ' + // need to explicitly coerce Symbol to a string (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), + // $FlowFixMe[unsafe-addition] '[' + + // $FlowFixMe[prop-missing] fiber.childExpirationTime + (fiber.pendingProps ? '*' : '') + ']', ); if (fiber.updateQueue) { - logUpdateQueue(fiber.updateQueue, depth); + logUpdateQueue( + // $FlowFixMe[incompatible-call] + fiber.updateQueue, + depth, + ); } // const childInProgress = fiber.progressedChild; // if (childInProgress && childInProgress !== fiber.child) { diff --git a/packages/react-reconciler/index.js b/packages/react-reconciler/index.js index afffd715cffe..0a119d7e3db8 100644 --- a/packages/react-reconciler/index.js +++ b/packages/react-reconciler/index.js @@ -7,4 +7,11 @@ * @flow */ +import typeof * as ReconcilerAPI from './src/ReactFiberReconciler'; +import typeof * as HostConfig from './src/ReactFiberConfig'; + export * from './src/ReactFiberReconciler'; + +// At build time, this module is wrapped as a factory function ($$$reconciler). +// Consumers pass a host config object and get back the reconciler API. +declare export default (hostConfig: HostConfig) => ReconcilerAPI; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js b/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js new file mode 100644 index 000000000000..9baaee1f5c9a --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js @@ -0,0 +1,282 @@ +/** + * 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 + */ + +// This is a host config that's used for the internal `react-noop-renderer`. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-reconciler` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-reconciler` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* reconciler code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +export * from 'react-noop-renderer/src/ReactFiberConfigNoop'; + +declare const $$$config: $FlowFixMe; +export opaque type Type = mixed; +export opaque type Props = mixed; +export opaque type Container = mixed; +export opaque type ActivityInstance = mixed; +export opaque type SuspenseInstance = mixed; +export opaque type HydratableInstance = mixed; +export opaque type UpdatePayload = mixed; +export opaque type ChildSet = mixed; +export opaque type TimeoutHandle = mixed; +export opaque type NoTimeout = mixed; +export opaque type RendererInspectionConfig = mixed; +export opaque type FormInstance = mixed; +export opaque type SuspendedState = mixed; +export type RunningViewTransition = mixed; +export type ViewTransitionInstance = null | {name: string, ...}; +export opaque type InstanceMeasurement = mixed; +export type EventResponder = any; +export type GestureTimeline = any; +export type FragmentInstanceType = null; + +export const rendererVersion = $$$config.rendererVersion; +export const rendererPackageName = $$$config.rendererPackageName; +export const extraDevToolsConfig = $$$config.extraDevToolsConfig; + +export const getPublicInstance = $$$config.getPublicInstance; +export const getRootHostContext = $$$config.getRootHostContext; +export const getChildHostContext = $$$config.getChildHostContext; +export const prepareForCommit = $$$config.prepareForCommit; +export const resetAfterCommit = $$$config.resetAfterCommit; +export const createInstance = $$$config.createInstance; +export const cloneMutableInstance = $$$config.cloneMutableInstance; +export const appendInitialChild = $$$config.appendInitialChild; +export const finalizeInitialChildren = $$$config.finalizeInitialChildren; +export const shouldSetTextContent = $$$config.shouldSetTextContent; +export const createTextInstance = $$$config.createTextInstance; +export const cloneMutableTextInstance = $$$config.cloneMutableTextInstance; +export const scheduleTimeout = $$$config.scheduleTimeout; +export const cancelTimeout = $$$config.cancelTimeout; +export const noTimeout = $$$config.noTimeout; +export const isPrimaryRenderer = $$$config.isPrimaryRenderer; +export const warnsIfNotActing = $$$config.warnsIfNotActing; +export const supportsMutation = $$$config.supportsMutation; +export const supportsPersistence = $$$config.supportsPersistence; +export const supportsHydration = $$$config.supportsHydration; +export const getInstanceFromNode = $$$config.getInstanceFromNode; +export const beforeActiveInstanceBlur = $$$config.beforeActiveInstanceBlur; +export const afterActiveInstanceBlur = $$$config.afterActiveInstanceBlur; +export const preparePortalMount = $$$config.preparePortalMount; +export const prepareScopeUpdate = $$$config.prepareScopeUpdate; +export const getInstanceFromScope = $$$config.getInstanceFromScope; +export const setCurrentUpdatePriority = $$$config.setCurrentUpdatePriority; +export const getCurrentUpdatePriority = $$$config.getCurrentUpdatePriority; +export const resolveUpdatePriority = $$$config.resolveUpdatePriority; +export const trackSchedulerEvent = $$$config.trackSchedulerEvent; +export const resolveEventType = $$$config.resolveEventType; +export const resolveEventTimeStamp = $$$config.resolveEventTimeStamp; +export const shouldAttemptEagerTransition = + $$$config.shouldAttemptEagerTransition; +export const detachDeletedInstance = $$$config.detachDeletedInstance; +export const requestPostPaintCallback = $$$config.requestPostPaintCallback; +export const maySuspendCommit = $$$config.maySuspendCommit; +export const maySuspendCommitOnUpdate = $$$config.maySuspendCommitOnUpdate; +export const maySuspendCommitInSyncRender = + $$$config.maySuspendCommitInSyncRender; +export const preloadInstance = $$$config.preloadInstance; +export const startSuspendingCommit = $$$config.startSuspendingCommit; +export const suspendInstance = $$$config.suspendInstance; +export const suspendOnActiveViewTransition = + $$$config.suspendOnActiveViewTransition; +export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady; +export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason; +export const NotPendingTransition = $$$config.NotPendingTransition; +export const HostTransitionContext = $$$config.HostTransitionContext; +export const resetFormInstance = $$$config.resetFormInstance; +export const bindToConsole = $$$config.bindToConsole; + +// ------------------- +// Microtasks +// (optional) +// ------------------- +export const supportsMicrotasks = $$$config.supportsMicrotasks; +export const scheduleMicrotask = $$$config.scheduleMicrotask; + +// ------------------- +// Test selectors +// (optional) +// ------------------- +export const supportsTestSelectors = $$$config.supportsTestSelectors; +export const findFiberRoot = $$$config.findFiberRoot; +export const getBoundingRect = $$$config.getBoundingRect; +export const getTextContent = $$$config.getTextContent; +export const isHiddenSubtree = $$$config.isHiddenSubtree; +export const matchAccessibilityRole = $$$config.matchAccessibilityRole; +export const setFocusIfFocusable = $$$config.setFocusIfFocusable; +export const setupIntersectionObserver = $$$config.setupIntersectionObserver; + +// ------------------- +// Mutation +// (optional) +// ------------------- +export const appendChild = $$$config.appendChild; +export const appendChildToContainer = $$$config.appendChildToContainer; +export const commitTextUpdate = $$$config.commitTextUpdate; +export const commitMount = $$$config.commitMount; +export const commitUpdate = $$$config.commitUpdate; +export const insertBefore = $$$config.insertBefore; +export const insertInContainerBefore = $$$config.insertInContainerBefore; +export const removeChild = $$$config.removeChild; +export const removeChildFromContainer = $$$config.removeChildFromContainer; +export const resetTextContent = $$$config.resetTextContent; +export const hideInstance = $$$config.hideInstance; +export const hideTextInstance = $$$config.hideTextInstance; +export const unhideInstance = $$$config.unhideInstance; +export const unhideTextInstance = $$$config.unhideTextInstance; +export const applyViewTransitionName = $$$config.applyViewTransitionName; +export const restoreViewTransitionName = $$$config.restoreViewTransitionName; +export const cancelViewTransitionName = $$$config.cancelViewTransitionName; +export const cancelRootViewTransitionName = + $$$config.cancelRootViewTransitionName; +export const restoreRootViewTransitionName = + $$$config.restoreRootViewTransitionName; +export const cloneRootViewTransitionContainer = + $$$config.cloneRootViewTransitionContainer; +export const removeRootViewTransitionClone = + $$$config.removeRootViewTransitionClone; +export const measureInstance = $$$config.measureInstance; +export const measureClonedInstance = $$$config.measureClonedInstance; +export const wasInstanceInViewport = $$$config.wasInstanceInViewport; +export const hasInstanceChanged = $$$config.hasInstanceChanged; +export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent; +export const startViewTransition = $$$config.startViewTransition; +export const startGestureTransition = $$$config.startGestureTransition; +export const stopViewTransition = $$$config.stopViewTransition; +export const addViewTransitionFinishedListener = + $$$config.addViewTransitionFinishedListener; +export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset; +export const createViewTransitionInstance = + $$$config.createViewTransitionInstance; +export const clearContainer = $$$config.clearContainer; +export const createFragmentInstance = $$$config.createFragmentInstance; +export const updateFragmentInstanceFiber = + $$$config.updateFragmentInstanceFiber; +export const commitNewChildToFragmentInstance = + $$$config.commitNewChildToFragmentInstance; +export const deleteChildFromFragmentInstance = + $$$config.deleteChildFromFragmentInstance; + +// ------------------- +// Persistence +// (optional) +// ------------------- +export const cloneInstance = $$$config.cloneInstance; +export const createContainerChildSet = $$$config.createContainerChildSet; +export const appendChildToContainerChildSet = + $$$config.appendChildToContainerChildSet; +export const finalizeContainerChildren = $$$config.finalizeContainerChildren; +export const replaceContainerChildren = $$$config.replaceContainerChildren; +export const cloneHiddenInstance = $$$config.cloneHiddenInstance; +export const cloneHiddenTextInstance = $$$config.cloneHiddenTextInstance; + +// ------------------- +// Hydration +// (optional) +// ------------------- +export const isSuspenseInstancePending = $$$config.isSuspenseInstancePending; +export const isSuspenseInstanceFallback = $$$config.isSuspenseInstanceFallback; +export const getSuspenseInstanceFallbackErrorDetails = + $$$config.getSuspenseInstanceFallbackErrorDetails; +export const registerSuspenseInstanceRetry = + $$$config.registerSuspenseInstanceRetry; +export const canHydrateFormStateMarker = $$$config.canHydrateFormStateMarker; +export const isFormStateMarkerMatching = $$$config.isFormStateMarkerMatching; +export const getNextHydratableSibling = $$$config.getNextHydratableSibling; +export const getNextHydratableSiblingAfterSingleton = + $$$config.getNextHydratableSiblingAfterSingleton; +export const getFirstHydratableChild = $$$config.getFirstHydratableChild; +export const getFirstHydratableChildWithinContainer = + $$$config.getFirstHydratableChildWithinContainer; +export const getFirstHydratableChildWithinActivityInstance = + $$$config.getFirstHydratableChildWithinActivityInstance; +export const getFirstHydratableChildWithinSuspenseInstance = + $$$config.getFirstHydratableChildWithinSuspenseInstance; +export const getFirstHydratableChildWithinSingleton = + $$$config.getFirstHydratableChildWithinSingleton; +export const canHydrateInstance = $$$config.canHydrateInstance; +export const canHydrateTextInstance = $$$config.canHydrateTextInstance; +export const canHydrateActivityInstance = $$$config.canHydrateActivityInstance; +export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance; +export const hydrateInstance = $$$config.hydrateInstance; +export const hydrateTextInstance = $$$config.hydrateTextInstance; +export const hydrateActivityInstance = $$$config.hydrateActivityInstance; +export const hydrateSuspenseInstance = $$$config.hydrateSuspenseInstance; +export const getNextHydratableInstanceAfterActivityInstance = + $$$config.getNextHydratableInstanceAfterActivityInstance; +export const getNextHydratableInstanceAfterSuspenseInstance = + $$$config.getNextHydratableInstanceAfterSuspenseInstance; +export const commitHydratedInstance = $$$config.commitHydratedInstance; +export const commitHydratedContainer = $$$config.commitHydratedContainer; +export const commitHydratedActivityInstance = + $$$config.commitHydratedActivityInstance; +export const commitHydratedSuspenseInstance = + $$$config.commitHydratedSuspenseInstance; +export const finalizeHydratedChildren = $$$config.finalizeHydratedChildren; +export const flushHydrationEvents = $$$config.flushHydrationEvents; +export const clearActivityBoundary = $$$config.clearActivityBoundary; +export const clearSuspenseBoundary = $$$config.clearSuspenseBoundary; +export const clearActivityBoundaryFromContainer = + $$$config.clearActivityBoundaryFromContainer; +export const clearSuspenseBoundaryFromContainer = + $$$config.clearSuspenseBoundaryFromContainer; +export const hideDehydratedBoundary = $$$config.hideDehydratedBoundary; +export const unhideDehydratedBoundary = $$$config.unhideDehydratedBoundary; +export const shouldDeleteUnhydratedTailInstances = + $$$config.shouldDeleteUnhydratedTailInstances; +export const diffHydratedPropsForDevWarnings = + $$$config.diffHydratedPropsForDevWarnings; +export const diffHydratedTextForDevWarnings = + $$$config.diffHydratedTextForDevWarnings; +export const describeHydratableInstanceForDevWarnings = + $$$config.describeHydratableInstanceForDevWarnings; +export const validateHydratableInstance = $$$config.validateHydratableInstance; +export const validateHydratableTextInstance = + $$$config.validateHydratableTextInstance; + +// ------------------- +// Resources +// (optional) +// ------------------- +export type HoistableRoot = mixed; +export type Resource = mixed; +export const supportsResources = $$$config.supportsResources; +export const isHostHoistableType = $$$config.isHostHoistableType; +export const getHoistableRoot = $$$config.getHoistableRoot; +export const getResource = $$$config.getResource; +export const acquireResource = $$$config.acquireResource; +export const releaseResource = $$$config.releaseResource; +export const hydrateHoistable = $$$config.hydrateHoistable; +export const mountHoistable = $$$config.mountHoistable; +export const unmountHoistable = $$$config.unmountHoistable; +export const createHoistableInstance = $$$config.createHoistableInstance; +export const prepareToCommitHoistables = $$$config.prepareToCommitHoistables; +export const mayResourceSuspendCommit = $$$config.mayResourceSuspendCommit; +export const preloadResource = $$$config.preloadResource; +export const suspendResource = $$$config.suspendResource; + +// ------------------- +// Singletons +// (optional) +// ------------------- +export const supportsSingletons = $$$config.supportsSingletons; +export const resolveSingletonInstance = $$$config.resolveSingletonInstance; +export const acquireSingletonInstance = $$$config.acquireSingletonInstance; +export const releaseSingletonInstance = $$$config.releaseSingletonInstance; +export const isHostSingletonType = $$$config.isHostSingletonType; +export const isSingletonScope = $$$config.isSingletonScope; diff --git a/packages/react-server/flight.js b/packages/react-server/flight.js index 4c5bb964cb7e..fe608c9db089 100644 --- a/packages/react-server/flight.js +++ b/packages/react-server/flight.js @@ -7,4 +7,11 @@ * @flow */ +import typeof * as FlightServerAPI from './src/ReactFlightServer'; +import typeof * as HostConfig from './src/ReactFlightServerConfig'; + export * from './src/ReactFlightServer'; + +// At build time, this module is wrapped as a factory function ($$$reconciler). +// Consumers pass a host config object and get back the Flight server API. +declare export default (hostConfig: HostConfig) => FlightServerAPI; diff --git a/packages/react-server/index.js b/packages/react-server/index.js index ca299b414321..57cd731aa107 100644 --- a/packages/react-server/index.js +++ b/packages/react-server/index.js @@ -7,4 +7,11 @@ * @flow */ +import typeof * as FizzAPI from './src/ReactFizzServer'; +import typeof * as HostConfig from './src/ReactFizzConfig'; + export * from './src/ReactFizzServer'; + +// At build time, this module is wrapped as a factory function ($$$reconciler). +// Consumers pass a host config object and get back the Fizz server API. +declare export default (hostConfig: HostConfig) => FizzAPI; diff --git a/packages/react-server/src/forks/ReactFizzConfig.noop.js b/packages/react-server/src/forks/ReactFizzConfig.noop.js new file mode 100644 index 000000000000..791c402bf773 --- /dev/null +++ b/packages/react-server/src/forks/ReactFizzConfig.noop.js @@ -0,0 +1,108 @@ +/** + * 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 + */ + +// This is a host config that's used for the `react-server` package on npm. +// It is only used by third-party renderers. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-server` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-server` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* renderer code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +import type {Request} from 'react-server/src/ReactFizzServer'; +import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; + +declare const $$$config: $FlowFixMe; +export opaque type Destination = mixed; +export opaque type RenderState = mixed; +export opaque type HoistableState = mixed; +export opaque type ResumableState = mixed; +export opaque type PreambleState = mixed; +export opaque type FormatContext = mixed; +export opaque type HeadersDescriptor = mixed; +export type {TransitionStatus}; + +export const isPrimaryRenderer = false; + +export const supportsClientAPIs = true; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + +export const bindToConsole = $$$config.bindToConsole; + +export const resetResumableState = $$$config.resetResumableState; +export const completeResumableState = $$$config.completeResumableState; +export const getChildFormatContext = $$$config.getChildFormatContext; +export const getSuspenseFallbackFormatContext = + $$$config.getSuspenseFallbackFormatContext; +export const getSuspenseContentFormatContext = + $$$config.getSuspenseContentFormatContext; +export const getViewTransitionFormatContext = + $$$config.getViewTransitionFormatContext; +export const makeId = $$$config.makeId; +export const pushTextInstance = $$$config.pushTextInstance; +export const pushStartInstance = $$$config.pushStartInstance; +export const pushEndInstance = $$$config.pushEndInstance; +export const pushSegmentFinale = $$$config.pushSegmentFinale; +export const pushFormStateMarkerIsMatching = + $$$config.pushFormStateMarkerIsMatching; +export const pushFormStateMarkerIsNotMatching = + $$$config.pushFormStateMarkerIsNotMatching; +export const writeCompletedRoot = $$$config.writeCompletedRoot; +export const writePlaceholder = $$$config.writePlaceholder; +export const pushStartActivityBoundary = $$$config.pushStartActivityBoundary; +export const pushEndActivityBoundary = $$$config.pushEndActivityBoundary; +export const writeStartCompletedSuspenseBoundary = + $$$config.writeStartCompletedSuspenseBoundary; +export const writeStartPendingSuspenseBoundary = + $$$config.writeStartPendingSuspenseBoundary; +export const writeStartClientRenderedSuspenseBoundary = + $$$config.writeStartClientRenderedSuspenseBoundary; +export const writeEndCompletedSuspenseBoundary = + $$$config.writeEndCompletedSuspenseBoundary; +export const writeEndPendingSuspenseBoundary = + $$$config.writeEndPendingSuspenseBoundary; +export const writeEndClientRenderedSuspenseBoundary = + $$$config.writeEndClientRenderedSuspenseBoundary; +export const writeStartSegment = $$$config.writeStartSegment; +export const writeEndSegment = $$$config.writeEndSegment; +export const writeCompletedSegmentInstruction = + $$$config.writeCompletedSegmentInstruction; +export const writeCompletedBoundaryInstruction = + $$$config.writeCompletedBoundaryInstruction; +export const writeClientRenderBoundaryInstruction = + $$$config.writeClientRenderBoundaryInstruction; +export const NotPendingTransition = $$$config.NotPendingTransition; +export const createPreambleState = $$$config.createPreambleState; +export const canHavePreamble = $$$config.canHavePreamble; +export const isPreambleContext = $$$config.isPreambleContext; +export const isPreambleReady = $$$config.isPreambleReady; +export const hoistPreambleState = $$$config.hoistPreambleState; + +// ------------------------- +// Resources +// ------------------------- +export const writePreambleStart = $$$config.writePreambleStart; +export const writePreambleEnd = $$$config.writePreambleEnd; +export const writeHoistables = $$$config.writeHoistables; +export const writeHoistablesForBoundary = $$$config.writeHoistablesForBoundary; +export const writePostamble = $$$config.writePostamble; +export const hoistHoistables = $$$config.hoistHoistables; +export const createHoistableState = $$$config.createHoistableState; +export const hasSuspenseyContent = $$$config.hasSuspenseyContent; +export const emitEarlyPreloads = $$$config.emitEarlyPreloads; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.noop.js b/packages/react-server/src/forks/ReactFlightServerConfig.noop.js new file mode 100644 index 000000000000..2d1fb68a07a7 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.noop.js @@ -0,0 +1,47 @@ +/** + * 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 {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from '../ReactFlightServerConfigBundlerCustom'; + +export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; +export * from '../ReactServerConsoleConfigPlain'; + +export type Hints = null; +export type HintCode = string; +export type HintModel = null; // eslint-disable-line no-unused-vars + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + +export const supportsComponentStorage = false; +export const componentStorage: AsyncLocalStorage = + (null: any); + +export function createHints(): Hints { + return null; +} + +export type FormatContext = null; + +export function createRootFormatContext(): FormatContext { + return null; +} + +export function getChildFormatContext( + parentContext: FormatContext, + type: string, + props: Object, +): FormatContext { + return parentContext; +} diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.noop.js b/packages/react-server/src/forks/ReactServerStreamConfig.noop.js new file mode 100644 index 000000000000..a929d13a7045 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.noop.js @@ -0,0 +1,48 @@ +/** + * 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 + */ + +// This is a host config that's used for the `react-server` package on npm. +// It is only used by third-party renderers. +// +// Its API lets you pass the host config as an argument. +// However, inside the `react-server` we treat host config as a module. +// This file is a shim between two worlds. +// +// It works because the `react-server` bundle is wrapped in something like: +// +// module.exports = function ($$$config) { +// /* renderer code */ +// } +// +// So `$$$config` looks like a global variable, but it's +// really an argument to a top-level wrapping function. + +declare const $$$config: $FlowFixMe; +export opaque type Destination = mixed; + +export opaque type PrecomputedChunk = mixed; +export opaque type Chunk = mixed; +export opaque type BinaryChunk = mixed; + +export const scheduleWork = $$$config.scheduleWork; +export const scheduleMicrotask = $$$config.scheduleMicrotask; +export const beginWriting = $$$config.beginWriting; +export const writeChunk = $$$config.writeChunk; +export const writeChunkAndReturn = $$$config.writeChunkAndReturn; +export const completeWriting = $$$config.completeWriting; +export const flushBuffered = $$$config.flushBuffered; +export const close = $$$config.close; +export const closeWithError = $$$config.closeWithError; +export const stringToChunk = $$$config.stringToChunk; +export const stringToPrecomputedChunk = $$$config.stringToPrecomputedChunk; +export const typedArrayToBinaryChunk = $$$config.typedArrayToBinaryChunk; +export const byteLengthOfChunk = $$$config.byteLengthOfChunk; +export const byteLengthOfBinaryChunk = $$$config.byteLengthOfBinaryChunk; +export const createFastHash = $$$config.createFastHash; +export const readAsDataURL = $$$config.readAsDataURL; diff --git a/scripts/flow/config/flowconfig b/scripts/flow/config/flowconfig index 93b557d92d94..3f14b50c4dbc 100644 --- a/scripts/flow/config/flowconfig +++ b/scripts/flow/config/flowconfig @@ -15,9 +15,6 @@ .*/__tests__/.* -# TODO: noop should get its own inlinedHostConfig entry -.*/packages/react-noop-renderer/.* - %REACT_RENDERER_FLOW_IGNORES% [libs] diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index aacfaf116fcd..1f2df96457a0 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -248,3 +248,9 @@ jest.mock('shared/ReactDOMSharedInternals', () => ); jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); + +if (global.__PERSISTENT__) { + jest.mock('react-noop-renderer', () => + jest.requireActual('react-noop-renderer/persistent') + ); +} diff --git a/scripts/jest/setupTests.persistent.js b/scripts/jest/setupTests.persistent.js index 1d3f33a68e58..e3e6d6434aa5 100644 --- a/scripts/jest/setupTests.persistent.js +++ b/scripts/jest/setupTests.persistent.js @@ -1,7 +1,3 @@ 'use strict'; -jest.mock('react-noop-renderer', () => - jest.requireActual('react-noop-renderer/persistent') -); - global.__PERSISTENT__ = true; diff --git a/scripts/jest/setupTests.xplat.js b/scripts/jest/setupTests.xplat.js index 3b05a474a043..35e1a8a9d24a 100644 --- a/scripts/jest/setupTests.xplat.js +++ b/scripts/jest/setupTests.xplat.js @@ -28,9 +28,5 @@ jest.mock('shared/ReactFeatureFlags', () => { return actual; }); -jest.mock('react-noop-renderer', () => - jest.requireActual('react-noop-renderer/persistent') -); - global.__PERSISTENT__ = true; global.__XPLAT__ = true; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 0bb9da231d91..0135dda88767 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -661,6 +661,25 @@ module.exports = [ isFlowTyped: false, // TODO: type it. isServerSupported: false, }, + { + shortName: 'noop', + entryPoints: [ + 'react-noop-renderer', + 'react-noop-renderer/persistent', + 'react-noop-renderer/server', + 'react-noop-renderer/flight-server', + 'react-noop-renderer/flight-client', + ], + paths: [ + 'react-noop-renderer', + 'react-client/flight', + 'react-server/flight', + 'react-server/src/ReactFlightServerConfigDebugNoop.js', + ], + isFlowTyped: true, + isServerSupported: true, + isFlightSupported: true, + }, { shortName: 'custom', entryPoints: [ From 3bc2d414287e62a7b74731c6c7b837270353a339 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 4 Mar 2026 14:20:43 +0100 Subject: [PATCH 4/4] [noop] Fix `createContainer` argument order in the Fiber implementation (#35945) --- .eslintrc.js | 1 + .../src/ReactFiberConfigNoop.js | 15 + .../src/ReactFiberConfigNoopHydration.js | 65 ++ .../src/ReactFiberConfigNoopNoMutation.js | 61 ++ .../src/ReactFiberConfigNoopNoPersistence.js | 29 + .../src/ReactFiberConfigNoopResources.js | 38 ++ .../src/ReactFiberConfigNoopScopes.js | 23 + .../src/ReactFiberConfigNoopSingletons.js | 27 + .../src/ReactFiberConfigNoopTestSelectors.js | 29 + .../src/createReactNoop.js | 633 +++++++++--------- .../ViewTransitionReactServer-test.js | 2 +- .../src/forks/ReactFiberConfig.noop.js | 1 - 12 files changed, 590 insertions(+), 334 deletions(-) create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopHydration.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopNoMutation.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopNoPersistence.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopResources.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopScopes.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopSingletons.js create mode 100644 packages/react-noop-renderer/src/ReactFiberConfigNoopTestSelectors.js diff --git a/.eslintrc.js b/.eslintrc.js index 2cebe8698c7c..16a8050ee75f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -626,6 +626,7 @@ module.exports = { FinalizationRegistry: 'readonly', Exclude: 'readonly', Omit: 'readonly', + Pick: 'readonly', Keyframe: 'readonly', PropertyIndexedKeyframes: 'readonly', KeyframeAnimationOptions: 'readonly', diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoop.js b/packages/react-noop-renderer/src/ReactFiberConfigNoop.js index fd8aa0bbc191..359d58a6cfdc 100644 --- a/packages/react-noop-renderer/src/ReactFiberConfigNoop.js +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoop.js @@ -7,6 +7,15 @@ * @flow */ +export * from './ReactFiberConfigNoopHydration'; +export * from './ReactFiberConfigNoopScopes'; +export * from './ReactFiberConfigNoopTestSelectors'; +export * from './ReactFiberConfigNoopResources'; +export * from './ReactFiberConfigNoopSingletons'; +// createReactNoop will overwrite these with the mutation or persistence versions. +export * from './ReactFiberConfigNoopNoMutation'; +export * from './ReactFiberConfigNoopNoPersistence'; + export type HostContext = Object; export type TextInstance = { @@ -31,3 +40,9 @@ export type Instance = { export type PublicInstance = Instance; export type TransitionStatus = mixed; + +export type Container = { + rootID: string, + children: Array, + pendingChildren: Array, +}; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopHydration.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopHydration.js new file mode 100644 index 000000000000..13a5dcf90b66 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopHydration.js @@ -0,0 +1,65 @@ +/** + * 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 + */ + +// Renderers that don't support hydration +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'react-noop-renderer does not support hydration. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Hydration (when unsupported) +export type ActivityInstance = mixed; +export type SuspenseInstance = mixed; +export const supportsHydration = false; +export const isSuspenseInstancePending = shim; +export const isSuspenseInstanceFallback = shim; +export const getSuspenseInstanceFallbackErrorDetails = shim; +export const registerSuspenseInstanceRetry = shim; +export const canHydrateFormStateMarker = shim; +export const isFormStateMarkerMatching = shim; +export const getNextHydratableSibling = shim; +export const getNextHydratableSiblingAfterSingleton = shim; +export const getFirstHydratableChild = shim; +export const getFirstHydratableChildWithinContainer = shim; +export const getFirstHydratableChildWithinActivityInstance = shim; +export const getFirstHydratableChildWithinSuspenseInstance = shim; +export const getFirstHydratableChildWithinSingleton = shim; +export const canHydrateInstance = shim; +export const canHydrateTextInstance = shim; +export const canHydrateActivityInstance = shim; +export const canHydrateSuspenseInstance = shim; +export const hydrateInstance = shim; +export const hydrateTextInstance = shim; +export const hydrateActivityInstance = shim; +export const hydrateSuspenseInstance = shim; +export const getNextHydratableInstanceAfterActivityInstance = shim; +export const getNextHydratableInstanceAfterSuspenseInstance = shim; +export const finalizeHydratedChildren = shim; +export const commitHydratedInstance = shim; +export const commitHydratedContainer = shim; +export const commitHydratedActivityInstance = shim; +export const commitHydratedSuspenseInstance = shim; +export const flushHydrationEvents = shim; +export const clearActivityBoundary = shim; +export const clearSuspenseBoundary = shim; +export const clearActivityBoundaryFromContainer = shim; +export const clearSuspenseBoundaryFromContainer = shim; +export const hideDehydratedBoundary = shim; +export const unhideDehydratedBoundary = shim; +export const shouldDeleteUnhydratedTailInstances = shim; +export const diffHydratedPropsForDevWarnings = shim; +export const diffHydratedTextForDevWarnings = shim; +export const describeHydratableInstanceForDevWarnings = shim; +export const validateHydratableInstance = shim; +export const validateHydratableTextInstance = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopNoMutation.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopNoMutation.js new file mode 100644 index 000000000000..a9cf7d73458f --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopNoMutation.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 + */ + +// Renderers that don't support mutation +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'This entrypoint of react-noop-renderer does not support mutation. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Mutation (when unsupported) +export const supportsMutation = false; +export const cloneMutableInstance = shim; +export const cloneMutableTextInstance = shim; +export const appendChild = shim; +export const appendChildToContainer = shim; +export const commitTextUpdate = shim; +export const commitMount = shim; +export const commitUpdate = shim; +export const insertBefore = shim; +export const insertInContainerBefore = shim; +export const removeChild = shim; +export const removeChildFromContainer = shim; +export const resetTextContent = shim; +export const hideInstance = shim; +export const hideTextInstance = shim; +export const unhideInstance = shim; +export const unhideTextInstance = shim; +export const clearContainer = shim; +export const applyViewTransitionName = shim; +export const restoreViewTransitionName = shim; +export const cancelViewTransitionName = shim; +export const cancelRootViewTransitionName = shim; +export const restoreRootViewTransitionName = shim; +export const cloneRootViewTransitionContainer = shim; +export const removeRootViewTransitionClone = shim; +export type InstanceMeasurement = null; +export const measureInstance = shim; +export const measureClonedInstance = shim; +export const wasInstanceInViewport = shim; +export const hasInstanceChanged = shim; +export const hasInstanceAffectedParent = shim; +export const startViewTransition = shim; +export type RunningViewTransition = null; +export const startGestureTransition = shim; +export const stopViewTransition = shim; +export const addViewTransitionFinishedListener = shim; +export type ViewTransitionInstance = null | {name: string, ...}; +export const createViewTransitionInstance = shim; +export type GestureTimeline = any; +export const getCurrentGestureOffset = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopNoPersistence.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopNoPersistence.js new file mode 100644 index 000000000000..5b64978eafa0 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopNoPersistence.js @@ -0,0 +1,29 @@ +/** + * 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 + */ + +// Renderers that don't support persistence +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'This entrypoint of react-noop-renderer does not support persistence. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Persistence (when unsupported) +export const supportsPersistence = false; +export const cloneInstance = shim; +export const createContainerChildSet = shim; +export const appendChildToContainerChildSet = shim; +export const finalizeContainerChildren = shim; +export const replaceContainerChildren = shim; +export const cloneHiddenInstance = shim; +export const cloneHiddenTextInstance = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopResources.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopResources.js new file mode 100644 index 000000000000..a00466918589 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopResources.js @@ -0,0 +1,38 @@ +/** + * 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 + */ + +// Renderers that don't support hydration +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'react-noop-renderer does not support Resources. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +export type HoistableRoot = mixed; +export type Resource = mixed; + +// Resources (when unsupported) +export const supportsResources = false; +export const isHostHoistableType = shim; +export const getHoistableRoot = shim; +export const getResource = shim; +export const acquireResource = shim; +export const releaseResource = shim; +export const hydrateHoistable = shim; +export const mountHoistable = shim; +export const unmountHoistable = shim; +export const createHoistableInstance = shim; +export const prepareToCommitHoistables = shim; +export const mayResourceSuspendCommit = shim; +export const preloadResource = shim; +export const suspendResource = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopScopes.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopScopes.js new file mode 100644 index 000000000000..0aaee7931895 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopScopes.js @@ -0,0 +1,23 @@ +/** + * 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 + */ + +// Renderers that don't support React Scopes +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'react-noop-renderer does not support React Scopes. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// React Scopes (when unsupported) +export const prepareScopeUpdate = shim; +export const getInstanceFromScope = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopSingletons.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopSingletons.js new file mode 100644 index 000000000000..5da6914bb5e5 --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopSingletons.js @@ -0,0 +1,27 @@ +/** + * 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 + */ + +// Renderers that don't support mutation +// can re-export everything from this module. + +function shim(...args: any): any { + throw new Error( + 'react-noop-renderer does not support Singletons. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Resources (when unsupported) +export const supportsSingletons = false; +export const resolveSingletonInstance = shim; +export const acquireSingletonInstance = shim; +export const releaseSingletonInstance = shim; +export const isHostSingletonType = shim; +export const isSingletonScope = shim; diff --git a/packages/react-noop-renderer/src/ReactFiberConfigNoopTestSelectors.js b/packages/react-noop-renderer/src/ReactFiberConfigNoopTestSelectors.js new file mode 100644 index 000000000000..7604d79d677d --- /dev/null +++ b/packages/react-noop-renderer/src/ReactFiberConfigNoopTestSelectors.js @@ -0,0 +1,29 @@ +/** + * 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 + */ + +// Renderers that don't support test selectors +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'react-noop-renderer does not support test selectors. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// Test selectors (when unsupported) +export const supportsTestSelectors = false; +export const findFiberRoot = shim; +export const getBoundingRect = shim; +export const getTextContent = shim; +export const isHiddenSubtree = shim; +export const matchAccessibilityRole = shim; +export const setFocusIfFocusable = shim; +export const setupIntersectionObserver = shim; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 2980ea8e25bc..4535580cd2cb 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -24,8 +24,12 @@ import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; import typeof * as HostConfig from 'react-reconciler/src/ReactFiberConfig'; +import typeof * as ReactFiberConfigWithNoMutation from 'react-reconciler/src/ReactFiberConfigWithNoMutation'; +import typeof * as ReactFiberConfigWithNoPersistence from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; + import typeof * as ReconcilerAPI from 'react-reconciler/src/ReactFiberReconciler'; import type { + Container, HostContext, Instance, PublicInstance, @@ -44,17 +48,13 @@ import { ConcurrentRoot, LegacyRoot, } from 'react-reconciler/constants'; +import * as DefaultConfig from './ReactFiberConfigNoop'; + import {disableLegacyMode} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactVersion from 'shared/ReactVersion'; -type Container = { - rootID: string, - children: Array, - pendingChildren: Array, - ... -}; type Props = { prop: any, hidden: boolean, @@ -107,7 +107,6 @@ if (__DEV__) { Object.freeze(NO_CONTEXT); } -// $FlowFixMe[signature-verification-failure] function createReactNoop( reconciler: (hostConfig: HostConfig) => ReconcilerAPI, useMutation: boolean, @@ -121,12 +120,19 @@ function createReactNoop( child: Instance | TextInstance, ): void { const prevParent = child.parent; - // $FlowFixMe[prop-missing] - if (prevParent !== -1 && prevParent !== parentInstance.id) { + + if ( + prevParent !== -1 && + prevParent !== + // $FlowFixMe[prop-missing] + (parentInstance: Instance).id + ) { throw new Error('Reparenting is not allowed'); } - // $FlowFixMe[prop-missing] - child.parent = parentInstance.id; + + child.parent = + // $FlowFixMe[prop-missing] + (parentInstance: Instance).id; const index = parentInstance.children.indexOf(child); if (index !== -1) { parentInstance.children.splice(index, 1); @@ -256,11 +262,14 @@ function createReactNoop( if (__DEV__) { checkPropStringCoercion(newProps.children, 'children'); } - const clone = { + const clone: Instance = { id: instance.id, type: type, parent: instance.parent, - children: keepChildren ? instance.children : (children ?? []), + children: keepChildren + ? instance.children + : // $FlowFixMe[incompatible-type] We're not typing immutable instances. + (children ?? []), text: shouldSetTextContent(type, newProps) ? computeText((newProps.children: any) + '', instance.context) : null, @@ -291,7 +300,6 @@ function createReactNoop( enumerable: false, }); hostCloneCounter++; - // $FlowFixMe[incompatible-return] return clone; } @@ -315,10 +323,7 @@ function createReactNoop( subscriptions: Array | null, }; - let suspenseyThingCache: Map< - SuspenseyThingRecord, - 'pending' | 'fulfilled', - > | null = null; + let suspenseyThingCache: Map | null = null; function startSuspendingCommit(): SuspendedState { // Represents a subscription for all the suspensey things that block a @@ -340,7 +345,7 @@ function createReactNoop( // Attach a listener to the suspensey thing and create a subscription // object that uses reference counting to track when all the suspensey // things have loaded. - // $FlowFixMe + // $FlowFixMe[incompatible-use] Still not nullable const record = suspenseyThingCache.get(src); if (record === undefined) { throw new Error('Could not find record for key.'); @@ -352,10 +357,8 @@ function createReactNoop( // Stash the subscription on the record. In `resolveSuspenseyThing`, // we'll use this fire the commit once all the things have loaded. if (record.subscriptions === null) { - // $FlowFixMe[incompatible-use] record.subscriptions = []; } - // $FlowFixMe[incompatible-use] record.subscriptions.push(state); } } else { @@ -369,10 +372,9 @@ function createReactNoop( function waitForCommitToBeReady( state: SuspendedState, timeoutOffset: number, - ): ((commit: () => mixed) => () => void) | null { + ): ((commit: () => void) => () => void) | null { if (state.pendingCount > 0) { - return (commit: () => mixed) => { - // $FlowFixMe[incompatible-type] + return (commit: () => void) => { state.commit = commit; const cancelCommit = () => { state.commit = null; @@ -383,11 +385,13 @@ function createReactNoop( return null; } - const sharedHostConfig = { + const sharedHostConfig: HostConfig = { rendererVersion: ReactVersion, rendererPackageName: 'react-noop', - supportsSingletons: false, + ...DefaultConfig, + + extraDevToolsConfig: null, getRootHostContext() { return NO_CONTEXT; @@ -407,6 +411,8 @@ function createReactNoop( return (instance: any); }, + HostTransitionContext: null, + createInstance( type: string, props: Props, @@ -466,10 +472,6 @@ function createReactNoop( return inst; }, - cloneMutableInstance(instance: Instance, keepChildren: boolean): Instance { - throw new Error('Not yet implemented.'); - }, - appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, @@ -521,10 +523,6 @@ function createReactNoop( return inst; }, - cloneMutableTextInstance(textInstance: TextInstance): TextInstance { - throw new Error('Not yet implemented.'); - }, - createFragmentInstance(fragmentFiber: mixed) { return null; }, @@ -590,11 +588,8 @@ function createReactNoop( return false; }, - now: Scheduler.unstable_now, - isPrimaryRenderer: true, warnsIfNotActing: true, - supportsHydration: false, getInstanceFromNode() { throw new Error('Not yet implemented.'); @@ -612,18 +607,8 @@ function createReactNoop( // NO-OP }, - prepareScopeUpdate() {}, - - getInstanceFromScope() { - throw new Error('Not yet implemented.'); - }, - detachDeletedInstance() {}, - logRecoverableError() { - // no-op - }, - requestPostPaintCallback(callback: (time: number) => void) { const endTime = Scheduler.unstable_now(); callback(endTime); @@ -657,16 +642,11 @@ function createReactNoop( return true; }, - mayResourceSuspendCommit(resource: mixed): boolean { - throw new Error( - 'Resources are not implemented for React Noop yet. This method should not be called', - ); - }, - preloadInstance(instance: Instance, type: string, props: Props): boolean { if (type !== 'suspensey-thing' || typeof props.src !== 'string') { throw new Error('Attempted to preload unexpected instance: ' + type); } + const src = props.src; // In addition to preloading an instance, this method asks whether the // instance is ready to be committed. If it's not, React may yield to the @@ -675,15 +655,14 @@ function createReactNoop( if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } - // $FlowFixMe - const record = suspenseyThingCache.get(props.src); + const record = suspenseyThingCache.get(src); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'pending', subscriptions: null, }; - // $FlowFixMe - suspenseyThingCache.set(props.src, newRecord); + // $FlowFixMe[incompatible-use] Still not nullable + suspenseyThingCache.set(src, newRecord); // $FlowFixMe[prop-missing] const onLoadStart = props.onLoadStart; if (typeof onLoadStart === 'function') { @@ -691,26 +670,13 @@ function createReactNoop( } return false; } else { - // $FlowFixMe[prop-missing] return record.status === 'fulfilled'; } }, - preloadResource(resource: mixed): number { - throw new Error( - 'Resources are not implemented for React Noop yet. This method should not be called', - ); - }, - startSuspendingCommit, suspendInstance, - suspendResource(state: SuspendedState, resource: mixed): void { - throw new Error( - 'Resources are not implemented for React Noop yet. This method should not be called', - ); - }, - suspendOnActiveViewTransition( state: SuspendedState, container: Container, @@ -741,271 +707,283 @@ function createReactNoop( }, }; - const hostConfig: HostConfig = useMutation - ? // $FlowFixMe[prop-missing] - { - ...sharedHostConfig, + const mutationHostConfig: Pick< + HostConfig, + $Keys, + > = { + supportsMutation: true, - supportsMutation: true, - supportsPersistence: false, + cloneMutableInstance() { + // required for enableGestureTransition + throw new Error('Not yet implemented.'); + }, - commitMount(instance: Instance, type: string, newProps: Props): void { - // Noop - }, + cloneMutableTextInstance() { + // required for enableGestureTransition + throw new Error('Not yet implemented.'); + }, - commitUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - ): void { - if (oldProps === null) { - throw new Error('Should have old props'); - } - hostUpdateCounter++; - instance.prop = newProps.prop; - instance.hidden = !!newProps.hidden; + commitMount(instance: Instance, type: string, newProps: Props): void { + // Noop + }, - if (type === 'suspensey-thing' && typeof newProps.src === 'string') { - // $FlowFixMe[prop-missing] - instance.src = newProps.src; - } + commitUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + ): void { + if (oldProps === null) { + throw new Error('Should have old props'); + } + hostUpdateCounter++; + instance.prop = newProps.prop; + instance.hidden = !!newProps.hidden; - if (shouldSetTextContent(type, newProps)) { - if (__DEV__) { - checkPropStringCoercion(newProps.children, 'children'); - } - instance.text = computeText( - (newProps.children: any) + '', - instance.context, - ); - } - }, + if (type === 'suspensey-thing' && typeof newProps.src === 'string') { + // $FlowFixMe[prop-missing] + instance.src = newProps.src; + } - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - hostUpdateCounter++; - textInstance.text = computeText(newText, textInstance.context); - }, + if (shouldSetTextContent(type, newProps)) { + if (__DEV__) { + checkPropStringCoercion(newProps.children, 'children'); + } + instance.text = computeText( + (newProps.children: any) + '', + instance.context, + ); + } + }, - appendChild, - appendChildToContainer, - insertBefore, - insertInContainerBefore, - removeChild, - removeChildFromContainer, - clearContainer, + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + hostUpdateCounter++; + textInstance.text = computeText(newText, textInstance.context); + }, - hideInstance(instance: Instance): void { - instance.hidden = true; - }, + appendChild, + appendChildToContainer, + insertBefore, + insertInContainerBefore, + removeChild, + removeChildFromContainer, + clearContainer, - hideTextInstance(textInstance: TextInstance): void { - textInstance.hidden = true; - }, + hideInstance(instance: Instance): void { + instance.hidden = true; + }, - unhideInstance(instance: Instance, props: Props): void { - if (!props.hidden) { - instance.hidden = false; - } - }, + hideTextInstance(textInstance: TextInstance): void { + textInstance.hidden = true; + }, - unhideTextInstance(textInstance: TextInstance, text: string): void { - textInstance.hidden = false; - }, + unhideInstance(instance: Instance, props: Props): void { + if (!props.hidden) { + instance.hidden = false; + } + }, - applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, - ): void {}, + unhideTextInstance(textInstance: TextInstance, text: string): void { + textInstance.hidden = false; + }, - restoreViewTransitionName(instance: Instance, props: Props): void {}, + applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, + ): void {}, - cancelViewTransitionName( - instance: Instance, - name: string, - props: Props, - ): void {}, + restoreViewTransitionName(instance: Instance, props: Props): void {}, - cancelRootViewTransitionName(rootContainer: Container): void {}, + cancelViewTransitionName( + instance: Instance, + name: string, + props: Props, + ): void {}, - restoreRootViewTransitionName(rootContainer: Container): void {}, + cancelRootViewTransitionName(rootContainer: Container): void {}, - cloneRootViewTransitionContainer(rootContainer: Container): Instance { - throw new Error('Not yet implemented.'); - }, + restoreRootViewTransitionName(rootContainer: Container): void {}, - removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, - ): void { - throw new Error('Not implemented.'); - }, + cloneRootViewTransitionContainer(rootContainer: Container): Instance { + throw new Error('Not yet implemented.'); + }, - measureInstance(instance: Instance): InstanceMeasurement { - return null; - }, + removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, + ): void { + throw new Error('Not implemented.'); + }, - measureClonedInstance(instance: Instance): InstanceMeasurement { - return null; - }, + measureInstance(instance: Instance): InstanceMeasurement { + return null; + }, - wasInstanceInViewport(measurement: InstanceMeasurement): boolean { - return true; - }, + measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; + }, - hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, - ): boolean { - return false; - }, + wasInstanceInViewport(measurement: InstanceMeasurement): boolean { + return true; + }, - hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, - ): boolean { - return false; - }, + hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, + ): boolean { + return false; + }, - startViewTransition( - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: mixed => void, - blockedCallback: string => void, // Profiling-only - finishedAnimation: () => void, // Profiling-only - ): null | RunningViewTransition { - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - // Skip passiveCallback(). Spawned work will schedule a task. - return null; - }, + hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, + ): boolean { + return false; + }, - startGestureTransition( - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: mixed => void, - ): null | RunningViewTransition { - mutationCallback(); - animateCallback(); - return null; - }, + startViewTransition( + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: mixed => void, + blockedCallback: string => void, // Profiling-only + finishedAnimation: () => void, // Profiling-only + ): null | RunningViewTransition { + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + // Skip passiveCallback(). Spawned work will schedule a task. + return null; + }, - stopViewTransition(transition: RunningViewTransition) {}, + startGestureTransition( + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: mixed => void, + ): null | RunningViewTransition { + mutationCallback(); + animateCallback(); + return null; + }, - addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, - ) { - callback(); - }, + stopViewTransition(transition: RunningViewTransition) {}, - createViewTransitionInstance(name: string): ViewTransitionInstance { - return null; - }, + addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, + ) { + callback(); + }, - getCurrentGestureOffset(provider: GestureTimeline): number { - return 0; - }, + createViewTransitionInstance(name: string): ViewTransitionInstance { + return null; + }, - resetTextContent(instance: Instance): void { - instance.text = null; - }, - } - : // $FlowFixMe[prop-missing] - { - ...sharedHostConfig, - supportsMutation: false, - supportsPersistence: true, + getCurrentGestureOffset(provider: GestureTimeline): number { + return 0; + }, - cloneInstance, - clearContainer, + resetTextContent(instance: Instance): void { + instance.text = null; + }, + }; - createContainerChildSet(): Array { - return []; - }, + const persistenceHostConfig: Pick< + HostConfig, + $Keys, + > = { + supportsPersistence: true, - appendChildToContainerChildSet( - childSet: Array, - child: Instance | TextInstance, - ): void { - childSet.push(child); - }, + cloneInstance, - finalizeContainerChildren( - container: Container, - newChildren: Array, - ): void { - container.pendingChildren = newChildren; - if ( - newChildren.length === 1 && - newChildren[0].text === 'Error when completing root' - ) { - // Trigger an error for testing purposes - throw Error('Error when completing root'); - } - }, + createContainerChildSet(): Array { + return []; + }, - replaceContainerChildren( - container: Container, - newChildren: Array, - ): void { - container.children = newChildren; - }, + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void { + childSet.push(child); + }, - cloneHiddenInstance( - instance: Instance, - type: string, - props: Props, - ): Instance { - const clone = cloneInstance(instance, type, props, props, true, null); - clone.hidden = true; - return clone; - }, + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.pendingChildren = newChildren; + if ( + newChildren.length === 1 && + newChildren[0].text === 'Error when completing root' + ) { + // Trigger an error for testing purposes + throw Error('Error when completing root'); + } + }, - cloneHiddenTextInstance( - instance: TextInstance, - text: string, - ): TextInstance { - const clone = { - text: instance.text, - id: instance.id, - parent: instance.parent, - hidden: true, - context: instance.context, - }; - // Hide from unit tests - Object.defineProperty(clone, 'id', { - value: clone.id, - enumerable: false, - }); - Object.defineProperty(clone, 'parent', { - value: clone.parent, - enumerable: false, - }); - Object.defineProperty(clone, 'context', { - value: clone.context, - enumerable: false, - }); - return clone; - }, + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.children = newChildren; + }, + + cloneHiddenInstance( + instance: Instance, + type: string, + props: Props, + ): Instance { + const clone = cloneInstance(instance, type, props, props, true, null); + clone.hidden = true; + return clone; + }, + + cloneHiddenTextInstance( + instance: TextInstance, + text: string, + ): TextInstance { + const clone = { + text: instance.text, + id: instance.id, + parent: instance.parent, + hidden: true, + context: instance.context, }; + // Hide from unit tests + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + Object.defineProperty(clone, 'parent', { + value: clone.parent, + enumerable: false, + }); + Object.defineProperty(clone, 'context', { + value: clone.context, + enumerable: false, + }); + return clone; + }, + }; + + const hostConfig: HostConfig = useMutation + ? {...sharedHostConfig, ...mutationHostConfig} + : {...sharedHostConfig, ...persistenceHostConfig}; const NoopRenderer = reconciler(hostConfig); @@ -1024,8 +1002,7 @@ function createReactNoop( let currentEventPriority = DefaultEventPriority; - // $FlowFixMe[missing-local-annot] - function createJSXElementForTestComparison(type, props) { + function createJSXElementForTestComparison(type: mixed, props: mixed) { if (__DEV__) { const element = { type: type, @@ -1052,8 +1029,10 @@ function createReactNoop( } } - // $FlowFixMe - function childToJSX(child, text) { + function childToJSX( + child: null | Instance | TextInstance | Array, + text: ?string, + ): mixed { if (text !== null) { return text; } @@ -1108,8 +1087,7 @@ function createReactNoop( return textInstance.text; } - // $FlowFixMe[missing-local-annot] - function getChildren(root) { + function getChildren(root: ?(Container | Instance)) { if (root) { return root.children; } else { @@ -1117,8 +1095,7 @@ function createReactNoop( } } - // $FlowFixMe[missing-local-annot] - function getPendingChildren(root) { + function getPendingChildren(root: ?(Container | Instance)) { if (root) { return root.children; } else { @@ -1126,8 +1103,7 @@ function createReactNoop( } } - // $FlowFixMe[missing-local-annot] - function getChildrenAsJSX(root) { + function getChildrenAsJSX(root: ?(Container | Instance)) { const children = childToJSX(getChildren(root), null); if (children === null) { return null; @@ -1138,8 +1114,7 @@ function createReactNoop( return children; } - // $FlowFixMe[missing-local-annot] - function getPendingChildrenAsJSX(root) { + function getPendingChildrenAsJSX(root: ?(Container | Instance)) { const children = childToJSX(getChildren(root), null); if (children === null) { return null; @@ -1150,7 +1125,7 @@ function createReactNoop( return children; } - function flushSync(fn: () => R): R { + function flushSync(fn: () => R): ?R { if (__DEV__) { if (NoopRenderer.isAlreadyRendering()) { console.error( @@ -1169,7 +1144,6 @@ function createReactNoop( if (fn) { return fn(); } else { - // $FlowFixMe[incompatible-return] return undefined; } } finally { @@ -1228,7 +1202,10 @@ function createReactNoop( return getPendingChildren(container); }, - getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) { + getOrCreateRootContainer( + rootID: string = DEFAULT_ROOT_ID, + tag: RootTag, + ): Container { let root = roots.get(rootID); if (!root) { const container: Container = { @@ -1307,7 +1284,7 @@ function createReactNoop( throw new Error('createLegacyRoot: Unsupported Legacy Mode API.'); } - const container = { + const container: Container = { rootID: '' + idCounter++, pendingChildren: [], children: [], @@ -1317,9 +1294,8 @@ function createReactNoop( container, LegacyRoot, null, - // $FlowFixMe[incompatible-call] -- TODO: Discovered when typechecking noop-renderer - null, false, + null, '', NoopRenderer.defaultOnUncaughtError, NoopRenderer.defaultOnCaughtError, @@ -1352,8 +1328,7 @@ function createReactNoop( return getPendingChildrenAsJSX(container); }, - // $FlowFixMe[missing-local-annot] - getSuspenseyThingStatus(src): string | null { + getSuspenseyThingStatus(src: string): string | null { if (suspenseyThingCache === null) { return null; } else { @@ -1367,24 +1342,19 @@ function createReactNoop( if (suspenseyThingCache === null) { suspenseyThingCache = new Map(); } - // $FlowFixMe[incompatible-call] const record = suspenseyThingCache.get(key); if (record === undefined) { const newRecord: SuspenseyThingRecord = { status: 'fulfilled', subscriptions: null, }; - // $FlowFixMe + // $FlowFixMe[incompatible-use] still non-nullable suspenseyThingCache.set(key, newRecord); } else { - // $FlowFixMe[prop-missing] if (record.status === 'pending') { - // $FlowFixMe[incompatible-use] record.status = 'fulfilled'; - // $FlowFixMe[prop-missing] const subscriptions = record.subscriptions; if (subscriptions !== null) { - // $FlowFixMe[incompatible-use] record.subscriptions = null; for (let i = 0; i < subscriptions.length; i++) { const subscription = subscriptions[i]; @@ -1392,6 +1362,11 @@ function createReactNoop( if (subscription.pendingCount === 0) { const commit = subscription.commit; subscription.commit = null; + if (commit === null) { + throw new Error( + 'Expected commit to be a function. This is a bug in React.', + ); + } commit(); } } @@ -1413,7 +1388,7 @@ function createReactNoop( }, // Shortcut for testing a single root - render(element: React$Element, callback: ?Function) { + render(element: React$Element, callback: ?Function): void { ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback); }, @@ -1462,13 +1437,11 @@ function createReactNoop( return component; } if (__DEV__) { - // $FlowFixMe[incompatible-return] return NoopRenderer.findHostInstanceWithWarning( component, 'findInstance', ); } - // $FlowFixMe[incompatible-return] return NoopRenderer.findHostInstance(component); }, @@ -1527,8 +1500,7 @@ function createReactNoop( discreteUpdates: NoopRenderer.discreteUpdates, - // $FlowFixMe[incompatible-return] - idleUpdates(fn: () => T): T { + idleUpdates(fn: () => T): void { const prevEventPriority = currentEventPriority; currentEventPriority = IdleEventPriority; try { @@ -1552,9 +1524,7 @@ function createReactNoop( } const bufferedLog: string[] = []; - // $FlowFixMe[missing-local-annot] function log(...args: string[]) { - // $FlowFixMe[incompatible-call] bufferedLog.push(...args, '\n'); } @@ -1614,8 +1584,7 @@ function createReactNoop( } } - // $FlowFixMe[missing-local-annot] - function logFiber(fiber: Fiber, depth) { + function logFiber(fiber: Fiber, depth: number) { log( ' '.repeat(depth) + '- ' + diff --git a/packages/react-reconciler/src/__tests__/ViewTransitionReactServer-test.js b/packages/react-reconciler/src/__tests__/ViewTransitionReactServer-test.js index f6fa939e8446..ea6791864bca 100644 --- a/packages/react-reconciler/src/__tests__/ViewTransitionReactServer-test.js +++ b/packages/react-reconciler/src/__tests__/ViewTransitionReactServer-test.js @@ -39,7 +39,7 @@ describe('ViewTransitionReactServer', () => { jest.restoreAllMocks(); }); - // @gate enableViewTransition || fb + // @gate enableViewTransition it('can be rendered in React Server', async () => { function App() { return ReactServer.createElement( diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js b/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js index 9baaee1f5c9a..b9e2866605a4 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.noop.js @@ -27,7 +27,6 @@ export * from 'react-noop-renderer/src/ReactFiberConfigNoop'; declare const $$$config: $FlowFixMe; export opaque type Type = mixed; export opaque type Props = mixed; -export opaque type Container = mixed; export opaque type ActivityInstance = mixed; export opaque type SuspenseInstance = mixed; export opaque type HydratableInstance = mixed;