diff --git a/packages/next/build/babel/loader/get-config.ts b/packages/next/build/babel/loader/get-config.ts index a3db3550ce767..73bdb3df97bcc 100644 --- a/packages/next/build/babel/loader/get-config.ts +++ b/packages/next/build/babel/loader/get-config.ts @@ -1,7 +1,8 @@ +import { readFileSync } from 'fs' + import { createConfigItem, loadOptions } from 'next/dist/compiled/babel/core' import loadConfig from 'next/dist/compiled/babel/core-lib-config' -import nextBabelPreset from '../preset' import { NextBabelLoaderOptions, NextJsLoaderContext } from './types' import { consumeIterator } from './util' @@ -31,8 +32,10 @@ interface CharacteristicsGermaneToCaching { isPageFile: boolean isNextDist: boolean hasModuleExports: boolean + fileExt: string } +const fileExtensionRegex = /\.([a-z]+)$/ function getCacheCharacteristics( loaderOptions: NextBabelLoaderOptions, source: string, @@ -42,12 +45,14 @@ function getCacheCharacteristics( const isPageFile = filename.startsWith(pagesDir) const isNextDist = nextDistPath.test(filename) const hasModuleExports = source.indexOf('module.exports') !== -1 + const fileExt = fileExtensionRegex.exec(filename)?.[1] || 'unknown' return { isServer, isPageFile, isNextDist, hasModuleExports, + fileExt, } } @@ -134,6 +139,46 @@ function getPlugins( ].filter(Boolean) } +const isJsonFile = /\.(json|babelrc)$/ +const isJsFile = /\.js$/ + +/** + * While this function does block execution while reading from disk, it + * should not introduce any issues. The function is only invoked when + * generating a fresh config, and only a small handful of configs should + * be generated during compilation. + */ +function getCustomBabelConfig(configFilePath: string) { + if (isJsonFile.exec(configFilePath)) { + const babelConfigRaw = readFileSync(configFilePath, 'utf8') + return JSON.parse(babelConfigRaw) + } else if (isJsFile.exec(configFilePath)) { + return require(configFilePath) + } + throw new Error( + 'The Next Babel loader does not support MJS or CJS config files.' + ) +} + +function getCustomPresets(presets: any[], customConfig: any) { + presets = [...presets, ...customConfig?.presets] + + const hasNextBabelPreset = (customConfig?.presets || []) + .filter((preset: any) => { + return ( + preset === 'next/babel' || + (Array.isArray(preset) && preset[0] === 'next/babel') + ) + }) + .reduce((memo: boolean, presetFound: boolean) => memo || presetFound, false) + + if (!hasNextBabelPreset) { + presets.push('next/babel') + } + + return presets +} + /** * Generate a new, flat Babel config, ready to be handed to Babel-traverse. * This config should have no unresolved overrides, presets, etc. @@ -146,19 +191,28 @@ function getFreshConfig( filename: string, inputSourceMap?: object | null ) { - const { + let { presets = [], isServer, pagesDir, development, - hasReactRefresh, hasJsxRuntime, - babelrc, + configFile, } = loaderOptions - const nextPresetItem = createConfigItem(nextBabelPreset, { type: 'preset' }) + + let customPlugins = [] + if (configFile) { + const customConfig = getCustomBabelConfig(configFile) + presets = getCustomPresets(presets, customConfig) + if (customConfig.plugins) { + customPlugins = customConfig.plugins + } + } else { + presets = [...presets, 'next/babel'] + } let options = { - babelrc, + babelrc: false, cloneInputAst: false, filename, inputSourceMap: inputSourceMap || undefined, @@ -167,7 +221,7 @@ function getFreshConfig( // but allow users to override if they want. sourceMaps: loaderOptions.sourceMaps === undefined - ? inputSourceMap + ? this.sourceMap : loaderOptions.sourceMaps, // Ensure that Webpack will get a full absolute path in the sourcemap @@ -175,9 +229,12 @@ function getFreshConfig( // modules. sourceFileName: filename, - plugins: getPlugins(loaderOptions, cacheCharacteristics), + plugins: [ + ...getPlugins(loaderOptions, cacheCharacteristics), + ...customPlugins, + ], - presets: [...presets, nextPresetItem], + presets, overrides: loaderOptions.overrides, @@ -197,8 +254,7 @@ function getFreshConfig( isServer, pagesDir, - development, - hasReactRefresh, + isDev: development, hasJsxRuntime, ...loaderOptions.caller, @@ -233,19 +289,21 @@ function getCacheKey(cacheCharacteristics: CharacteristicsGermaneToCaching) { isPageFile, isNextDist, hasModuleExports, + fileExt, } = cacheCharacteristics - return ( + const flags = 0 | (isServer ? 0b0001 : 0) | (isPageFile ? 0b0010 : 0) | (isNextDist ? 0b0100 : 0) | (hasModuleExports ? 0b1000 : 0) - ) + + return fileExt + flags } type BabelConfig = any -const configCache: Map = new Map() +const configCache: Map = new Map() export default function getConfig( this: NextJsLoaderContext, @@ -271,10 +329,17 @@ export default function getConfig( const cacheKey = getCacheKey(cacheCharacteristics) if (configCache.has(cacheKey)) { + const cachedConfig = configCache.get(cacheKey) + return { - ...configCache.get(cacheKey), - filename, - sourceFileName: filename, + ...cachedConfig, + options: { + ...cachedConfig.options, + cwd: loaderOptions.cwd, + root: loaderOptions.cwd, + filename, + sourceFileName: filename, + }, } } diff --git a/packages/next/build/babel/loader/index.ts b/packages/next/build/babel/loader/index.ts index eb480baa65baf..33f458b4e9eaa 100644 --- a/packages/next/build/babel/loader/index.ts +++ b/packages/next/build/babel/loader/index.ts @@ -1,4 +1,3 @@ -import { inspect } from 'util' import { getOptions } from 'next/dist/compiled/loader-utils' import { trace } from '../../../telemetry/trace' import { Span } from '../../../telemetry/trace' @@ -52,9 +51,6 @@ const nextBabelLoaderOuter = function nextBabelLoaderOuter( ([transformedSource, outputSourceMap]) => callback?.(null, transformedSource, outputSourceMap || inputSourceMap), (err) => { - console.error( - `Problem encountered in next-babel-turbo-loader. \n${inspect(err)}` - ) callback?.(err) } ) diff --git a/packages/next/build/babel/loader/types.d.ts b/packages/next/build/babel/loader/types.d.ts index 6eec4d0ed412a..6b5856bb83dbc 100644 --- a/packages/next/build/babel/loader/types.d.ts +++ b/packages/next/build/babel/loader/types.d.ts @@ -15,5 +15,6 @@ export interface NextBabelLoaderOptions { sourceMaps?: any[] overrides: any caller: any - babelrc: boolean + configFile: string | undefined + cwd: string } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 633a9de196375..e7bc6042d1fa2 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -209,7 +209,7 @@ export default async function getBaseWebpackConfig( // fixed in rc.1. semver.gte(reactVersion!, '17.0.0-rc.1') - const babelrc = await [ + const babelConfigFile = await [ '.babelrc', '.babelrc.json', '.babelrc.js', @@ -219,9 +219,13 @@ export default async function getBaseWebpackConfig( 'babel.config.json', 'babel.config.mjs', 'babel.config.cjs', - ].reduce(async (memo: boolean | Promise, filename) => { - return (await memo) || (await fileExists(path.join(dir, filename))) - }, false) + ].reduce(async (memo: Promise, filename) => { + const configFilePath = path.join(dir, filename) + return ( + (await memo) || + ((await fileExists(configFilePath)) ? configFilePath : undefined) + ) + }, Promise.resolve(undefined)) const distDir = path.join(dir, config.distDir) @@ -232,7 +236,7 @@ export default async function getBaseWebpackConfig( babel: { loader: babelLoader, options: { - babelrc, + configFile: babelConfigFile, isServer, distDir, pagesDir, diff --git a/test/integration/config-devtool-dev/test/index.test.js b/test/integration/config-devtool-dev/test/index.test.js index 8193133edcc70..b3f1e9f7b6753 100644 --- a/test/integration/config-devtool-dev/test/index.test.js +++ b/test/integration/config-devtool-dev/test/index.test.js @@ -15,7 +15,7 @@ jest.setTimeout(1000 * 30) const appDir = join(__dirname, '../') -describe('devtool set in developmemt mode in next config', () => { +describe('devtool set in development mode in next config', () => { it('should warn and revert when a devtool is set in development mode', async () => { let stderr = ''