Skip to content

Commit 4e8e191

Browse files
authored
RSC: handle commonjs in flight loader (#35563)
We need to handle cjs cases for client/server components when they're compiled to commonjs in some cases. e.g. if there's an internal `_app.server.js` in nextjs, the assets in the dist files are compiled to cjs by swc. Or any 3rd party libraries are consumed could be cjs only. ### How it works * Detect the source file is ESM or CJS first by detect if there's any ESM import/export * Append the new exports or collect exports info based on the module type
1 parent 79a8651 commit 4e8e191

7 files changed

Lines changed: 92 additions & 30 deletions

File tree

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { parse } from '../../swc'
9+
import { buildExports } from './utils'
910

1011
function addExportNames(names: string[], node: any) {
1112
switch (node.type) {
@@ -39,7 +40,7 @@ function addExportNames(names: string[], node: any) {
3940
}
4041
}
4142

42-
async function parseExportNamesInto(
43+
async function parseModuleInfo(
4344
resourcePath: string,
4445
transformedSource: string,
4546
names: Array<string>
@@ -56,7 +57,7 @@ async function parseExportNamesInto(
5657
case 'ExportDefaultExpression':
5758
case 'ExportDefaultDeclaration':
5859
names.push('default')
59-
continue
60+
break
6061
case 'ExportNamedDeclaration':
6162
if (node.declaration) {
6263
if (node.declaration.type === 'VariableDeclaration') {
@@ -74,12 +75,26 @@ async function parseExportNamesInto(
7475
addExportNames(names, specificers[j].exported)
7576
}
7677
}
77-
continue
78+
break
7879
case 'ExportDeclaration':
7980
if (node.declaration?.identifier) {
8081
addExportNames(names, node.declaration.identifier)
8182
}
82-
continue
83+
break
84+
case 'ExpressionStatement': {
85+
const {
86+
expression: { left },
87+
} = node
88+
// exports.xxx = xxx
89+
if (
90+
left.type === 'MemberExpression' &&
91+
left?.object.type === 'Identifier' &&
92+
left.object?.value === 'exports'
93+
) {
94+
addExportNames(names, left.property)
95+
}
96+
break
97+
}
8398
default:
8499
break
85100
}
@@ -98,28 +113,28 @@ export default async function transformSource(
98113
}
99114

100115
const names: string[] = []
101-
await parseExportNamesInto(resourcePath, transformedSource, names)
116+
await parseModuleInfo(resourcePath, transformedSource, names)
102117

103118
// next.js/packages/next/<component>.js
104119
if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
105120
names.push('default')
106121
}
107122

108-
let newSrc =
123+
const moduleRefDef =
109124
"const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"
110-
for (let i = 0; i < names.length; i++) {
111-
const name = names[i]
112-
if (name === 'default') {
113-
newSrc += 'export default '
114-
} else {
115-
newSrc += 'export const ' + name + ' = '
116-
}
117-
newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '
118-
newSrc += JSON.stringify(resourcePath)
119-
newSrc += ', name: '
120-
newSrc += JSON.stringify(name)
121-
newSrc += '};\n'
122-
}
123125

124-
return newSrc
126+
const clientRefsExports = names.reduce((res: any, name) => {
127+
const moduleRef =
128+
'{ $$typeof: MODULE_REFERENCE, filepath: ' +
129+
JSON.stringify(resourcePath) +
130+
', name: ' +
131+
JSON.stringify(name) +
132+
' };\n'
133+
res[name] = moduleRef
134+
return res
135+
}, {})
136+
137+
// still generate module references in ESM
138+
const output = moduleRefDef + buildExports(clientRefsExports, true)
139+
return output
125140
}

packages/next/build/webpack/loaders/next-flight-server-loader.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parse } from '../../swc'
22
import { getRawPageExtensions } from '../../utils'
3+
import { buildExports, isEsmNodeType } from './utils'
34

45
const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']
56

@@ -24,7 +25,7 @@ const createServerComponentFilter = (pageExtensions: string[]) => {
2425
return (importSource: string) => regex.test(importSource)
2526
}
2627

27-
async function parseImportsInfo({
28+
async function parseModuleInfo({
2829
resourcePath,
2930
source,
3031
isClientCompilation,
@@ -39,16 +40,18 @@ async function parseImportsInfo({
3940
}): Promise<{
4041
source: string
4142
imports: string
43+
isEsm: boolean
4244
}> {
4345
const ast = await parse(source, { filename: resourcePath, isModule: true })
4446
const { body } = ast
45-
4647
let transformedSource = ''
4748
let lastIndex = 0
4849
let imports = ''
50+
let isEsm = false
4951

5052
for (let i = 0; i < body.length; i++) {
5153
const node = body[i]
54+
isEsm = isEsm || isEsmNodeType(node.type)
5255
switch (node.type) {
5356
case 'ImportDeclaration': {
5457
const importSource = node.source.value
@@ -117,7 +120,7 @@ async function parseImportsInfo({
117120
transformedSource += source.substring(lastIndex)
118121
}
119122

120-
return { source: transformedSource, imports }
123+
return { source: transformedSource, imports, isEsm }
121124
}
122125

123126
export default async function transformSource(
@@ -152,7 +155,11 @@ export default async function transformSource(
152155
}
153156
}
154157

155-
const { source: transformedSource, imports } = await parseImportsInfo({
158+
const {
159+
source: transformedSource,
160+
imports,
161+
isEsm,
162+
} = await parseModuleInfo({
156163
resourcePath,
157164
source,
158165
isClientCompilation,
@@ -172,14 +179,17 @@ export default async function transformSource(
172179
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
173180
*/
174181

175-
let rscExports = `export const __next_rsc__={
176-
__webpack_require__,
177-
_: () => {${imports}}
178-
}`
182+
const rscExports: any = {
183+
__next_rsc__: `{
184+
__webpack_require__,
185+
_: () => {\n${imports}\n}
186+
}`,
187+
}
179188

180189
if (isClientCompilation) {
181-
rscExports += '\nexport default function RSC () {}'
190+
rscExports['default'] = 'function RSC() {}'
182191
}
183192

184-
return transformedSource + '\n' + rscExports
193+
const output = transformedSource + '\n' + buildExports(rscExports, isEsm)
194+
return output
185195
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function buildExports(moduleExports: any, isESM: boolean) {
2+
let ret = ''
3+
Object.keys(moduleExports).forEach((key) => {
4+
const exportExpression = isESM
5+
? `export ${key === 'default' ? key : `const ${key} =`} ${
6+
moduleExports[key]
7+
}`
8+
: `exports.${key} = ${moduleExports[key]}`
9+
10+
ret += exportExpression + '\n'
11+
})
12+
return ret
13+
}
14+
15+
const esmNodeTypes = [
16+
'ImportDeclaration',
17+
'ExportNamedDeclaration',
18+
'ExportDefaultExpression',
19+
'ExportDefaultDeclaration',
20+
]
21+
export const isEsmNodeType = (type: string) => esmNodeTypes.includes(type)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports.Cjs = function Cjs() {
2+
return 'cjs-client'
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports.Cjs = function Cjs() {
2+
return 'cjs-shared'
3+
}

test/integration/react-streaming-and-server-components/app/pages/various-exports.server.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { a, b, c, d, e } from '../components/shared-exports'
44
import DefaultArrow, {
55
Named as ClientNamed,
66
} from '../components/client-exports.client'
7+
import { Cjs as CjsShared } from '../components/cjs'
8+
import { Cjs as CjsClient } from '../components/cjs.client'
79

810
export default function Page() {
911
return (
@@ -21,6 +23,12 @@ export default function Page() {
2123
<div>
2224
<ClientNamed />
2325
</div>
26+
<di>
27+
<CjsShared />
28+
</di>
29+
<di>
30+
<CjsClient />
31+
</di>
2432
</div>
2533
)
2634
}

test/integration/react-streaming-and-server-components/test/rsc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export default function (context, { runtime, env }) {
206206
expect(hydratedContent).toContain('abcde')
207207
expect(hydratedContent).toContain('default-export-arrow.client')
208208
expect(hydratedContent).toContain('named.client')
209+
expect(hydratedContent).toContain('cjs-shared')
210+
expect(hydratedContent).toContain('cjs-client')
209211
})
210212

211213
it('should handle 404 requests and missing routes correctly', async () => {

0 commit comments

Comments
 (0)