Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix emitting ESM swc helpers for 3rd parties CJS libs in bundle #60169

Merged
merged 6 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 28 additions & 32 deletions packages/next/src/build/swc/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,33 @@ const regeneratorRuntimePath = require.resolve(
'next/dist/compiled/regenerator-runtime'
)

function isTypeScriptFile(filename: string) {
return filename.endsWith('.ts') || filename.endsWith('.tsx')
}

function isCommonJSFile(filename: string) {
return filename.endsWith('.cjs')
}

// Ensure Next.js internals and .cjs files are output as CJS modules,
// By default all modules are output as ESM or will treated as CJS if next-swc/auto-cjs plugin detects file is CJS.
function shouldOutputCommonJs(filename: string) {
return isCommonJSFile(filename) || nextDistPath.test(filename)
}

export function getParserOptions({ filename, jsConfig, ...rest }: any) {
const isTSFile = filename.endsWith('.ts')
const isTypeScript = isTSFile || filename.endsWith('.tsx')
const hasTsSyntax = isTypeScriptFile(filename)
const enableDecorators = Boolean(
jsConfig?.compilerOptions?.experimentalDecorators
)
return {
...rest,
syntax: isTypeScript ? 'typescript' : 'ecmascript',
syntax: hasTsSyntax ? 'typescript' : 'ecmascript',
dynamicImport: true,
decorators: enableDecorators,
// Exclude regular TypeScript files from React transformation to prevent e.g. generic parameters and angle-bracket type assertion from being interpreted as JSX tags.
[isTypeScript ? 'tsx' : 'jsx']: !isTSFile,
[hasTsSyntax ? 'tsx' : 'jsx']: !isTSFile,
importAssertions: true,
}
}
Expand Down Expand Up @@ -218,22 +232,6 @@ function getStyledComponentsOptions(
}
}

/*
Output module type

For app router where server components is enabled, we prefer to bundle es6 modules,
Use output module es6 to make sure:
- the esm module is present
- if the module is mixed syntax, the esm + cjs code are both present

For pages router will remain untouched
*/
function getModuleOptions(
esm: boolean | undefined = false
): { module: { type: 'es6' } } | {} {
return esm ? { module: { type: 'es6' } } : {}
}

function getEmotionOptions(
emotionConfig: undefined | boolean | EmotionConfig,
development: boolean
Expand Down Expand Up @@ -305,8 +303,7 @@ export function getJestSWCOptions({
serverComponents: false,
})

const isNextDist = nextDistPath.test(filename)

const useCjsModules = shouldOutputCommonJs(filename)
return {
...baseOptions,
env: {
Expand All @@ -316,7 +313,7 @@ export function getJestSWCOptions({
},
},
module: {
type: esm && !isNextDist ? 'es6' : 'commonjs',
type: esm && !useCjsModules ? 'es6' : 'commonjs',
},
disableNextSsg: true,
disablePageConfig: true,
Expand Down Expand Up @@ -422,14 +419,21 @@ export function getLoaderSWCOptions({
}
}

const isNextDist = nextDistPath.test(filename)
const isNodeModules = nodeModulesPath.test(filename)
const isAppBrowserLayer = bundleLayer === WEBPACK_LAYERS.appPagesBrowser
const moduleResolutionConfig = shouldOutputCommonJs(filename)
? {
module: {
type: 'commonjs',
},
}
: {}

let options: any
if (isServer) {
options = {
...baseOptions,
...moduleResolutionConfig,
// Disables getStaticProps/getServerSideProps tree shaking on the server compilation for pages
disableNextSsg: true,
disablePageConfig: true,
Expand All @@ -445,19 +449,11 @@ export function getLoaderSWCOptions({
node: process.versions.node,
},
},
...getModuleOptions(esm),
}
} else {
options = {
...baseOptions,
// Ensure Next.js internals are output as commonjs modules
...(isNextDist
? {
module: {
type: 'commonjs',
},
}
: getModuleOptions(esm)),
...moduleResolutionConfig,
disableNextSsg: !isPageFile,
isDevelopment: development,
isServerCompiler: isServer,
Expand Down
7 changes: 5 additions & 2 deletions test/e2e/app-dir/app-external/app-external.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ createNextDescribe(
{
files: __dirname,
dependencies: {
react: 'latest',
'react-dom': 'latest',
swr: 'latest',
},
packageJson: {
Expand Down Expand Up @@ -228,6 +226,11 @@ createNextDescribe(
})
})

it('should emit cjs helpers for external cjs modules when compiled', async () => {
const $ = await next.render$('/cjs/client')
expect($('#private-prop').text()).toBe('prop')
})

it('should export client module references in esm', async () => {
const html = await next.render('/esm-client-ref')
expect(html).toContain('hello')
Expand Down
11 changes: 11 additions & 0 deletions test/e2e/app-dir/app-external/app/cjs/client/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { instance } from 'cjs-modern-syntax'

export default function Page() {
return (
<>
<div id="private-prop">{instance.getProp()}</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class PrivatePropertyClass {
#prop = 'prop'
getProp() {
return this.#prop
}
}

exports.instance = new PrivatePropertyClass()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"exports": "./index.cjs"
}