@anansi/babel-preset
Version:
Production ready babel (with TypeScript!)
568 lines (540 loc) • 17.2 kB
JavaScript
var globToRegExp = require('glob-to-regexp');
const path = require('path');
/*
options:
targets,
nodeTarget,
modules,
useESModules,
polyfillMethod,
corejsVersion,
useBuiltIns,
corejs,
minify,
loose,
tsConfigPath,
*/
function buildPreset(api, options = {}) {
api.assertVersion(7);
const env = api.env();
const babelTargets = typeof api.targets === 'function' ? api.targets() : {};
const supportsModules = api.caller(
caller => caller && caller.supportsStaticESM,
);
// Next.js 16 detection and server/client mode detection
const isNextJsTurbopack = api.caller(
caller => caller && caller.name === 'next-babel-turbo-loader',
);
// In Next.js 16, isServer can be true, false, or undefined
// Only enable hot reload when isServer is explicitly false (client build)
const isClientBuild = api.caller(
caller => caller && caller.isServer === false,
);
const babelNode = api.caller(
caller => caller && caller.name === '@babel/node',
);
const isLinaria = api.caller(
caller => caller && ['wyw-in-js', 'linaria'].includes(caller.name),
);
let library = api.caller(caller => caller && caller.library);
if (library === undefined)
library = api.caller(caller =>
(
caller &&
['rollup-plugin-babel', '@rollup/plugin-babel'].includes(caller.name)
) ?
true
: false,
);
// babel cli will have no caller information, so in this case we should be aware and
// possibly default to different options
// (no caller info: https://github.com/babel/babel/issues/8930)
const babelCli = api.caller(caller => caller && caller.name === '@babel/cli');
// webpack and jest sends on context about this specific build target
const callerTarget = api.caller(caller => caller && caller.target);
const nodeTarget =
callerTarget === 'node' ?
// default to current or use node config set
(babelTargets && babelTargets.node) || 'current'
: callerTarget === undefined ?
// infer based on babel target if caller doesn't send anything
babelTargets && babelTargets.node
// if caller is not targetting node, we should ensure nodeTarget is not set
: undefined;
const hasJsxRuntime = Boolean(
api.caller(
caller =>
// default to true - only allow overrides of false
((!!caller && caller.hasJsxRuntime) || options.hasJsxRuntime) !== false,
),
);
const explicitNodeTarget = !!options.nodeTarget;
options = {
minify: false,
rootPathSuffix: './src',
rootPathPrefix: '~/',
reactRequire: !hasJsxRuntime,
reactConstantElementsOptions: {},
nodeTarget,
resolver: { root: [], alias: {} },
...options,
};
if (process.env.BABEL_MODULES) {
options.modules = process.env.BABEL_MODULES;
}
if (api.caller(caller => caller && caller.polyfillMethod) !== undefined) {
options.polyfillMethod = api.caller(caller => caller.polyfillMethod);
}
if (process.env.BABEL_POLYFILL_METHOD !== undefined) {
options.polyfillMethod = process.env.BABEL_POLYFILL_METHOD;
}
// For Next.js 16, we need to explicitly check isServer to avoid adding
// react-refresh to server builds (which causes $RefreshReg$ errors)
const shouldHotReload =
!babelNode &&
!options.nodeTarget &&
!isLinaria &&
// Skip hot reload for Next.js server builds
(!isNextJsTurbopack || isClientBuild) &&
callerCouldTargetWeb(callerTarget) &&
// For Next.js, rely on env === 'development' instead of isDev caller field
process.env.NO_HOT_RELOAD !== 'true' &&
process.env.NO_HOT_RELOAD !== true &&
api.caller(caller => !caller || !caller.noHotReload) &&
!['commonjs', 'cjs'].includes(options.modules);
const modules =
options.modules !== undefined ? options.modules
// cli won't say what it supports; but we assume if they are calling without a tool they are
// trying to make ESM (false = preserve modules)
: babelCli ? false
: supportsModules ? false
: 'auto';
let absoluteRuntimePath = undefined;
let runtimeVersion = undefined;
let corejsVersion = undefined;
let corejsVersionPure = undefined;
try {
corejsVersion = require('core-js/package.json')
.version.split('.')
.slice(0, 2)
.join('.');
// eslint-disable-next-line no-empty
} catch (e) {}
try {
corejsVersionPure = require('core-js-pure/package.json')
.version.split('.')
.slice(0, 2)
.join('.');
// eslint-disable-next-line no-empty
} catch (e) {}
// 'auto' polyfillMethod (represented by undefined)
// figure out what polyfillMethod we should use
if (options.polyfillMethod === undefined) {
options.polyfillMethod = getPolyfillMethodAuto(
options,
babelCli || library,
corejsVersion,
corejsVersionPure,
);
}
let runtimePkg =
options.runtimePkg ||
(options.polyfillMethod === 'usage-pure' ?
'@babel/runtime-corejs3'
: '@babel/runtime');
if (
// we don't care about bundle size, so maximum compatibility is no runtime transform (runtimeVersion is undefined)
!(babelNode || env === 'test')
) {
// select possible runtime transform
try {
// TODO: investigate if using this is useful in @babel/plugin-transform-runtime
absoluteRuntimePath = path.dirname(
require.resolve(`${runtimePkg}/package.json`),
);
runtimeVersion = require(`${runtimePkg}/package.json`).version;
} catch (e) {
runtimePkg = '@babel/runtime';
try {
absoluteRuntimePath = path.dirname(
require.resolve(`${runtimePkg}/package.json`),
);
runtimeVersion = require(`${runtimePkg}/package.json`).version;
// eslint-disable-next-line no-empty
} catch (e) {}
}
}
if (process.env.TS_CONFIG_PATH) {
options.tsConfigPath = process.env.TS_CONFIG_PATH;
}
if (options.tsConfigPath) {
let readTsConfig;
try {
readTsConfig = require('@anansi/ts-utils').readTsConfig;
} catch (e) {
throw new Error('tsConfigPath set, but typescript module not found');
}
const { dir, base } = path.parse(options.tsConfigPath);
const tsconfig =
base !== '.' && base !== '..' ?
readTsConfig(dir, base)
: readTsConfig(dir);
if (tsconfig.options.paths) {
for (const k in tsconfig.options.paths) {
const key = globToRegExp(k).toString().replace('.*', '(.*)');
options.resolver.alias[key.substring(1, key.length - 1)] =
'./' + tsconfig.options.paths[k][0].replace('*', '\\1');
}
options.resolver.root = [path.resolve(tsconfig.options.baseUrl)];
options.resolver.root = [
...(tsconfig.options.baseUrl ? [tsconfig.options.baseUrl] : []),
...(tsconfig.options.rootDir ?
[tsconfig.options.rootDir]
: tsconfig.options.rootDirs || []),
...options.resolver.root,
];
}
}
options.resolver.extensions = [
'.ts',
'.tsx',
'.mts',
'.mtsx',
'.cts',
'.ctsx',
'.js',
'.jsx',
'.es',
'.es6',
'.mjs',
'.cjs',
];
options.resolver.alias = {
...options.resolver.alias,
...((process.env.RESOLVER_ALIAS &&
JSON.parse(process.env.RESOLVER_ALIAS)) ||
options.resolverAlias),
};
options.resolver.root = [
...options.resolver.root,
...(process.env.RESOLVER_ROOT ?
[process.env.RESOLVER_ROOT]
: options.resolverRoot || []),
];
const envOptions = getEnvOptions(
babelNode,
env,
explicitNodeTarget,
callerTarget,
options,
modules,
isLinaria,
);
const polyfillTargets =
process.env.POLYFILL_TARGETS ||
options.polyfillTargets ||
envOptions.targets;
const preset = {
presets: [
[
require('@babel/preset-react').default,
{
development:
env === 'development' || (env !== 'production' && !hasJsxRuntime),
...(hasJsxRuntime ?
{ runtime: 'automatic' }
: { runtime: 'classic', useSpread: true }),
},
],
],
plugins: [
(Object.keys(options.resolver.alias).length ||
options.resolver.root.length ||
Object.keys(options.resolver).length > 3) && [
require('babel-plugin-module-resolver').default,
options.resolver,
],
[
require('babel-plugin-root-import').default,
{
root: process.env.ROOT_PATH_ROOT || options.rootPathRoot,
rootPathSuffix:
process.env.ROOT_PATH_SUFFIX || options.rootPathSuffix,
rootPathPrefix:
process.env.ROOT_PATH_PREFIX || options.rootPathPrefix,
},
],
absoluteRuntimePath &&
runtimeVersion &&
!isLinaria && [
require('@babel/plugin-transform-runtime').default,
{
helpers: true,
moduleName: options.runtimePkg && runtimePkg,
version: runtimeVersion,
useESModules: !options.nodeTarget && modules === false,
},
],
options.polyfillMethod !== false &&
!isLinaria && [
require('babel-plugin-polyfill-corejs3').default,
{
method: options.polyfillMethod,
targets: polyfillTargets,
exclude: ['es.array.push', 'es.array.unshift'],
version:
options.polyfillMethod === 'usage-pure' ?
corejsVersionPure
: corejsVersion,
},
],
// stage 2
[
require('@babel/plugin-proposal-record-and-tuple').default,
{
importPolyfill: !isLinaria,
syntaxType: 'hash',
},
],
],
};
preset.plugins = preset.plugins.filter(v => v);
// import.meta.url doesn't work in commonjs
if (modules !== false && supportsModules === false) {
preset.plugins.unshift(
require('babel-plugin-transform-import-meta').default,
);
}
switch (env) {
case 'production':
// don't optimize for linaria
if (isLinaria) {
break;
}
if (!hasJsxRuntime) {
// new jsx runtime obsoletes this optimization
preset.plugins.unshift(
require('@babel/plugin-transform-react-inline-elements').default,
);
}
if (typeof options.reactConstantElementsOptions === 'object') {
preset.plugins.unshift([
require('@babel/plugin-transform-react-constant-elements').default,
options.reactConstantElementsOptions,
]);
}
break;
case 'development':
if (!shouldHotReload || isLinaria) break;
try {
preset.plugins.push(require('react-refresh/babel'));
// eslint-disable-next-line no-empty
} catch (e) {}
break;
}
preset.presets.unshift([require('@babel/preset-env').default, envOptions]);
if (options.minify && env === 'production' && !isLinaria) {
try {
preset.presets.unshift(require('babel-minify'));
} catch (e) {
console.log('Minify enabled, but babel-minify not installed.');
}
}
/* block is at the end so they are unshifted to the start of plugins */
preset.plugins.unshift(require('babel-plugin-macros'));
if (
typeof options.decoratorsOptions !== 'object' &&
options.decoratorsOptions !== undefined
) {
throw new Error('decoratorsOptions must be an Object');
}
const decoratorsOptions = options.decoratorsOptions || {
version: '2023-05',
};
if (decoratorsOptions.version === '2018-09') {
decoratorsOptions.decoratorsBeforeExport = true;
}
const classPlugins = [
// stage 3, but must come before class-properties when legacy is used (see block below)
[require('@babel/plugin-proposal-decorators').default, decoratorsOptions],
];
// compatibility (see: https://babeljs.io/docs/babel-plugin-proposal-decorators#note-compatibility-with-babelplugin-transform-class-properties)
if (decoratorsOptions.version === 'legacy') {
const classPropertiesOptions = { loose: options.loose };
classPlugins.push([
// this is included in preset-env, but must come before class-properties
require('@babel/plugin-transform-class-static-block').default,
// stage 3 but must come before flow
[
require('@babel/plugin-transform-class-properties').default,
classPropertiesOptions,
],
// this is included in preset-env, but must come after typescript, and after other class transforms
[
require('@babel/plugin-transform-private-methods').default,
{ loose: options.loose },
],
]);
}
// using plugin so it can be placed before class transforms
const transformTypeScript =
require('@babel/plugin-transform-typescript').default;
const pluginOptions = isTSX => ({
isTSX,
allowDeclareFields: true,
allowNamespaces: true,
optimizeConstEnums: true,
});
preset.overrides = [
{
test: /\.(m|c)?ts$/,
plugins: [[transformTypeScript, pluginOptions(false)], ...classPlugins],
},
{
test: /\.(m|c)?tsx$/,
plugins: [[transformTypeScript, pluginOptions(true)], ...classPlugins],
},
{
test: /\.(m|c)?(js|jsx)$/,
plugins: [
// stage 1
// typescript currently doesn't support this, so we're only supporting it for js files
require('@babel/plugin-proposal-export-default-from').default,
...classPlugins,
],
},
];
if (env === 'production' && !isLinaria) {
// only add to js files as typescript won't use react prop types
preset.overrides[2].plugins.unshift(
require('babel-plugin-transform-react-remove-prop-types').default,
);
}
if (options.reactCompiler && env === 'production' && !isLinaria) {
preset.plugins.unshift([
require('babel-plugin-react-compiler'),
typeof options.reactCompiler === 'object' ? options.reactCompiler : {},
]);
}
/* end block */
return preset;
}
module.exports = buildPreset;
function getEnvOptions(
babelNode,
env,
explicitNodeTarget,
callerTarget,
options,
modules,
isLinaria,
) {
let envOptions = {};
// For Linaria builds, keep transforms minimal and preserve modules.
// Linaria runs evaluation in Node, so target current Node and avoid extra transforms.
if (isLinaria) {
envOptions = {
targets: {
node: options.nodeTarget || 'current',
},
bugfixes: false,
modules: false,
shippedProposals: true,
// modern versions this already defaults to false, but just ensure we don't do any polyfills
useBuiltIns: false,
// don't do fancy stuff just for 'correctness'. loose means less transforms.
loose: true,
// Keep same slow transform exclusion
exclude: ['transform-typeof-symbol'],
};
return envOptions;
}
if (
babelNode ||
env === 'test' ||
explicitNodeTarget ||
callerTarget === 'node'
) {
envOptions = {
targets: {
node: options.nodeTarget || 'current',
},
};
} else {
envOptions = {
targets: options.targets,
};
}
envOptions = {
bugfixes: true,
modules,
shippedProposals: true,
loose: options.loose,
// Exclude transforms that make all code slower
exclude: ['transform-typeof-symbol'],
...envOptions,
};
return envOptions;
}
function getPolyfillMethodAuto(
options,
isLibrary,
corejsVersion,
corejsVersionPure,
) {
// support legacy option
if (options.useBuiltIns !== undefined) {
return {
usage: 'usage-global',
entry: 'entry-global',
[false]: false,
}[options.useBuiltIns];
} else {
if (corejsVersion !== undefined && !isLibrary) {
if (corejsVersionPure !== undefined) {
const cwd = process.cwd();
try {
const pkg = require(`${cwd}/package.json`);
// when explicitly marked as dep, make pure a priority
if (
('core-js-pure' in pkg.dependencies &&
!('core-js' in pkg.dependencies)) ||
('core-js-pure' in pkg.peerDependencies &&
!('core-js' in pkg.peerDependencies)) ||
('core-js-pure' in pkg.devDependencies &&
!('core-js' in pkg.devDependencies))
) {
return 'usage-pure';
}
// eslint-disable-next-line no-empty
} catch (e) {}
}
try {
require.resolve('@babel/runtime/package.json');
return 'usage-global';
} catch (e) {
return 'entry-global';
}
} else if (corejsVersionPure !== undefined) {
try {
require.resolve('@babel/runtime-corejs3/package.json');
return 'usage-pure';
} catch (e) {
return false;
}
}
}
return false;
}
function callerCouldTargetWeb(target) {
// without a target set, we don't explicitly know what its attempting
if (target === undefined) return true;
if (Array.isArray(target)) {
return target.some(callerCouldTargetWeb);
}
// based on https://webpack.js.org/configuration/target/
return (
['web', 'webworker', 'browserslist'].includes(target) ||
target.startsWith('es')
);
}