diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 40dfdf4a71165..41e6f1b7f34f2 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -517,6 +517,13 @@ export default async function getBaseWebpackConfig( babel: useSWCLoader ? swcDefaultLoader : babelLoader!, } + const nextFlightLoader = { + loader: 'next-flight-loader', + options: { + isEdgeServer, + }, + } + const appServerLayerLoaders = hasAppDir ? [ // When using Babel, we will have to add the SWC loader @@ -530,7 +537,7 @@ export default async function getBaseWebpackConfig( : [] const instrumentLayerLoaders = [ - 'next-flight-loader', + nextFlightLoader, // When using Babel, we will have to add the SWC loader // as an additional pass to handle RSC correctly. // This will cause some performance overhead but @@ -540,7 +547,7 @@ export default async function getBaseWebpackConfig( ].filter(Boolean) const middlewareLayerLoaders = [ - 'next-flight-loader', + nextFlightLoader, // When using Babel, we will have to use SWC to do the optimization // for middleware to tree shake the unused default optimized imports like "next/server". // This will cause some performance overhead but @@ -1352,9 +1359,7 @@ export default async function getBaseWebpackConfig( isEdgeServer, }), }, - use: { - loader: 'next-flight-loader', - }, + use: nextFlightLoader, }, ] : []), diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts index 79a0ae48ef6ea..7792df2c3143b 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts @@ -54,6 +54,9 @@ export default function transformSource( throw new Error('Expected source to have been transformed to a string.') } + const options = this.getOptions() + const { isEdgeServer } = options + // Assign the RSC meta information to buildInfo. // Exclude next internal files which are not marked as client files const buildInfo = getModuleBuildInfo(this._module) @@ -97,21 +100,29 @@ export default function transformSource( return } + // `proxy` is the module proxy that we treat the module as a client boundary. + // For ESM, we access the property of the module proxy directly for each export. + // This is bit hacky that treating using a CJS like module proxy for ESM's exports, + // but this will avoid creating nested proxies for each export. It will be improved in the future. + + // Explanation for: await createProxy(...) + // We need to await the module proxy creation because it can be async module for SSR layer + // due to having async dependencies. + // We only apply `the await` for Node.js as only Edge doesn't have external dependencies. let esmSource = `\ import { createProxy } from "${MODULE_PROXY_PATH}" + +const proxy = ${isEdgeServer ? '' : 'await'} createProxy(String.raw\`${resourceKey}\`) ` let cnt = 0 for (const ref of clientRefs) { if (ref === '') { - esmSource += `\nexports[''] = createProxy(String.raw\`${resourceKey}#\`);` + esmSource += `exports[''] = proxy['']\n` } else if (ref === 'default') { - esmSource += `\ -export default createProxy(String.raw\`${resourceKey}#default\`); -` + esmSource += `export default proxy.default;\n` } else { - esmSource += ` -const e${cnt} = createProxy(String.raw\`${resourceKey}#${ref}\`); -export { e${cnt++} as ${ref} };` + esmSource += `const e${cnt} = proxy["${ref}"];\n` + esmSource += `export { e${cnt++} as ${ref} };\n` } } diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index 1081f78c4b9bd..3e6f0b6a746a8 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -262,7 +262,7 @@ async function createComponentTreeInternal({ } const LayoutOrPage: React.ComponentType | undefined = layoutOrPageMod - ? await interopDefault(layoutOrPageMod) + ? interopDefault(layoutOrPageMod) : undefined /** diff --git a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx index 31b6e459e93d8..5950b1d82ce2f 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx @@ -15,7 +15,7 @@ function convertModule

( // Cases: // mod: { default: Component } // mod: Component - // mod: { $$typeof, default: proxy(Component) } + // mod: { default: proxy(Component) } // mod: proxy(Component) const hasDefault = mod && 'default' in mod return { diff --git a/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts b/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts index a98b19ff6eb37..b0a5079ac0a64 100644 --- a/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts +++ b/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts @@ -1,16 +1,14 @@ -import { FileRef, nextTestSetup } from 'e2e-utils' -import path from 'path' +import { nextTestSetup } from 'e2e-utils' describe('referencing a client component in an app route', () => { const { next } = nextTestSetup({ - files: new FileRef(path.join(__dirname)), + files: __dirname, }) it('responds without error', async () => { expect(JSON.parse(await next.render('/runtime'))).toEqual({ - // Turbopack's proxy components are functions - clientComponent: process.env.TURBOPACK ? 'function' : 'object', - myModuleClientComponent: process.env.TURBOPACK ? 'function' : 'object', + clientComponent: 'function', + myModuleClientComponent: 'function', }) }) }) diff --git a/test/e2e/app-dir/dynamic/app/dynamic/named-export/page.js b/test/e2e/app-dir/dynamic/app/dynamic/named-export/page.js index b767273ab6077..3285b72c5f559 100644 --- a/test/e2e/app-dir/dynamic/app/dynamic/named-export/page.js +++ b/test/e2e/app-dir/dynamic/app/dynamic/named-export/page.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic' const Button = dynamic(() => import('./client').then((mod) => { - return mod.Button + return { default: mod.Button } }) )