From 25ca58f1be456b8561f8bc5dd7566348e3f9bc03 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 7 Sep 2017 15:38:43 -0600 Subject: [PATCH] feat(build): support building for the browser with rollup --- .eslintrc | 1 + package.json | 13 ++- src/config/babelrc.js | 12 +-- src/config/rollup.config.js | 105 +++++++++++++++++++++++ src/index.js | 0 src/scripts/{build.js => build/babel.js} | 10 +-- src/scripts/build/index.js | 5 ++ src/scripts/build/rollup.js | 55 ++++++++++++ src/scripts/lint.js | 8 +- src/scripts/test.js | 1 + src/scripts/validate.js | 51 ++++------- src/utils.js | 46 +++++++++- 12 files changed, 251 insertions(+), 56 deletions(-) create mode 100644 src/config/rollup.config.js mode change 100644 => 100755 src/index.js rename src/scripts/{build.js => build/babel.js} (75%) create mode 100644 src/scripts/build/index.js create mode 100644 src/scripts/build/rollup.js diff --git a/.eslintrc b/.eslintrc index 21683276..4711d053 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,5 +16,6 @@ {argsIgnorePattern: "^_", varsIgnorePattern: "^ignored"}, ], "no-console": "off", + "no-nested-ternary": "off", } } diff --git a/package.json b/package.json index a45ecedc..051b3561 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "babel-preset-env": "^1.6.0", "babel-preset-react": "^6.24.1", "concurrently": "^3.5.0", + "cross-env": "^5.0.5", "cross-spawn": "^5.1.0", "eslint": "^4.2.0", "eslint-config-kentcdodds": "^12.4.1", @@ -52,12 +53,20 @@ "husky": "^0.14.3", "jest": "^20.0.4", "lint-staged": "^4.0.1", + "lodash.camelcase": "^4.3.0", "lodash.has": "^4.5.2", "lodash.merge": "^4.6.0", "prettier": "^1.6.1", - "read-pkg-up": "^2.0.0", "resolve": "^1.4.0", - "rimraf": "^2.6.1" + "rimraf": "^2.6.1", + "rollup": "^0.48.2", + "rollup-plugin-alias": "^1.3.1", + "rollup-plugin-babel": "^3.0.2", + "rollup-plugin-commonjs": "^8.2.0", + "rollup-plugin-json": "^2.3.0", + "rollup-plugin-node-resolve": "^3.0.0", + "rollup-plugin-uglify": "^2.0.1", + "rollup-watch": "^4.3.1" }, "repository": { "type": "git", diff --git a/src/config/babelrc.js b/src/config/babelrc.js index 2c34d2a5..5edea425 100644 --- a/src/config/babelrc.js +++ b/src/config/babelrc.js @@ -1,10 +1,10 @@ -const {ifAnyDep} = require('../utils') +const {ifAnyDep, parseEnv} = require('../utils') const isTest = process.env.NODE_ENV === 'test' -const isPreact = process.env.LIBRARY === 'preact' -const isRollup = JSON.parse(process.env.ROLLUP_BUILD || 'false') -const isWebpack = JSON.parse(process.env.WEBPACK_BUILD || 'false') -const treeshake = isRollup || isWebpack +const isPreact = parseEnv('BUILD_PREACT', false) +const isRollup = parseEnv('BUILD_ROLLUP', false) +const isWebpack = parseEnv('BUILD_WEBPACK', false) +const treeshake = parseEnv('BUILD_TREESHAKE', isRollup || isWebpack) module.exports = { presets: [ @@ -18,7 +18,7 @@ module.exports = { }, }, ], - ifAnyDep('react', require.resolve('babel-preset-react')), + ifAnyDep(['react', 'preact'], require.resolve('babel-preset-react')), ].filter(Boolean), plugins: [ isRollup ? require.resolve('babel-plugin-external-helpers') : null, diff --git a/src/config/rollup.config.js b/src/config/rollup.config.js new file mode 100644 index 00000000..d11eefef --- /dev/null +++ b/src/config/rollup.config.js @@ -0,0 +1,105 @@ +const path = require('path') +const camelcase = require('lodash.camelcase') +const rollupBabel = require('rollup-plugin-babel') +const commonjs = require('rollup-plugin-commonjs') +const nodeResolve = require('rollup-plugin-node-resolve') +const json = require('rollup-plugin-json') +const uglify = require('rollup-plugin-uglify') +const rollupAlias = require('rollup-plugin-alias') +const {getPkg, hasFile, hasPkgProp, parseEnv} = require('../utils') + +const pkg = getPkg() +const here = p => path.join(__dirname, p) + +const minify = parseEnv('BUILD_MINIFY', false) +const format = process.env.BUILD_FORMAT +const isPreact = parseEnv('BUILD_PREACT', false) + +const capitalize = s => s[0].toUpperCase() + s.slice(1) + +const defaultGlobals = Object.keys(pkg.peerDependencies).reduce((deps, dep) => { + deps[dep] = capitalize(camelcase(dep)) + return deps +}, {}) + +const defaultExternal = Object.keys(pkg.peerDependencies) + +const filenameSuffix = parseEnv( + 'BUILD_FILENAME_SUFFIX', + isPreact ? '.preact' : '', +) +const globals = parseEnv( + 'BUILD_GLOBALS', + isPreact ? Object.assign(defaultGlobals, {preact: 'preact'}) : defaultGlobals, +) +const external = parseEnv( + 'BUILD_EXTERNAL', + isPreact ? defaultExternal.concat(['preact', 'prop-types']) : defaultExternal, +).filter((e, i, arry) => arry.indexOf(e) === i) + +if (isPreact) { + delete globals.react + delete globals['prop-types'] // TODO: is this necessary? + external.splice(external.indexOf('react'), 1) +} + +const alias = parseEnv('BUILD_ALIAS', isPreact ? {react: 'preact'} : null) +const esm = format === 'esm' +const umd = format === 'umd' +const cjs = format === 'cjs' + +let output + +const filename = path.join('dist', pkg.name) + +if (esm) { + output = [{file: `${filename}${filenameSuffix}.es.js`, format: 'es'}] +} else if (umd) { + if (minify) { + output = [ + { + file: `${filename}${filenameSuffix}.umd.min.js`, + format: 'umd', + sourcemap: true, + }, + ] + } else { + output = [ + { + file: `${filename}${filenameSuffix}.umd.js`, + format: 'umd', + sourcemap: true, + }, + ] + } +} else if (cjs) { + output = [{file: `${filename}${filenameSuffix}.cjs.js`, format: 'cjs'}] +} else if (format) { + throw new Error(`invalid format specified: "${format}".`) +} else { + throw new Error('no format specified. --environment FORMAT:xxx') +} + +const useBuiltinConfig = !hasFile('.babelrc') && !hasPkgProp('babel') +const babelPresets = useBuiltinConfig ? [here('../config/babelrc.js')] : [] + +module.exports = { + input: 'src/index.js', + output, + exports: esm ? 'named' : 'default', + name: capitalize(camelcase(pkg.name)), + external, + globals, + plugins: [ + alias ? rollupAlias(alias) : null, + nodeResolve({jsnext: true, main: true}), + commonjs({include: 'node_modules/**'}), + json(), + rollupBabel({ + exclude: 'node_modules/**', + presets: babelPresets, + babelrc: true, + }), + minify ? uglify() : null, + ].filter(Boolean), +} diff --git a/src/index.js b/src/index.js old mode 100644 new mode 100755 diff --git a/src/scripts/build.js b/src/scripts/build/babel.js similarity index 75% rename from src/scripts/build.js rename to src/scripts/build/babel.js index bf7c938f..49094093 100644 --- a/src/scripts/build.js +++ b/src/scripts/build/babel.js @@ -1,19 +1,15 @@ -const fs = require('fs') const path = require('path') const spawn = require('cross-spawn') const rimraf = require('rimraf') -const {fromRoot} = require('../utils') -const {hasPkgProp, resolveBin} = require('../utils') +const {hasPkgProp, fromRoot, resolveBin, hasFile} = require('../../utils') const args = process.argv.slice(2) const here = p => path.join(__dirname, p) const useBuiltinConfig = - !args.includes('--presets') && - !fs.existsSync(fromRoot('.babelrc')) && - !hasPkgProp('babel') + !args.includes('--presets') && !hasFile('.babelrc') && !hasPkgProp('babel') const config = useBuiltinConfig - ? ['--presets', here('../config/babelrc.js')] + ? ['--presets', here('../../config/babelrc.js')] : [] const ignore = args.includes('--ignore') diff --git a/src/scripts/build/index.js b/src/scripts/build/index.js new file mode 100644 index 00000000..7660cdb8 --- /dev/null +++ b/src/scripts/build/index.js @@ -0,0 +1,5 @@ +if (process.argv.includes('--browser')) { + require('./rollup') +} else { + require('./babel') +} diff --git a/src/scripts/build/rollup.js b/src/scripts/build/rollup.js new file mode 100644 index 00000000..2e1565ae --- /dev/null +++ b/src/scripts/build/rollup.js @@ -0,0 +1,55 @@ +const path = require('path') +const spawn = require('cross-spawn') +const rimraf = require('rimraf') +const { + hasFile, + resolveBin, + fromRoot, + getConcurrentlyArgs, +} = require('../../utils') + +const crossEnv = resolveBin('cross-env') +const rollup = resolveBin('rollup') +const args = process.argv.slice(2) +const here = p => path.join(__dirname, p) + +const useBuiltinConfig = + !args.includes('--config') && !hasFile('rollup.config.js') +const config = useBuiltinConfig + ? `--config ${here('../../config/rollup.config.js')}` + : args.includes('--config') ? '' : '--config' // --config will pick up the rollup.config.js file + +const getCommand = env => + `${crossEnv} BUILD_ROLLUP=true ${env} ${rollup} ${config}` + +const scripts = args.includes('--p-react') + ? getPReactScripts() + : getConcurrentlyArgs({ + esm: getCommand('BUILD_FORMAT=esm'), + cjs: getCommand('BUILD_FORMAT=cjs'), + umd: getCommand('BUILD_FORMAT=umd'), + 'umd.min': getCommand('BUILD_FORMAT=umd BUILD_MINIFY=true'), + }) + +rimraf.sync(fromRoot('dist')) + +const result = spawn.sync(resolveBin('concurrently'), scripts, { + stdio: 'inherit', +}) + +function getPReactScripts() { + return getConcurrentlyArgs({ + 'react.esm': getCommand('BUILD_FORMAT=esm'), + 'react.cjs': getCommand('BUILD_FORMAT=cjs'), + 'react.umd': getCommand('BUILD_FORMAT=umd'), + 'react.umd.min': getCommand('BUILD_FORMAT=umd BUILD_MINIFY=true'), + 'preact.esm': getCommand('BUILD_PREACT=true BUILD_FORMAT=esm'), + 'preact.cjs': getCommand('BUILD_PREACT=true BUILD_FORMAT=cjs'), + 'preact.umd': getCommand('BUILD_PREACT=true BUILD_FORMAT=umd'), + 'preact.umd.min': getCommand( + 'BUILD_PREACT=true BUILD_FORMAT=umd BUILD_MINIFY=true', + ), + }) +} + +process.exit(result.status) diff --git a/src/scripts/lint.js b/src/scripts/lint.js index d4436b83..18e56791 100644 --- a/src/scripts/lint.js +++ b/src/scripts/lint.js @@ -1,15 +1,13 @@ -const fs = require('fs') const path = require('path') const spawn = require('cross-spawn') -const {fromRoot} = require('../utils') -const {hasPkgProp, resolveBin} = require('../utils') +const {hasPkgProp, resolveBin, hasFile} = require('../utils') const args = process.argv.slice(2) const here = p => path.join(__dirname, p) const useBuiltinConfig = !args.includes('--config') && - !fs.existsSync(fromRoot('.eslintrc')) && + !hasFile('.eslintrc') && !hasPkgProp('eslintConfig') const config = useBuiltinConfig ? ['--config', here('../config/eslintrc.js')] @@ -17,7 +15,7 @@ const config = useBuiltinConfig const useBuiltinIgnore = !args.includes('--ignore-path') && - !fs.existsSync(fromRoot('.eslintignore')) && + !hasFile('.eslintignore') && !hasPkgProp('eslintIgnore') const ignore = useBuiltinIgnore diff --git a/src/scripts/test.js b/src/scripts/test.js index 3dd37ee0..301a83e4 100644 --- a/src/scripts/test.js +++ b/src/scripts/test.js @@ -11,6 +11,7 @@ const args = process.argv.slice(2) const watch = !process.env.CI && + !args.includes('--no-watch') && !args.includes('--coverage') && !args.includes('--updateSnapshot') ? ['--watch'] diff --git a/src/scripts/validate.js b/src/scripts/validate.js index 851dc29f..5a59893b 100644 --- a/src/scripts/validate.js +++ b/src/scripts/validate.js @@ -1,46 +1,29 @@ const spawn = require('cross-spawn') -const {ifScript, resolveBin} = require('../utils') +const {getConcurrentlyArgs, hasScript, resolveBin} = require('../utils') -const validateScripts = process.argv[3] - -const scriptNames = (...scripts) => - scripts - .map(s => ifScript(s, s)) - .filter(Boolean) - .join(',') +const validateScripts = process.argv[2] const useDefaultScripts = typeof validateScripts !== 'string' const scripts = useDefaultScripts - ? [ - ifScript('build', 'npm run build --silent'), - ifScript('lint', 'npm run lint --silent'), - ifScript('test', 'npm run test --silent -- --coverage'), - ].filter(Boolean) - : validateScripts.split(',').map(npmScript => `npm run ${npmScript} -s`) -const names = useDefaultScripts - ? scriptNames('build', 'lint', 'test') - : validateScripts.split(',') - -const colors = [ - 'bgBlue.bold', - 'bgGreen.bold', - 'bgMagenta.bold', - 'black.bgWhite.bold', - 'white.bgBlack.bold', - 'bgRed.bold', -].join(',') + ? Object.entries({ + build: 'npm run build --silent', + lint: 'npm run lint --silent', + test: 'npm run test --silent -- --coverage', + }).reduce((scriptsToRun, [name, script]) => { + if (hasScript(name)) { + scriptsToRun[name] = script + } + return scriptsToRun + }, {}) + : validateScripts.split(',').reduce((scriptsToRun, name) => { + scriptsToRun[name] = `npm run ${name} --silent` + return scriptsToRun + }, {}) const result = spawn.sync( resolveBin('concurrently'), - // prettier-ignore - [ - '--kill-others-on-fail', - '--prefix', '[{name}]', - '--names', names, - '--prefix-colors', colors, - ...scripts, - ], + getConcurrentlyArgs(scripts), {stdio: 'inherit'}, ) diff --git a/src/utils.js b/src/utils.js index c0786e7a..d948ea4e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,5 @@ const fs = require('fs') const path = require('path') -const readPkgUp = require('read-pkg-up') const arrify = require('arrify') const has = require('lodash.has') @@ -16,8 +15,9 @@ function resolveBin(modName, {executable = modName} = {}) { const appDirectory = fs.realpathSync(process.cwd()) const fromRoot = (...p) => path.join(appDirectory, ...p) +const hasFile = (...p) => fs.existsSync(fromRoot(...p)) -const getPkg = () => readPkgUp.sync({cwd: process.cwd()}).pkg || {} +const getPkg = () => require(fromRoot('package.json')) const hasPkgProp = props => arrify(props).some(prop => has(getPkg(), prop)) @@ -27,6 +27,7 @@ const hasPkgSubProp = pkgProp => props => const ifPkgSubProp = pkgProp => (props, t, f) => hasPkgSubProp(pkgProp, props) ? t : f +const hasScript = hasPkgSubProp('script') const hasPeerDep = hasPkgSubProp('peerDependencies') const hasDep = hasPkgSubProp('dependencies') const hasDevDep = hasPkgSubProp('devDependencies') @@ -39,6 +40,42 @@ const ifDevDep = ifPkgSubProp('devDependencies') const ifAnyDep = (deps, t, f) => (hasAnyDep(deps) ? t : f) const ifScript = ifPkgSubProp('scripts') +function parseEnv(name, def) { + if (process.env.hasOwnProperty(name)) { + return JSON.parse(process.env[name]) + } + return def +} + +function getConcurrentlyArgs(scripts) { + const colors = [ + 'bgBlue', + 'bgGreen', + 'bgMagenta', + 'bgCyan', + 'bgWhite', + 'bgRed', + 'bgBlack', + 'bgYellow', + ] + const prefixColors = Object.keys(scripts) + .reduce( + (pColors, _s, i) => + pColors.concat([`${colors[i % colors.length]}.bold.reset`]), + [], + ) + .join(',') + + // prettier-ignore + return [ + '--kill-others-on-fail', + '--prefix', '[{name}]', + '--names', Object.keys(scripts).join(','), + '--prefix-colors', prefixColors, + ...Object.values(scripts).map(s => JSON.stringify(s)), // stringify escapes quotes ✨ + ] +} + module.exports = { ifDevDep, ifPeerDep, @@ -48,5 +85,10 @@ module.exports = { hasPkgProp, appDirectory, fromRoot, + hasScript, resolveBin, + parseEnv, + getPkg, + hasFile, + getConcurrentlyArgs, }