diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index fd2b4e7d80291..c73a997d48cff 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -44,7 +44,7 @@ export function getCurrentParentStackInDev(): string { return ''; } -function getCurrentFiberStackInDev(stack: Error): string { +function getCurrentFiberStackInDev(stack: null | Error): string { if (__DEV__) { if (current === null) { return ''; diff --git a/packages/react-reconciler/src/__tests__/ReactOwnerStacks-test.js b/packages/react-reconciler/src/__tests__/ReactOwnerStacks-test.js new file mode 100644 index 0000000000000..ca0e9fd12cc11 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactOwnerStacks-test.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. + * + * @emails react-core + * @jest-environment node + */ +'use strict'; + +let React; +let ReactNoop; +let act; + +describe('ReactOwnerStacks', () => { + beforeEach(function () { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + act = require('internal-test-utils').act; + }); + + function normalizeCodeLocInfo(str) { + return ( + str && + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)'; + }) + ); + } + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks during rendering in dev', async () => { + let stack; + + function Foo() { + return ; + } + function Bar() { + return ( +
+ +
+ ); + } + function Baz() { + stack = React.captureOwnerStack(); + return hi; + } + + await act(() => { + ReactNoop.render( +
+ +
, + ); + }); + + expect(normalizeCodeLocInfo(stack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); +}); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index e23bc1f1663c2..e642fef49d1cb 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -61,6 +61,7 @@ import { disableLegacyMode, } from 'shared/ReactFeatureFlags'; +// $FlowFixMe[prop-missing]: This is only in the development export. const act = React.act; // TODO: Remove from public bundle diff --git a/packages/react/index.development.js b/packages/react/index.development.js new file mode 100644 index 0000000000000..c94c460b97416 --- /dev/null +++ b/packages/react/index.development.js @@ -0,0 +1,80 @@ +/** + * 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 + */ + +// Keep in sync with https://github.com/facebook/flow/blob/main/lib/react.js +export type ComponentType<-P> = React$ComponentType

; +export type AbstractComponent< + -Config, + +Instance = mixed, +> = React$AbstractComponent; +export type ElementType = React$ElementType; +export type Element<+C> = React$Element; +export type Key = React$Key; +export type Ref = React$Ref; +export type Node = React$Node; +export type Context = React$Context; +export type Portal = React$Portal; +export type ElementProps = React$ElementProps; +export type ElementConfig = React$ElementConfig; +export type ElementRef = React$ElementRef; +export type Config = React$Config; +export type ChildrenArray<+T> = $ReadOnlyArray> | T; + +// Export all exports so that they're available in tests. +// We can't use export * from in Flow for some reason. +export { + __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, + Children, + Component, + Fragment, + Profiler, + PureComponent, + StrictMode, + Suspense, + cloneElement, + createContext, + createElement, + createRef, + use, + forwardRef, + isValidElement, + lazy, + memo, + cache, + startTransition, + unstable_DebugTracingMode, + unstable_LegacyHidden, + unstable_Activity, + unstable_Scope, + unstable_SuspenseList, + unstable_TracingMarker, + unstable_getCacheForType, + unstable_useCacheRefresh, + useId, + useCallback, + useContext, + useDebugValue, + useDeferredValue, + useEffect, + experimental_useEffectEvent, + useImperativeHandle, + useInsertionEffect, + useLayoutEffect, + useMemo, + useOptimistic, + useSyncExternalStore, + useReducer, + useRef, + useState, + useTransition, + useActionState, + version, + act, // DEV-only + captureOwnerStack, // DEV-only +} from './src/ReactClient'; diff --git a/packages/react/index.experimental.development.js b/packages/react/index.experimental.development.js new file mode 100644 index 0000000000000..4ffcdea0d242e --- /dev/null +++ b/packages/react/index.experimental.development.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, + Children, + Component, + Fragment, + Profiler, + PureComponent, + StrictMode, + Suspense, + cloneElement, + createContext, + createElement, + createRef, + use, + forwardRef, + isValidElement, + lazy, + memo, + cache, + startTransition, + unstable_DebugTracingMode, + unstable_Activity, + unstable_postpone, + unstable_getCacheForType, + unstable_SuspenseList, + unstable_useCacheRefresh, + useId, + useCallback, + useContext, + useDebugValue, + useDeferredValue, + useEffect, + experimental_useEffectEvent, + useImperativeHandle, + useInsertionEffect, + useLayoutEffect, + useMemo, + useOptimistic, + useReducer, + useRef, + useState, + useSyncExternalStore, + useTransition, + useActionState, + version, + act, // DEV-only + captureOwnerStack, // DEV-only +} from './src/ReactClient'; + +import {useOptimistic} from './src/ReactClient'; + +export function experimental_useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, +): [S, (A) => void] { + if (__DEV__) { + console.error( + 'useOptimistic is now in canary. Remove the experimental_ prefix. ' + + 'The prefixed alias will be removed in an upcoming release.', + ); + } + return useOptimistic(passthrough, reducer); +} diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index efac1257849be..6f2ab835135c0 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -9,7 +9,6 @@ export { __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, - act, Children, Component, Fragment, diff --git a/packages/react/index.js b/packages/react/index.js index 8808d259583ee..bad2c40e6c3dd 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -30,7 +30,6 @@ export type ChildrenArray<+T> = $ReadOnlyArray> | T; // We can't use export * from in Flow for some reason. export { __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, - act, Children, Component, Fragment, diff --git a/packages/react/src/ReactClient.js b/packages/react/src/ReactClient.js index 4d5ec0d3ea5f8..fc668246a988b 100644 --- a/packages/react/src/ReactClient.js +++ b/packages/react/src/ReactClient.js @@ -61,6 +61,7 @@ import { import ReactSharedInternals from './ReactSharedInternalsClient'; import {startTransition} from './ReactStartTransition'; import {act} from './ReactAct'; +import {captureOwnerStack} from './ReactOwnerStack'; const Children = { map, @@ -121,5 +122,6 @@ export { // enableTransitionTracing REACT_TRACING_MARKER_TYPE as unstable_TracingMarker, useId, - act, + act, // DEV-only + captureOwnerStack, // DEV-only }; diff --git a/packages/react/src/ReactOwnerStack.js b/packages/react/src/ReactOwnerStack.js new file mode 100644 index 0000000000000..f6944b08cde32 --- /dev/null +++ b/packages/react/src/ReactOwnerStack.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) Meta Platforms, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +export function captureOwnerStack(): null | string { + if (!enableOwnerStacks || !__DEV__) { + return null; + } + const getCurrentStack = ReactSharedInternals.getCurrentStack; + if (getCurrentStack === null) { + return null; + } + // The current stack will be the owner stack if enableOwnerStacks is true + // which it is always here. Otherwise it's the parent stack. + return getCurrentStack(null); +} diff --git a/packages/react/src/ReactServer.experimental.development.js b/packages/react/src/ReactServer.experimental.development.js new file mode 100644 index 0000000000000..ccf423d79ecc7 --- /dev/null +++ b/packages/react/src/ReactServer.experimental.development.js @@ -0,0 +1,85 @@ +/** + * 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 {default as __SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE} from './ReactSharedInternalsServer'; + +import {forEach, map, count, toArray, only} from './ReactChildren'; +import { + REACT_FRAGMENT_TYPE, + REACT_PROFILER_TYPE, + REACT_STRICT_MODE_TYPE, + REACT_SUSPENSE_TYPE, + REACT_DEBUG_TRACING_MODE_TYPE, +} from 'shared/ReactSymbols'; +import { + cloneElement, + createElement, + isValidElement, +} from './jsx/ReactJSXElement'; +import {createRef} from './ReactCreateRef'; +import { + use, + useId, + useCallback, + useDebugValue, + useMemo, + useActionState, + getCacheForType, +} from './ReactHooks'; +import {forwardRef} from './ReactForwardRef'; +import {lazy} from './ReactLazy'; +import {memo} from './ReactMemo'; +import {cache} from './ReactCacheServer'; +import {startTransition} from './ReactStartTransition'; +import {postpone} from './ReactPostpone'; +import {captureOwnerStack} from './ReactOwnerStack'; +import version from 'shared/ReactVersion'; + +const Children = { + map, + forEach, + count, + toArray, + only, +}; + +// These are server-only +export { + taintUniqueValue as experimental_taintUniqueValue, + taintObjectReference as experimental_taintObjectReference, +} from './ReactTaint'; + +export { + Children, + REACT_FRAGMENT_TYPE as Fragment, + REACT_PROFILER_TYPE as Profiler, + REACT_STRICT_MODE_TYPE as StrictMode, + REACT_SUSPENSE_TYPE as Suspense, + cloneElement, + createElement, + createRef, + use, + forwardRef, + isValidElement, + lazy, + memo, + cache, + startTransition, + REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode, + REACT_SUSPENSE_TYPE as unstable_SuspenseList, + getCacheForType as unstable_getCacheForType, + postpone as unstable_postpone, + useId, + useCallback, + useDebugValue, + useMemo, + useActionState, + version, + captureOwnerStack, // DEV-only +}; diff --git a/packages/react/src/ReactSharedInternalsClient.js b/packages/react/src/ReactSharedInternalsClient.js index 6a54c73be30f1..8b00f98a51bf3 100644 --- a/packages/react/src/ReactSharedInternalsClient.js +++ b/packages/react/src/ReactSharedInternalsClient.js @@ -35,7 +35,7 @@ export type SharedStateClient = { thrownErrors: Array, // ReactDebugCurrentFrame - getCurrentStack: null | ((stack: Error) => string), + getCurrentStack: null | ((stack: null | Error) => string), }; export type RendererTask = boolean => RendererTask | null; @@ -56,7 +56,7 @@ if (__DEV__) { // Stack implementation injected by the current renderer. ReactSharedInternals.getCurrentStack = (null: | null - | ((stack: Error) => string)); + | ((stack: null | Error) => string)); } export default ReactSharedInternals; diff --git a/packages/react/src/ReactSharedInternalsServer.js b/packages/react/src/ReactSharedInternalsServer.js index 749ce5c3ad44a..0bfeb26fb8007 100644 --- a/packages/react/src/ReactSharedInternalsServer.js +++ b/packages/react/src/ReactSharedInternalsServer.js @@ -38,7 +38,7 @@ export type SharedStateServer = { // DEV-only // ReactDebugCurrentFrame - getCurrentStack: null | ((stack: Error) => string), + getCurrentStack: null | ((stack: null | Error) => string), }; export type RendererTask = boolean => RendererTask | null; @@ -60,7 +60,7 @@ if (__DEV__) { // Stack implementation injected by the current renderer. ReactSharedInternals.getCurrentStack = (null: | null - | ((stack: Error) => string)); + | ((stack: null | Error) => string)); } export default ReactSharedInternals; diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index fcd1ed213030e..b52fac3201ebf 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -12,6 +12,8 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { // .stable.js // .experimental.js // .js + // or any of those plus .development.js + if (isFBBundle) { // FB builds for react-dom need to alias both react-dom and react-dom/client to the same // entrypoint since there is only a single build for them. @@ -41,6 +43,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { } resolvedEntry = nodePath.join(resolvedEntry, '..', entrypoint); + const developmentEntry = resolvedEntry.replace('.js', '.development.js'); + if (fs.existsSync(developmentEntry)) { + return developmentEntry; + } if (fs.existsSync(resolvedEntry)) { return resolvedEntry; } @@ -53,6 +59,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { '.js', __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js' ); + const devFBEntry = resolvedFBEntry.replace('.js', '.development.js'); + if (fs.existsSync(devFBEntry)) { + return devFBEntry; + } if (fs.existsSync(resolvedFBEntry)) { return resolvedFBEntry; } @@ -66,9 +76,17 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { '.js', __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js' ); + const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js'); + if (fs.existsSync(devForkedEntry)) { + return devForkedEntry; + } if (fs.existsSync(resolvedForkedEntry)) { return resolvedForkedEntry; } + const plainDevEntry = resolvedEntry.replace('.js', '.development.js'); + if (fs.existsSync(plainDevEntry)) { + return plainDevEntry; + } // Just use the plain .js one. return resolvedEntry; } @@ -77,7 +95,8 @@ function mockReact() { jest.mock('react', () => { const resolvedEntryPoint = resolveEntryFork( require.resolve('react'), - global.__WWW__ || global.__XPLAT__ + global.__WWW__ || global.__XPLAT__, + global.__DEV__ ); return jest.requireActual(resolvedEntryPoint); }); @@ -100,7 +119,8 @@ jest.mock('react/react.react-server', () => { }); const resolvedEntryPoint = resolveEntryFork( require.resolve('react/src/ReactServer'), - global.__WWW__ || global.__XPLAT__ + global.__WWW__ || global.__XPLAT__, + global.__DEV__ ); return jest.requireActual(resolvedEntryPoint); }); @@ -198,7 +218,8 @@ inlinedHostConfigs.forEach(rendererInfo => { mockAllConfigs(rendererInfo); const resolvedEntryPoint = resolveEntryFork( require.resolve(entryPoint), - global.__WWW__ || global.__XPLAT__ + global.__WWW__ || global.__XPLAT__, + global.__DEV__ ); return jest.requireActual(resolvedEntryPoint); }); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index df9fb00b09424..ce87ba57d4481 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -567,16 +567,31 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { // .stable.js // .experimental.js // .js + // or any of those plus .development.js if (isFBBundle) { const resolvedFBEntry = resolvedEntry.replace( '.js', __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js' ); + const developmentFBEntry = resolvedFBEntry.replace( + '.js', + '.development.js' + ); + if (fs.existsSync(developmentFBEntry)) { + return developmentFBEntry; + } if (fs.existsSync(resolvedFBEntry)) { return resolvedFBEntry; } const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js'); + const developmentGenericFBEntry = resolvedGenericFBEntry.replace( + '.js', + '.development.js' + ); + if (fs.existsSync(developmentGenericFBEntry)) { + return developmentGenericFBEntry; + } if (fs.existsSync(resolvedGenericFBEntry)) { return resolvedGenericFBEntry; } @@ -586,6 +601,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { '.js', __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js' ); + const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js'); + if (fs.existsSync(devForkedEntry)) { + return devForkedEntry; + } if (fs.existsSync(resolvedForkedEntry)) { return resolvedForkedEntry; } @@ -604,7 +623,8 @@ async function createBundle(bundle, bundleType) { let resolvedEntry = resolveEntryFork( require.resolve(bundle.entry), - isFBWWWBundle || isFBRNBundle + isFBWWWBundle || isFBRNBundle, + !isProductionBundleType(bundleType) ); const peerGlobals = Modules.getPeerGlobals(bundle.externals, bundleType);