react/scripts/rollup/forks.js
Josh Story cb151849e1
[react-dom] move all client code to react-dom/client (#28271)
This PR reorganizes the `react-dom` entrypoint to only pull in code that
is environment agnostic. Previously if you required anything from this
entrypoint in any environment the entire client reconciler was loaded.
In a prior release we added a server rendering stub which you could
alias in server environments to omit this unecessary code. After landing
this change this entrypoint should not load any environment specific
code.

While a few APIs are truly client (browser) only such as createRoot and
hydrateRoot many of the APIs you import from this package are only
useful in the browser but could concievably be imported in shared code
(components running in Fizz or shared components as part of an RSC app).
To avoid making these require opting into the client bundle we are
keeping them in the `react-dom` entrypoint and changing their
implementation so that in environments where they are not particularly
useful they do something benign and expected.

#### Removed APIs
The following APIs are being removed in the next major. Largely they
have all been deprecated already and are part of legacy rendering modes
where concurrent features of React are not available
* `render`
* `hydrate`
* `findDOMNode`
* `unmountComponentAtNode`
* `unstable_createEventHandle`
* `unstable_renderSubtreeIntoContainer`
* `unstable_runWithPrioirty`

#### moved Client APIs
These APIs were available on both `react-dom` (with a warning) and
`react-dom/client`. After this change they are only available on
`react-dom/client`
* `createRoot`
* `hydrateRoot`

#### retained APIs
These APIs still exist on the `react-dom` entrypoint but have normalized
behavior depending on which renderers are currently in scope
* `flushSync`: will execute the function (if provided) inside the
flushSync implemention of FlightServer, Fizz, and Fiber DOM renderers.
* `unstable_batchedUpdates`: This is a noop in concurrent mode because
it is now the only supported behavior because there is no legacy
rendering mode
* `createPortal`: This just produces an object. It can be called from
anywhere but since you will probably not have a handle on a DOM node to
pass to it it will likely warn in environments other than the browser
* preloading APIS such as `preload`: These methods will execute the
preload across all renderers currently in scope. Since we resolve the
Request object on the server using AsyncLocalStorage or the current
function stack in practice only one renderer should act upon the
preload.

In addition to these changes the server rendering stub now just rexports
everything from `react-dom`. In a future minor we will add a warning
when using the stub and in the next major we will remove the stub
altogether
2024-04-24 08:50:32 -07:00

452 lines
14 KiB
JavaScript

'use strict';
const fs = require('node:fs');
const {bundleTypes, moduleTypes} = require('./bundles');
const inlinedHostConfigs = require('../shared/inlinedHostConfigs');
const {
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
RN_OSS_DEV,
RN_OSS_PROD,
RN_OSS_PROFILING,
RN_FB_DEV,
RN_FB_PROD,
RN_FB_PROFILING,
} = bundleTypes;
const {RENDERER, RECONCILER} = moduleTypes;
const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL;
// Default to building in experimental mode. If the release channel is set via
// an environment variable, then check if it's "experimental".
const __EXPERIMENTAL__ =
typeof RELEASE_CHANNEL === 'string'
? RELEASE_CHANNEL === 'experimental'
: true;
function findNearestExistingForkFile(path, segmentedIdentifier, suffix) {
const segments = segmentedIdentifier.split('-');
while (segments.length) {
const candidate = segments.join('-');
const forkPath = path + candidate + suffix;
try {
fs.statSync(forkPath);
return forkPath;
} catch (error) {
// Try the next candidate.
}
segments.pop();
}
return null;
}
// If you need to replace a file with another file for a specific environment,
// add it to this list with the logic for choosing the right replacement.
// Fork paths are relative to the project root. They must include the full path,
// including the extension. We intentionally don't use Node's module resolution
// algorithm because 1) require.resolve doesn't work with ESM modules, and 2)
// the behavior is easier to predict.
const forks = Object.freeze({
// Without this fork, importing `shared/ReactSharedInternals` inside
// the `react` package itself would not work due to a cyclical dependency.
'./packages/shared/ReactSharedInternals.js': (
bundleType,
entry,
dependencies,
_moduleType,
bundle
) => {
if (entry === 'react') {
return './packages/react/src/ReactSharedInternalsClient.js';
}
if (entry === 'react/src/ReactServer.js') {
return './packages/react/src/ReactSharedInternalsServer.js';
}
if (bundle.condition === 'react-server') {
return './packages/react-server/src/ReactSharedInternalsServer.js';
}
if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) {
// React internals are unavailable if we can't reference the package.
// We return an error because we only want to throw if this module gets used.
return new Error(
'Cannot use a module that depends on ReactSharedInternals ' +
'from "' +
entry +
'" because it does not declare "react" in the package ' +
'dependencies or peerDependencies.'
);
}
return null;
},
// Without this fork, importing `shared/ReactDOMSharedInternals` inside
// the `react-dom` package itself would not work due to a cyclical dependency.
'./packages/shared/ReactDOMSharedInternals.js': (
bundleType,
entry,
dependencies
) => {
if (
entry === 'react-dom' ||
entry === 'react-dom/src/ReactDOMFB.js' ||
entry === 'react-dom/src/ReactDOMTestingFB.js' ||
entry === 'react-dom/src/ReactDOMServer.js'
) {
if (
bundleType === FB_WWW_DEV ||
bundleType === FB_WWW_PROD ||
bundleType === FB_WWW_PROFILING
) {
return './packages/react-dom/src/ReactDOMSharedInternalsFB.js';
} else {
return './packages/react-dom/src/ReactDOMSharedInternals.js';
}
}
if (
!entry.startsWith('react-dom/') &&
dependencies.indexOf('react-dom') === -1
) {
// React DOM internals are unavailable if we can't reference the package.
// We return an error because we only want to throw if this module gets used.
return new Error(
'Cannot use a module that depends on ReactDOMSharedInternals ' +
'from "' +
entry +
'" because it does not declare "react-dom" in the package ' +
'dependencies or peerDependencies.'
);
}
return null;
},
// We have a few forks for different environments.
'./packages/shared/ReactFeatureFlags.js': (bundleType, entry) => {
switch (entry) {
case 'react-native-renderer':
switch (bundleType) {
case RN_FB_DEV:
case RN_FB_PROD:
case RN_FB_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.native-fb.js';
case RN_OSS_DEV:
case RN_OSS_PROD:
case RN_OSS_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.native-oss.js';
default:
throw Error(
`Unexpected entry (${entry}) and bundleType (${bundleType})`
);
}
case 'react-native-renderer/fabric':
switch (bundleType) {
case RN_FB_DEV:
case RN_FB_PROD:
case RN_FB_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.native-fb.js';
case RN_OSS_DEV:
case RN_OSS_PROD:
case RN_OSS_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.native-oss.js';
default:
throw Error(
`Unexpected entry (${entry}) and bundleType (${bundleType})`
);
}
case 'react-test-renderer':
switch (bundleType) {
case RN_FB_DEV:
case RN_FB_PROD:
case RN_FB_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js';
case FB_WWW_DEV:
case FB_WWW_PROD:
case FB_WWW_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.test-renderer.www.js';
}
return './packages/shared/forks/ReactFeatureFlags.test-renderer.js';
default:
switch (bundleType) {
case FB_WWW_DEV:
case FB_WWW_PROD:
case FB_WWW_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.www.js';
case RN_FB_DEV:
case RN_FB_PROD:
case RN_FB_PROFILING:
return './packages/shared/forks/ReactFeatureFlags.native-fb.js';
}
}
return null;
},
'./packages/scheduler/src/SchedulerFeatureFlags.js': (
bundleType,
entry,
dependencies
) => {
if (
bundleType === FB_WWW_DEV ||
bundleType === FB_WWW_PROD ||
bundleType === FB_WWW_PROFILING
) {
return './packages/scheduler/src/forks/SchedulerFeatureFlags.www.js';
}
return './packages/scheduler/src/SchedulerFeatureFlags.js';
},
'./packages/shared/consoleWithStackDev.js': (bundleType, entry) => {
switch (bundleType) {
case FB_WWW_DEV:
return './packages/shared/forks/consoleWithStackDev.www.js';
default:
return null;
}
},
'./packages/react-reconciler/src/ReactFiberConfig.js': (
bundleType,
entry,
dependencies,
moduleType
) => {
if (dependencies.indexOf('react-reconciler') !== -1) {
return null;
}
if (moduleType !== RENDERER && moduleType !== RECONCILER) {
return null;
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (let rendererInfo of inlinedHostConfigs) {
if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
const foundFork = findNearestExistingForkFile(
'./packages/react-reconciler/src/forks/ReactFiberConfig.',
rendererInfo.shortName,
'.js'
);
if (foundFork) {
return foundFork;
}
// fall through to error
break;
}
}
throw new Error(
'Expected ReactFiberConfig to always be replaced with a shim, but ' +
`found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
'Did you mean to add it there to associate it with a specific renderer?'
);
},
'./packages/react-server/src/ReactServerStreamConfig.js': (
bundleType,
entry,
dependencies,
moduleType
) => {
if (dependencies.indexOf('react-server') !== -1) {
return null;
}
if (moduleType !== RENDERER && moduleType !== RECONCILER) {
return null;
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (let rendererInfo of inlinedHostConfigs) {
if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
if (!rendererInfo.isServerSupported) {
return null;
}
const foundFork = findNearestExistingForkFile(
'./packages/react-server/src/forks/ReactServerStreamConfig.',
rendererInfo.shortName,
'.js'
);
if (foundFork) {
return foundFork;
}
// fall through to error
break;
}
}
throw new Error(
'Expected ReactServerStreamConfig to always be replaced with a shim, but ' +
`found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
'Did you mean to add it there to associate it with a specific renderer?'
);
},
'./packages/react-server/src/ReactFizzConfig.js': (
bundleType,
entry,
dependencies,
moduleType
) => {
if (dependencies.indexOf('react-server') !== -1) {
return null;
}
if (moduleType !== RENDERER && moduleType !== RECONCILER) {
return null;
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (let rendererInfo of inlinedHostConfigs) {
if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
if (!rendererInfo.isServerSupported) {
return null;
}
const foundFork = findNearestExistingForkFile(
'./packages/react-server/src/forks/ReactFizzConfig.',
rendererInfo.shortName,
'.js'
);
if (foundFork) {
return foundFork;
}
// fall through to error
break;
}
}
throw new Error(
'Expected ReactFizzConfig to always be replaced with a shim, but ' +
`found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
'Did you mean to add it there to associate it with a specific renderer?'
);
},
'./packages/react-server/src/ReactFlightServerConfig.js': (
bundleType,
entry,
dependencies,
moduleType
) => {
if (dependencies.indexOf('react-server') !== -1) {
return null;
}
if (moduleType !== RENDERER && moduleType !== RECONCILER) {
return null;
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (let rendererInfo of inlinedHostConfigs) {
if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
if (!rendererInfo.isServerSupported) {
return null;
}
if (rendererInfo.isFlightSupported === false) {
return new Error(
`Expected not to use ReactFlightServerConfig with "${entry}" entry point ` +
'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' +
'activate flight suppport and add a matching fork implementation for ReactFlightServerConfig.'
);
}
const foundFork = findNearestExistingForkFile(
'./packages/react-server/src/forks/ReactFlightServerConfig.',
rendererInfo.shortName,
'.js'
);
if (foundFork) {
return foundFork;
}
// fall through to error
break;
}
}
throw new Error(
'Expected ReactFlightServerConfig to always be replaced with a shim, but ' +
`found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
'Did you mean to add it there to associate it with a specific renderer?'
);
},
'./packages/react-client/src/ReactFlightClientConfig.js': (
bundleType,
entry,
dependencies,
moduleType
) => {
if (dependencies.indexOf('react-client') !== -1) {
return null;
}
if (moduleType !== RENDERER && moduleType !== RECONCILER) {
return null;
}
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (let rendererInfo of inlinedHostConfigs) {
if (rendererInfo.entryPoints.indexOf(entry) !== -1) {
if (!rendererInfo.isServerSupported) {
return null;
}
if (rendererInfo.isFlightSupported === false) {
return new Error(
`Expected not to use ReactFlightClientConfig with "${entry}" entry point ` +
'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' +
'activate flight suppport and add a matching fork implementation for ReactFlightClientConfig.'
);
}
const foundFork = findNearestExistingForkFile(
'./packages/react-client/src/forks/ReactFlightClientConfig.',
rendererInfo.shortName,
'.js'
);
if (foundFork) {
return foundFork;
}
// fall through to error
break;
}
}
throw new Error(
'Expected ReactFlightClientConfig to always be replaced with a shim, but ' +
`found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` +
'Did you mean to add it there to associate it with a specific renderer?'
);
},
// We wrap top-level listeners into guards on www.
'./packages/react-dom-bindings/src/events/EventListener.js': (
bundleType,
entry
) => {
switch (bundleType) {
case FB_WWW_DEV:
case FB_WWW_PROD:
case FB_WWW_PROFILING:
if (__EXPERIMENTAL__) {
// In modern builds we don't use the indirection. We just use raw DOM.
return null;
} else {
// Use the www fork which is integrated with TimeSlice profiling.
return './packages/react-dom-bindings/src/events/forks/EventListener-www.js';
}
default:
return null;
}
},
'./packages/use-sync-external-store/src/useSyncExternalStore.js': (
bundleType,
entry
) => {
if (entry.startsWith('use-sync-external-store/shim')) {
return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js';
}
if (entry !== 'use-sync-external-store') {
// Internal modules that aren't shims should use the native API from the
// react package.
return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js';
}
return null;
},
'./packages/use-sync-external-store/src/isServerEnvironment.js': (
bundleType,
entry
) => {
if (entry.endsWith('.native')) {
return './packages/use-sync-external-store/src/forks/isServerEnvironment.native.js';
}
},
});
module.exports = forks;