2021-04-28 11:51:57 +02:00
|
|
|
import { readFileSync } from 'fs'
|
|
|
|
|
2021-04-08 14:03:02 +02:00
|
|
|
import { createConfigItem, loadOptions } from 'next/dist/compiled/babel/core'
|
|
|
|
import loadConfig from 'next/dist/compiled/babel/core-lib-config'
|
|
|
|
|
|
|
|
import { NextBabelLoaderOptions, NextJsLoaderContext } from './types'
|
|
|
|
import { consumeIterator } from './util'
|
|
|
|
|
2021-04-20 10:45:57 +02:00
|
|
|
const nextDistPath = /(next[\\/]dist[\\/]next-server[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The properties defined here are the conditions with which subsets of inputs
|
|
|
|
* can be identified that are able to share a common Babel config. For example,
|
|
|
|
* in dev mode, different transforms must be applied to a source file depending
|
|
|
|
* on whether you're compiling for the client or for the server - thus `isServer`
|
|
|
|
* is germane.
|
|
|
|
*
|
|
|
|
* However, these characteristics need not protect against circumstances that
|
|
|
|
* will not be encountered in Next.js. For example, a source file may be
|
|
|
|
* transformed differently depending on whether we're doing a production compile
|
|
|
|
* or for HMR in dev mode. However, those two circumstances will never be
|
|
|
|
* encountered within the context of a single V8 context (and, thus, shared
|
|
|
|
* cache). Therefore, hasReactRefresh is _not_ germane to caching.
|
|
|
|
*
|
|
|
|
* NOTE: This approach does not support multiple `.babelrc` files in a
|
|
|
|
* single project. A per-cache-key config will be generated once and,
|
|
|
|
* if `.babelrc` is present, that config will be used for any subsequent
|
|
|
|
* transformations.
|
|
|
|
*/
|
|
|
|
interface CharacteristicsGermaneToCaching {
|
|
|
|
isServer: boolean
|
|
|
|
isPageFile: boolean
|
|
|
|
isNextDist: boolean
|
|
|
|
hasModuleExports: boolean
|
2021-04-28 11:51:57 +02:00
|
|
|
fileExt: string
|
2021-04-20 10:45:57 +02:00
|
|
|
}
|
|
|
|
|
2021-04-28 11:51:57 +02:00
|
|
|
const fileExtensionRegex = /\.([a-z]+)$/
|
2021-04-20 10:45:57 +02:00
|
|
|
function getCacheCharacteristics(
|
2021-04-08 14:03:02 +02:00
|
|
|
loaderOptions: NextBabelLoaderOptions,
|
|
|
|
source: string,
|
|
|
|
filename: string
|
2021-04-20 10:45:57 +02:00
|
|
|
): CharacteristicsGermaneToCaching {
|
|
|
|
const { isServer, pagesDir } = loaderOptions
|
2021-04-08 14:03:02 +02:00
|
|
|
const isPageFile = filename.startsWith(pagesDir)
|
2021-04-20 10:45:57 +02:00
|
|
|
const isNextDist = nextDistPath.test(filename)
|
|
|
|
const hasModuleExports = source.indexOf('module.exports') !== -1
|
2021-04-28 11:51:57 +02:00
|
|
|
const fileExt = fileExtensionRegex.exec(filename)?.[1] || 'unknown'
|
2021-04-20 10:45:57 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
isServer,
|
|
|
|
isPageFile,
|
|
|
|
isNextDist,
|
|
|
|
hasModuleExports,
|
2021-04-28 11:51:57 +02:00
|
|
|
fileExt,
|
2021-04-20 10:45:57 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-08 14:03:02 +02:00
|
|
|
|
2021-04-20 10:45:57 +02:00
|
|
|
/**
|
|
|
|
* Return an array of Babel plugins, conditioned upon loader options and
|
|
|
|
* source file characteristics.
|
|
|
|
*/
|
|
|
|
function getPlugins(
|
|
|
|
loaderOptions: NextBabelLoaderOptions,
|
|
|
|
cacheCharacteristics: CharacteristicsGermaneToCaching
|
|
|
|
) {
|
|
|
|
const {
|
|
|
|
isServer,
|
|
|
|
isPageFile,
|
|
|
|
isNextDist,
|
|
|
|
hasModuleExports,
|
|
|
|
} = cacheCharacteristics
|
|
|
|
|
|
|
|
const { hasReactRefresh, development } = loaderOptions
|
|
|
|
|
|
|
|
const applyCommonJsItem = hasModuleExports
|
|
|
|
? createConfigItem(require('../plugins/commonjs'), { type: 'plugin' })
|
|
|
|
: null
|
2021-04-08 14:03:02 +02:00
|
|
|
const reactRefreshItem = hasReactRefresh
|
|
|
|
? createConfigItem(
|
|
|
|
[require('react-refresh/babel'), { skipEnvCheck: true }],
|
|
|
|
{ type: 'plugin' }
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
const noAnonymousDefaultExportItem =
|
|
|
|
hasReactRefresh && !isServer
|
|
|
|
? createConfigItem(
|
|
|
|
[require('../plugins/no-anonymous-default-export'), {}],
|
|
|
|
{ type: 'plugin' }
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
const pageConfigItem =
|
|
|
|
!isServer && isPageFile
|
|
|
|
? createConfigItem([require('../plugins/next-page-config')], {
|
|
|
|
type: 'plugin',
|
|
|
|
})
|
|
|
|
: null
|
|
|
|
const disallowExportAllItem =
|
|
|
|
!isServer && isPageFile
|
|
|
|
? createConfigItem(
|
|
|
|
[require('../plugins/next-page-disallow-re-export-all-exports')],
|
|
|
|
{ type: 'plugin' }
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
const transformDefineItem = createConfigItem(
|
|
|
|
[
|
|
|
|
require.resolve('next/dist/compiled/babel/plugin-transform-define'),
|
|
|
|
{
|
|
|
|
'process.env.NODE_ENV': development ? 'development' : 'production',
|
|
|
|
'typeof window': isServer ? 'undefined' : 'object',
|
|
|
|
'process.browser': isServer ? false : true,
|
|
|
|
},
|
|
|
|
'next-js-transform-define-instance',
|
|
|
|
],
|
|
|
|
{ type: 'plugin' }
|
|
|
|
)
|
|
|
|
const nextSsgItem =
|
|
|
|
!isServer && isPageFile
|
|
|
|
? createConfigItem([require.resolve('../plugins/next-ssg-transform')], {
|
|
|
|
type: 'plugin',
|
|
|
|
})
|
|
|
|
: null
|
2021-04-20 10:45:57 +02:00
|
|
|
const commonJsItem = isNextDist
|
|
|
|
? createConfigItem(
|
|
|
|
require('next/dist/compiled/babel/plugin-transform-modules-commonjs'),
|
|
|
|
{ type: 'plugin' }
|
|
|
|
)
|
|
|
|
: null
|
2021-04-08 14:03:02 +02:00
|
|
|
|
|
|
|
return [
|
|
|
|
noAnonymousDefaultExportItem,
|
|
|
|
reactRefreshItem,
|
|
|
|
pageConfigItem,
|
|
|
|
disallowExportAllItem,
|
|
|
|
applyCommonJsItem,
|
|
|
|
transformDefineItem,
|
|
|
|
nextSsgItem,
|
2021-04-20 10:45:57 +02:00
|
|
|
commonJsItem,
|
2021-04-08 14:03:02 +02:00
|
|
|
].filter(Boolean)
|
|
|
|
}
|
|
|
|
|
2021-04-28 11:51:57 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-04-20 10:45:57 +02:00
|
|
|
/**
|
|
|
|
* Generate a new, flat Babel config, ready to be handed to Babel-traverse.
|
|
|
|
* This config should have no unresolved overrides, presets, etc.
|
|
|
|
*/
|
|
|
|
function getFreshConfig(
|
2021-04-08 14:03:02 +02:00
|
|
|
this: NextJsLoaderContext,
|
2021-04-20 10:45:57 +02:00
|
|
|
cacheCharacteristics: CharacteristicsGermaneToCaching,
|
|
|
|
loaderOptions: NextBabelLoaderOptions,
|
|
|
|
target: string,
|
|
|
|
filename: string,
|
|
|
|
inputSourceMap?: object | null
|
2021-04-08 14:03:02 +02:00
|
|
|
) {
|
2021-04-28 11:51:57 +02:00
|
|
|
let {
|
2021-04-08 14:03:02 +02:00
|
|
|
presets = [],
|
|
|
|
isServer,
|
|
|
|
pagesDir,
|
|
|
|
development,
|
|
|
|
hasJsxRuntime,
|
2021-04-28 11:51:57 +02:00
|
|
|
configFile,
|
2021-04-08 14:03:02 +02:00
|
|
|
} = loaderOptions
|
2021-04-28 11:51:57 +02:00
|
|
|
|
|
|
|
let customPlugins = []
|
|
|
|
if (configFile) {
|
|
|
|
const customConfig = getCustomBabelConfig(configFile)
|
|
|
|
presets = getCustomPresets(presets, customConfig)
|
|
|
|
if (customConfig.plugins) {
|
|
|
|
customPlugins = customConfig.plugins
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
presets = [...presets, 'next/babel']
|
|
|
|
}
|
2021-04-08 14:03:02 +02:00
|
|
|
|
|
|
|
let options = {
|
2021-04-28 11:51:57 +02:00
|
|
|
babelrc: false,
|
2021-04-08 14:03:02 +02:00
|
|
|
cloneInputAst: false,
|
|
|
|
filename,
|
|
|
|
inputSourceMap: inputSourceMap || undefined,
|
|
|
|
|
|
|
|
// Set the default sourcemap behavior based on Webpack's mapping flag,
|
|
|
|
// but allow users to override if they want.
|
|
|
|
sourceMaps:
|
|
|
|
loaderOptions.sourceMaps === undefined
|
2021-04-28 11:51:57 +02:00
|
|
|
? this.sourceMap
|
2021-04-08 14:03:02 +02:00
|
|
|
: loaderOptions.sourceMaps,
|
|
|
|
|
|
|
|
// Ensure that Webpack will get a full absolute path in the sourcemap
|
|
|
|
// so that it can properly map the module back to its internal cached
|
|
|
|
// modules.
|
|
|
|
sourceFileName: filename,
|
|
|
|
|
2021-04-28 11:51:57 +02:00
|
|
|
plugins: [
|
|
|
|
...getPlugins(loaderOptions, cacheCharacteristics),
|
|
|
|
...customPlugins,
|
|
|
|
],
|
2021-04-08 14:03:02 +02:00
|
|
|
|
2021-04-28 11:51:57 +02:00
|
|
|
presets,
|
2021-04-08 14:03:02 +02:00
|
|
|
|
2021-04-20 10:45:57 +02:00
|
|
|
overrides: loaderOptions.overrides,
|
2021-04-08 14:03:02 +02:00
|
|
|
|
|
|
|
caller: {
|
|
|
|
name: 'next-babel-turbo-loader',
|
|
|
|
supportsStaticESM: true,
|
|
|
|
supportsDynamicImport: true,
|
|
|
|
|
|
|
|
// Provide plugins with insight into webpack target.
|
|
|
|
// https://github.com/babel/babel-loader/issues/787
|
|
|
|
target: target,
|
|
|
|
|
|
|
|
// Webpack 5 supports TLA behind a flag. We enable it by default
|
|
|
|
// for Babel, and then webpack will throw an error if the experimental
|
|
|
|
// flag isn't enabled.
|
|
|
|
supportsTopLevelAwait: true,
|
|
|
|
|
|
|
|
isServer,
|
|
|
|
pagesDir,
|
2021-04-28 11:51:57 +02:00
|
|
|
isDev: development,
|
2021-04-08 14:03:02 +02:00
|
|
|
hasJsxRuntime,
|
|
|
|
|
|
|
|
...loaderOptions.caller,
|
|
|
|
},
|
|
|
|
} as any
|
|
|
|
|
|
|
|
Object.defineProperty(options.caller, 'onWarning', {
|
|
|
|
enumerable: false,
|
|
|
|
writable: false,
|
|
|
|
value: (reason: any) => {
|
|
|
|
if (!(reason instanceof Error)) {
|
|
|
|
reason = new Error(reason)
|
|
|
|
}
|
|
|
|
this.emitWarning(reason)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
const loadedOptions = loadOptions(options)
|
|
|
|
const config = consumeIterator(loadConfig(loadedOptions))
|
|
|
|
|
|
|
|
return config
|
|
|
|
}
|
2021-04-20 10:45:57 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Each key returned here corresponds with a Babel config that can be shared.
|
|
|
|
* The conditions of permissible sharing between files is dependent on specific
|
|
|
|
* file attributes and Next.js compiler states: `CharacteristicsGermaneToCaching`.
|
|
|
|
*/
|
|
|
|
function getCacheKey(cacheCharacteristics: CharacteristicsGermaneToCaching) {
|
|
|
|
const {
|
|
|
|
isServer,
|
|
|
|
isPageFile,
|
|
|
|
isNextDist,
|
|
|
|
hasModuleExports,
|
2021-04-28 11:51:57 +02:00
|
|
|
fileExt,
|
2021-04-20 10:45:57 +02:00
|
|
|
} = cacheCharacteristics
|
|
|
|
|
2021-04-28 11:51:57 +02:00
|
|
|
const flags =
|
2021-04-20 10:45:57 +02:00
|
|
|
0 |
|
|
|
|
(isServer ? 0b0001 : 0) |
|
|
|
|
(isPageFile ? 0b0010 : 0) |
|
|
|
|
(isNextDist ? 0b0100 : 0) |
|
|
|
|
(hasModuleExports ? 0b1000 : 0)
|
2021-04-28 11:51:57 +02:00
|
|
|
|
|
|
|
return fileExt + flags
|
2021-04-20 10:45:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type BabelConfig = any
|
2021-04-28 11:51:57 +02:00
|
|
|
const configCache: Map<any, BabelConfig> = new Map()
|
2021-04-20 10:45:57 +02:00
|
|
|
|
|
|
|
export default function getConfig(
|
|
|
|
this: NextJsLoaderContext,
|
|
|
|
{
|
|
|
|
source,
|
|
|
|
loaderOptions,
|
|
|
|
target,
|
|
|
|
filename,
|
|
|
|
inputSourceMap,
|
|
|
|
}: {
|
|
|
|
source: string
|
|
|
|
loaderOptions: NextBabelLoaderOptions
|
|
|
|
target: string
|
|
|
|
filename: string
|
|
|
|
inputSourceMap?: object | null
|
|
|
|
}
|
|
|
|
): BabelConfig {
|
|
|
|
const cacheCharacteristics = getCacheCharacteristics(
|
|
|
|
loaderOptions,
|
|
|
|
source,
|
|
|
|
filename
|
|
|
|
)
|
|
|
|
|
|
|
|
const cacheKey = getCacheKey(cacheCharacteristics)
|
|
|
|
if (configCache.has(cacheKey)) {
|
2021-04-28 11:51:57 +02:00
|
|
|
const cachedConfig = configCache.get(cacheKey)
|
|
|
|
|
2021-04-20 10:45:57 +02:00
|
|
|
return {
|
2021-04-28 11:51:57 +02:00
|
|
|
...cachedConfig,
|
|
|
|
options: {
|
|
|
|
...cachedConfig.options,
|
|
|
|
cwd: loaderOptions.cwd,
|
|
|
|
root: loaderOptions.cwd,
|
|
|
|
filename,
|
|
|
|
sourceFileName: filename,
|
|
|
|
},
|
2021-04-20 10:45:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const freshConfig = getFreshConfig.call(
|
|
|
|
this,
|
|
|
|
cacheCharacteristics,
|
|
|
|
loaderOptions,
|
|
|
|
target,
|
|
|
|
filename,
|
|
|
|
inputSourceMap
|
|
|
|
)
|
|
|
|
|
|
|
|
configCache.set(cacheKey, freshConfig)
|
|
|
|
|
|
|
|
return freshConfig
|
|
|
|
}
|