Skip to content

Commit 8d01d49

Browse files
committed
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
1 parent de84e3a commit 8d01d49

File tree

9 files changed

+131
-22
lines changed

9 files changed

+131
-22
lines changed

packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,8 @@ export class FlightClientEntryPlugin {
670670
mod,
671671
modRequest,
672672
clientComponentImports,
673-
importedIdentifiers
673+
importedIdentifiers,
674+
false
674675
)
675676
}
676677
return
@@ -707,19 +708,27 @@ export class FlightClientEntryPlugin {
707708
mod,
708709
modRequest,
709710
clientComponentImports,
710-
importedIdentifiers
711+
importedIdentifiers,
712+
true
711713
)
712714

713715
return
714716
}
715717

716718
getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach(
717719
(connection: any) => {
718-
const dependencyIds: string[] = []
719-
if (connection.dependency?.ids?.length) {
720+
let dependencyIds: string[] = []
721+
const depModule = connection.resolvedModule
722+
723+
// `ids` are the identifiers that are imported from the dependency,
724+
// if it's present, it's an array of strings.
725+
if (connection.dependency?.ids) {
720726
dependencyIds.push(...connection.dependency.ids)
727+
} else {
728+
dependencyIds = ['*']
721729
}
722-
filterClientComponents(connection.resolvedModule, dependencyIds)
730+
731+
filterClientComponents(depModule, dependencyIds)
723732
}
724733
)
725734
}
@@ -1030,7 +1039,8 @@ function addClientImport(
10301039
mod: webpack.NormalModule,
10311040
modRequest: string,
10321041
clientComponentImports: ClientComponentImports,
1033-
importedIdentifiers: string[]
1042+
importedIdentifiers: string[],
1043+
isFirstImport: boolean
10341044
) {
10351045
const clientEntryType = getModuleBuildInfo(mod).rsc?.clientEntryType
10361046
const isCjsModule = clientEntryType === 'cjs'
@@ -1039,23 +1049,34 @@ function addClientImport(
10391049
isCjsModule ? 'commonjs' : 'auto'
10401050
)
10411051

1042-
const isAutoModuleSourceType = assumedSourceType === 'auto'
1043-
if (isAutoModuleSourceType) {
1044-
clientComponentImports[modRequest] = new Set(['*'])
1052+
const clientImportsSet = clientComponentImports[modRequest]
1053+
1054+
if (importedIdentifiers[0] === '*') {
1055+
// If there's collected import path with named import identifiers,
1056+
// or there's nothing in collected imports are empty.
1057+
// we should include the whole module.
1058+
if (!isFirstImport && [...clientImportsSet][0] !== '*') {
1059+
clientComponentImports[modRequest] = new Set(['*'])
1060+
}
10451061
} else {
1046-
// If it's not analyzed as named ESM exports, e.g. if it's mixing `export *` with named exports,
1047-
// We'll include all modules since it's not able to do tree-shaking.
1048-
for (const name of importedIdentifiers) {
1049-
// For cjs module default import, we include the whole module since
1050-
const isCjsDefaultImport = isCjsModule && name === 'default'
1051-
1052-
// Always include __esModule along with cjs module default export,
1053-
// to make sure it work with client module proxy from React.
1054-
if (isCjsDefaultImport) {
1055-
clientComponentImports[modRequest].add('__esModule')
1056-
}
1062+
const isAutoModuleSourceType = assumedSourceType === 'auto'
1063+
if (isAutoModuleSourceType) {
1064+
clientComponentImports[modRequest] = new Set(['*'])
1065+
} else {
1066+
// If it's not analyzed as named ESM exports, e.g. if it's mixing `export *` with named exports,
1067+
// We'll include all modules since it's not able to do tree-shaking.
1068+
for (const name of importedIdentifiers) {
1069+
// For cjs module default import, we include the whole module since
1070+
const isCjsDefaultImport = isCjsModule && name === 'default'
1071+
1072+
// Always include __esModule along with cjs module default export,
1073+
// to make sure it work with client module proxy from React.
1074+
if (isCjsDefaultImport) {
1075+
clientComponentImports[modRequest].add('__esModule')
1076+
}
10571077

1058-
clientComponentImports[modRequest].add(name)
1078+
clientComponentImports[modRequest].add(name)
1079+
}
10591080
}
10601081
}
10611082
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
export function ClientModExportA() {
4+
return 'client-mod:export-a'
5+
}
6+
7+
export function ClientModExportB() {
8+
return 'client-mod:export-b'
9+
}
10+
11+
export function ClientModExportC() {
12+
return 'client-mod:export-c'
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
export {
4+
ClientModExportA,
5+
ClientModExportB,
6+
ClientModExportC,
7+
} from './client-module'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client'
2+
3+
export function ClientMod2ExportA() {
4+
return 'client-mod2:export-a'
5+
}
6+
7+
export function ClientMod2ExportB() {
8+
return 'client-mod2:export-b'
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use client'
2+
3+
export { ClientMod2ExportA, ClientMod2ExportB } from './client-module'
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as clientMod from './client-module'
2+
import { ClientModExportC } from './client-module'
3+
import * as clientMod2 from './client-module2'
4+
5+
const mod1Map = {
6+
...clientMod,
7+
}
8+
9+
const mod2Map = {
10+
...clientMod2,
11+
}
12+
13+
const A = mod1Map.ClientModExportA
14+
const B = mod1Map.ClientModExportB
15+
const C = mod1Map.ClientModExportC
16+
const A2 = mod2Map.ClientMod2ExportA
17+
const B2 = mod2Map.ClientMod2ExportB
18+
19+
export default function Page() {
20+
return (
21+
<div>
22+
<p id="a">
23+
<A />
24+
</p>
25+
<p id="b">
26+
<B />
27+
</p>
28+
<p id="c">
29+
<C />
30+
</p>
31+
<p id="named-c">
32+
<ClientModExportC />
33+
</p>
34+
<p id="a2">
35+
<A2 />
36+
</p>
37+
<p id="b2">
38+
<B2 />
39+
</p>
40+
</div>
41+
)
42+
}

test/production/app-dir/client-components-tree-shaking/app/relative-dep/page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ImportedComp } from './client'
1+
import { ImportedComp } from './client-relative-dep'
22

33
export default function Page() {
44
return <ImportedComp />

test/production/app-dir/client-components-tree-shaking/index.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,19 @@ createNextDescribe(
103103

104104
expect($('p').text()).toContain('client:mod-export-default')
105105
})
106+
107+
it('should handle mixing namespace imports and named imports from client components', async () => {
108+
const $ = await next.render$('/client-import-namespace')
109+
110+
// mixing namespace imports and named imports
111+
expect($('#a').text()).toContain('client-mod:export-a')
112+
expect($('#b').text()).toContain('client-mod:export-b')
113+
expect($('#c').text()).toContain('client-mod:export-c')
114+
expect($('#named-c').text()).toContain('client-mod:export-c')
115+
116+
// only named exports
117+
expect($('#a2').text()).toContain('client-mod2:export-a')
118+
expect($('#b2').text()).toContain('client-mod2:export-b')
119+
})
106120
}
107121
)

0 commit comments

Comments
 (0)