Skip to content

Commit de84e3a

Browse files
committed
Fix: resolve mixed re-exports module as cjs (#64681)
### Why If you have a client entry that mixing `default` re-export and `*` re-export, atm we cannot statically analyze all the exports from this the boundary, unless we can apply barrel file optimization for every import which could slow down speed. ```js // index.js 'use client' export * from './client' export { default } from './client' ``` Before that happen we high recommend you don't mixing that and try to add the client directive to the leaf level client module. We're not able to determine what the identifiers are imported from the wildcard import path. This would work if we resolved the actual file but currently we can't. ### What When we found the mixing client entry module like that, we treat it as a CJS client module and include all the bundle in client like before what we have the client components import optimization. Ideally we could warn users don't apply the client directive to these kinda of barrel file, and only apply them to where we needed. Fixes #64518 Closes NEXT-3119
1 parent c850e4a commit de84e3a

File tree

7 files changed

+123
-41
lines changed

7 files changed

+123
-41
lines changed

packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export default function transformSource(
4848

4949
// When we cannot determine the export names, we use eager mode to include the whole module.
5050
// Otherwise, we use eager mode with webpackExports to only include the necessary exports.
51-
if (ids.length === 0) {
51+
// If we have '*' in the ids, we include all the imports
52+
if (ids.length === 0 || ids.includes('*')) {
5253
return `import(/* webpackMode: "eager" */ ${importPath});\n`
5354
} else {
5455
return `import(/* webpackMode: "eager", webpackExports: ${JSON.stringify(

packages/next/src/build/webpack/loaders/next-flight-loader/index.ts

+39-25
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { webpack } from 'next/dist/compiled/webpack/webpack'
12
import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants'
23
import {
34
BARREL_OPTIMIZATION_PREFIX,
@@ -13,6 +14,36 @@ const noopHeadPath = require.resolve('next/dist/client/components/noop-head')
1314
const MODULE_PROXY_PATH =
1415
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy'
1516

17+
type SourceType = 'auto' | 'commonjs' | 'module'
18+
export function getAssumedSourceType(
19+
mod: webpack.Module,
20+
sourceType: SourceType
21+
): SourceType {
22+
const buildInfo = getModuleBuildInfo(mod)
23+
const detectedClientEntryType = buildInfo?.rsc?.clientEntryType
24+
const clientRefs = buildInfo?.rsc?.clientRefs || []
25+
26+
// It's tricky to detect the type of a client boundary, but we should always
27+
// use the `module` type when we can, to support `export *` and `export from`
28+
// syntax in other modules that import this client boundary.
29+
let assumedSourceType = sourceType
30+
if (assumedSourceType === 'auto' && detectedClientEntryType === 'auto') {
31+
if (
32+
clientRefs.length === 0 ||
33+
(clientRefs.length === 1 && clientRefs[0] === '')
34+
) {
35+
// If there's zero export detected in the client boundary, and it's the
36+
// `auto` type, we can safely assume it's a CJS module because it doesn't
37+
// have ESM exports.
38+
assumedSourceType = 'commonjs'
39+
} else if (!clientRefs.includes('*')) {
40+
// Otherwise, we assume it's an ESM module.
41+
assumedSourceType = 'module'
42+
}
43+
}
44+
return assumedSourceType
45+
}
46+
1647
export default function transformSource(
1748
this: any,
1849
source: string,
@@ -50,29 +81,12 @@ export default function transformSource(
5081

5182
// A client boundary.
5283
if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
53-
const sourceType = this._module?.parser?.sourceType
54-
const detectedClientEntryType = buildInfo.rsc.clientEntryType
84+
const assumedSourceType = getAssumedSourceType(
85+
this._module,
86+
this._module?.parser?.sourceType
87+
)
5588
const clientRefs = buildInfo.rsc.clientRefs!
5689

57-
// It's tricky to detect the type of a client boundary, but we should always
58-
// use the `module` type when we can, to support `export *` and `export from`
59-
// syntax in other modules that import this client boundary.
60-
let assumedSourceType = sourceType
61-
if (assumedSourceType === 'auto' && detectedClientEntryType === 'auto') {
62-
if (
63-
clientRefs.length === 0 ||
64-
(clientRefs.length === 1 && clientRefs[0] === '')
65-
) {
66-
// If there's zero export detected in the client boundary, and it's the
67-
// `auto` type, we can safely assume it's a CJS module because it doesn't
68-
// have ESM exports.
69-
assumedSourceType = 'commonjs'
70-
} else if (!clientRefs.includes('*')) {
71-
// Otherwise, we assume it's an ESM module.
72-
assumedSourceType = 'module'
73-
}
74-
}
75-
7690
if (assumedSourceType === 'module') {
7791
if (clientRefs.includes('*')) {
7892
this.callback(
@@ -123,9 +137,9 @@ export { e${cnt++} as ${ref} };`
123137
}
124138
}
125139

126-
this.callback(
127-
null,
128-
source.replace(RSC_MOD_REF_PROXY_ALIAS, MODULE_PROXY_PATH),
129-
sourceMap
140+
const replacedSource = source.replace(
141+
RSC_MOD_REF_PROXY_ALIAS,
142+
MODULE_PROXY_PATH
130143
)
144+
this.callback(null, replacedSource, sourceMap)
131145
}

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

+48-15
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { getProxiedPluginState } from '../../build-context'
4242
import { PAGE_TYPES } from '../../../lib/page-types'
4343
import { isWebpackServerOnlyLayer } from '../../utils'
4444
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
45+
import { getAssumedSourceType } from '../loaders/next-flight-loader'
4546

4647
interface Options {
4748
dev: boolean
@@ -665,18 +666,12 @@ export class FlightClientEntryPlugin {
665666
if (!modRequest) return
666667
if (visited.has(modRequest)) {
667668
if (clientComponentImports[modRequest]) {
668-
const isCjsModule =
669-
getModuleBuildInfo(mod).rsc?.clientEntryType === 'cjs'
670-
for (const name of importedIdentifiers) {
671-
// For cjs module default import, we include the whole module since
672-
const isCjsDefaultImport = isCjsModule && name === 'default'
673-
// Always include __esModule along with cjs module default export,
674-
// to make sure it work with client module proxy from React.
675-
if (isCjsDefaultImport) {
676-
clientComponentImports[modRequest].add('__esModule')
677-
}
678-
clientComponentImports[modRequest].add(name)
679-
}
669+
addClientImport(
670+
mod,
671+
modRequest,
672+
clientComponentImports,
673+
importedIdentifiers
674+
)
680675
}
681676
return
682677
}
@@ -708,9 +703,13 @@ export class FlightClientEntryPlugin {
708703
if (!clientComponentImports[modRequest]) {
709704
clientComponentImports[modRequest] = new Set()
710705
}
711-
for (const name of importedIdentifiers) {
712-
clientComponentImports[modRequest].add(name)
713-
}
706+
addClientImport(
707+
mod,
708+
modRequest,
709+
clientComponentImports,
710+
importedIdentifiers
711+
)
712+
714713
return
715714
}
716715

@@ -1026,3 +1025,37 @@ export class FlightClientEntryPlugin {
10261025
new sources.RawSource(json) as unknown as webpack.sources.RawSource
10271026
}
10281027
}
1028+
1029+
function addClientImport(
1030+
mod: webpack.NormalModule,
1031+
modRequest: string,
1032+
clientComponentImports: ClientComponentImports,
1033+
importedIdentifiers: string[]
1034+
) {
1035+
const clientEntryType = getModuleBuildInfo(mod).rsc?.clientEntryType
1036+
const isCjsModule = clientEntryType === 'cjs'
1037+
const assumedSourceType = getAssumedSourceType(
1038+
mod,
1039+
isCjsModule ? 'commonjs' : 'auto'
1040+
)
1041+
1042+
const isAutoModuleSourceType = assumedSourceType === 'auto'
1043+
if (isAutoModuleSourceType) {
1044+
clientComponentImports[modRequest] = new Set(['*'])
1045+
} 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+
}
1057+
1058+
clientComponentImports[modRequest].add(name)
1059+
}
1060+
}
1061+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
export default function ClientModExportDefault() {
4+
return 'client:mod-export-default'
5+
}
6+
7+
export function ClientModExportA() {
8+
return 'client:mod-export-a'
9+
}
10+
11+
export function ClientModExportB() {
12+
return 'client:mod-export-b'
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
'use client'
2+
3+
export { default } from './client'
4+
export * from './client'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ClientDefault from './client-module'
2+
3+
export default function Page() {
4+
return (
5+
<div>
6+
<p>
7+
<ClientDefault />
8+
</p>
9+
</div>
10+
)
11+
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,11 @@ createNextDescribe(
9797
chunkContents.every((content) => content.includes('cjs-client:foo'))
9898
).toBe(false)
9999
})
100+
101+
it('should able to resolve the client module entry with mixing rexports', async () => {
102+
const $ = await next.render$('/client-reexport-index')
103+
104+
expect($('p').text()).toContain('client:mod-export-default')
105+
})
100106
}
101107
)

0 commit comments

Comments
 (0)