From da8d299111ade846066035bc28c8c6ed38d1c400 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 27 Sep 2022 11:31:25 +0200 Subject: [PATCH] Fix SWC loader ignore for the server layer when Babel is used (#40939) Specific logic to handle the file transpilation on the server layer is implemented in SWC (#40603). When Babel is enabled, that SWC transform is ignored at the moment. In this PR we add an additional SWC pass after Babel to handle that. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/next/build/webpack-config.ts | 98 ++++++++++++--------- test/e2e/app-dir/with-babel.test.ts | 35 ++++++++ test/e2e/app-dir/with-babel/app/layout.js | 10 +++ test/e2e/app-dir/with-babel/app/page.js | 8 ++ test/e2e/app-dir/with-babel/babel.config.js | 3 + test/e2e/app-dir/with-babel/next.config.js | 5 ++ 6 files changed, 115 insertions(+), 44 deletions(-) create mode 100644 test/e2e/app-dir/with-babel.test.ts create mode 100644 test/e2e/app-dir/with-babel/app/layout.js create mode 100644 test/e2e/app-dir/with-babel/app/page.js create mode 100644 test/e2e/app-dir/with-babel/babel.config.js create mode 100644 test/e2e/app-dir/with-babel/next.config.js diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index cf1a86226c30938..1f45f69fa69672c 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -628,52 +628,59 @@ export default async function getBaseWebpackConfig( loggedIgnoredCompilerOptions = true } - const getBabelOrSwcLoader = () => { - if (useSWCLoader && config?.experimental?.swcTraceProfiling) { + const getBabelLoader = () => { + return { + loader: require.resolve('./babel/loader/index'), + options: { + configFile: babelConfigFile, + isServer: isNodeServer || isEdgeServer, + distDir, + pagesDir, + cwd: dir, + development: dev, + hasReactRefresh: dev && isClient, + hasJsxRuntime: true, + }, + } + } + + let swcTraceProfilingInitialized = false + const getSwcLoader = (extraOptions?: any) => { + if ( + config?.experimental?.swcTraceProfiling && + !swcTraceProfilingInitialized + ) { // This will init subscribers once only in a single process lifecycle, // even though it can be called multiple times. // Subscriber need to be initialized _before_ any actual swc's call (transform, etcs) // to collect correct trace spans when they are called. + swcTraceProfilingInitialized = true require('./swc')?.initCustomTraceSubscriber?.( path.join(distDir, `swc-trace-profile-${Date.now()}.json`) ) } - return useSWCLoader - ? { - loader: 'next-swc-loader', - options: { - isServer: isNodeServer || isEdgeServer, - rootDir: dir, - pagesDir, - hasReactRefresh: dev && isClient, - fileReading: config.experimental.swcFileReading, - nextConfig: config, - jsConfig, - supportedBrowsers: config.experimental.browsersListForSwc - ? supportedBrowsers - : undefined, - swcCacheDir: path.join( - dir, - config?.distDir ?? '.next', - 'cache', - 'swc' - ), - }, - } - : { - loader: require.resolve('./babel/loader/index'), - options: { - configFile: babelConfigFile, - isServer: isNodeServer || isEdgeServer, - distDir, - pagesDir, - cwd: dir, - development: dev, - hasReactRefresh: dev && isClient, - hasJsxRuntime: true, - }, - } + return { + loader: 'next-swc-loader', + options: { + isServer: isNodeServer || isEdgeServer, + rootDir: dir, + pagesDir, + hasReactRefresh: dev && isClient, + fileReading: config.experimental.swcFileReading, + nextConfig: config, + jsConfig, + supportedBrowsers: config.experimental.browsersListForSwc + ? supportedBrowsers + : undefined, + swcCacheDir: path.join(dir, config?.distDir ?? '.next', 'cache', 'swc'), + ...extraOptions, + }, + } + } + + const getBabelOrSwcLoader = () => { + return useSWCLoader ? getSwcLoader() : getBabelLoader() } const defaultLoaders = { @@ -1613,13 +1620,16 @@ export default async function getBaseWebpackConfig( { test: codeCondition.test, issuerLayer: WEBPACK_LAYERS.server, - use: { - ...defaultLoaders.babel, - options: { - ...defaultLoaders.babel.options, - isServerLayer: true, - }, - }, + use: useSWCLoader + ? getSwcLoader({ isServerLayer: true }) + : // When using Babel, we will have to add the SWC loader + // as an additional pass to handle RSC correctly. + // This will cause some performance overhead but + // acceptable as Babel will not be recommended. + [ + getSwcLoader({ isServerLayer: true }), + getBabelLoader(), + ], }, ] : []), diff --git a/test/e2e/app-dir/with-babel.test.ts b/test/e2e/app-dir/with-babel.test.ts new file mode 100644 index 000000000000000..6244e00c7ed62a6 --- /dev/null +++ b/test/e2e/app-dir/with-babel.test.ts @@ -0,0 +1,35 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import path from 'path' +import cheerio from 'cheerio' + +describe('with babel', () => { + if ((global as any).isNextDeploy) { + it('should skip next deploy for now', () => {}) + return + } + + if (process.env.NEXT_TEST_REACT_VERSION === '^17') { + it('should skip for react v17', () => {}) + return + } + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.join(__dirname, 'with-babel')), + dependencies: { + react: 'experimental', + 'react-dom': 'experimental', + }, + }) + }) + afterAll(() => next.destroy()) + + it('should support babel in app dir', async () => { + const html = await renderViaHTTP(next.url, '/') + const $ = cheerio.load(html) + expect($('h1').text()).toBe('hello') + }) +}) diff --git a/test/e2e/app-dir/with-babel/app/layout.js b/test/e2e/app-dir/with-babel/app/layout.js new file mode 100644 index 000000000000000..e66000e445cfc3d --- /dev/null +++ b/test/e2e/app-dir/with-babel/app/layout.js @@ -0,0 +1,10 @@ +export default function Root({ children }) { + return ( + + + hello + + {children} + + ) +} diff --git a/test/e2e/app-dir/with-babel/app/page.js b/test/e2e/app-dir/with-babel/app/page.js new file mode 100644 index 000000000000000..df85d684c2f35a8 --- /dev/null +++ b/test/e2e/app-dir/with-babel/app/page.js @@ -0,0 +1,8 @@ +'client' + +import { useState } from 'react' + +export default function Page() { + const state = useState('hello')[0] + return

{state}

+} diff --git a/test/e2e/app-dir/with-babel/babel.config.js b/test/e2e/app-dir/with-babel/babel.config.js new file mode 100644 index 000000000000000..bc6c5100868f51f --- /dev/null +++ b/test/e2e/app-dir/with-babel/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['next/babel'], +} diff --git a/test/e2e/app-dir/with-babel/next.config.js b/test/e2e/app-dir/with-babel/next.config.js new file mode 100644 index 000000000000000..cfa3ac3d7aa94b3 --- /dev/null +++ b/test/e2e/app-dir/with-babel/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + appDir: true, + }, +}