@samitouri / QOS-React / commits / 6b4646cbd0

Export captureOwnerStacks() only in DEV "react" builds (#29923)

Only with the enableOwnerStacks flag (which is not on in www). This is a new DEV-only API to be able to implement what we do for console.error in user space. This API does not actually include the current stack that you'd get from `new Error().stack`. That you'd have to add yourself. This adds the ability to have conditional development exports because we plan on eventually having separate ESM builds that use the "development" or "production" export conditions. NOTE: This removes the export of `act` from `react` in prod (as opposed to a function that throws) - inline with what we do with other conditional exports.

Sebastian Markbåge committed Jun 19, 2024 at 14:19 UTC 6b4646cbd0fd20a7c37f5dfcb0f12193259a6289
14 files changed +380 -12
packages/react-reconciler/src/ReactCurrentFiber.js
+1 -1
@@ -44,7 +44,7 @@ export function getCurrentParentStackInDev(): string {
44 return '';
45 }
46
47 -function getCurrentFiberStackInDev(stack: Error): string {
47 +function getCurrentFiberStackInDev(stack: null | Error): string {
48 if (__DEV__) {
49 if (current === null) {
50 return '';
packages/react-reconciler/src/__tests__/ReactOwnerStacks-test.js new
+65
@@ -0,0 +1,65 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +'use strict';
11 +
12 +let React;
13 +let ReactNoop;
14 +let act;
15 +
16 +describe('ReactOwnerStacks', () => {
17 + beforeEach(function () {
18 + jest.resetModules();
19 +
20 + React = require('react');
21 + ReactNoop = require('react-noop-renderer');
22 + act = require('internal-test-utils').act;
23 + });
24 +
25 + function normalizeCodeLocInfo(str) {
26 + return (
27 + str &&
28 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
29 + return '\n in ' + name + ' (at **)';
30 + })
31 + );
32 + }
33 +
34 + // @gate __DEV__ && enableOwnerStacks
35 + it('can get the component owner stacks during rendering in dev', async () => {
36 + let stack;
37 +
38 + function Foo() {
39 + return <Bar />;
40 + }
41 + function Bar() {
42 + return (
43 + <div>
44 + <Baz />
45 + </div>
46 + );
47 + }
48 + function Baz() {
49 + stack = React.captureOwnerStack();
50 + return <span>hi</span>;
51 + }
52 +
53 + await act(() => {
54 + ReactNoop.render(
55 + <div>
56 + <Foo />
57 + </div>,
58 + );
59 + });
60 +
61 + expect(normalizeCodeLocInfo(stack)).toBe(
62 + '\n in Bar (at **)' + '\n in Foo (at **)',
63 + );
64 + });
65 +});
packages/react-test-renderer/src/ReactTestRenderer.js
+1
@@ -61,6 +61,7 @@ import {
61 disableLegacyMode,
62 } from 'shared/ReactFeatureFlags';
63
64 +// $FlowFixMe[prop-missing]: This is only in the development export.
65 const act = React.act;
66
67 // TODO: Remove from public bundle
packages/react/index.development.js new
+80
@@ -0,0 +1,80 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// Keep in sync with https://github.com/facebook/flow/blob/main/lib/react.js
11 +export type ComponentType<-P> = React$ComponentType<P>;
12 +export type AbstractComponent<
13 + -Config,
14 + +Instance = mixed,
15 +> = React$AbstractComponent<Config, Instance>;
16 +export type ElementType = React$ElementType;
17 +export type Element<+C> = React$Element<C>;
18 +export type Key = React$Key;
19 +export type Ref<C> = React$Ref<C>;
20 +export type Node = React$Node;
21 +export type Context<T> = React$Context<T>;
22 +export type Portal = React$Portal;
23 +export type ElementProps<C> = React$ElementProps<C>;
24 +export type ElementConfig<C> = React$ElementConfig<C>;
25 +export type ElementRef<C> = React$ElementRef<C>;
26 +export type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>;
27 +export type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T;
28 +
29 +// Export all exports so that they're available in tests.
30 +// We can't use export * from in Flow for some reason.
31 +export {
32 + __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
33 + Children,
34 + Component,
35 + Fragment,
36 + Profiler,
37 + PureComponent,
38 + StrictMode,
39 + Suspense,
40 + cloneElement,
41 + createContext,
42 + createElement,
43 + createRef,
44 + use,
45 + forwardRef,
46 + isValidElement,
47 + lazy,
48 + memo,
49 + cache,
50 + startTransition,
51 + unstable_DebugTracingMode,
52 + unstable_LegacyHidden,
53 + unstable_Activity,
54 + unstable_Scope,
55 + unstable_SuspenseList,
56 + unstable_TracingMarker,
57 + unstable_getCacheForType,
58 + unstable_useCacheRefresh,
59 + useId,
60 + useCallback,
61 + useContext,
62 + useDebugValue,
63 + useDeferredValue,
64 + useEffect,
65 + experimental_useEffectEvent,
66 + useImperativeHandle,
67 + useInsertionEffect,
68 + useLayoutEffect,
69 + useMemo,
70 + useOptimistic,
71 + useSyncExternalStore,
72 + useReducer,
73 + useRef,
74 + useState,
75 + useTransition,
76 + useActionState,
77 + version,
78 + act, // DEV-only
79 + captureOwnerStack, // DEV-only
80 +} from './src/ReactClient';
packages/react/index.experimental.development.js new
+72
@@ -0,0 +1,72 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
12 + Children,
13 + Component,
14 + Fragment,
15 + Profiler,
16 + PureComponent,
17 + StrictMode,
18 + Suspense,
19 + cloneElement,
20 + createContext,
21 + createElement,
22 + createRef,
23 + use,
24 + forwardRef,
25 + isValidElement,
26 + lazy,
27 + memo,
28 + cache,
29 + startTransition,
30 + unstable_DebugTracingMode,
31 + unstable_Activity,
32 + unstable_postpone,
33 + unstable_getCacheForType,
34 + unstable_SuspenseList,
35 + unstable_useCacheRefresh,
36 + useId,
37 + useCallback,
38 + useContext,
39 + useDebugValue,
40 + useDeferredValue,
41 + useEffect,
42 + experimental_useEffectEvent,
43 + useImperativeHandle,
44 + useInsertionEffect,
45 + useLayoutEffect,
46 + useMemo,
47 + useOptimistic,
48 + useReducer,
49 + useRef,
50 + useState,
51 + useSyncExternalStore,
52 + useTransition,
53 + useActionState,
54 + version,
55 + act, // DEV-only
56 + captureOwnerStack, // DEV-only
57 +} from './src/ReactClient';
58 +
59 +import {useOptimistic} from './src/ReactClient';
60 +
61 +export function experimental_useOptimistic<S, A>(
62 + passthrough: S,
63 + reducer: ?(S, A) => S,
64 +): [S, (A) => void] {
65 + if (__DEV__) {
66 + console.error(
67 + 'useOptimistic is now in canary. Remove the experimental_ prefix. ' +
68 + 'The prefixed alias will be removed in an upcoming release.',
69 + );
70 + }
71 + return useOptimistic(passthrough, reducer);
72 +}
packages/react/index.experimental.js
-1
@@ -9,7 +9,6 @@
9
10 export {
11 __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
12 - act,
12 Children,
13 Component,
14 Fragment,
packages/react/index.js
-1
@@ -30,7 +30,6 @@ export type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T;
30 // We can't use export * from in Flow for some reason.
31 export {
32 __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
33 - act,
33 Children,
34 Component,
35 Fragment,
packages/react/src/ReactClient.js
+3 -1
@@ -61,6 +61,7 @@ import {
61 import ReactSharedInternals from './ReactSharedInternalsClient';
62 import {startTransition} from './ReactStartTransition';
63 import {act} from './ReactAct';
64 +import {captureOwnerStack} from './ReactOwnerStack';
65
66 const Children = {
67 map,
@@ -121,5 +122,6 @@ export {
122 // enableTransitionTracing
123 REACT_TRACING_MARKER_TYPE as unstable_TracingMarker,
124 useId,
124 - act,
125 + act, // DEV-only
126 + captureOwnerStack, // DEV-only
127 };
packages/react/src/ReactOwnerStack.js new
+24
@@ -0,0 +1,24 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
11 +import ReactSharedInternals from 'shared/ReactSharedInternals';
12 +
13 +export function captureOwnerStack(): null | string {
14 + if (!enableOwnerStacks || !__DEV__) {
15 + return null;
16 + }
17 + const getCurrentStack = ReactSharedInternals.getCurrentStack;
18 + if (getCurrentStack === null) {
19 + return null;
20 + }
21 + // The current stack will be the owner stack if enableOwnerStacks is true
22 + // which it is always here. Otherwise it's the parent stack.
23 + return getCurrentStack(null);
24 +}
packages/react/src/ReactServer.experimental.development.js new
+85
@@ -0,0 +1,85 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {default as __SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE} from './ReactSharedInternalsServer';
11 +
12 +import {forEach, map, count, toArray, only} from './ReactChildren';
13 +import {
14 + REACT_FRAGMENT_TYPE,
15 + REACT_PROFILER_TYPE,
16 + REACT_STRICT_MODE_TYPE,
17 + REACT_SUSPENSE_TYPE,
18 + REACT_DEBUG_TRACING_MODE_TYPE,
19 +} from 'shared/ReactSymbols';
20 +import {
21 + cloneElement,
22 + createElement,
23 + isValidElement,
24 +} from './jsx/ReactJSXElement';
25 +import {createRef} from './ReactCreateRef';
26 +import {
27 + use,
28 + useId,
29 + useCallback,
30 + useDebugValue,
31 + useMemo,
32 + useActionState,
33 + getCacheForType,
34 +} from './ReactHooks';
35 +import {forwardRef} from './ReactForwardRef';
36 +import {lazy} from './ReactLazy';
37 +import {memo} from './ReactMemo';
38 +import {cache} from './ReactCacheServer';
39 +import {startTransition} from './ReactStartTransition';
40 +import {postpone} from './ReactPostpone';
41 +import {captureOwnerStack} from './ReactOwnerStack';
42 +import version from 'shared/ReactVersion';
43 +
44 +const Children = {
45 + map,
46 + forEach,
47 + count,
48 + toArray,
49 + only,
50 +};
51 +
52 +// These are server-only
53 +export {
54 + taintUniqueValue as experimental_taintUniqueValue,
55 + taintObjectReference as experimental_taintObjectReference,
56 +} from './ReactTaint';
57 +
58 +export {
59 + Children,
60 + REACT_FRAGMENT_TYPE as Fragment,
61 + REACT_PROFILER_TYPE as Profiler,
62 + REACT_STRICT_MODE_TYPE as StrictMode,
63 + REACT_SUSPENSE_TYPE as Suspense,
64 + cloneElement,
65 + createElement,
66 + createRef,
67 + use,
68 + forwardRef,
69 + isValidElement,
70 + lazy,
71 + memo,
72 + cache,
73 + startTransition,
74 + REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode,
75 + REACT_SUSPENSE_TYPE as unstable_SuspenseList,
76 + getCacheForType as unstable_getCacheForType,
77 + postpone as unstable_postpone,
78 + useId,
79 + useCallback,
80 + useDebugValue,
81 + useMemo,
82 + useActionState,
83 + version,
84 + captureOwnerStack, // DEV-only
85 +};
packages/react/src/ReactSharedInternalsClient.js
+2 -2
@@ -35,7 +35,7 @@ export type SharedStateClient = {
35 thrownErrors: Array<mixed>,
36
37 // ReactDebugCurrentFrame
38 - getCurrentStack: null | ((stack: Error) => string),
38 + getCurrentStack: null | ((stack: null | Error) => string),
39 };
40
41 export type RendererTask = boolean => RendererTask | null;
@@ -56,7 +56,7 @@ if (__DEV__) {
56 // Stack implementation injected by the current renderer.
57 ReactSharedInternals.getCurrentStack = (null:
58 | null
59 - | ((stack: Error) => string));
59 + | ((stack: null | Error) => string));
60 }
61
62 export default ReactSharedInternals;
packages/react/src/ReactSharedInternalsServer.js
+2 -2
@@ -38,7 +38,7 @@ export type SharedStateServer = {
38 // DEV-only
39
40 // ReactDebugCurrentFrame
41 - getCurrentStack: null | ((stack: Error) => string),
41 + getCurrentStack: null | ((stack: null | Error) => string),
42 };
43
44 export type RendererTask = boolean => RendererTask | null;
@@ -60,7 +60,7 @@ if (__DEV__) {
60 // Stack implementation injected by the current renderer.
61 ReactSharedInternals.getCurrentStack = (null:
62 | null
63 - | ((stack: Error) => string));
63 + | ((stack: null | Error) => string));
64 }
65
66 export default ReactSharedInternals;
scripts/jest/setupHostConfigs.js
+24 -3
@@ -12,6 +12,8 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
12 // .stable.js
13 // .experimental.js
14 // .js
15 + // or any of those plus .development.js
16 +
17 if (isFBBundle) {
18 // FB builds for react-dom need to alias both react-dom and react-dom/client to the same
19 // entrypoint since there is only a single build for them.
@@ -41,6 +43,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
43 }
44
45 resolvedEntry = nodePath.join(resolvedEntry, '..', entrypoint);
46 + const developmentEntry = resolvedEntry.replace('.js', '.development.js');
47 + if (fs.existsSync(developmentEntry)) {
48 + return developmentEntry;
49 + }
50 if (fs.existsSync(resolvedEntry)) {
51 return resolvedEntry;
52 }
@@ -53,6 +59,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
59 '.js',
60 __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'
61 );
62 + const devFBEntry = resolvedFBEntry.replace('.js', '.development.js');
63 + if (fs.existsSync(devFBEntry)) {
64 + return devFBEntry;
65 + }
66 if (fs.existsSync(resolvedFBEntry)) {
67 return resolvedFBEntry;
68 }
@@ -66,9 +76,17 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
76 '.js',
77 __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'
78 );
79 + const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js');
80 + if (fs.existsSync(devForkedEntry)) {
81 + return devForkedEntry;
82 + }
83 if (fs.existsSync(resolvedForkedEntry)) {
84 return resolvedForkedEntry;
85 }
86 + const plainDevEntry = resolvedEntry.replace('.js', '.development.js');
87 + if (fs.existsSync(plainDevEntry)) {
88 + return plainDevEntry;
89 + }
90 // Just use the plain .js one.
91 return resolvedEntry;
92 }
@@ -77,7 +95,8 @@ function mockReact() {
95 jest.mock('react', () => {
96 const resolvedEntryPoint = resolveEntryFork(
97 require.resolve('react'),
80 - global.__WWW__ || global.__XPLAT__
98 + global.__WWW__ || global.__XPLAT__,
99 + global.__DEV__
100 );
101 return jest.requireActual(resolvedEntryPoint);
102 });
@@ -100,7 +119,8 @@ jest.mock('react/react.react-server', () => {
119 });
120 const resolvedEntryPoint = resolveEntryFork(
121 require.resolve('react/src/ReactServer'),
103 - global.__WWW__ || global.__XPLAT__
122 + global.__WWW__ || global.__XPLAT__,
123 + global.__DEV__
124 );
125 return jest.requireActual(resolvedEntryPoint);
126 });
@@ -198,7 +218,8 @@ inlinedHostConfigs.forEach(rendererInfo => {
218 mockAllConfigs(rendererInfo);
219 const resolvedEntryPoint = resolveEntryFork(
220 require.resolve(entryPoint),
201 - global.__WWW__ || global.__XPLAT__
221 + global.__WWW__ || global.__XPLAT__,
222 + global.__DEV__
223 );
224 return jest.requireActual(resolvedEntryPoint);
225 });
scripts/rollup/build.js
+21 -1
@@ -567,16 +567,31 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
567 // .stable.js
568 // .experimental.js
569 // .js
570 + // or any of those plus .development.js
571
572 if (isFBBundle) {
573 const resolvedFBEntry = resolvedEntry.replace(
574 '.js',
575 __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'
576 );
577 + const developmentFBEntry = resolvedFBEntry.replace(
578 + '.js',
579 + '.development.js'
580 + );
581 + if (fs.existsSync(developmentFBEntry)) {
582 + return developmentFBEntry;
583 + }
584 if (fs.existsSync(resolvedFBEntry)) {
585 return resolvedFBEntry;
586 }
587 const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js');
588 + const developmentGenericFBEntry = resolvedGenericFBEntry.replace(
589 + '.js',
590 + '.development.js'
591 + );
592 + if (fs.existsSync(developmentGenericFBEntry)) {
593 + return developmentGenericFBEntry;
594 + }
595 if (fs.existsSync(resolvedGenericFBEntry)) {
596 return resolvedGenericFBEntry;
597 }
@@ -586,6 +601,10 @@ function resolveEntryFork(resolvedEntry, isFBBundle) {
601 '.js',
602 __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'
603 );
604 + const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js');
605 + if (fs.existsSync(devForkedEntry)) {
606 + return devForkedEntry;
607 + }
608 if (fs.existsSync(resolvedForkedEntry)) {
609 return resolvedForkedEntry;
610 }
@@ -604,7 +623,8 @@ async function createBundle(bundle, bundleType) {
623
624 let resolvedEntry = resolveEntryFork(
625 require.resolve(bundle.entry),
607 - isFBWWWBundle || isFBRNBundle
626 + isFBWWWBundle || isFBRNBundle,
627 + !isProductionBundleType(bundleType)
628 );
629
630 const peerGlobals = Modules.getPeerGlobals(bundle.externals, bundleType);