microbundle/src/index.js
Jason Miller e8062290f3
lint fix
2017-12-12 12:03:10 -05:00

231 lines
7.0 KiB
JavaScript

import fs from 'fs';
import { resolve, relative, dirname, basename } from 'path';
import chalk from 'chalk';
import { map, series } from 'asyncro';
import promisify from 'es6-promisify';
import { rollup, watch } from 'rollup';
import nodent from 'rollup-plugin-nodent';
import commonjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';
import buble from 'rollup-plugin-buble';
import uglify from 'rollup-plugin-uglify';
import replace from 'rollup-plugin-post-replace';
import es3 from 'rollup-plugin-es3';
import gzipSize from 'gzip-size';
import prettyBytes from 'pretty-bytes';
import shebangPlugin from 'rollup-plugin-preserve-shebang';
import flow from 'rollup-plugin-flow';
const readFile = promisify(fs.readFile);
const stat = promisify(fs.stat);
const isDir = name => stat(name).then( stats => stats.isDirectory() ).catch( () => false );
const isFile = name => stat(name).then( stats => stats.isFile() ).catch( () => false );
const safeVariableName = name => name.replace(/(?:^[^a-z$_]|([^a-z0-9_$]+))/g, '_');
const FORMATS = ['es', 'cjs', 'umd'];
const WATCH_OPTS = {
exclude: 'node_modules/**'
};
export default async function microbundle(options) {
let cwd = options.cwd = resolve(process.cwd(), options.cwd || '.');
try {
options.pkg = JSON.parse(await readFile(resolve(cwd, 'package.json'), 'utf8'));
}
catch (err) {
console.warn(`Unable to find package.json:\n ${err.message}`);
options.pkg = {
name: basename(options.cwd)
};
}
options.input = [].concat(
options.entries && options.entries.length ? options.entries : options.pkg.source || (await isDir(resolve(cwd, 'src')) && 'src/index.js') || (await isFile(resolve(cwd, 'index.js')) && 'index.js') || options.pkg.module
).map( file => resolve(cwd, file) );
let main = resolve(cwd, options.output || options.pkg.main || 'dist');
if (!main.match(/\.[a-z]+$/) || await isDir(main)) {
main = resolve(main, `${options.pkg.name}.js`);
}
options.output = main;
let entries = await map([].concat(options.input), async file => {
file = resolve(cwd, file);
if (await isDir(file)) {
file = resolve(file, 'index.js');
}
return file;
});
options.entries = entries;
options.multipleEntries = entries.length>1;
let formats = [].concat(options.format || options.formats || FORMATS);
let steps = [];
for (let i=0; i<entries.length; i++) {
for (let j=0; j<formats.length; j++) {
steps.push(createConfig(options, entries[i], formats[j]));
}
}
async function getSizeInfo(code, filename) {
let size = await gzipSize(code);
let prettySize = prettyBytes(size);
let color = size < 5000 ? 'green' : size > 40000 ? 'red' : 'yellow';
return `${' '.repeat(10-prettySize.length)}${chalk[color](prettySize)}: ${chalk.white(basename(filename))}`;
}
if (options.watch) {
return new Promise( (resolve, reject) => {
process.stdout.write(chalk.blue(`Watching source, compiling to ${relative(cwd, dirname(options.output))}:\n`));
steps.map( options => {
watch(Object.assign({
output: options.outputOptions,
watch: WATCH_OPTS
}, options.inputOptions)).on('event', e => {
if (e.code==='ERROR' || e.code==='FATAL') {
return reject(e);
}
if (e.code==='END') {
getSizeInfo(options._code, options.outputOptions.file).then( text => {
process.stdout.write(`Wrote ${text.trim()}\n`);
});
}
});
});
});
}
let cache;
let out = await series(steps.map( ({ inputOptions, outputOptions }) => async () => {
inputOptions.cache = cache;
let bundle = await rollup(inputOptions);
cache = bundle;
await bundle.write(outputOptions);
return await getSizeInfo(bundle._code, outputOptions.file);
}));
return chalk.blue(`Build output to ${relative(cwd, dirname(options.output)) || '.'}:`) + '\n ' + out.join('\n ');
}
function createConfig(options, entry, format) {
let { pkg } = options;
let external = ['dns', 'fs', 'path', 'url'].concat(
Object.keys(pkg.peerDependencies || {}),
options.entries.filter( e => e!==entry )
);
let aliases = {};
// since we transform src/index.js, we need to rename imports for it:
if (options.multipleEntries) {
aliases['.'] = './' + basename(options.output);
external.push('.');
}
let useNodeResolve = true;
if (options.external==='all' || options.inline==='none') {
useNodeResolve = false;
external = external.concat(Object.keys(pkg.dependencies));
}
let globals = external.reduce( (globals, name) => {
// valid JS identifiers are usually library globals:
if (name.match(/^[a-z_$][a-z0-9_$]*$/)) {
globals[name] = name;
}
return globals;
}, {});
function replaceName(filename, name) {
return resolve(dirname(filename), name + basename(filename).replace(/^[^.]+/, ''));
}
let mainNoExtension = options.output;
if (options.multipleEntries) {
let name = entry.match(/\/index(\.(umd|cjs|es|m))?\.js$/) ? mainNoExtension : entry;
mainNoExtension = resolve(dirname(mainNoExtension), basename(name));
}
mainNoExtension = mainNoExtension.replace(/(\.(umd|cjs|es|m))?\.js$/, '');
let moduleMain = replaceName(pkg.module && !pkg.module.match(/src\//) ? pkg.module : pkg['jsnext:main'] || 'x.m.js', mainNoExtension);
let cjsMain = replaceName(pkg['cjs:main'] || 'x.js', mainNoExtension);
let umdMain = replaceName(pkg['umd:main'] || 'x.umd.js', mainNoExtension);
let rollupName = safeVariableName(basename(entry).replace(/\.js$/, ''));
let config = {
inputOptions: {
input: entry,
external,
plugins: [
flow({ all: true }),
nodent({
exclude: 'node_modules/**',
noRuntime: true,
promises: true,
transformations: {
forOf: false
}
}),
buble({
exclude: 'node_modules/**',
jsx: options.jsx || 'h',
objectAssign: options.assign || 'Object.assign',
transforms: { dangerousForOf: true }
}),
useNodeResolve && commonjs({
include: 'node_modules/**'
}),
useNodeResolve && nodeResolve({
modulesOnly: true,
jsnext: true
}),
es3(),
format==='cjs' && replace({
[`module.exports = ${rollupName};`]: '',
[`var ${rollupName} =`]: 'module.exports ='
}),
// This works for the general case, but could cause nasty scope bugs.
// format==='umd' && replace({
// [`return ${rollupName};`]: '',
// [`var ${rollupName} =`]: 'return'
// }),
format==='es' && replace({
[`export default ${rollupName};`]: '',
[`var ${rollupName} =`]: 'export default'
}),
format!=='es' && options.compress!==false && uglify({
output: { comments: false },
mangle: {
toplevel: format==='cjs'
}
}),
{
ongenerate({ bundle }, { code }) {
config._code = bundle._code = code;
}
},
shebangPlugin()
].filter(Boolean)
},
outputOptions: {
paths: aliases,
globals,
strict: options.strict===true,
sourcemap: true,
format,
name: options.name || pkg.amdName || safeVariableName(pkg.name),
file: resolve(options.cwd, (format==='es' && moduleMain) || (format==='umd' && umdMain) || cjsMain)
}
};
return config;
}