| 1 | 'use strict'; |
| 2 | |
| 3 | const fs = require('node:fs'); |
| 4 | const {bundleTypes, moduleTypes} = require('./bundles'); |
| 5 | const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); |
| 6 | |
| 7 | const { |
| 8 | FB_WWW_DEV, |
| 9 | FB_WWW_PROD, |
| 10 | FB_WWW_PROFILING, |
| 11 | RN_OSS_DEV, |
| 12 | RN_OSS_PROD, |
| 13 | RN_OSS_PROFILING, |
| 14 | RN_FB_DEV, |
| 15 | RN_FB_PROD, |
| 16 | RN_FB_PROFILING, |
| 17 | } = bundleTypes; |
| 18 | const {RENDERER, RECONCILER} = moduleTypes; |
| 19 | |
| 20 | const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; |
| 21 | |
| 22 | // Default to building in experimental mode. If the release channel is set via |
| 23 | // an environment variable, then check if it's "experimental". |
| 24 | const __EXPERIMENTAL__ = |
| 25 | typeof RELEASE_CHANNEL === 'string' |
| 26 | ? RELEASE_CHANNEL === 'experimental' |
| 27 | : true; |
| 28 | |
| 29 | function findNearestExistingForkFile(path, segmentedIdentifier, suffix) { |
| 30 | const segments = segmentedIdentifier.split('-'); |
| 31 | while (segments.length) { |
| 32 | const candidate = segments.join('-'); |
| 33 | const forkPath = path + candidate + suffix; |
| 34 | try { |
| 35 | fs.statSync(forkPath); |
| 36 | return forkPath; |
| 37 | } catch (error) { |
| 38 | // Try the next candidate. |
| 39 | } |
| 40 | segments.pop(); |
| 41 | } |
| 42 | return null; |
| 43 | } |
| 44 | |
| 45 | // If you need to replace a file with another file for a specific environment, |
| 46 | // add it to this list with the logic for choosing the right replacement. |
| 47 | |
| 48 | // Fork paths are relative to the project root. They must include the full path, |
| 49 | // including the extension. We intentionally don't use Node's module resolution |
| 50 | // algorithm because 1) require.resolve doesn't work with ESM modules, and 2) |
| 51 | // the behavior is easier to predict. |
| 52 | const forks = Object.freeze({ |
| 53 | // Without this fork, importing `shared/ReactSharedInternals` inside |
| 54 | // the `react` package itself would not work due to a cyclical dependency. |
| 55 | './packages/shared/ReactSharedInternals.js': ( |
| 56 | bundleType, |
| 57 | entry, |
| 58 | dependencies, |
| 59 | _moduleType, |
| 60 | bundle |
| 61 | ) => { |
| 62 | if (entry === 'react') { |
| 63 | return './packages/react/src/ReactSharedInternalsClient.js'; |
| 64 | } |
| 65 | if (entry === 'react/src/ReactServer.js') { |
| 66 | return './packages/react/src/ReactSharedInternalsServer.js'; |
| 67 | } |
| 68 | if (entry === 'react-markup/src/ReactMarkupServer.js') { |
| 69 | // Inside the ReactMarkupServer render we don't refer to any shared internals |
| 70 | // but instead use our own internal copy of the state because you cannot use |
| 71 | // any of this state from a component anyway. E.g. you can't use a client hook. |
| 72 | return './packages/react/src/ReactSharedInternalsClient.js'; |
| 73 | } |
| 74 | if (bundle.condition === 'react-server') { |
| 75 | return './packages/react-server/src/ReactSharedInternalsServer.js'; |
| 76 | } |
| 77 | if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) { |
| 78 | // React internals are unavailable if we can't reference the package. |
| 79 | // We return an error because we only want to throw if this module gets used. |
| 80 | return new Error( |
| 81 | 'Cannot use a module that depends on ReactSharedInternals ' + |
| 82 | 'from "' + |
| 83 | entry + |
| 84 | '" because it does not declare "react" in the package ' + |
| 85 | 'dependencies or peerDependencies.' |
| 86 | ); |
| 87 | } |
| 88 | return null; |
| 89 | }, |
| 90 | |
| 91 | // Without this fork, importing `shared/ReactDOMSharedInternals` inside |
| 92 | // the `react-dom` package itself would not work due to a cyclical dependency. |
| 93 | './packages/shared/ReactDOMSharedInternals.js': ( |
| 94 | bundleType, |
| 95 | entry, |
| 96 | dependencies |
| 97 | ) => { |
| 98 | if ( |
| 99 | entry === 'react-dom' || |
| 100 | entry === 'react-dom/src/ReactDOMFB.js' || |
| 101 | entry === 'react-dom/src/ReactDOMTestingFB.js' || |
| 102 | entry === 'react-dom/src/ReactDOMServer.js' || |
| 103 | entry === 'react-markup/src/ReactMarkupClient.js' || |
| 104 | entry === 'react-markup/src/ReactMarkupServer.js' |
| 105 | ) { |
| 106 | if ( |
| 107 | bundleType === FB_WWW_DEV || |
| 108 | bundleType === FB_WWW_PROD || |
| 109 | bundleType === FB_WWW_PROFILING |
| 110 | ) { |
| 111 | return './packages/react-dom/src/ReactDOMSharedInternalsFB.js'; |
| 112 | } else { |
| 113 | return './packages/react-dom/src/ReactDOMSharedInternals.js'; |
| 114 | } |
| 115 | } |
| 116 | if ( |
| 117 | !entry.startsWith('react-dom/') && |
| 118 | dependencies.indexOf('react-dom') === -1 |
| 119 | ) { |
| 120 | // React DOM internals are unavailable if we can't reference the package. |
| 121 | // We return an error because we only want to throw if this module gets used. |
| 122 | return new Error( |
| 123 | 'Cannot use a module that depends on ReactDOMSharedInternals ' + |
| 124 | 'from "' + |
| 125 | entry + |
| 126 | '" because it does not declare "react-dom" in the package ' + |
| 127 | 'dependencies or peerDependencies.' |
| 128 | ); |
| 129 | } |
| 130 | return null; |
| 131 | }, |
| 132 | |
| 133 | // We have a few forks for different environments. |
| 134 | './packages/shared/ReactFeatureFlags.js': (bundleType, entry) => { |
| 135 | switch (entry) { |
| 136 | case 'react-native-renderer/fabric': |
| 137 | switch (bundleType) { |
| 138 | case RN_FB_DEV: |
| 139 | case RN_FB_PROD: |
| 140 | case RN_FB_PROFILING: |
| 141 | return './packages/shared/forks/ReactFeatureFlags.native-fb.js'; |
| 142 | case RN_OSS_DEV: |
| 143 | case RN_OSS_PROD: |
| 144 | case RN_OSS_PROFILING: |
| 145 | return './packages/shared/forks/ReactFeatureFlags.native-oss.js'; |
| 146 | default: |
| 147 | throw Error( |
| 148 | `Unexpected entry (${entry}) and bundleType (${bundleType})` |
| 149 | ); |
| 150 | } |
| 151 | case 'eslint-plugin-react-hooks/src/index.ts': |
| 152 | switch (bundleType) { |
| 153 | case FB_WWW_DEV: |
| 154 | case FB_WWW_PROD: |
| 155 | case FB_WWW_PROFILING: |
| 156 | return './packages/shared/forks/ReactFeatureFlags.eslint-plugin.www.js'; |
| 157 | } |
| 158 | return null; |
| 159 | case 'react-test-renderer': |
| 160 | switch (bundleType) { |
| 161 | case RN_FB_DEV: |
| 162 | case RN_FB_PROD: |
| 163 | case RN_FB_PROFILING: |
| 164 | return './packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js'; |
| 165 | case FB_WWW_DEV: |
| 166 | case FB_WWW_PROD: |
| 167 | case FB_WWW_PROFILING: |
| 168 | return './packages/shared/forks/ReactFeatureFlags.test-renderer.www.js'; |
| 169 | } |
| 170 | return './packages/shared/forks/ReactFeatureFlags.test-renderer.js'; |
| 171 | default: |
| 172 | switch (bundleType) { |
| 173 | case FB_WWW_DEV: |
| 174 | case FB_WWW_PROD: |
| 175 | case FB_WWW_PROFILING: |
| 176 | return './packages/shared/forks/ReactFeatureFlags.www.js'; |
| 177 | case RN_FB_DEV: |
| 178 | case RN_FB_PROD: |
| 179 | case RN_FB_PROFILING: |
| 180 | return './packages/shared/forks/ReactFeatureFlags.native-fb.js'; |
| 181 | } |
| 182 | } |
| 183 | return null; |
| 184 | }, |
| 185 | |
| 186 | './packages/scheduler/src/SchedulerFeatureFlags.js': ( |
| 187 | bundleType, |
| 188 | entry, |
| 189 | dependencies |
| 190 | ) => { |
| 191 | switch (bundleType) { |
| 192 | case FB_WWW_DEV: |
| 193 | case FB_WWW_PROD: |
| 194 | case FB_WWW_PROFILING: |
| 195 | return './packages/scheduler/src/forks/SchedulerFeatureFlags.www.js'; |
| 196 | case RN_FB_DEV: |
| 197 | case RN_FB_PROD: |
| 198 | case RN_FB_PROFILING: |
| 199 | return './packages/scheduler/src/forks/SchedulerFeatureFlags.native-fb.js'; |
| 200 | default: |
| 201 | return './packages/scheduler/src/SchedulerFeatureFlags.js'; |
| 202 | } |
| 203 | }, |
| 204 | |
| 205 | './packages/shared/DefaultPrepareStackTrace.js': ( |
| 206 | bundleType, |
| 207 | entry, |
| 208 | dependencies, |
| 209 | moduleType |
| 210 | ) => { |
| 211 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 212 | return null; |
| 213 | } |
| 214 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 215 | for (let rendererInfo of inlinedHostConfigs) { |
| 216 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 217 | if (!rendererInfo.isServerSupported) { |
| 218 | return null; |
| 219 | } |
| 220 | const foundFork = findNearestExistingForkFile( |
| 221 | './packages/shared/forks/DefaultPrepareStackTrace.', |
| 222 | rendererInfo.shortName, |
| 223 | '.js' |
| 224 | ); |
| 225 | if (foundFork) { |
| 226 | return foundFork; |
| 227 | } |
| 228 | // fall through to error |
| 229 | break; |
| 230 | } |
| 231 | } |
| 232 | return null; |
| 233 | }, |
| 234 | |
| 235 | './packages/react-reconciler/src/ReactFiberConfig.js': ( |
| 236 | bundleType, |
| 237 | entry, |
| 238 | dependencies, |
| 239 | moduleType |
| 240 | ) => { |
| 241 | if (dependencies.indexOf('react-reconciler') !== -1) { |
| 242 | return null; |
| 243 | } |
| 244 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 245 | return null; |
| 246 | } |
| 247 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 248 | for (let rendererInfo of inlinedHostConfigs) { |
| 249 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 250 | const foundFork = findNearestExistingForkFile( |
| 251 | './packages/react-reconciler/src/forks/ReactFiberConfig.', |
| 252 | rendererInfo.shortName, |
| 253 | '.js' |
| 254 | ); |
| 255 | if (foundFork) { |
| 256 | return foundFork; |
| 257 | } |
| 258 | // fall through to error |
| 259 | break; |
| 260 | } |
| 261 | } |
| 262 | throw new Error( |
| 263 | 'Expected ReactFiberConfig to always be replaced with a shim, but ' + |
| 264 | `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + |
| 265 | 'Did you mean to add it there to associate it with a specific renderer?' |
| 266 | ); |
| 267 | }, |
| 268 | |
| 269 | './packages/react-server/src/ReactServerStreamConfig.js': ( |
| 270 | bundleType, |
| 271 | entry, |
| 272 | dependencies, |
| 273 | moduleType |
| 274 | ) => { |
| 275 | if (dependencies.indexOf('react-server') !== -1) { |
| 276 | return null; |
| 277 | } |
| 278 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 279 | return null; |
| 280 | } |
| 281 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 282 | for (let rendererInfo of inlinedHostConfigs) { |
| 283 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 284 | if (!rendererInfo.isServerSupported) { |
| 285 | return null; |
| 286 | } |
| 287 | const foundFork = findNearestExistingForkFile( |
| 288 | './packages/react-server/src/forks/ReactServerStreamConfig.', |
| 289 | rendererInfo.shortName, |
| 290 | '.js' |
| 291 | ); |
| 292 | if (foundFork) { |
| 293 | return foundFork; |
| 294 | } |
| 295 | // fall through to error |
| 296 | break; |
| 297 | } |
| 298 | } |
| 299 | throw new Error( |
| 300 | 'Expected ReactServerStreamConfig to always be replaced with a shim, but ' + |
| 301 | `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + |
| 302 | 'Did you mean to add it there to associate it with a specific renderer?' |
| 303 | ); |
| 304 | }, |
| 305 | |
| 306 | './packages/react-server/src/ReactFizzConfig.js': ( |
| 307 | bundleType, |
| 308 | entry, |
| 309 | dependencies, |
| 310 | moduleType |
| 311 | ) => { |
| 312 | if (dependencies.indexOf('react-server') !== -1) { |
| 313 | return null; |
| 314 | } |
| 315 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 316 | return null; |
| 317 | } |
| 318 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 319 | for (let rendererInfo of inlinedHostConfigs) { |
| 320 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 321 | if (!rendererInfo.isServerSupported) { |
| 322 | return null; |
| 323 | } |
| 324 | const foundFork = findNearestExistingForkFile( |
| 325 | './packages/react-server/src/forks/ReactFizzConfig.', |
| 326 | rendererInfo.shortName, |
| 327 | '.js' |
| 328 | ); |
| 329 | if (foundFork) { |
| 330 | return foundFork; |
| 331 | } |
| 332 | // fall through to error |
| 333 | break; |
| 334 | } |
| 335 | } |
| 336 | throw new Error( |
| 337 | 'Expected ReactFizzConfig to always be replaced with a shim, but ' + |
| 338 | `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + |
| 339 | 'Did you mean to add it there to associate it with a specific renderer?' |
| 340 | ); |
| 341 | }, |
| 342 | |
| 343 | './packages/react-server/src/ReactFlightServerConfig.js': ( |
| 344 | bundleType, |
| 345 | entry, |
| 346 | dependencies, |
| 347 | moduleType |
| 348 | ) => { |
| 349 | if (dependencies.indexOf('react-server') !== -1) { |
| 350 | return null; |
| 351 | } |
| 352 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 353 | return null; |
| 354 | } |
| 355 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 356 | for (let rendererInfo of inlinedHostConfigs) { |
| 357 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 358 | if (!rendererInfo.isServerSupported) { |
| 359 | return null; |
| 360 | } |
| 361 | if (rendererInfo.isFlightSupported === false) { |
| 362 | return new Error( |
| 363 | `Expected not to use ReactFlightServerConfig with "${entry}" entry point ` + |
| 364 | 'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' + |
| 365 | 'activate flight suppport and add a matching fork implementation for ReactFlightServerConfig.' |
| 366 | ); |
| 367 | } |
| 368 | const foundFork = findNearestExistingForkFile( |
| 369 | './packages/react-server/src/forks/ReactFlightServerConfig.', |
| 370 | rendererInfo.shortName, |
| 371 | '.js' |
| 372 | ); |
| 373 | if (foundFork) { |
| 374 | return foundFork; |
| 375 | } |
| 376 | // fall through to error |
| 377 | break; |
| 378 | } |
| 379 | } |
| 380 | throw new Error( |
| 381 | 'Expected ReactFlightServerConfig to always be replaced with a shim, but ' + |
| 382 | `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + |
| 383 | 'Did you mean to add it there to associate it with a specific renderer?' |
| 384 | ); |
| 385 | }, |
| 386 | |
| 387 | './packages/react-client/src/ReactFlightClientConfig.js': ( |
| 388 | bundleType, |
| 389 | entry, |
| 390 | dependencies, |
| 391 | moduleType |
| 392 | ) => { |
| 393 | if (dependencies.indexOf('react-client') !== -1) { |
| 394 | return null; |
| 395 | } |
| 396 | if (moduleType !== RENDERER && moduleType !== RECONCILER) { |
| 397 | return null; |
| 398 | } |
| 399 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 400 | for (let rendererInfo of inlinedHostConfigs) { |
| 401 | if (rendererInfo.entryPoints.indexOf(entry) !== -1) { |
| 402 | if (!rendererInfo.isServerSupported) { |
| 403 | return null; |
| 404 | } |
| 405 | if (rendererInfo.isFlightSupported === false) { |
| 406 | return new Error( |
| 407 | `Expected not to use ReactFlightClientConfig with "${entry}" entry point ` + |
| 408 | 'in ./scripts/shared/inlinedHostConfigs.js. Update the renderer config to ' + |
| 409 | 'activate flight suppport and add a matching fork implementation for ReactFlightClientConfig.' |
| 410 | ); |
| 411 | } |
| 412 | const foundFork = findNearestExistingForkFile( |
| 413 | './packages/react-client/src/forks/ReactFlightClientConfig.', |
| 414 | rendererInfo.shortName, |
| 415 | '.js' |
| 416 | ); |
| 417 | if (foundFork) { |
| 418 | return foundFork; |
| 419 | } |
| 420 | // fall through to error |
| 421 | break; |
| 422 | } |
| 423 | } |
| 424 | throw new Error( |
| 425 | 'Expected ReactFlightClientConfig to always be replaced with a shim, but ' + |
| 426 | `found no mention of "${entry}" entry point in ./scripts/shared/inlinedHostConfigs.js. ` + |
| 427 | 'Did you mean to add it there to associate it with a specific renderer?' |
| 428 | ); |
| 429 | }, |
| 430 | |
| 431 | // We wrap top-level listeners into guards on www. |
| 432 | './packages/react-dom-bindings/src/events/EventListener.js': ( |
| 433 | bundleType, |
| 434 | entry |
| 435 | ) => { |
| 436 | switch (bundleType) { |
| 437 | case FB_WWW_DEV: |
| 438 | case FB_WWW_PROD: |
| 439 | case FB_WWW_PROFILING: |
| 440 | if (__EXPERIMENTAL__) { |
| 441 | // In modern builds we don't use the indirection. We just use raw DOM. |
| 442 | return null; |
| 443 | } else { |
| 444 | // Use the www fork which is integrated with TimeSlice profiling. |
| 445 | return './packages/react-dom-bindings/src/events/forks/EventListener-www.js'; |
| 446 | } |
| 447 | default: |
| 448 | return null; |
| 449 | } |
| 450 | }, |
| 451 | |
| 452 | './packages/use-sync-external-store/src/useSyncExternalStore.js': ( |
| 453 | bundleType, |
| 454 | entry |
| 455 | ) => { |
| 456 | if (entry.startsWith('use-sync-external-store/shim')) { |
| 457 | return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-shim.js'; |
| 458 | } |
| 459 | if (entry !== 'use-sync-external-store') { |
| 460 | // Internal modules that aren't shims should use the native API from the |
| 461 | // react package. |
| 462 | return './packages/use-sync-external-store/src/forks/useSyncExternalStore.forward-to-built-in.js'; |
| 463 | } |
| 464 | return null; |
| 465 | }, |
| 466 | |
| 467 | './packages/use-sync-external-store/src/isServerEnvironment.js': ( |
| 468 | bundleType, |
| 469 | entry |
| 470 | ) => { |
| 471 | if (entry.endsWith('.native')) { |
| 472 | return './packages/use-sync-external-store/src/forks/isServerEnvironment.native.js'; |
| 473 | } |
| 474 | }, |
| 475 | }); |
| 476 | |
| 477 | module.exports = forks; |