import type { PluginItem, TransformOptions as ConfigStructure } from '@babel/core'; import { Artifact } from '../Artifact'; import { BROWSER_TARGETS, ELECTRON_TARGETS, NATIVE_TARGETS, NODE_SUPPORTED_VERSIONS, } from '../constants'; import { shouldKeepDynamicImport } from '../helpers/shouldKeepDynamicImport'; import type { ConfigFile, FeatureFlags, Format, PackemonPackageFeatures, Platform, Support, } from '../types'; import { resolve, resolveFromBabel } from './resolve'; // https://babeljs.io/docs/en/babel-preset-env export interface PresetEnvOptions { browserslistEnv?: string; bugfixes?: boolean; corejs?: 2 | 3 | { version: 2 | 3; proposals: boolean }; debug?: boolean; exclude?: string[]; forceAllTransforms?: boolean; ignoreBrowserslistConfig?: boolean; include?: string[]; loose?: boolean; modules?: 'amd' | 'auto' | 'cjs' | 'commonjs' | 'systemjs' | 'umd' | false; shippedProposals?: boolean; spec?: boolean; targets?: Record | string[] | string; useBuiltIns?: 'entry' | 'usage' | false; } function getPlatformEnvOptions( platform: Platform, support: Support, format: Format, ): PresetEnvOptions { let modules: PresetEnvOptions['modules'] = false; if (format === 'umd') { modules = 'umd'; } else if (format === 'cjs' || format === 'lib') { modules = 'cjs'; // Babel CommonJS } const exclude = [ // https://caniuse.com/async-functions '@babel/plugin-transform-regenerator', '@babel/plugin-transform-async-to-generator', ]; // https://caniuse.com/es6-module-dynamic-import if (shouldKeepDynamicImport(platform, format)) { exclude.push('@babel/plugin-proposal-dynamic-import'); } switch (platform) { case 'browser': return { exclude, modules, targets: { browsers: BROWSER_TARGETS[support] }, }; case 'electron': return { exclude, modules, targets: { electron: ELECTRON_TARGETS[support] }, }; case 'native': return { exclude, modules, targets: { browsers: NATIVE_TARGETS[support] }, }; case 'node': return { exclude, modules, targets: { node: NODE_SUPPORTED_VERSIONS[support] }, }; default: throw new Error(`Unknown platform "${platform}".`); } } function injectHelperPlugins( plugins: PluginItem[], babelHelpers: PackemonPackageFeatures['helpers'], isESM: boolean, ) { if (babelHelpers === 'runtime') { plugins.push([ resolve('@babel/plugin-transform-runtime'), { corejs: false, helpers: true, regenerator: false, useESModules: isESM }, ]); } else if (babelHelpers === 'external') { plugins.push(resolve('@babel/plugin-external-helpers')); } } function getSharedConfig( plugins: PluginItem[], presets: PluginItem[], features: FeatureFlags, ): ConfigStructure { plugins.push( resolve('@babel/plugin-syntax-import-attributes'), // TODO enable in the future // resolve('@babel/plugin-proposal-json-modules'), ); return { caller: { name: 'packemon', }, comments: true, parserOpts: { sourceType: 'unambiguous', strictMode: features.strict, }, plugins, presets, // Do NOT load root `babel.config.js` or `.babelrc` as we need full control configFile: false, babelrc: false, }; } // The input config should only parse special syntax, not transform and downlevel. // This applies to all formats within a build target. export function getBabelInputConfig( artifact: Artifact, features: FeatureFlags, packemonConfig: ConfigFile = {}, ): Omit { const plugins: PluginItem[] = []; const presets: PluginItem[] = []; const tsOptions = { allowDeclareFields: true, onlyRemoveTypeImports: false, optimizeConstEnums: true, }; if (features.flow) { presets.push([resolve('@babel/preset-flow'), { allowDeclareFields: true }]); } if (features.typescript) { presets.push([resolve('@babel/preset-typescript'), tsOptions]); // When decorators are used, class properties must be loose if (features.decorators) { plugins.push( [resolve('@babel/plugin-transform-typescript'), tsOptions], [resolve('@babel/plugin-proposal-decorators'), { legacy: true }], [resolveFromBabel('@babel/plugin-transform-class-properties'), { loose: true }], [resolveFromBabel('@babel/plugin-transform-private-methods'), { loose: true }], [resolveFromBabel('@babel/plugin-transform-private-property-in-object'), { loose: true }], ); } } if (features.react) { presets.push([ resolve('@babel/preset-react'), { // development: __DEV__, runtime: features.react, throwIfNamespace: true, useBuiltIns: true, }, ]); } if (features.solid) { presets.push(resolve('babel-preset-solid')); } injectHelperPlugins(plugins, artifact.features.helpers, true); const config = getSharedConfig(plugins, presets, features); // Allow consumers to mutate packemonConfig.babelInput?.(config); return config; } // The output config does all the transformation and downleveling through the preset-env. // This is handled per output since we need to configure based on target + format combinations. export function getBabelOutputConfig( platform: Platform, support: Support, format: Format, features: FeatureFlags, packemonConfig: ConfigFile = {}, babelHelpers?: PackemonPackageFeatures['helpers'], ): ConfigStructure { const plugins: PluginItem[] = []; const presets: PluginItem[] = []; const isESM = format === 'esm' || format === 'mjs'; // PRESETS const envOptions: PresetEnvOptions = { // Prefer spec compliance in development spec: __DEV__, loose: false, // Consumers must polyfill accordingly useBuiltIns: false, // Transform features accordingly bugfixes: true, shippedProposals: true, // Platform specific ...getPlatformEnvOptions(platform, support, format), }; presets.push([resolve('@babel/preset-env'), envOptions]); // PLUGINS plugins.push( // Use `Object.assign` when available [resolveFromBabel('@babel/plugin-transform-destructuring'), { useBuiltIns: true }], [resolveFromBabel('@babel/plugin-transform-object-rest-spread'), { useBuiltIns: true }], ); if (platform === 'node') { plugins.push([resolve('babel-plugin-cjs-esm-interop'), { format: isESM ? 'mjs' : 'cjs' }]); // Node 14 does not support ??=, etc if (support === 'legacy') { plugins.push( resolveFromBabel('@babel/plugin-transform-logical-assignment-operators'), resolveFromBabel('@babel/plugin-transform-nullish-coalescing-operator'), ); } } plugins.push( resolve('babel-plugin-conditional-invariant'), resolve('babel-plugin-env-constants'), ); injectHelperPlugins(plugins, babelHelpers, isESM); const config = getSharedConfig(plugins, presets, features); // Allow consumers to mutate packemonConfig.babelOutput?.(config, { features, format, platform, support, }); return config; }