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.