Skip to content

feat(config): allow override of rollup & plugins config by project config #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 130 additions & 108 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 => ({
Expand All @@ -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,
Expand Down Expand Up @@ -617,5 +639,5 @@ function createConfig(options, entry, format, writeMeta) {
},
};

return config;
return configOverride.config(config);
}
43 changes: 43 additions & 0 deletions src/lib/config-override.js
Original file line number Diff line number Diff line change
@@ -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);
}