rsnext/packages/next/build/babel/loader/get-config.ts
2021-08-17 09:18:08 +02:00

356 lines
10 KiB
TypeScript

import { readFileSync } from 'fs'
import JSON5 from 'next/dist/compiled/json5'
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'
import * as Log from '../../output/log'
const nextDistPath =
/(next[\\/]dist[\\/]shared[\\/]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
fileExt: string
}
const fileExtensionRegex = /\.([a-z]+)$/
function getCacheCharacteristics(
loaderOptions: NextBabelLoaderOptions,
source: string,
filename: string
): CharacteristicsGermaneToCaching {
const { isServer, pagesDir } = loaderOptions
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,
}
}
/**
* 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
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
const commonJsItem = isNextDist
? createConfigItem(
require('next/dist/compiled/babel/plugin-transform-modules-commonjs'),
{ type: 'plugin' }
)
: null
return [
noAnonymousDefaultExportItem,
reactRefreshItem,
pageConfigItem,
disallowExportAllItem,
applyCommonJsItem,
transformDefineItem,
nextSsgItem,
commonJsItem,
].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 JSON5.parse(babelConfigRaw)
} else if (isJsFile.exec(configFilePath)) {
return require(configFilePath)
}
throw new Error(
'The Next.js Babel loader does not support .mjs or .cjs config files.'
)
}
/**
* Generate a new, flat Babel config, ready to be handed to Babel-traverse.
* This config should have no unresolved overrides, presets, etc.
*/
function getFreshConfig(
this: NextJsLoaderContext,
cacheCharacteristics: CharacteristicsGermaneToCaching,
loaderOptions: NextBabelLoaderOptions,
target: string,
filename: string,
inputSourceMap?: object | null
) {
let { isServer, pagesDir, development, hasJsxRuntime, configFile } =
loaderOptions
let customConfig: any = configFile
? getCustomBabelConfig(configFile)
: undefined
let options = {
babelrc: false,
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
? this.sourceMap
: 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,
plugins: [
...getPlugins(loaderOptions, cacheCharacteristics),
...(customConfig?.plugins || []),
],
// target can be provided in babelrc
target: isServer ? undefined : customConfig?.target,
// env can be provided in babelrc
env: customConfig?.env,
presets: (() => {
// If presets is defined the user will have next/babel in their babelrc
if (customConfig?.presets) {
return customConfig.presets
}
// If presets is not defined the user will likely have "env" in their babelrc
if (customConfig) {
return undefined
}
// If no custom config is provided the default is to use next/babel
return ['next/babel']
})(),
overrides: loaderOptions.overrides,
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,
isDev: development,
hasJsxRuntime,
...loaderOptions.caller,
},
} as any
// Babel does strict checks on the config so undefined is not allowed
if (typeof options.target === 'undefined') {
delete options.target
}
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
}
/**
* 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, fileExt } =
cacheCharacteristics
const flags =
0 |
(isServer ? 0b0001 : 0) |
(isPageFile ? 0b0010 : 0) |
(isNextDist ? 0b0100 : 0) |
(hasModuleExports ? 0b1000 : 0)
return fileExt + flags
}
type BabelConfig = any
const configCache: Map<any, BabelConfig> = new Map()
const configFiles: Set<string> = new Set()
export default function getConfig(
this: NextJsLoaderContext,
{
source,
target,
loaderOptions,
filename,
inputSourceMap,
}: {
source: string
loaderOptions: NextBabelLoaderOptions
target: string
filename: string
inputSourceMap?: object | null
}
): BabelConfig {
const cacheCharacteristics = getCacheCharacteristics(
loaderOptions,
source,
filename
)
if (loaderOptions.configFile) {
// Ensures webpack invalidates the cache for this loader when the config file changes
this.addDependency(loaderOptions.configFile)
}
const cacheKey = getCacheKey(cacheCharacteristics)
if (configCache.has(cacheKey)) {
const cachedConfig = configCache.get(cacheKey)
return {
...cachedConfig,
options: {
...cachedConfig.options,
cwd: loaderOptions.cwd,
root: loaderOptions.cwd,
filename,
sourceFileName: filename,
},
}
}
if (loaderOptions.configFile && !configFiles.has(loaderOptions.configFile)) {
configFiles.add(loaderOptions.configFile)
Log.info(
`Using external babel configuration from ${loaderOptions.configFile}`
)
}
const freshConfig = getFreshConfig.call(
this,
cacheCharacteristics,
loaderOptions,
target,
filename,
inputSourceMap
)
configCache.set(cacheKey, freshConfig)
return freshConfig
}