Fix esm property def in flight loader (#66990)

This commit is contained in:
Jiachi Liu 2024-06-19 18:26:43 +02:00 committed by GitHub
parent 50b9966ba9
commit 0c6dac466a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 35 additions and 21 deletions

View file

@ -517,6 +517,13 @@ export default async function getBaseWebpackConfig(
babel: useSWCLoader ? swcDefaultLoader : babelLoader!, babel: useSWCLoader ? swcDefaultLoader : babelLoader!,
} }
const nextFlightLoader = {
loader: 'next-flight-loader',
options: {
isEdgeServer,
},
}
const appServerLayerLoaders = hasAppDir const appServerLayerLoaders = hasAppDir
? [ ? [
// When using Babel, we will have to add the SWC loader // When using Babel, we will have to add the SWC loader
@ -530,7 +537,7 @@ export default async function getBaseWebpackConfig(
: [] : []
const instrumentLayerLoaders = [ const instrumentLayerLoaders = [
'next-flight-loader', nextFlightLoader,
// When using Babel, we will have to add the SWC loader // When using Babel, we will have to add the SWC loader
// as an additional pass to handle RSC correctly. // as an additional pass to handle RSC correctly.
// This will cause some performance overhead but // This will cause some performance overhead but
@ -540,7 +547,7 @@ export default async function getBaseWebpackConfig(
].filter(Boolean) ].filter(Boolean)
const middlewareLayerLoaders = [ const middlewareLayerLoaders = [
'next-flight-loader', nextFlightLoader,
// When using Babel, we will have to use SWC to do the optimization // 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". // for middleware to tree shake the unused default optimized imports like "next/server".
// This will cause some performance overhead but // This will cause some performance overhead but
@ -1352,9 +1359,7 @@ export default async function getBaseWebpackConfig(
isEdgeServer, isEdgeServer,
}), }),
}, },
use: { use: nextFlightLoader,
loader: 'next-flight-loader',
},
}, },
] ]
: []), : []),

View file

@ -54,6 +54,9 @@ export default function transformSource(
throw new Error('Expected source to have been transformed to a string.') 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. // Assign the RSC meta information to buildInfo.
// Exclude next internal files which are not marked as client files // Exclude next internal files which are not marked as client files
const buildInfo = getModuleBuildInfo(this._module) const buildInfo = getModuleBuildInfo(this._module)
@ -97,21 +100,29 @@ export default function transformSource(
return 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 = `\ let esmSource = `\
import { createProxy } from "${MODULE_PROXY_PATH}" import { createProxy } from "${MODULE_PROXY_PATH}"
const proxy = ${isEdgeServer ? '' : 'await'} createProxy(String.raw\`${resourceKey}\`)
` `
let cnt = 0 let cnt = 0
for (const ref of clientRefs) { for (const ref of clientRefs) {
if (ref === '') { if (ref === '') {
esmSource += `\nexports[''] = createProxy(String.raw\`${resourceKey}#\`);` esmSource += `exports[''] = proxy['']\n`
} else if (ref === 'default') { } else if (ref === 'default') {
esmSource += `\ esmSource += `export default proxy.default;\n`
export default createProxy(String.raw\`${resourceKey}#default\`);
`
} else { } else {
esmSource += ` esmSource += `const e${cnt} = proxy["${ref}"];\n`
const e${cnt} = createProxy(String.raw\`${resourceKey}#${ref}\`); esmSource += `export { e${cnt++} as ${ref} };\n`
export { e${cnt++} as ${ref} };`
} }
} }

View file

@ -262,7 +262,7 @@ async function createComponentTreeInternal({
} }
const LayoutOrPage: React.ComponentType<any> | undefined = layoutOrPageMod const LayoutOrPage: React.ComponentType<any> | undefined = layoutOrPageMod
? await interopDefault(layoutOrPageMod) ? interopDefault(layoutOrPageMod)
: undefined : undefined
/** /**

View file

@ -15,7 +15,7 @@ function convertModule<P>(
// Cases: // Cases:
// mod: { default: Component } // mod: { default: Component }
// mod: Component // mod: Component
// mod: { $$typeof, default: proxy(Component) } // mod: { default: proxy(Component) }
// mod: proxy(Component) // mod: proxy(Component)
const hasDefault = mod && 'default' in mod const hasDefault = mod && 'default' in mod
return { return {

View file

@ -1,16 +1,14 @@
import { FileRef, nextTestSetup } from 'e2e-utils' import { nextTestSetup } from 'e2e-utils'
import path from 'path'
describe('referencing a client component in an app route', () => { describe('referencing a client component in an app route', () => {
const { next } = nextTestSetup({ const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname)), files: __dirname,
}) })
it('responds without error', async () => { it('responds without error', async () => {
expect(JSON.parse(await next.render('/runtime'))).toEqual({ expect(JSON.parse(await next.render('/runtime'))).toEqual({
// Turbopack's proxy components are functions clientComponent: 'function',
clientComponent: process.env.TURBOPACK ? 'function' : 'object', myModuleClientComponent: 'function',
myModuleClientComponent: process.env.TURBOPACK ? 'function' : 'object',
}) })
}) })
}) })

View file

@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'
const Button = dynamic(() => const Button = dynamic(() =>
import('./client').then((mod) => { import('./client').then((mod) => {
return mod.Button return { default: mod.Button }
}) })
) )