Files
Jiachi Liu 8d01d496fe fix: mixing namespace import and named import client components (#64809)
### 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
2024-04-23 17:32:21 +02:00

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')
})
}
)