From c7ba8c098889b6dc47fa9c807bbba3975a658584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 29 Sep 2023 18:24:05 -0400 Subject: [PATCH] Enforce that the "react-server" build of "react" is used (#27436) I do this by simply renaming the secret export name in the "subset" bundle and this renamed version is what the FlightServer uses. This requires us to be more diligent about always using the correct instance of "react" in our tests so there's a bunch of clean up for that. --- .eslintrc.js | 1 + .../src/__tests__/ReactFlight-test.js | 94 +- packages/react-dom/package.json | 1 + .../src/__tests__/ReactFlightDOM-test.js | 2 +- .../__tests__/ReactFlightDOMBrowser-test.js | 1179 +---------------- .../src/__tests__/ReactFlightDOMEdge-test.js | 8 +- .../src/__tests__/ReactFlightDOMForm-test.js | 5 +- .../src/__tests__/ReactFlightDOMNode-test.js | 2 +- .../src/__tests__/ReactFlightDOMReply-test.js | 2 + .../src/__tests__/ReactFlightDOM-test.js | 2 +- .../__tests__/ReactFlightDOMBrowser-test.js | 49 +- .../src/__tests__/ReactFlightDOMEdge-test.js | 5 +- .../src/__tests__/ReactFlightDOMForm-test.js | 5 +- .../src/__tests__/ReactFlightDOMNode-test.js | 2 +- .../src/__tests__/ReactFlightDOMReply-test.js | 2 + .../react-server/src/ReactFlightServer.js | 7 +- .../src/ReactServerSharedInternals.js | 24 + packages/react/src/React.js | 2 +- packages/react/src/ReactServerContext.js | 4 +- .../react/src/ReactServerSharedInternals.js | 16 + ...rnals.js => ReactSharedInternalsClient.js} | 0 .../react/src/ReactSharedInternalsServer.js | 25 + .../src/ReactSharedSubset.experimental.js | 5 +- packages/react/src/ReactSharedSubset.js | 5 +- .../react/src/__tests__/ReactFetch-test.js | 15 +- .../src/__tests__/ReactFetchEdge-test.js | 6 +- scripts/error-codes/codes.json | 3 +- scripts/jest/setupHostConfigs.js | 23 +- scripts/jest/setupTests.build.js | 2 + scripts/rollup/forks.js | 7 +- 30 files changed, 245 insertions(+), 1258 deletions(-) create mode 100644 packages/react-server/src/ReactServerSharedInternals.js create mode 100644 packages/react/src/ReactServerSharedInternals.js rename packages/react/src/{ReactSharedInternals.js => ReactSharedInternalsClient.js} (100%) create mode 100644 packages/react/src/ReactSharedInternalsServer.js diff --git a/.eslintrc.js b/.eslintrc.js index 4d53738e281ae..a00174fea7122 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -517,6 +517,7 @@ module.exports = { __TEST__: 'readonly', __UMD__: 'readonly', __VARIANT__: 'readonly', + __unmockReact: 'readonly', gate: 'readonly', trustedTypes: 'readonly', IS_REACT_ACT_ENVIRONMENT: 'readonly', diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 2ebbb9f2d52f5..30cfda19af6ec 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -14,6 +14,7 @@ let act; let use; let startTransition; let React; +let ReactServer; let ReactNoop; let ReactNoopFlightServer; let ReactNoopFlightClient; @@ -25,12 +26,18 @@ let assertLog; describe('ReactFlight', () => { beforeEach(() => { jest.resetModules(); - + jest.mock('react', () => require('react/react.shared-subset')); + ReactServer = require('react'); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + // This stores the state so we need to preserve it + const flightModules = require('react-noop-renderer/flight-modules'); + __unmockReact(); + jest.resetModules(); + jest.mock('react-noop-renderer/flight-modules', () => flightModules); React = require('react'); startTransition = React.startTransition; use = React.use; ReactNoop = require('react-noop-renderer'); - ReactNoopFlightServer = require('react-noop-renderer/flight-server'); ReactNoopFlightClient = require('react-noop-renderer/flight-client'); act = require('internal-test-utils').act; Scheduler = require('scheduler'); @@ -111,6 +118,19 @@ describe('ReactFlight', () => { return ctx; } + function createServerServerContext(globalName, defaultValue, withStack) { + let ctx; + expect(() => { + ctx = ReactServer.createServerContext(globalName, defaultValue); + }).toErrorDev( + 'Server Context is deprecated and will soon be removed. ' + + 'It was never documented and we have found it not to be useful ' + + 'enough to warrant the downside it imposes on all apps.', + {withoutStack: !withStack}, + ); + return ctx; + } + function clientReference(value) { return Object.defineProperties( function () { @@ -970,7 +990,7 @@ describe('ReactFlight', () => { const Context = React.createContext(); const ClientContext = clientReference(Context); function ServerComponent() { - return React.useContext(ClientContext); + return ReactServer.useContext(ClientContext); } expect(() => { const transport = ReactNoopFlightServer.render(); @@ -982,7 +1002,7 @@ describe('ReactFlight', () => { describe('Hooks', () => { function DivWithId({children}) { - const id = React.useId(); + const id = ReactServer.useId(); return
{children}
; } @@ -1039,7 +1059,7 @@ describe('ReactFlight', () => { // so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now // we just accept that it is a nuance of useId in Flight function App() { - const id = React.useId(); + const id = ReactServer.useId(); const div =
{id}
; return ; } @@ -1076,19 +1096,17 @@ describe('ReactFlight', () => { describe('ServerContext', () => { // @gate enableServerContext it('supports basic createServerContext usage', async () => { - const ServerContext = createServerContext( + const ServerContext = createServerServerContext( 'ServerContext', 'hello from server', ); function Foo() { - const context = React.useContext(ServerContext); + const context = ReactServer.useContext(ServerContext); return
{context}
; } const transport = ReactNoopFlightServer.render(); await act(async () => { - ServerContext._currentRenderer = null; - ServerContext._currentRenderer2 = null; ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); @@ -1097,7 +1115,10 @@ describe('ReactFlight', () => { // @gate enableServerContext it('propagates ServerContext providers in flight', async () => { - const ServerContext = createServerContext('ServerContext', 'default'); + const ServerContext = createServerServerContext( + 'ServerContext', + 'default', + ); function Foo() { return ( @@ -1109,14 +1130,12 @@ describe('ReactFlight', () => { ); } function Bar() { - const context = React.useContext(ServerContext); + const context = ReactServer.useContext(ServerContext); return context; } const transport = ReactNoopFlightServer.render(); await act(async () => { - ServerContext._currentRenderer = null; - ServerContext._currentRenderer2 = null; ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); @@ -1125,7 +1144,7 @@ describe('ReactFlight', () => { // @gate enableServerContext it('errors if you try passing JSX through ServerContext value', () => { - const ServerContext = createServerContext('ServerContext', { + const ServerContext = createServerServerContext('ServerContext', { foo: { bar: hi this is default, }, @@ -1146,7 +1165,7 @@ describe('ReactFlight', () => { ); } function Bar() { - const context = React.useContext(ServerContext); + const context = ReactServer.useContext(ServerContext); return context.foo.bar; } @@ -1159,7 +1178,10 @@ describe('ReactFlight', () => { // @gate enableServerContext it('propagates ServerContext and cleans up the providers in flight', async () => { - const ServerContext = createServerContext('ServerContext', 'default'); + const ServerContext = createServerServerContext( + 'ServerContext', + 'default', + ); function Foo() { return ( @@ -1181,7 +1203,7 @@ describe('ReactFlight', () => { ); } function Bar() { - const context = React.useContext(ServerContext); + const context = ReactServer.useContext(ServerContext); return {context}; } @@ -1203,7 +1225,10 @@ describe('ReactFlight', () => { // @gate enableServerContext it('propagates ServerContext providers in flight after suspending', async () => { - const ServerContext = createServerContext('ServerContext', 'default'); + const ServerContext = createServerServerContext( + 'ServerContext', + 'default', + ); function Foo() { return ( @@ -1231,7 +1256,7 @@ describe('ReactFlight', () => { throw promise; } Scheduler.log('rendered'); - const context = React.useContext(ServerContext); + const context = ReactServer.useContext(ServerContext); return context; } @@ -1248,8 +1273,6 @@ describe('ReactFlight', () => { assertLog(['rendered']); await act(async () => { - ServerContext._currentRenderer = null; - ServerContext._currentRenderer2 = null; ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); @@ -1258,11 +1281,15 @@ describe('ReactFlight', () => { // @gate enableServerContext it('serializes ServerContext to client', async () => { - const ServerContext = createServerContext('ServerContext', 'default'); + const ServerContext = createServerServerContext( + 'ServerContext', + 'default', + ); + const ClientContext = createServerContext('ServerContext', 'default'); function ClientBar() { Scheduler.log('ClientBar'); - const context = React.useContext(ServerContext); + const context = React.useContext(ClientContext); return {context}; } @@ -1285,8 +1312,6 @@ describe('ReactFlight', () => { assertLog([]); await act(async () => { - ServerContext._currentRenderer = null; - ServerContext._currentRenderer2 = null; const flightModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(flightModel.foo); }); @@ -1301,9 +1326,12 @@ describe('ReactFlight', () => { // @gate enableServerContext it('takes ServerContext from the client for refetching use cases', async () => { - const ServerContext = createServerContext('ServerContext', 'default'); + const ServerContext = createServerServerContext( + 'ServerContext', + 'default', + ); function Bar() { - return {React.useContext(ServerContext)}; + return {ReactServer.useContext(ServerContext)}; } const transport = ReactNoopFlightServer.render(, { context: [['ServerContext', 'Override']], @@ -1321,7 +1349,7 @@ describe('ReactFlight', () => { let ServerContext; function inlineLazyServerContextInitialization() { if (!ServerContext) { - ServerContext = createServerContext('ServerContext', 'default'); + ServerContext = createServerServerContext('ServerContext', 'default'); } return ServerContext; } @@ -1346,7 +1374,7 @@ describe('ReactFlight', () => { return (
- {React.useContext(inlineLazyServerContextInitialization())} + {ReactServer.useContext(inlineLazyServerContextInitialization())}
@@ -1381,11 +1409,17 @@ describe('ReactFlight', () => { // Reset all modules, except flight-modules which keeps the registry of Client Components const flightModules = require('react-noop-renderer/flight-modules'); jest.resetModules(); + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-noop-renderer/flight-modules', () => flightModules); + ReactServer = require('react'); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + + __unmockReact(); + jest.resetModules(); + jest.mock('react-noop-renderer/flight-modules', () => flightModules); React = require('react'); ReactNoop = require('react-noop-renderer'); - ReactNoopFlightServer = require('react-noop-renderer/flight-server'); ReactNoopFlightClient = require('react-noop-renderer/flight-client'); act = require('internal-test-utils').act; Scheduler = require('scheduler'); diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index fbe4d1c51ee27..739e045447ddf 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -42,6 +42,7 @@ "test-utils.js", "unstable_testing.js", "unstable_server-external-runtime.js", + "react-dom.shared-subset.js", "cjs/", "umd/" ], diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js index dc57bd0e94255..0a3773fa6f3e4 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js @@ -62,7 +62,7 @@ describe('ReactFlightDOM', () => { // This reset is to load modules for the SSR/Browser scope. jest.resetModules(); - jest.unmock('react'); + __unmockReact(); act = require('internal-test-utils').act; Stream = require('stream'); React = require('react'); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js index b8e95f8b84ea1..3aa7f712a0025 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -15,147 +15,27 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; -let clientExports; -let serverExports; -let turbopackMap; -let turbopackServerMap; -let act; let React; -let ReactDOM; -let ReactDOMClient; -let ReactDOMFizzServer; let ReactServerDOMServer; let ReactServerDOMClient; -let Suspense; -let use; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.browser'), ); - act = require('internal-test-utils').act; - const TurbopackMock = require('./utils/TurbopackMock'); - clientExports = TurbopackMock.clientExports; - serverExports = TurbopackMock.serverExports; - turbopackMap = TurbopackMock.turbopackMap; - turbopackServerMap = TurbopackMock.turbopackServerMap; - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMClient = require('react-dom/client'); - ReactDOMFizzServer = require('react-dom/server.browser'); ReactServerDOMServer = require('react-server-dom-turbopack/server.browser'); - ReactServerDOMClient = require('react-server-dom-turbopack/client'); - Suspense = React.Suspense; - use = React.use; - }); - - function makeDelayedText(Model) { - let error, _resolve, _reject; - let promise = new Promise((resolve, reject) => { - _resolve = () => { - promise = null; - resolve(); - }; - _reject = e => { - error = e; - promise = null; - reject(e); - }; - }); - function DelayedText({children}, data) { - if (promise) { - throw promise; - } - if (error) { - throw error; - } - return {children}; - } - return [DelayedText, _resolve, _reject]; - } - - const theInfinitePromise = new Promise(() => {}); - function InfiniteSuspend() { - throw theInfinitePromise; - } - - function requireServerRef(ref) { - let name = ''; - let resolvedModuleData = turbopackServerMap[ref]; - if (resolvedModuleData) { - // The potentially aliased name. - name = resolvedModuleData.name; - } else { - // We didn't find this specific export name but we might have the * export - // which contains this name as well. - // TODO: It's unfortunate that we now have to parse this string. We should - // probably go back to encoding path and name separately on the client reference. - const idx = ref.lastIndexOf('#'); - if (idx !== -1) { - name = ref.slice(idx + 1); - resolvedModuleData = turbopackServerMap[ref.slice(0, idx)]; - } - if (!resolvedModuleData) { - throw new Error( - 'Could not find the module "' + - ref + - '" in the React Client Manifest. ' + - 'This is probably a bug in the React Server Components bundler.', - ); - } - } - const mod = __turbopack_require__(resolvedModuleData.id); - if (name === '*') { - return mod; - } - return mod[name]; - } - - async function callServer(actionId, body) { - const fn = requireServerRef(actionId); - const args = await ReactServerDOMServer.decodeReply( - body, - turbopackServerMap, - ); - return fn.apply(null, args); - } - it('should resolve HTML using W3C streams', async () => { - function Text({children}) { - return {children}; - } - function HTML() { - return ( -
- hello - world -
- ); - } - - function App() { - const model = { - html: , - }; - return model; - } + __unmockReact(); + jest.resetModules(); - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - const model = await response; - expect(model).toEqual({ - html: ( -
- hello - world -
- ), - }); + React = require('react'); + ReactServerDOMClient = require('react-server-dom-turbopack/client'); }); it('should resolve HTML using W3C streams', async () => { @@ -190,1053 +70,4 @@ describe('ReactFlightDOMBrowser', () => { ), }); }); - - it('should progressively reveal server components', async () => { - let reportedErrors = []; - - // Client Components - - class ErrorBoundary extends React.Component { - state = {hasError: false, error: null}; - static getDerivedStateFromError(error) { - return { - hasError: true, - error, - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback(this.state.error); - } - return this.props.children; - } - } - - let errorBoundaryFn; - if (__DEV__) { - errorBoundaryFn = e => ( -

- {e.message} + {e.digest} -

- ); - } else { - errorBoundaryFn = e => { - expect(e.message).toBe( - 'An error occurred in the Server Components render. The specific message is omitted in production' + - ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' + - ' may provide additional details about the nature of the error.', - ); - return

{e.digest}

; - }; - } - - function MyErrorBoundary({children}) { - return ( - {children} - ); - } - - // Model - function Text({children}) { - return children; - } - - const [Friends, resolveFriends] = makeDelayedText(Text); - const [Name, resolveName] = makeDelayedText(Text); - const [Posts, resolvePosts] = makeDelayedText(Text); - const [Photos, resolvePhotos] = makeDelayedText(Text); - const [Games, , rejectGames] = makeDelayedText(Text); - - // View - function ProfileDetails({avatar}) { - return ( -
- :name: - {avatar} -
- ); - } - function ProfileSidebar({friends}) { - return ( -
- :photos: - {friends} -
- ); - } - function ProfilePosts({posts}) { - return
{posts}
; - } - function ProfileGames({games}) { - return
{games}
; - } - - const MyErrorBoundaryClient = clientExports(MyErrorBoundary); - - function ProfileContent() { - return ( - <> - :avatar:} /> - (loading sidebar)

}> - :friends:} /> -
- (loading posts)

}> - :posts:} /> -
- - (loading games)

}> - :games:} /> -
-
- - ); - } - - const model = { - rootContent: , - }; - - function ProfilePage({response}) { - return use(response).rootContent; - } - - const stream = ReactServerDOMServer.renderToReadableStream( - model, - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - return __DEV__ ? `a dev digest` : `digest("${x.message}")`; - }, - }, - ); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - (loading)

}> - -
, - ); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - // This isn't enough to show anything. - await act(() => { - resolveFriends(); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - // We can now show the details. Sidebar and posts are still loading. - await act(() => { - resolveName(); - }); - // Advance time enough to trigger a nested fallback. - jest.advanceTimersByTime(500); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '

(loading sidebar)

' + - '

(loading posts)

' + - '

(loading games)

', - ); - - expect(reportedErrors).toEqual([]); - - const theError = new Error('Game over'); - // Let's *fail* loading games. - await act(() => { - rejectGames(theError); - }); - - const gamesExpectedValue = __DEV__ - ? '

Game over + a dev digest

' - : '

digest("Game over")

'; - - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '

(loading sidebar)

' + - '

(loading posts)

' + - gamesExpectedValue, - ); - - expect(reportedErrors).toEqual([theError]); - reportedErrors = []; - - // We can now show the sidebar. - await act(() => { - resolvePhotos(); - }); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '
:photos::friends:
' + - '

(loading posts)

' + - gamesExpectedValue, - ); - - // Show everything. - await act(() => { - resolvePosts(); - }); - expect(container.innerHTML).toBe( - '
:name::avatar:
' + - '
:photos::friends:
' + - '
:posts:
' + - gamesExpectedValue, - ); - - expect(reportedErrors).toEqual([]); - }); - - it('should close the stream upon completion when rendering to W3C streams', async () => { - // Model - function Text({children}) { - return children; - } - - const [Friends, resolveFriends] = makeDelayedText(Text); - const [Name, resolveName] = makeDelayedText(Text); - const [Posts, resolvePosts] = makeDelayedText(Text); - const [Photos, resolvePhotos] = makeDelayedText(Text); - - // View - function ProfileDetails({avatar}) { - return ( -
- :name: - {avatar} -
- ); - } - function ProfileSidebar({friends}) { - return ( -
- :photos: - {friends} -
- ); - } - function ProfilePosts({posts}) { - return
{posts}
; - } - - function ProfileContent() { - return ( - - :avatar:} /> - (loading sidebar)

}> - :friends:} /> -
- (loading posts)

}> - :posts:} /> -
-
- ); - } - - const model = { - rootContent: , - }; - - const stream = ReactServerDOMServer.renderToReadableStream( - model, - turbopackMap, - ); - - const reader = stream.getReader(); - const decoder = new TextDecoder(); - - let flightResponse = ''; - let isDone = false; - - reader.read().then(function progress({done, value}) { - if (done) { - isDone = true; - return; - } - - flightResponse += decoder.decode(value); - - return reader.read().then(progress); - }); - - // Advance time enough to trigger a nested fallback. - jest.advanceTimersByTime(500); - - await act(() => {}); - - expect(flightResponse).toContain('(loading everything)'); - expect(flightResponse).toContain('(loading sidebar)'); - expect(flightResponse).toContain('(loading posts)'); - expect(flightResponse).not.toContain(':friends:'); - expect(flightResponse).not.toContain(':name:'); - - await act(() => { - resolveFriends(); - }); - - expect(flightResponse).toContain(':friends:'); - - await act(() => { - resolveName(); - }); - - expect(flightResponse).toContain(':name:'); - - await act(() => { - resolvePhotos(); - }); - - expect(flightResponse).toContain(':photos:'); - - await act(() => { - resolvePosts(); - }); - - expect(flightResponse).toContain(':posts:'); - - // Final pending chunk is written; stream should be closed. - expect(isDone).toBeTruthy(); - }); - - it('should be able to complete after aborting and throw the reason client-side', async () => { - const reportedErrors = []; - - let errorBoundaryFn; - if (__DEV__) { - errorBoundaryFn = e => ( -

- {e.message} + {e.digest} -

- ); - } else { - errorBoundaryFn = e => { - expect(e.message).toBe( - 'An error occurred in the Server Components render. The specific message is omitted in production' + - ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' + - ' may provide additional details about the nature of the error.', - ); - return

{e.digest}

; - }; - } - - class ErrorBoundary extends React.Component { - state = {hasError: false, error: null}; - static getDerivedStateFromError(error) { - return { - hasError: true, - error, - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback(this.state.error); - } - return this.props.children; - } - } - - const controller = new AbortController(); - const stream = ReactServerDOMServer.renderToReadableStream( -
- -
, - turbopackMap, - { - signal: controller.signal, - onError(x) { - const message = typeof x === 'string' ? x : x.message; - reportedErrors.push(x); - return __DEV__ ? 'a dev digest' : `digest("${message}")`; - }, - }, - ); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App({res}) { - return use(res); - } - - await act(() => { - root.render( - - (loading)

}> - -
-
, - ); - }); - expect(container.innerHTML).toBe('

(loading)

'); - - await act(() => { - controller.abort('for reasons'); - }); - const expectedValue = __DEV__ - ? '

Error: for reasons + a dev digest

' - : '

digest("for reasons")

'; - expect(container.innerHTML).toBe(expectedValue); - - expect(reportedErrors).toEqual(['for reasons']); - }); - - it('basic use(promise)', async () => { - function Server() { - return ( - use(Promise.resolve('A')) + - use(Promise.resolve('B')) + - use(Promise.resolve('C')) - ); - } - - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(container.innerHTML).toBe('ABC'); - }); - - it('basic use(context)', async () => { - let ContextA; - let ContextB; - expect(() => { - ContextA = React.createServerContext('ContextA', ''); - ContextB = React.createServerContext('ContextB', 'B'); - }).toErrorDev( - [ - 'Server Context is deprecated and will soon be removed. ' + - 'It was never documented and we have found it not to be useful ' + - 'enough to warrant the downside it imposes on all apps.', - 'Server Context is deprecated and will soon be removed. ' + - 'It was never documented and we have found it not to be useful ' + - 'enough to warrant the downside it imposes on all apps.', - ], - {withoutStack: true}, - ); - - function ServerComponent() { - return use(ContextA) + use(ContextB); - } - function Server() { - return ( - - - - ); - } - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - // Client uses a different renderer. - // We reset _currentRenderer here to not trigger a warning about multiple - // renderers concurrently using this context - ContextA._currentRenderer = null; - root.render(); - }); - expect(container.innerHTML).toBe('AB'); - }); - - it('use(promise) in multiple components', async () => { - function Child({prefix}) { - return prefix + use(Promise.resolve('C')) + use(Promise.resolve('D')); - } - - function Parent() { - return ( - - ); - } - - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(container.innerHTML).toBe('ABCD'); - }); - - it('using a rejected promise will throw', async () => { - const promiseA = Promise.resolve('A'); - const promiseB = Promise.reject(new Error('Oops!')); - const promiseC = Promise.resolve('C'); - - // Jest/Node will raise an unhandled rejected error unless we await this. It - // works fine in the browser, though. - await expect(promiseB).rejects.toThrow('Oops!'); - - function Server() { - return use(promiseA) + use(promiseB) + use(promiseC); - } - - const reportedErrors = []; - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - { - onError(x) { - reportedErrors.push(x); - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; - }, - }, - ); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - class ErrorBoundary extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - return {error}; - } - render() { - if (this.state.error) { - return __DEV__ - ? this.state.error.message + ' + ' + this.state.error.digest - : this.state.error.digest; - } - return this.props.children; - } - } - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - expect(container.innerHTML).toBe( - __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")', - ); - expect(reportedErrors.length).toBe(1); - expect(reportedErrors[0].message).toBe('Oops!'); - }); - - it("use a promise that's already been instrumented and resolved", async () => { - const thenable = { - status: 'fulfilled', - value: 'Hi', - then() {}, - }; - - // This will never suspend because the thenable already resolved - function Server() { - return use(thenable); - } - - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Hi'); - }); - - it('unwraps thenable that fulfills synchronously without suspending', async () => { - function Server() { - const thenable = { - then(resolve) { - // This thenable immediately resolves, synchronously, without waiting - // a microtask. - resolve('Hi'); - }, - }; - try { - return use(thenable); - } catch { - throw new Error( - '`use` should not suspend because the thenable resolved synchronously.', - ); - } - } - - // Because the thenable resolves synchronously, we should be able to finish - // rendering synchronously, with no fallback. - const stream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Hi'); - }); - - it('can pass a higher order function by reference from server to client', async () => { - let actionProxy; - - function Client({action}) { - actionProxy = action; - return 'Click Me'; - } - - function greet(transform, text) { - return 'Hello ' + transform(text); - } - - function upper(text) { - return text.toUpperCase(); - } - - const ServerModuleA = serverExports({ - greet, - }); - const ServerModuleB = serverExports({ - upper, - }); - const ClientRef = clientExports(Client); - - const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper); - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - const response = ReactServerDOMClient.createFromReadableStream(stream, { - async callServer(ref, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return callServer(ref, body); - }, - }); - - function App() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Click Me'); - expect(typeof actionProxy).toBe('function'); - expect(actionProxy).not.toBe(boundFn); - - const result = await actionProxy('hi'); - expect(result).toBe('Hello HI'); - }); - - it('can call a module split server function', async () => { - let actionProxy; - - function Client({action}) { - actionProxy = action; - return 'Click Me'; - } - - function greet(text) { - return 'Hello ' + text; - } - - const ServerModule = serverExports({ - // This gets split into another module - split: greet, - }); - const ClientRef = clientExports(Client); - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - const response = ReactServerDOMClient.createFromReadableStream(stream, { - async callServer(ref, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return callServer(ref, body); - }, - }); - - function App() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Click Me'); - expect(typeof actionProxy).toBe('function'); - - const result = await actionProxy('Split'); - expect(result).toBe('Hello Split'); - }); - - it('can pass a server function by importing from client back to server', async () => { - function greet(transform, text) { - return 'Hello ' + transform(text); - } - - function upper(text) { - return text.toUpperCase(); - } - - const ServerModuleA = serverExports({ - greet, - }); - const ServerModuleB = serverExports({ - upper, - }); - - let actionProxy; - - // This is a Proxy representing ServerModuleB in the Client bundle. - const ServerModuleBImportedOnClient = { - upper: ReactServerDOMClient.createServerReference( - ServerModuleB.upper.$$id, - async function (ref, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return callServer(ref, body); - }, - ), - }; - - function Client({action}) { - // Client side pass a Server Reference into an action. - actionProxy = text => action(ServerModuleBImportedOnClient.upper, text); - return 'Click Me'; - } - - const ClientRef = clientExports(Client); - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - const response = ReactServerDOMClient.createFromReadableStream(stream, { - async callServer(ref, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return callServer(ref, body); - }, - }); - - function App() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Click Me'); - - const result = await actionProxy('hi'); - expect(result).toBe('Hello HI'); - }); - - it('can bind arguments to a server reference', async () => { - let actionProxy; - - function Client({action}) { - actionProxy = action; - return 'Click Me'; - } - - const greet = serverExports(function greet(a, b, c) { - return a + ' ' + b + c; - }); - const ClientRef = clientExports(Client); - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - const response = ReactServerDOMClient.createFromReadableStream(stream, { - async callServer(actionId, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return callServer(actionId, body); - }, - }); - - function App() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(container.innerHTML).toBe('Click Me'); - expect(typeof actionProxy).toBe('function'); - expect(actionProxy).not.toBe(greet); - - const result = await actionProxy('!'); - expect(result).toBe('Hello World!'); - }); - - it('propagates server reference errors to the client', async () => { - let actionProxy; - - function Client({action}) { - actionProxy = action; - return 'Click Me'; - } - - async function send(text) { - return Promise.reject(new Error(`Error for ${text}`)); - } - - const ServerModule = serverExports({send}); - const ClientRef = clientExports(Client); - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - const response = ReactServerDOMClient.createFromReadableStream(stream, { - async callServer(actionId, args) { - const body = await ReactServerDOMClient.encodeReply(args); - return ReactServerDOMClient.createFromReadableStream( - ReactServerDOMServer.renderToReadableStream( - callServer(actionId, body), - null, - {onError: error => 'test-error-digest'}, - ), - ); - }, - }); - - function App() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - - if (__DEV__) { - await expect(actionProxy('test')).rejects.toThrow('Error for test'); - } else { - let thrownError; - - try { - await actionProxy('test'); - } catch (error) { - thrownError = error; - } - - expect(thrownError).toEqual( - new Error( - 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.', - ), - ); - - expect(thrownError.digest).toBe('test-error-digest'); - } - }); - - it('supports Float hints before the first await in server components in Fiber', async () => { - function Component() { - return

hello world

; - } - - const ClientComponent = clientExports(Component); - - async function ServerComponent() { - ReactDOM.preload('before', {as: 'style'}); - await 1; - ReactDOM.preload('after', {as: 'style'}); - return ; - } - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - let response = null; - function getResponse() { - if (response === null) { - response = ReactServerDOMClient.createFromReadableStream(stream); - } - return response; - } - - function App() { - return getResponse(); - } - - // pausing to let Flight runtime tick. This is a test only artifact of the fact that - // we aren't operating separate module graphs for flight and fiber. In a real app - // each would have their own dispatcher and there would be no cross dispatching. - await 1; - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - expect(document.head.innerHTML).toBe( - '', - ); - expect(container.innerHTML).toBe('

hello world

'); - }); - - it('Does not support Float hints in server components anywhere in Fizz', async () => { - // In environments that do not support AsyncLocalStorage the Flight client has no ability - // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because - // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have - // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage. - function Component() { - return

hello world

; - } - - const ClientComponent = clientExports(Component); - - async function ServerComponent() { - ReactDOM.preload('before', {as: 'style'}); - await 1; - ReactDOM.preload('after', {as: 'style'}); - return ; - } - - const stream = ReactServerDOMServer.renderToReadableStream( - , - turbopackMap, - ); - - let response = null; - function getResponse() { - if (response === null) { - response = ReactServerDOMClient.createFromReadableStream(stream); - } - return response; - } - - function App() { - return ( - - {getResponse()} - - ); - } - - // pausing to let Flight runtime tick. This is a test only artifact of the fact that - // we aren't operating separate module graphs for flight and fiber. In a real app - // each would have their own dispatcher and there would be no cross dispatching. - await 1; - - let fizzStream; - await act(async () => { - fizzStream = await ReactDOMFizzServer.renderToReadableStream(); - }); - - const decoder = new TextDecoder(); - const reader = fizzStream.getReader(); - let content = ''; - while (true) { - const {done, value} = await reader.read(); - if (done) { - content += decoder.decode(); - break; - } - content += decoder.decode(value, {stream: true}); - } - - expect(content).toEqual( - '' + - '

hello world

', - ); - }); - - // @gate enablePostpone - it('supports postpone in Server Components', async () => { - function Server() { - React.unstable_postpone('testing postpone'); - return 'Not shown'; - } - - let postponed = null; - - const stream = ReactServerDOMServer.renderToReadableStream( - - - , - null, - { - onPostpone(reason) { - postponed = reason; - }, - }, - ); - const response = ReactServerDOMClient.createFromReadableStream(stream); - - function Client() { - return use(response); - } - - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(async () => { - root.render( -
- Shell: -
, - ); - }); - // We should have reserved the shell already. Which means that the Server - // Component should've been a lazy component. - expect(container.innerHTML).toContain('Shell:'); - expect(container.innerHTML).toContain('Loading...'); - expect(container.innerHTML).not.toContain('Not shown'); - - expect(postponed).toBe('testing postpone'); - }); }); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js index 2dfdf97841bd7..3538b4f7a60d9 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js @@ -33,6 +33,7 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); @@ -41,9 +42,14 @@ describe('ReactFlightDOMEdge', () => { clientExports = TurbopackMock.clientExports; turbopackMap = TurbopackMock.turbopackMap; turbopackModules = TurbopackMock.turbopackModules; + + ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); + + jest.resetModules(); + __unmockReact(); + React = require('react'); ReactDOMServer = require('react-dom/server.edge'); - ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); ReactServerDOMClient = require('react-server-dom-turbopack/client.edge'); use = React.use; }); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js index 4c99860e0bb2e..176003eb4c822 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js @@ -33,14 +33,17 @@ describe('ReactFlightDOMForm', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); + ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); const TurbopackMock = require('./utils/TurbopackMock'); serverExports = TurbopackMock.serverExports; turbopackServerMap = TurbopackMock.turbopackServerMap; + __unmockReact(); + jest.resetModules(); React = require('react'); - ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); ReactServerDOMClient = require('react-server-dom-turbopack/client.edge'); ReactDOMServer = require('react-dom/server.edge'); container = document.createElement('div'); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js index 5a7b14b3bbd4c..eebabfce5d0de 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js @@ -42,7 +42,7 @@ describe('ReactFlightDOMNode', () => { turbopackModuleLoading = TurbopackMock.moduleLoading; jest.resetModules(); - jest.unmock('react'); + __unmockReact(); jest.unmock('react-server-dom-turbopack/server'); jest.mock('react-server-dom-turbopack/client', () => require('react-server-dom-turbopack/client.node'), diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js index d6c4c318d7b38..d8475a8762f23 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js @@ -24,6 +24,7 @@ describe('ReactFlightDOMReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.browser'), ); @@ -31,6 +32,7 @@ describe('ReactFlightDOMReply', () => { // serverExports = TurbopackMock.serverExports; turbopackServerMap = TurbopackMock.turbopackServerMap; ReactServerDOMServer = require('react-server-dom-turbopack/server.browser'); + jest.resetModules(); ReactServerDOMClient = require('react-server-dom-turbopack/client'); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index e26db0fdceb45..eebca4710e954 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -62,7 +62,7 @@ describe('ReactFlightDOM', () => { // This reset is to load modules for the SSR/Browser scope. jest.unmock('react-server-dom-webpack/server'); - jest.unmock('react'); + __unmockReact(); jest.resetModules(); act = require('internal-test-utils').act; Stream = require('stream'); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index af474dc148bf7..7eef417035aca 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -28,27 +28,37 @@ let ReactServerDOMServer; let ReactServerDOMClient; let Suspense; let use; +let ReactServer; +let ReactServerDOM; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); - act = require('internal-test-utils').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; serverExports = WebpackMock.serverExports; webpackMap = WebpackMock.webpackMap; webpackServerMap = WebpackMock.webpackServerMap; + + ReactServer = require('react'); + ReactServerDOM = require('react-dom'); + ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + + __unmockReact(); + jest.resetModules(); + + act = require('internal-test-utils').act; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server.browser'); - ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); ReactServerDOMClient = require('react-server-dom-webpack/client'); Suspense = React.Suspense; use = React.use; @@ -583,9 +593,9 @@ describe('ReactFlightDOMBrowser', () => { it('basic use(promise)', async () => { function Server() { return ( - use(Promise.resolve('A')) + - use(Promise.resolve('B')) + - use(Promise.resolve('C')) + ReactServer.use(Promise.resolve('A')) + + ReactServer.use(Promise.resolve('B')) + + ReactServer.use(Promise.resolve('C')) ); } @@ -627,7 +637,7 @@ describe('ReactFlightDOMBrowser', () => { ); function ServerComponent() { - return use(ContextA) + use(ContextB); + return ReactServer.use(ContextA) + ReactServer.use(ContextB); } function Server() { return ( @@ -657,12 +667,21 @@ describe('ReactFlightDOMBrowser', () => { it('use(promise) in multiple components', async () => { function Child({prefix}) { - return prefix + use(Promise.resolve('C')) + use(Promise.resolve('D')); + return ( + prefix + + ReactServer.use(Promise.resolve('C')) + + ReactServer.use(Promise.resolve('D')) + ); } function Parent() { return ( - + ); } @@ -695,7 +714,11 @@ describe('ReactFlightDOMBrowser', () => { await expect(promiseB).rejects.toThrow('Oops!'); function Server() { - return use(promiseA) + use(promiseB) + use(promiseC); + return ( + ReactServer.use(promiseA) + + ReactServer.use(promiseB) + + ReactServer.use(promiseC) + ); } const reportedErrors = []; @@ -755,7 +778,7 @@ describe('ReactFlightDOMBrowser', () => { // This will never suspend because the thenable already resolved function Server() { - return use(thenable); + return ReactServer.use(thenable); } const stream = ReactServerDOMServer.renderToReadableStream(); @@ -783,7 +806,7 @@ describe('ReactFlightDOMBrowser', () => { }, }; try { - return use(thenable); + return ReactServer.use(thenable); } catch { throw new Error( '`use` should not suspend because the thenable resolved synchronously.', @@ -1087,9 +1110,9 @@ describe('ReactFlightDOMBrowser', () => { const ClientComponent = clientExports(Component); async function ServerComponent() { - ReactDOM.preload('before', {as: 'style'}); + ReactServerDOM.preload('before', {as: 'style'}); await 1; - ReactDOM.preload('after', {as: 'style'}); + ReactServerDOM.preload('after', {as: 'style'}); return ; } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 22ee0696027b1..a81ec52583fe6 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -38,7 +38,6 @@ describe('ReactFlightDOMEdge', () => { jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); - ReactServerDOMServer = require('react-server-dom-webpack/server'); const WebpackMock = require('./utils/WebpackMock'); @@ -47,8 +46,10 @@ describe('ReactFlightDOMEdge', () => { webpackModules = WebpackMock.webpackModules; webpackModuleLoading = WebpackMock.moduleLoading; + ReactServerDOMServer = require('react-server-dom-webpack/server'); + jest.resetModules(); - jest.unmock('react'); + __unmockReact(); jest.unmock('react-server-dom-webpack/server'); jest.mock('react-server-dom-webpack/client', () => require('react-server-dom-webpack/client.edge'), diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 4de3f5528dce8..1ba0e100740f3 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -38,16 +38,19 @@ describe('ReactFlightDOMForm', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); + ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; serverExports = WebpackMock.serverExports; webpackMap = WebpackMock.webpackMap; webpackServerMap = WebpackMock.webpackServerMap; + __unmockReact(); + jest.resetModules(); React = require('react'); - ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); ReactServerDOMClient = require('react-server-dom-webpack/client.edge'); ReactDOMServer = require('react-dom/server.edge'); ReactDOMClient = require('react-dom/client'); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 490c321689d17..6fd51a5d1fb49 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -42,7 +42,7 @@ describe('ReactFlightDOMNode', () => { webpackModuleLoading = WebpackMock.moduleLoading; jest.resetModules(); - jest.unmock('react'); + __unmockReact(); jest.unmock('react-server-dom-webpack/server'); jest.mock('react-server-dom-webpack/client', () => require('react-server-dom-webpack/client.node'), diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js index 894f444640558..019d28a00ce98 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -24,6 +24,7 @@ describe('ReactFlightDOMReply', () => { beforeEach(() => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); @@ -31,6 +32,7 @@ describe('ReactFlightDOMReply', () => { // serverExports = WebpackMock.serverExports; webpackServerMap = WebpackMock.webpackServerMap; ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + jest.resetModules(); ReactServerDOMClient = require('react-server-dom-webpack/client'); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3e73253e26ab0..230994011fb93 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -104,7 +104,7 @@ import { } from 'shared/ReactSerializationErrors'; import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; +import ReactServerSharedInternals from './ReactServerSharedInternals'; import isArray from 'shared/isArray'; import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; @@ -197,8 +197,9 @@ export type Request = { toJSON: (key: string, value: ReactClientValue) => ReactJSONValue, }; -const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; -const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache; +const ReactCurrentDispatcher = + ReactServerSharedInternals.ReactCurrentDispatcher; +const ReactCurrentCache = ReactServerSharedInternals.ReactCurrentCache; function defaultErrorHandler(error: mixed) { console['error'](error); diff --git a/packages/react-server/src/ReactServerSharedInternals.js b/packages/react-server/src/ReactServerSharedInternals.js new file mode 100644 index 0000000000000..6d1a5a7c6858a --- /dev/null +++ b/packages/react-server/src/ReactServerSharedInternals.js @@ -0,0 +1,24 @@ +/** + * 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 + */ + +import * as React from 'react'; + +const ReactSharedServerInternals = + // $FlowFixMe: It's defined in the one we resolve to. + React.__SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + +if (!ReactSharedServerInternals) { + throw new Error( + 'The "react" package in this environment is not configured correctly. ' + + 'The "react-server" condition must be enabled in any environment that ' + + 'runs React Server Components.', + ); +} + +export default ReactSharedServerInternals; diff --git a/packages/react/src/React.js b/packages/react/src/React.js index b45a0cda05370..9c0cda47a8de7 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -67,7 +67,7 @@ import { cloneElementWithValidation, } from './ReactElementValidator'; import {createServerContext} from './ReactServerContext'; -import ReactSharedInternals from './ReactSharedInternals'; +import ReactSharedInternals from './ReactSharedInternalsClient'; import {startTransition} from './ReactStartTransition'; import {act} from './ReactAct'; diff --git a/packages/react/src/ReactServerContext.js b/packages/react/src/ReactServerContext.js index 3748a05b1262d..e2ed9bea7d0d4 100644 --- a/packages/react/src/ReactServerContext.js +++ b/packages/react/src/ReactServerContext.js @@ -19,9 +19,7 @@ import type { } from 'shared/ReactTypes'; import {enableServerContext} from 'shared/ReactFeatureFlags'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; - -const ContextRegistry = ReactSharedInternals.ContextRegistry; +import {ContextRegistry} from './ReactServerContextRegistry'; export function createServerContext( globalName: string, diff --git a/packages/react/src/ReactServerSharedInternals.js b/packages/react/src/ReactServerSharedInternals.js new file mode 100644 index 0000000000000..3e9b81f4ec149 --- /dev/null +++ b/packages/react/src/ReactServerSharedInternals.js @@ -0,0 +1,16 @@ +/** + * 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. + */ + +import ReactCurrentDispatcher from './ReactCurrentDispatcher'; +import ReactCurrentCache from './ReactCurrentCache'; + +const ReactServerSharedInternals = { + ReactCurrentDispatcher, + ReactCurrentCache, +}; + +export default ReactServerSharedInternals; diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternalsClient.js similarity index 100% rename from packages/react/src/ReactSharedInternals.js rename to packages/react/src/ReactSharedInternalsClient.js diff --git a/packages/react/src/ReactSharedInternalsServer.js b/packages/react/src/ReactSharedInternalsServer.js new file mode 100644 index 0000000000000..5e4aa64c3fd1a --- /dev/null +++ b/packages/react/src/ReactSharedInternalsServer.js @@ -0,0 +1,25 @@ +/** + * 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. + */ + +import ReactCurrentOwner from './ReactCurrentOwner'; +import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; +import {enableServerContext} from 'shared/ReactFeatureFlags'; +import {ContextRegistry} from './ReactServerContextRegistry'; + +const ReactSharedInternals = { + ReactCurrentOwner, +}; + +if (__DEV__) { + ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame; +} + +if (enableServerContext) { + ReactSharedInternals.ContextRegistry = ContextRegistry; +} + +export default ReactSharedInternals; diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactSharedSubset.experimental.js index ed5340df9de03..80d50805c23b4 100644 --- a/packages/react/src/ReactSharedSubset.experimental.js +++ b/packages/react/src/ReactSharedSubset.experimental.js @@ -10,8 +10,11 @@ // Patch fetch import './ReactFetch'; +export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactSharedInternalsServer'; + +export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactServerSharedInternals'; + export { - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, Children, Fragment, Profiler, diff --git a/packages/react/src/ReactSharedSubset.js b/packages/react/src/ReactSharedSubset.js index c880cc9ada182..1bc2b3036f455 100644 --- a/packages/react/src/ReactSharedSubset.js +++ b/packages/react/src/ReactSharedSubset.js @@ -10,8 +10,11 @@ // Patch fetch import './ReactFetch'; +export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactSharedInternalsServer'; + +export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactServerSharedInternals'; + export { - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, Children, Fragment, Profiler, diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index c778f7e6162d7..e91c6e09941d8 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -32,6 +32,7 @@ async function fetchMock(resource, options) { } let React; +let ReactServer; let ReactServerDOMServer; let ReactServerDOMClient; let use; @@ -43,23 +44,21 @@ describe('ReactFetch', () => { fetchCount = 0; global.fetch = fetchMock; - if (gate(flags => !flags.www)) { - jest.mock('react', () => require('react/react.shared-subset')); - } + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.browser'), ); require('react-server-dom-webpack/src/__tests__/utils/WebpackMock'); - - React = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server'); + ReactServer = require('react'); jest.resetModules(); - jest.unmock('react'); + __unmockReact(); jest.unmock('react-server-dom-webpack/server'); ReactServerDOMClient = require('react-server-dom-webpack/client'); - use = React.use; - cache = React.cache; + React = require('react'); + use = ReactServer.use; + cache = ReactServer.cache; }); async function render(Component) { diff --git a/packages/react/src/__tests__/ReactFetchEdge-test.js b/packages/react/src/__tests__/ReactFetchEdge-test.js index 9f9555251823e..b4e313faa728f 100644 --- a/packages/react/src/__tests__/ReactFetchEdge-test.js +++ b/packages/react/src/__tests__/ReactFetchEdge-test.js @@ -48,9 +48,7 @@ describe('ReactFetch', () => { fetchCount = 0; global.fetch = fetchMock; - if (gate(flags => !flags.www)) { - jest.mock('react', () => require('react/react.shared-subset')); - } + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); @@ -60,7 +58,7 @@ describe('ReactFetch', () => { ReactServerDOMServer = require('react-server-dom-webpack/server'); jest.resetModules(); - jest.unmock('react'); + __unmockReact(); jest.unmock('react-server-dom-webpack/server'); ReactServerDOMClient = require('react-server-dom-webpack/client'); use = React.use; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index dae06b5c68079..43d448ca977b4 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -476,5 +476,6 @@ "488": "Couldn't find all resumable slots by key/index during replaying. The tree doesn't match so React will fallback to client rendering.", "489": "Expected to see a component of type \"%s\" in this slot. The tree doesn't match so React will fallback to client rendering.", "490": "Expected to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering.", - "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React." + "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React.", + "492": "The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components." } \ No newline at end of file diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index 48eaf0fd5b731..b296f65224eb7 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -43,13 +43,20 @@ function resolveEntryFork(resolvedEntry, isFBBundle) { return resolvedEntry; } -jest.mock('react', () => { - const resolvedEntryPoint = resolveEntryFork( - require.resolve('react'), - global.__WWW__ - ); - return jest.requireActual(resolvedEntryPoint); -}); +function mockReact() { + jest.mock('react', () => { + const resolvedEntryPoint = resolveEntryFork( + require.resolve('react'), + global.__WWW__ + ); + return jest.requireActual(resolvedEntryPoint); + }); +} + +// When we want to unmock React we really need to mock it again. +global.__unmockReact = mockReact; + +mockReact(); jest.mock('react/react.shared-subset', () => { const resolvedEntryPoint = resolveEntryFork( @@ -162,7 +169,7 @@ inlinedHostConfigs.forEach(rendererInfo => { // Make it possible to import this module inside // the React package itself. jest.mock('shared/ReactSharedInternals', () => - jest.requireActual('react/src/ReactSharedInternals') + jest.requireActual('react/src/ReactSharedInternalsClient') ); // Make it possible to import this module inside diff --git a/scripts/jest/setupTests.build.js b/scripts/jest/setupTests.build.js index d7014c7720e42..2759ddae5404f 100644 --- a/scripts/jest/setupTests.build.js +++ b/scripts/jest/setupTests.build.js @@ -1,3 +1,5 @@ 'use strict'; jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); + +global.__unmockReact = () => jest.unmock('react'); diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index d46dbb2b85c45..fa9a232bef40c 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -60,8 +60,11 @@ const forks = Object.freeze({ entry, dependencies ) => { - if (entry === 'react' || entry === 'react/src/ReactSharedSubset.js') { - return './packages/react/src/ReactSharedInternals.js'; + if (entry === 'react') { + return './packages/react/src/ReactSharedInternalsClient.js'; + } + if (entry === 'react/src/ReactSharedSubset.js') { + return './packages/react/src/ReactSharedInternalsServer.js'; } if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) { // React internals are unavailable if we can't reference the package.