mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What Reported by @MaxLeiter, when you mixing named import and wildcard import to a client component, and if you clone the module it will missed others exports except the named ones. This lead to an issue that rendered React element type is `undefined`. ### Why We're using a tree-shaking strategy that collects the imported identifiers from client components on server components side. But in our code `connection.dependency.ids` can be undefined when you're using `import *`. So for that case we include all the exports. In the flight client entry plugin, if we found there's named imports that collected later, and the module is already being marked as namespace import `*`, we merge the ids into "*", so the whole module and all exports are respected. Now there're few possible cases for a client component import: During webpack build, in the outout going connections, there're connection with empty imported ids (`[]`), cannot unable to detect the imported ids (`['*']`) and detected named imported ids (`['a', 'b', ..]`). First two represnt to include the whole module and all exports, but we might collect the named imports could come later than the whole module. So if we found the existing collection already has `['*']` then we keep using that regardless the collected named imports. This can avoid the collected named imports cover "exports all" case, where we will expose less exports for that collection module lead to the undefined component error. Closes NEXT-3177
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import fs from 'fs'
|
|
import { createNextDescribe } from 'e2e-utils'
|
|
import { join } from 'path'
|
|
|
|
createNextDescribe(
|
|
'app-dir client-components-tree-shaking',
|
|
{
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
},
|
|
({ next }) => {
|
|
it('should only include imported relative components in browser bundle with direct imports', async () => {
|
|
const clientChunksDir = join(
|
|
next.testDir,
|
|
'.next',
|
|
'static',
|
|
'chunks',
|
|
'app',
|
|
'relative-dep'
|
|
)
|
|
const staticChunksDirents = fs.readdirSync(clientChunksDir, {
|
|
withFileTypes: true,
|
|
})
|
|
const chunkContents = staticChunksDirents
|
|
.filter((dirent) => dirent.isFile())
|
|
.map((chunkDirent) =>
|
|
fs.readFileSync(join(chunkDirent.path, chunkDirent.name), 'utf8')
|
|
)
|
|
expect(
|
|
chunkContents.some((content) =>
|
|
content.includes('client-comp-imported')
|
|
)
|
|
).toBe(true)
|
|
expect(
|
|
chunkContents.every((content) => content.includes('client-comp-unused'))
|
|
).toBe(false)
|
|
expect(
|
|
chunkContents.every((content) =>
|
|
content.includes('client-comp-default')
|
|
)
|
|
).toBe(false)
|
|
})
|
|
|
|
it('should only include imported components 3rd party package in browser bundle with direct imports', async () => {
|
|
const clientChunksDir = join(
|
|
next.testDir,
|
|
'.next',
|
|
'static',
|
|
'chunks',
|
|
'app',
|
|
'third-party-dep'
|
|
)
|
|
const staticChunksDirents = fs.readdirSync(clientChunksDir, {
|
|
withFileTypes: true,
|
|
})
|
|
const chunkContents = staticChunksDirents
|
|
.filter((dirent) => dirent.isFile())
|
|
.map((chunkDirent) =>
|
|
fs.readFileSync(join(chunkDirent.path, chunkDirent.name), 'utf8')
|
|
)
|
|
expect(
|
|
chunkContents.some((content) => content.includes('client-dep-bar:esm'))
|
|
).toBe(true)
|
|
expect(
|
|
chunkContents.every((content) => content.includes('client-dep-foo:esm'))
|
|
).toBe(false)
|
|
expect(
|
|
chunkContents.every((content) =>
|
|
content.includes('client-dep-default:esm')
|
|
)
|
|
).toBe(false)
|
|
})
|
|
|
|
it('should only include the imported identifier of CJS module in browser bundle', async () => {
|
|
const clientChunksDir = join(
|
|
next.testDir,
|
|
'.next',
|
|
'static',
|
|
'chunks',
|
|
'app',
|
|
'cjs-dep'
|
|
)
|
|
|
|
const chunkContents = fs
|
|
.readdirSync(clientChunksDir, {
|
|
withFileTypes: true,
|
|
})
|
|
.filter((dirent) => dirent.isFile())
|
|
.map((chunkDirent) =>
|
|
fs.readFileSync(join(chunkDirent.path, chunkDirent.name), 'utf8')
|
|
)
|
|
|
|
expect(
|
|
chunkContents.some((content) => content.includes('cjs-client:default'))
|
|
).toBe(true)
|
|
expect(
|
|
chunkContents.every((content) => content.includes('cjs-client:foo'))
|
|
).toBe(false)
|
|
})
|
|
|
|
it('should able to resolve the client module entry with mixing rexports', async () => {
|
|
const $ = await next.render$('/client-reexport-index')
|
|
|
|
expect($('p').text()).toContain('client:mod-export-default')
|
|
})
|
|
|
|
it('should handle mixing namespace imports and named imports from client components', async () => {
|
|
const $ = await next.render$('/client-import-namespace')
|
|
|
|
// mixing namespace imports and named imports
|
|
expect($('#a').text()).toContain('client-mod:export-a')
|
|
expect($('#b').text()).toContain('client-mod:export-b')
|
|
expect($('#c').text()).toContain('client-mod:export-c')
|
|
expect($('#named-c').text()).toContain('client-mod:export-c')
|
|
|
|
// only named exports
|
|
expect($('#a2').text()).toContain('client-mod2:export-a')
|
|
expect($('#b2').text()).toContain('client-mod2:export-b')
|
|
})
|
|
}
|
|
)
|