| 1 | 'use strict'; |
| 2 | |
| 3 | const fs = require('fs'); |
| 4 | const nodePath = require('path'); |
| 5 | const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); |
| 6 | |
| 7 | function resolveEntryFork(resolvedEntry, isFBBundle) { |
| 8 | // Pick which entry point fork to use: |
| 9 | // .modern.fb.js |
| 10 | // .classic.fb.js |
| 11 | // .fb.js |
| 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. |
| 20 | if ( |
| 21 | resolvedEntry.endsWith('react-dom/index.js') || |
| 22 | resolvedEntry.endsWith('react-dom/client.js') || |
| 23 | resolvedEntry.endsWith('react-dom/unstable_testing.js') |
| 24 | ) { |
| 25 | let specifier; |
| 26 | let entrypoint; |
| 27 | if (resolvedEntry.endsWith('index.js')) { |
| 28 | specifier = 'react-dom'; |
| 29 | entrypoint = __EXPERIMENTAL__ |
| 30 | ? 'src/ReactDOMFB.modern.js' |
| 31 | : 'src/ReactDOMFB.js'; |
| 32 | } else if (resolvedEntry.endsWith('client.js')) { |
| 33 | specifier = 'react-dom/client'; |
| 34 | entrypoint = __EXPERIMENTAL__ |
| 35 | ? 'src/ReactDOMFB.modern.js' |
| 36 | : 'src/ReactDOMFB.js'; |
| 37 | } else { |
| 38 | // must be unstable_testing |
| 39 | specifier = 'react-dom/unstable_testing'; |
| 40 | entrypoint = __EXPERIMENTAL__ |
| 41 | ? 'src/ReactDOMTestingFB.modern.js' |
| 42 | : 'src/ReactDOMTestingFB.js'; |
| 43 | } |
| 44 | |
| 45 | resolvedEntry = nodePath.join(resolvedEntry, '..', entrypoint); |
| 46 | const devEntry = resolvedEntry.replace('.js', '.development.js'); |
| 47 | if (__DEV__ && fs.existsSync(devEntry)) { |
| 48 | return devEntry; |
| 49 | } |
| 50 | if (fs.existsSync(resolvedEntry)) { |
| 51 | return resolvedEntry; |
| 52 | } |
| 53 | const fbReleaseChannel = __EXPERIMENTAL__ ? 'www-modern' : 'www-classic'; |
| 54 | throw new Error( |
| 55 | `${fbReleaseChannel} tests are expected to alias ${specifier} to ${entrypoint} but this file was not found` |
| 56 | ); |
| 57 | } |
| 58 | const resolvedFBEntry = resolvedEntry.replace( |
| 59 | '.js', |
| 60 | __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js' |
| 61 | ); |
| 62 | const devFBEntry = resolvedFBEntry.replace('.js', '.development.js'); |
| 63 | if (__DEV__ && fs.existsSync(devFBEntry)) { |
| 64 | return devFBEntry; |
| 65 | } |
| 66 | if (fs.existsSync(resolvedFBEntry)) { |
| 67 | return resolvedFBEntry; |
| 68 | } |
| 69 | const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js'); |
| 70 | const devGenericFBEntry = resolvedGenericFBEntry.replace( |
| 71 | '.js', |
| 72 | '.development.js' |
| 73 | ); |
| 74 | if (__DEV__ && fs.existsSync(devGenericFBEntry)) { |
| 75 | return devGenericFBEntry; |
| 76 | } |
| 77 | if (fs.existsSync(resolvedGenericFBEntry)) { |
| 78 | return resolvedGenericFBEntry; |
| 79 | } |
| 80 | // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork. |
| 81 | } |
| 82 | const resolvedForkedEntry = resolvedEntry.replace( |
| 83 | '.js', |
| 84 | __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js' |
| 85 | ); |
| 86 | const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js'); |
| 87 | if (__DEV__ && fs.existsSync(devForkedEntry)) { |
| 88 | return devForkedEntry; |
| 89 | } |
| 90 | if (fs.existsSync(resolvedForkedEntry)) { |
| 91 | return resolvedForkedEntry; |
| 92 | } |
| 93 | const plainDevEntry = resolvedEntry.replace('.js', '.development.js'); |
| 94 | if (__DEV__ && fs.existsSync(plainDevEntry)) { |
| 95 | return plainDevEntry; |
| 96 | } |
| 97 | // Just use the plain .js one. |
| 98 | return resolvedEntry; |
| 99 | } |
| 100 | |
| 101 | function mockReact() { |
| 102 | jest.mock('react', () => { |
| 103 | const resolvedEntryPoint = resolveEntryFork( |
| 104 | require.resolve('react'), |
| 105 | global.__WWW__ || global.__XPLAT__, |
| 106 | global.__DEV__ |
| 107 | ); |
| 108 | return jest.requireActual(resolvedEntryPoint); |
| 109 | }); |
| 110 | // Make it possible to import this module inside |
| 111 | // the React package itself. |
| 112 | jest.mock('shared/ReactSharedInternals', () => { |
| 113 | return jest.requireActual('react/src/ReactSharedInternalsClient'); |
| 114 | }); |
| 115 | } |
| 116 | |
| 117 | // When we want to unmock React we really need to mock it again. |
| 118 | global.__unmockReact = mockReact; |
| 119 | |
| 120 | mockReact(); |
| 121 | |
| 122 | jest.mock('react/react.react-server', () => { |
| 123 | // If we're requiring an RSC environment, use those internals instead. |
| 124 | jest.mock('shared/ReactSharedInternals', () => { |
| 125 | return jest.requireActual('react/src/ReactSharedInternalsServer'); |
| 126 | }); |
| 127 | const resolvedEntryPoint = resolveEntryFork( |
| 128 | require.resolve('react/src/ReactServer'), |
| 129 | global.__WWW__ || global.__XPLAT__, |
| 130 | global.__DEV__ |
| 131 | ); |
| 132 | return jest.requireActual(resolvedEntryPoint); |
| 133 | }); |
| 134 | |
| 135 | // When testing the custom renderer code path through `react-reconciler`, |
| 136 | // turn the export into a function, and use the argument as host config. |
| 137 | const shimHostConfigPath = 'react-reconciler/src/ReactFiberConfig'; |
| 138 | jest.mock('react-reconciler', () => { |
| 139 | return config => { |
| 140 | jest.mock(shimHostConfigPath, () => config); |
| 141 | return jest.requireActual('react-reconciler'); |
| 142 | }; |
| 143 | }); |
| 144 | const shimServerStreamConfigPath = 'react-server/src/ReactServerStreamConfig'; |
| 145 | const shimServerConfigPath = 'react-server/src/ReactFizzConfig'; |
| 146 | const shimFlightServerConfigPath = 'react-server/src/ReactFlightServerConfig'; |
| 147 | jest.mock('react-server', () => { |
| 148 | return config => { |
| 149 | jest.mock(shimServerStreamConfigPath, () => config); |
| 150 | jest.mock(shimServerConfigPath, () => config); |
| 151 | return jest.requireActual('react-server'); |
| 152 | }; |
| 153 | }); |
| 154 | jest.mock('react-server/flight', () => { |
| 155 | return config => { |
| 156 | jest.mock(shimServerStreamConfigPath, () => config); |
| 157 | jest.mock(shimServerConfigPath, () => config); |
| 158 | jest.mock('react-server/src/ReactFlightServerConfigBundlerCustom', () => ({ |
| 159 | isClientReference: config.isClientReference, |
| 160 | isServerReference: config.isServerReference, |
| 161 | getClientReferenceKey: config.getClientReferenceKey, |
| 162 | resolveClientReferenceMetadata: config.resolveClientReferenceMetadata, |
| 163 | })); |
| 164 | jest.mock(shimFlightServerConfigPath, () => |
| 165 | jest.requireActual( |
| 166 | 'react-server/src/forks/ReactFlightServerConfig.custom' |
| 167 | ) |
| 168 | ); |
| 169 | return jest.requireActual('react-server/flight'); |
| 170 | }; |
| 171 | }); |
| 172 | const shimFlightClientConfigPath = 'react-client/src/ReactFlightClientConfig'; |
| 173 | jest.mock('react-client/flight', () => { |
| 174 | return config => { |
| 175 | jest.mock(shimFlightClientConfigPath, () => config); |
| 176 | return jest.requireActual('react-client/flight'); |
| 177 | }; |
| 178 | }); |
| 179 | |
| 180 | const configPaths = [ |
| 181 | 'react-reconciler/src/ReactFiberConfig', |
| 182 | 'react-client/src/ReactFlightClientConfig', |
| 183 | 'react-server/src/ReactServerStreamConfig', |
| 184 | 'react-server/src/ReactFizzConfig', |
| 185 | 'react-server/src/ReactFlightServerConfig', |
| 186 | ]; |
| 187 | |
| 188 | function mockAllConfigs(rendererInfo) { |
| 189 | configPaths.forEach(path => { |
| 190 | // We want the reconciler to pick up the host config for this renderer. |
| 191 | jest.mock(path, () => { |
| 192 | let idx = path.lastIndexOf('/'); |
| 193 | let forkPath = path.slice(0, idx) + '/forks' + path.slice(idx); |
| 194 | let parts = rendererInfo.shortName.split('-'); |
| 195 | while (parts.length) { |
| 196 | try { |
| 197 | const candidate = `${forkPath}.${parts.join('-')}.js`; |
| 198 | fs.statSync(nodePath.join(process.cwd(), 'packages', candidate)); |
| 199 | return jest.requireActual(candidate); |
| 200 | } catch (error) { |
| 201 | if (error.code !== 'ENOENT') { |
| 202 | throw error; |
| 203 | } |
| 204 | // try without a part |
| 205 | } |
| 206 | parts.pop(); |
| 207 | } |
| 208 | throw new Error( |
| 209 | `Expected to find a fork for ${path} but did not find one.` |
| 210 | ); |
| 211 | }); |
| 212 | }); |
| 213 | } |
| 214 | |
| 215 | // But for inlined host configs (such as React DOM, Native, etc), we |
| 216 | // mock their named entry points to establish a host config mapping. |
| 217 | inlinedHostConfigs.forEach(rendererInfo => { |
| 218 | if (rendererInfo.shortName === 'custom') { |
| 219 | // There is no inline entry point for the custom renderers. |
| 220 | // Instead, it's handled by the generic `react-reconciler` entry point above. |
| 221 | return; |
| 222 | } |
| 223 | rendererInfo.entryPoints.forEach(entryPoint => { |
| 224 | jest.mock(entryPoint, () => { |
| 225 | mockAllConfigs(rendererInfo); |
| 226 | const resolvedEntryPoint = resolveEntryFork( |
| 227 | require.resolve(entryPoint), |
| 228 | global.__WWW__ || global.__XPLAT__, |
| 229 | global.__DEV__ |
| 230 | ); |
| 231 | return jest.requireActual(resolvedEntryPoint); |
| 232 | }); |
| 233 | }); |
| 234 | }); |
| 235 | |
| 236 | jest.mock('react-server/src/ReactFlightServer', () => { |
| 237 | // If we're requiring an RSC environment, use those internals instead. |
| 238 | jest.mock('shared/ReactSharedInternals', () => { |
| 239 | return jest.requireActual('react/src/ReactSharedInternalsServer'); |
| 240 | }); |
| 241 | return jest.requireActual('react-server/src/ReactFlightServer'); |
| 242 | }); |
| 243 | |
| 244 | // Make it possible to import this module inside |
| 245 | // the ReactDOM package itself. |
| 246 | jest.mock('shared/ReactDOMSharedInternals', () => |
| 247 | jest.requireActual('react-dom/src/ReactDOMSharedInternals') |
| 248 | ); |
| 249 | |
| 250 | jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); |
| 251 | |
| 252 | if (global.__PERSISTENT__) { |
| 253 | jest.mock('react-noop-renderer', () => |
| 254 | jest.requireActual('react-noop-renderer/persistent') |
| 255 | ); |
| 256 | } |