diff --git a/src/index.js b/src/index.js index 765bd0eb..ef6375d3 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ import { } from './lib/option-normalization'; import { getConfigFromPkgJson, getName } from './lib/package-info'; import { shouldCssModules, cssModulesConfig } from './lib/css-modules'; +import { getConfigOverride } from './lib/config-override'; // Extensions to use when resolving modules const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs']; @@ -92,7 +93,7 @@ export default async function microbundle(inputOptions) { for (let i = 0; i < options.entries.length; i++) { for (let j = 0; j < formats.length; j++) { steps.push( - createConfig( + await createConfig( options, options.entries[i], formats[j], @@ -291,8 +292,9 @@ function getMain({ options, entry, format }) { // shebang cache map because the transform only gets run once const shebang = {}; -function createConfig(options, entry, format, writeMeta) { +async function createConfig(options, entry, format, writeMeta) { let { pkg } = options; + const context = { options, entry, format, writeMeta }; /** @type {(string|RegExp)[]} */ let external = ['dns', 'fs', 'path', 'url'].concat( @@ -402,6 +404,8 @@ function createConfig(options, entry, format, writeMeta) { const outputDir = dirname(absMain); const outputEntryFileName = basename(absMain); + const configOverride = await getConfigOverride(context); + let config = { /** @type {import('rollup').InputOptions} */ inputOptions: { @@ -443,38 +447,46 @@ function createConfig(options, entry, format, writeMeta) { plugins: [] .concat( - postcss({ - plugins: [ - autoprefixer(), - options.compress !== false && - cssnano({ - preset: 'default', - }), - ].filter(Boolean), - autoModules: shouldCssModules(options), - modules: cssModulesConfig(options), - // only write out CSS for the first bundle (avoids pointless extra files): - inject: false, - extract: !!writeMeta, - }), + postcss( + configOverride.pluginConfig('postcss', { + plugins: [ + autoprefixer(), + options.compress !== false && + cssnano({ + preset: 'default', + }), + ].filter(Boolean), + autoModules: shouldCssModules(options), + modules: cssModulesConfig(options), + // only write out CSS for the first bundle (avoids pointless extra files): + inject: false, + extract: !!writeMeta, + }), + ), moduleAliases.length > 0 && - alias({ - // @TODO: this is no longer supported, but didn't appear to be required? - // resolve: EXTENSIONS, - entries: moduleAliases, + alias( + configOverride.pluginConfig('alias', { + // @TODO: this is no longer supported, but didn't appear to be required? + // resolve: EXTENSIONS, + entries: moduleAliases, + }), + ), + nodeResolve( + configOverride.pluginConfig('nodeResolve', { + mainFields: ['module', 'jsnext', 'main'], + browser: options.target !== 'node', + // defaults + .jsx + extensions: ['.mjs', '.js', '.jsx', '.json', '.node'], + preferBuiltins: options.target === 'node', }), - nodeResolve({ - mainFields: ['module', 'jsnext', 'main'], - browser: options.target !== 'node', - // defaults + .jsx - extensions: ['.mjs', '.js', '.jsx', '.json', '.node'], - preferBuiltins: options.target === 'node', - }), - commonjs({ - // use a regex to make sure to include eventual hoisted packages - include: /\/node_modules\//, - }), - json(), + ), + commonjs( + configOverride.pluginConfig('commonjs', { + // use a regex to make sure to include eventual hoisted packages + include: /\/node_modules\//, + }), + ), + json(configOverride.pluginConfig('json', {})), { // We have to remove shebang so it doesn't end up in the middle of the code somewhere transform: code => ({ @@ -485,89 +497,99 @@ function createConfig(options, entry, format, writeMeta) { }), }, useTypescript && - typescript({ - typescript: require('typescript'), - cacheRoot: `./node_modules/.cache/.rts2_cache_${format}`, - useTsconfigDeclarationDir: true, - tsconfigDefaults: { - compilerOptions: { - sourceMap: options.sourcemap, - declaration: true, - declarationDir: getDeclarationDir({ options, pkg }), - jsx: 'preserve', - jsxFactory: - // TypeScript fails to resolve Fragments when jsxFactory - // is set, even when it's the same as the default value. - options.jsx === 'React.createElement' - ? undefined - : options.jsx || 'h', + typescript( + configOverride.pluginConfig('typescript', { + typescript: require('typescript'), + cacheRoot: `./node_modules/.cache/.rts2_cache_${format}`, + useTsconfigDeclarationDir: true, + tsconfigDefaults: { + compilerOptions: { + sourceMap: options.sourcemap, + declaration: true, + declarationDir: getDeclarationDir({ options, pkg }), + jsx: 'preserve', + jsxFactory: + // TypeScript fails to resolve Fragments when jsxFactory + // is set, even when it's the same as the default value. + options.jsx === 'React.createElement' + ? undefined + : options.jsx || 'h', + }, + files: options.entries, }, - files: options.entries, - }, - tsconfig: options.tsconfig, - tsconfigOverride: { - compilerOptions: { - module: 'ESNext', - target: 'esnext', + tsconfig: options.tsconfig, + tsconfigOverride: { + compilerOptions: { + module: 'ESNext', + target: 'esnext', + }, }, - }, - }), + }), + ), // if defines is not set, we shouldn't run babel through node_modules isTruthy(defines) && - babel({ - babelHelpers: 'bundled', - babelrc: false, - compact: false, - configFile: false, - include: 'node_modules/**', - plugins: [ - [ - require.resolve('babel-plugin-transform-replace-expressions'), - { replace: defines }, + babel( + configOverride.pluginConfig('babel', { + babelHelpers: 'bundled', + babelrc: false, + compact: false, + configFile: false, + include: 'node_modules/**', + plugins: [ + [ + require.resolve( + 'babel-plugin-transform-replace-expressions', + ), + { replace: defines }, + ], ], - ], + }), + ), + customBabel()( + configOverride.pluginConfig('customBabel', { + babelHelpers: 'bundled', + extensions: EXTENSIONS, + exclude: 'node_modules/**', + passPerPreset: true, // @see https://babeljs.io/docs/en/options#passperpreset + custom: { + defines, + modern, + compress: options.compress !== false, + targets: options.target === 'node' ? { node: '8' } : undefined, + pragma: options.jsx || 'h', + pragmaFrag: options.jsxFragment || 'Fragment', + typescript: !!useTypescript, + jsxImportSource: options.jsxImportSource || false, + }, }), - customBabel()({ - babelHelpers: 'bundled', - extensions: EXTENSIONS, - exclude: 'node_modules/**', - passPerPreset: true, // @see https://babeljs.io/docs/en/options#passperpreset - custom: { - defines, - modern, - compress: options.compress !== false, - targets: options.target === 'node' ? { node: '8' } : undefined, - pragma: options.jsx || 'h', - pragmaFrag: options.jsxFragment || 'Fragment', - typescript: !!useTypescript, - jsxImportSource: options.jsxImportSource || false, - }, - }), + ), options.compress !== false && [ - terser({ - sourcemap: true, - compress: Object.assign( - { - keep_infinity: true, - pure_getters: true, - // Ideally we'd just get Terser to respect existing Arrow functions... - // unsafe_arrows: true, - passes: 10, + terser( + configOverride.pluginConfig('terser', { + sourcemap: true, + compress: Object.assign( + { + keep_infinity: true, + pure_getters: true, + // Ideally we'd just get Terser to respect existing Arrow functions... + // unsafe_arrows: true, + passes: 10, + }, + minifyOptions.compress || {}, + ), + output: { + // By default, Terser wraps function arguments in extra parens to trigger eager parsing. + // Whether this is a good idea is way too specific to guess, so we optimize for size by default: + wrap_func_args: false, + comments: false, }, - minifyOptions.compress || {}, - ), - output: { - // By default, Terser wraps function arguments in extra parens to trigger eager parsing. - // Whether this is a good idea is way too specific to guess, so we optimize for size by default: - wrap_func_args: false, - comments: false, - }, - warnings: true, - ecma: modern ? 9 : 5, - toplevel: modern || format === 'cjs' || format === 'es', - mangle: Object.assign({}, minifyOptions.mangle || {}), - nameCache, - }), + warnings: true, + ecma: modern ? 9 : 5, + toplevel: modern || format === 'cjs' || format === 'es', + mangle: Object.assign({}, minifyOptions.mangle || {}), + nameCache, + }), + ), nameCache && { // before hook options: loadNameCache, @@ -617,5 +639,5 @@ function createConfig(options, entry, format, writeMeta) { }, }; - return config; + return configOverride.config(config); } diff --git a/src/lib/config-override.js b/src/lib/config-override.js new file mode 100644 index 00000000..06ff6876 --- /dev/null +++ b/src/lib/config-override.js @@ -0,0 +1,43 @@ +import { isFile } from '../utils'; +import { resolve } from 'path'; + +function configOverrider(projectOverride, context) { + return { + /** + * Override the configuration for a given plugin. + * + * @param {string} pluginName + * @param {Object} config the + */ + pluginConfig(pluginName, config) { + // No override if no plugins override is defined + if (!projectOverride.plugins) { + return config; + } + + // Expect provided override to be a function returning modified config + const override = projectOverride.plugins[pluginName]; + return override ? override(config, context) : config; + }, + + /** + * Override the full rollup config before it's used + * + * @param {Object} config + */ + config(config) { + if (!projectOverride.config) { + return config; + } + + return projectOverride.config(config, context); + }, + }; +} + +export async function getConfigOverride(context) { + const path = resolve(context.options.cwd, 'microbundle.config.js'); + const hasProjectConfig = await isFile(path); + + return configOverrider(hasProjectConfig ? require(path) : {}, context); +}