diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js index 0cf2876fedba6..d4fc59b826208 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js @@ -20,6 +20,7 @@ global.TextDecoder = require('util').TextDecoder; let act; let use; let clientExports; +let clientExportsESM; let turbopackMap; let Stream; let React; @@ -29,6 +30,7 @@ let ReactServerDOMClient; let Suspense; let ReactServerScheduler; let reactServerAct; +let ErrorBoundary; describe('ReactFlightTurbopackDOM', () => { beforeEach(() => { @@ -49,6 +51,7 @@ describe('ReactFlightTurbopackDOM', () => { const TurbopackMock = require('./utils/TurbopackMock'); clientExports = TurbopackMock.clientExports; + clientExportsESM = TurbopackMock.clientExportsESM; turbopackMap = TurbopackMock.turbopackMap; ReactServerDOMServer = require('react-server-dom-turbopack/server'); @@ -63,6 +66,22 @@ describe('ReactFlightTurbopackDOM', () => { Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); ReactServerDOMClient = require('react-server-dom-turbopack/client'); + + ErrorBoundary = class 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; + } + }; }); async function serverAct(callback) { @@ -220,4 +239,105 @@ describe('ReactFlightTurbopackDOM', () => { }); expect(container.innerHTML).toBe('
Async: Module
'); }); + + it('should unwrap async ESM module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return{use(response)}
; + } + + function App({response}) { + return ( +Async: Module
'); + }); + + it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => { + const AsyncModule = Promise.resolve(function AsyncModule() { + return 'This should not be rendered'; + }); + + function Print({response}) { + return{use(response)}
; + } + + function App({response}) { + return ( ++ {__DEV__ ? error.message + ' + ' : null} + {error.digest} +
+ )}> +${errorMessage} + a dev digest
` + : `digest(${errorMessage})
`, + ); + }); }); diff --git a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js index 5ed6fa5357408..2e81d55fa692c 100644 --- a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js +++ b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js @@ -23,6 +23,7 @@ global.__turbopack_require__ = function (id) { }; const Server = require('react-server-dom-turbopack/server'); +const registerClientReference = Server.registerClientReference; const registerServerReference = Server.registerServerReference; const createClientModuleProxy = Server.createClientModuleProxy; @@ -83,6 +84,65 @@ exports.clientExports = function clientExports(moduleExports, chunkUrl) { return createClientModuleProxy(path); }; +exports.clientExportsESM = function clientExportsESM( + moduleExports, + options?: {forceClientModuleProxy?: boolean} = {}, +) { + const chunks = []; + const idx = '' + turbopackModuleIdx++; + turbopackClientModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + + const createClientReferencesForExports = ({exports, async}) => { + turbopackClientMap[path] = { + id: idx, + chunks, + name: '*', + async: true, + }; + + if (options.forceClientModuleProxy) { + return createClientModuleProxy(path); + } + + if (typeof exports === 'object') { + const references = {}; + + for (const name in exports) { + const id = path + '#' + name; + turbopackClientMap[path + '#' + name] = { + id: idx, + chunks, + name: name, + async, + }; + references[name] = registerClientReference(() => {}, id, name); + } + + return references; + } + + return registerClientReference(() => {}, path, '*'); + }; + + if ( + moduleExports && + typeof moduleExports === 'object' && + typeof moduleExports.then === 'function' + ) { + return moduleExports.then( + asyncModuleExports => + createClientReferencesForExports({ + exports: asyncModuleExports, + async: true, + }), + () => {}, + ); + } + + return createClientReferencesForExports({exports: moduleExports}); +}; + // This tests server to server references. There's another case of client to server references. exports.serverExports = function serverExports(moduleExports) { const idx = '' + turbopackModuleIdx++; diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js index d8224aff341dc..219391f8f819e 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightServerConfigTurbopackBundler.js @@ -71,7 +71,15 @@ export function resolveClientReferenceMetadataAsync Text
'); }); + it('should unwrap async ESM module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return{use(response)}
; + } + + function App({response}) { + return ( +Async: Module
'); + }); + + it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => { + const AsyncModule = Promise.resolve(function AsyncModule() { + return 'This should not be rendered'; + }); + + function Print({response}) { + return{use(response)}
; + } + + function App({response}) { + return ( ++ {__DEV__ ? error.message + ' + ' : null} + {error.digest} +
+ )}> +${errorMessage} + a dev digest
` + : `digest(${errorMessage})
`, + ); + }); + it('should be able to import a name called "then"', async () => { const thenExports = { then: function then() { diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 4527118c1de8b..654bcdc9b6d5c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -44,6 +44,10 @@ if (previousCompile === nodeCompile) { Module.prototype._compile = previousCompile; +const Server = require('react-server-dom-webpack/server'); +const registerClientReference = Server.registerClientReference; +const createClientModuleProxy = Server.createClientModuleProxy; + exports.webpackMap = webpackClientMap; exports.webpackModules = webpackClientModules; exports.webpackServerMap = webpackServerMap; @@ -126,6 +130,65 @@ exports.clientExports = function clientExports( return mod.exports; }; +exports.clientExportsESM = function clientExportsESM( + moduleExports, + options?: {forceClientModuleProxy?: boolean} = {}, +) { + const chunks = []; + const idx = '' + webpackModuleIdx++; + webpackClientModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + + const createClientReferencesForExports = ({exports, async}) => { + webpackClientMap[path] = { + id: idx, + chunks, + name: '*', + async: true, + }; + + if (options.forceClientModuleProxy) { + return createClientModuleProxy(path); + } + + if (typeof exports === 'object') { + const references = {}; + + for (const name in exports) { + const id = path + '#' + name; + webpackClientMap[path + '#' + name] = { + id: idx, + chunks, + name: name, + async, + }; + references[name] = registerClientReference(() => {}, id, name); + } + + return references; + } + + return registerClientReference(() => {}, path, '*'); + }; + + if ( + moduleExports && + typeof moduleExports === 'object' && + typeof moduleExports.then === 'function' + ) { + return moduleExports.then( + asyncModuleExports => + createClientReferencesForExports({ + exports: asyncModuleExports, + async: true, + }), + () => {}, + ); + } + + return createClientReferencesForExports({exports: moduleExports}); +}; + // This tests server to server references. There's another case of client to server references. exports.serverExports = function serverExports(moduleExports, blockOnChunk) { const idx = '' + webpackModuleIdx++; diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js index d29516ff946ea..f9d9bf4ea9169 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js @@ -71,7 +71,15 @@ export function resolveClientReferenceMetadata