Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Respect async flag in client manifest #30959

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ global.TextDecoder = require('util').TextDecoder;
let act;
let use;
let clientExports;
let clientExportsESM;
let turbopackMap;
let Stream;
let React;
Expand All @@ -29,6 +30,7 @@ let ReactServerDOMClient;
let Suspense;
let ReactServerScheduler;
let reactServerAct;
let ErrorBoundary;

describe('ReactFlightTurbopackDOM', () => {
beforeEach(() => {
Expand All @@ -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');
Expand All @@ -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) {
Expand Down Expand Up @@ -220,4 +239,105 @@ describe('ReactFlightTurbopackDOM', () => {
});
expect(container.innerHTML).toBe('<p>Async: Module</p>');
});

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 <p>{use(response)}</p>;
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

const AsyncModuleRef = await clientExportsESM(AsyncModule);
const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);

const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef text={AsyncModuleRef2.exportName} />,
turbopackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Async: Module</p>');
});

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 <p>{use(response)}</p>;
}

function App({response}) {
return (
<ErrorBoundary
fallback={error => (
<p>
{__DEV__ ? error.message + ' + ' : null}
{error.digest}
</p>
)}>
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
</ErrorBoundary>
);
}

const AsyncModuleRef = await clientExportsESM(AsyncModule, {
forceClientModuleProxy: true,
});

const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef />,
turbopackMap,
{
onError(error) {
return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});

const errorMessage = `The module "${Object.keys(turbopackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;

expect(container.innerHTML).toBe(
__DEV__
? `<p>${errorMessage} + a dev digest</p>`
: `<p>digest(${errorMessage})</p>`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ export function resolveClientReferenceMetadata<T>(
);
}
}
if (clientReference.$$async === true) {
if (resolvedModuleData.async === true && clientReference.$$async === true) {
throw new Error(
'The module "' +
modulePath +
'" is marked as an async ESM module but was loaded as a CJS proxy. ' +
'This is probably a bug in the React Server Components bundler.',
);
}
if (resolvedModuleData.async === true || clientReference.$$async === true) {
return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1];
} else {
return [resolvedModuleData.id, resolvedModuleData.chunks, name];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ImportManifestEntry = {
// chunks is an array of filenames
chunks: Array<string>,
name: string,
async?: boolean,
};

// This is the parsed shape of the wire format which is why it is
Expand Down
103 changes: 103 additions & 0 deletions packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ global.TextDecoder = require('util').TextDecoder;
let act;
let use;
let clientExports;
let clientExportsESM;
let clientModuleError;
let webpackMap;
let Stream;
Expand Down Expand Up @@ -68,6 +69,7 @@ describe('ReactFlightDOM', () => {
}
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
clientExportsESM = WebpackMock.clientExportsESM;
clientModuleError = WebpackMock.clientModuleError;
webpackMap = WebpackMock.webpackMap;

Expand Down Expand Up @@ -583,6 +585,107 @@ describe('ReactFlightDOM', () => {
expect(container.innerHTML).toBe('<p>Async Text</p>');
});

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 <p>{use(response)}</p>;
}

function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}

const AsyncModuleRef = await clientExportsESM(AsyncModule);
const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);

const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef text={AsyncModuleRef2.exportName} />,
webpackMap,
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<p>Async: Module</p>');
});

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 <p>{use(response)}</p>;
}

function App({response}) {
return (
<ErrorBoundary
fallback={error => (
<p>
{__DEV__ ? error.message + ' + ' : null}
{error.digest}
</p>
)}>
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
</ErrorBoundary>
);
}

const AsyncModuleRef = await clientExportsESM(AsyncModule, {
forceClientModuleProxy: true,
});

const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
<AsyncModuleRef />,
webpackMap,
{
onError(error) {
return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
},
},
),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});

const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;

expect(container.innerHTML).toBe(
__DEV__
? `<p>${errorMessage} + a dev digest</p>`
: `<p>digest(${errorMessage})</p>`,
);
});

it('should be able to import a name called "then"', async () => {
const thenExports = {
then: function then() {
Expand Down
Loading
Loading