Skip to content

Commit 87e0b44

Browse files
authored
Fix mixed module swc compilation for app router (#58967)
1 parent cc42e43 commit 87e0b44

File tree

13 files changed

+131
-29
lines changed

13 files changed

+131
-29
lines changed

packages/next/src/build/handle-externals.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ export function isResourceInPackages(
2828
resource: string,
2929
packageNames?: string[],
3030
packageDirMapping?: Map<string, string>
31-
) {
32-
return packageNames?.some((p: string) =>
31+
): boolean {
32+
if (!packageNames) return false
33+
return packageNames.some((p: string) =>
3334
packageDirMapping && packageDirMapping.has(p)
3435
? resource.startsWith(packageDirMapping.get(p)! + path.sep)
3536
: resource.includes(

packages/next/src/build/swc/options.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,22 @@ function getStyledComponentsOptions(
209209
}
210210
}
211211

212+
/*
213+
Output module type
214+
215+
For app router where server components is enabled, we prefer to bundle es6 modules,
216+
Use output module es6 to make sure:
217+
- the esm module is present
218+
- if the module is mixed syntax, the esm + cjs code are both present
219+
220+
For pages router will remain untouched
221+
*/
222+
function getModuleOptions(
223+
esm: boolean | undefined = false
224+
): { module: { type: 'es6' } } | {} {
225+
return esm ? { module: { type: 'es6' } } : {}
226+
}
227+
212228
function getEmotionOptions(
213229
emotionConfig: undefined | boolean | EmotionConfig,
214230
development: boolean
@@ -319,6 +335,7 @@ export function getLoaderSWCOptions({
319335
relativeFilePathFromRoot,
320336
serverComponents,
321337
isReactServerLayer,
338+
esm,
322339
}: {
323340
filename: string
324341
development: boolean
@@ -338,6 +355,7 @@ export function getLoaderSWCOptions({
338355
supportedBrowsers: string[] | undefined
339356
swcCacheDir: string
340357
relativeFilePathFromRoot: string
358+
esm?: boolean
341359
serverComponents?: boolean
342360
isReactServerLayer?: boolean
343361
}) {
@@ -412,6 +430,7 @@ export function getLoaderSWCOptions({
412430
node: process.versions.node,
413431
},
414432
},
433+
...getModuleOptions(esm),
415434
}
416435
} else {
417436
const options = {
@@ -423,7 +442,7 @@ export function getLoaderSWCOptions({
423442
type: 'commonjs',
424443
},
425444
}
426-
: {}),
445+
: getModuleOptions(esm)),
427446
disableNextSsg: !isPageFile,
428447
isDevelopment: development,
429448
isServerCompiler: isServer,

packages/next/src/build/webpack-config-rules/resolve.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ export const edgeConditionNames = [
1212
]
1313

1414
const mainFieldsPerCompiler: Record<
15-
CompilerNameValues | 'app-router-server',
15+
CompilerNameValues | 'server-esm',
1616
string[]
1717
> = {
1818
// For default case, prefer CJS over ESM on server side. e.g. pages dir SSR
1919
[COMPILER_NAMES.server]: ['main', 'module'],
2020
[COMPILER_NAMES.client]: ['browser', 'module', 'main'],
2121
[COMPILER_NAMES.edgeServer]: edgeConditionNames,
22-
// For app router since everything is bundled, prefer ESM over CJS
23-
'app-router-server': ['module', 'main'],
22+
// For bundling-all strategy, prefer ESM over CJS
23+
'server-esm': ['module', 'main'],
2424
}
2525

2626
export function getMainField(
27-
pageType: 'app' | 'pages',
28-
compilerType: CompilerNameValues
27+
compilerType: CompilerNameValues,
28+
preferEsm: boolean
2929
) {
3030
if (compilerType === COMPILER_NAMES.edgeServer) {
3131
return edgeConditionNames
@@ -34,7 +34,7 @@ export function getMainField(
3434
}
3535

3636
// Prefer module fields over main fields for isomorphic packages on server layer
37-
return pageType === 'app'
38-
? mainFieldsPerCompiler['app-router-server']
37+
return preferEsm
38+
? mainFieldsPerCompiler['server-esm']
3939
: mainFieldsPerCompiler[COMPILER_NAMES.server]
4040
}

packages/next/src/build/webpack-config.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -437,17 +437,26 @@ export default async function getBaseWebpackConfig(
437437
}
438438
}
439439

440+
// RSC loaders, prefer ESM, set `esm` to true
440441
const swcServerLayerLoader = getSwcLoader({
441442
serverComponents: true,
442443
isReactServerLayer: true,
444+
esm: true,
443445
})
444446
const swcClientLayerLoader = getSwcLoader({
445447
serverComponents: true,
446448
isReactServerLayer: false,
449+
esm: true,
450+
})
451+
// Default swc loaders for pages doesn't prefer ESM.
452+
const swcDefaultLoader = getSwcLoader({
453+
serverComponents: true,
454+
isReactServerLayer: false,
455+
esm: false,
447456
})
448457

449458
const defaultLoaders = {
450-
babel: useSWCLoader ? swcClientLayerLoader : babelLoader!,
459+
babel: useSWCLoader ? swcDefaultLoader : babelLoader!,
451460
}
452461

453462
const swcLoaderForServerLayer = hasAppDir
@@ -621,7 +630,7 @@ export default async function getBaseWebpackConfig(
621630
}
622631
: undefined),
623632
// default main fields use pages dir ones, and customize app router ones in loaders.
624-
mainFields: getMainField('pages', compilerType),
633+
mainFields: getMainField(compilerType, false),
625634
...(isEdgeServer && {
626635
conditionNames: edgeConditionNames,
627636
}),
@@ -736,8 +745,13 @@ export default async function getBaseWebpackConfig(
736745
const shouldIncludeExternalDirs =
737746
config.experimental.externalDir || !!config.transpilePackages
738747

739-
function createLoaderRuleExclude(skipNodeModules: boolean) {
740-
return (excludePath: string) => {
748+
const codeCondition = {
749+
test: /\.(tsx|ts|js|cjs|mjs|jsx)$/,
750+
...(shouldIncludeExternalDirs
751+
? // Allowing importing TS/TSX files from outside of the root dir.
752+
{}
753+
: { include: [dir, ...babelIncludeRegexes] }),
754+
exclude: (excludePath: string) => {
741755
if (babelIncludeRegexes.some((r) => r.test(excludePath))) {
742756
return false
743757
}
@@ -748,17 +762,8 @@ export default async function getBaseWebpackConfig(
748762
)
749763
if (shouldBeBundled) return false
750764

751-
return skipNodeModules && excludePath.includes('node_modules')
752-
}
753-
}
754-
755-
const codeCondition = {
756-
test: /\.(tsx|ts|js|cjs|mjs|jsx)$/,
757-
...(shouldIncludeExternalDirs
758-
? // Allowing importing TS/TSX files from outside of the root dir.
759-
{}
760-
: { include: [dir, ...babelIncludeRegexes] }),
761-
exclude: createLoaderRuleExclude(true),
765+
return excludePath.includes('node_modules')
766+
},
762767
}
763768

764769
let webpackConfig: webpack.Configuration = {
@@ -1281,7 +1286,7 @@ export default async function getBaseWebpackConfig(
12811286
],
12821287
},
12831288
resolve: {
1284-
mainFields: getMainField('app', compilerType),
1289+
mainFields: getMainField(compilerType, true),
12851290
conditionNames: reactServerCondition,
12861291
// If missing the alias override here, the default alias will be used which aliases
12871292
// react to the direct file path, not the package name. In that case the condition
@@ -1416,15 +1421,15 @@ export default async function getBaseWebpackConfig(
14161421
issuerLayer: [WEBPACK_LAYERS.appPagesBrowser],
14171422
use: swcLoaderForClientLayer,
14181423
resolve: {
1419-
mainFields: getMainField('app', compilerType),
1424+
mainFields: getMainField(compilerType, true),
14201425
},
14211426
},
14221427
{
14231428
test: codeCondition.test,
14241429
issuerLayer: [WEBPACK_LAYERS.serverSideRendering],
14251430
use: swcLoaderForClientLayer,
14261431
resolve: {
1427-
mainFields: getMainField('app', compilerType),
1432+
mainFields: getMainField(compilerType, true),
14281433
},
14291434
},
14301435
]

packages/next/src/build/webpack/loaders/next-swc-loader.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface SWCLoaderOptions {
4444
swcCacheDir: string
4545
serverComponents?: boolean
4646
isReactServerLayer?: boolean
47+
esm?: boolean
4748
}
4849

4950
async function loaderTransform(
@@ -69,6 +70,7 @@ async function loaderTransform(
6970
swcCacheDir,
7071
serverComponents,
7172
isReactServerLayer,
73+
esm,
7274
} = loaderOptions
7375
const isPageFile = filename.startsWith(pagesDir)
7476
const relativeFilePathFromRoot = path.relative(rootDir, filename)
@@ -92,6 +94,7 @@ async function loaderTransform(
9294
relativeFilePathFromRoot,
9395
serverComponents,
9496
isReactServerLayer,
97+
esm,
9598
})
9699

97100
const programmaticOptions = {

test/e2e/app-dir/app-external/app-external.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,24 @@ createNextDescribe(
210210
})
211211
})
212212

213+
describe('mixed syntax external modules', () => {
214+
it('should handle mixed module with next/dynamic', async () => {
215+
const browser = await next.browser('/mixed/dynamic')
216+
expect(await browser.elementByCss('#component').text()).toContain(
217+
'mixed-syntax-esm'
218+
)
219+
})
220+
221+
it('should handle mixed module in server and client components', async () => {
222+
const $ = await next.render$('/mixed/import')
223+
expect(await $('#server').text()).toContain('server:mixed-syntax-esm')
224+
expect(await $('#client').text()).toContain('client:mixed-syntax-esm')
225+
expect(await $('#relative-mixed').text()).toContain(
226+
'relative-mixed-syntax-esm'
227+
)
228+
})
229+
})
230+
213231
it('should export client module references in esm', async () => {
214232
const html = await next.render('/esm-client-ref')
215233
expect(html).toContain('hello')
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client'
2+
3+
import dynamic from 'next/dynamic'
4+
5+
const Dynamic = dynamic(
6+
() => import('mixed-syntax-esm').then((mod) => mod.Component),
7+
{ ssr: false }
8+
)
9+
10+
export default function Page() {
11+
return <Dynamic />
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import { value } from 'mixed-syntax-esm'
4+
5+
export function Client() {
6+
return 'client:' + value
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
;(module) => {
2+
module.exports = {}
3+
}
4+
5+
export const value = 'relative-mixed-syntax-esm'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { value } from 'mixed-syntax-esm'
2+
import { Client } from './client'
3+
import { value as relativeMixedValue } from './mixed-mod.mjs'
4+
5+
export default function Page() {
6+
return (
7+
<>
8+
<p id="server">{'server:' + value}</p>
9+
<p id="client">
10+
<Client />
11+
</p>
12+
<p id="relative-mixed">{relativeMixedValue}</p>
13+
</>
14+
)
15+
}

0 commit comments

Comments
 (0)