├── .gitattributes ├── test ├── fixtures │ ├── babel │ │ ├── with-config │ │ │ ├── .babelrc │ │ │ └── index.js │ │ └── object-rest-spread │ │ │ └── index.js │ ├── default.js │ ├── banner │ │ ├── index.js │ │ └── package.json │ ├── defaults │ │ └── index.js │ ├── uglify │ │ └── index.js │ ├── exclude-file │ │ ├── foo.js │ │ └── index.js │ ├── extend-options │ │ ├── bar.js │ │ └── foo.js │ ├── no-js-transform │ │ └── index.js │ ├── target │ │ └── node │ │ │ └── index.js │ ├── buble │ │ ├── react-jsx.js │ │ ├── vue-jsx.js │ │ ├── async.js │ │ └── async-dot-dot-dot.js │ ├── async │ │ └── index.js │ └── inline │ │ └── index.js ├── demo │ └── index.js ├── index.test.js └── __snapshots__ │ └── index.test.js.snap ├── babel.js ├── examples ├── cli │ ├── index.js │ ├── cli.js │ └── package.json ├── with-css │ ├── index.js │ ├── style.css │ └── package.json └── with-vue-component │ ├── Component.vue │ └── package.json ├── .gitignore ├── .vscode └── settings.json ├── src ├── util.js ├── bili-error.js ├── emoji.js ├── template.js ├── progress-plugin.js ├── handle-error.js ├── virtual-modules-plugin.js ├── get-config.js ├── get-banner.js ├── cli.js ├── babel.js ├── logger.js └── index.js ├── .bilirc ├── .editorconfig ├── docs ├── recipes │ ├── inline-3rd-party-modules.md │ ├── transpile-css-files.md │ ├── transpile-vue-files.md │ ├── update-package-json.md │ ├── use-rollup-plugins.md │ ├── transpile-js-files.md │ └── bundle-multi-files.md ├── README.md ├── index.html └── api.md ├── .babelrc ├── circle.yml ├── LICENSE ├── package.json ├── README.md └── media └── preview.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /test/fixtures/babel/with-config/.babelrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/default.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/babel') 2 | -------------------------------------------------------------------------------- /examples/cli/index.js: -------------------------------------------------------------------------------- 1 | export default () => 42 2 | -------------------------------------------------------------------------------- /examples/with-css/index.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | -------------------------------------------------------------------------------- /test/fixtures/banner/index.js: -------------------------------------------------------------------------------- 1 | export default 42 2 | -------------------------------------------------------------------------------- /test/fixtures/defaults/index.js: -------------------------------------------------------------------------------- 1 | export default 42 2 | -------------------------------------------------------------------------------- /test/fixtures/uglify/index.js: -------------------------------------------------------------------------------- 1 | export const a = 1 2 | -------------------------------------------------------------------------------- /test/fixtures/exclude-file/foo.js: -------------------------------------------------------------------------------- 1 | export default 'foo' 2 | -------------------------------------------------------------------------------- /examples/with-css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/extend-options/bar.js: -------------------------------------------------------------------------------- 1 | export default 'bar' 2 | -------------------------------------------------------------------------------- /test/fixtures/extend-options/foo.js: -------------------------------------------------------------------------------- 1 | export default 'foo' 2 | -------------------------------------------------------------------------------- /test/fixtures/no-js-transform/index.js: -------------------------------------------------------------------------------- 1 | export default () => {} -------------------------------------------------------------------------------- /test/fixtures/babel/with-config/index.js: -------------------------------------------------------------------------------- 1 | export default class {} 2 | -------------------------------------------------------------------------------- /test/fixtures/target/node/index.js: -------------------------------------------------------------------------------- 1 | export default class Foo {} 2 | -------------------------------------------------------------------------------- /test/fixtures/buble/react-jsx.js: -------------------------------------------------------------------------------- 1 | export default
hi
2 | -------------------------------------------------------------------------------- /test/fixtures/babel/object-rest-spread/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ...a 3 | } -------------------------------------------------------------------------------- /test/fixtures/exclude-file/index.js: -------------------------------------------------------------------------------- 1 | import foo from './foo' 2 | 3 | export default foo 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | examples/*/yarn.lock 5 | examples/*/package-lock.json 6 | coverage/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/async/index.js: -------------------------------------------------------------------------------- 1 | const a = { a: 1 } 2 | 3 | export default async () => { 4 | return { ...a } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/buble/vue-jsx.js: -------------------------------------------------------------------------------- 1 | export default { 2 | render() { 3 | return
hi
4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import main from '.' 3 | 4 | const answer = main() 5 | console.log(answer) 6 | -------------------------------------------------------------------------------- /test/fixtures/buble/async.js: -------------------------------------------------------------------------------- 1 | const a = async () => ({ a: 'a' }) 2 | 3 | export default a().then(res => ({ ...res })) 4 | -------------------------------------------------------------------------------- /test/fixtures/inline/index.js: -------------------------------------------------------------------------------- 1 | import all from 'p-all' 2 | import map from 'p-map' 3 | 4 | export default { all, map } 5 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const relativePath = p => path.relative(process.cwd(), path.resolve(p)) 4 | -------------------------------------------------------------------------------- /test/fixtures/buble/async-dot-dot-dot.js: -------------------------------------------------------------------------------- 1 | export default async function () { 2 | return { 3 | ...(await this.bar()) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/demo/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import cac from 'cac' 3 | 4 | class Foo { 5 | async foo() {} 6 | } 7 | 8 | console.log(cac()) 9 | -------------------------------------------------------------------------------- /src/bili-error.js: -------------------------------------------------------------------------------- 1 | export default class BiliError extends Error { 2 | constructor(message) { 3 | super(message) 4 | this.name = 'BiliError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "bili *.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.bilirc: -------------------------------------------------------------------------------- 1 | { 2 | "input": "src/{index,cli,babel}.js", 3 | "filename": "[name].js", 4 | "babel": { 5 | "babelrc": false 6 | }, 7 | "target": "node", 8 | "external": ["./src/index.js"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-css", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "bili index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/banner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "0.0.0", 4 | "year": 2015, 5 | "license": "MIT", 6 | "author": { 7 | "name": "name", 8 | "email": "email@email.com" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /docs/recipes/inline-3rd-party-modules.md: -------------------------------------------------------------------------------- 1 | # Inline 3rd-party modules 2 | 3 | By default Bili inlines all 3rd-party modules in `umd` and `iife` format into your bundle, however you can use [`inline`](/api#inline) option to toggle this. 4 | 5 | ```bash 6 | # Don't inline modules in UMD format 7 | bili --format umd --no-inline 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/with-vue-component/Component.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": 6 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | [ 14 | "@babel/plugin-proposal-object-rest-spread", 15 | { 16 | "useBuiltIns": true 17 | } 18 | ] 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/emoji.js: -------------------------------------------------------------------------------- 1 | const supportsEmoji = 2 | process.platform !== 'win32' || process.env.TERM === 'xterm-256color' 3 | 4 | // Fallback symbols for Windows from https://en.wikipedia.org/wiki/Code_page_437 5 | export default { 6 | progress: supportsEmoji ? '⏳' : '∞', 7 | success: supportsEmoji ? '✨' : '√', 8 | error: supportsEmoji ? '🚨' : '×', 9 | warning: supportsEmoji ? '⚠️' : '‼' 10 | } 11 | -------------------------------------------------------------------------------- /examples/with-vue-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-component", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "bili Component.vue --plugin vue" 8 | }, 9 | "devDependencies": { 10 | "rollup-plugin-vue": "3.0.0" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.13", 14 | "vue-template-compiler": "^2.5.13" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/recipes/transpile-css-files.md: -------------------------------------------------------------------------------- 1 | # Transpile CSS files 2 | 3 | `Bili` supports CSS by default with the help from [rollup-plugin-postcss](https://github.com/egoist/rollup-plugin-postcss): 4 | 5 | The default options for `rollup-plugin-postcss`: 6 | 7 | ```js 8 | { 9 | extract: true 10 | } 11 | ``` 12 | 13 | By default CSS files will be extracted to the same location where the JS is generated but with `.css` extension. 14 | -------------------------------------------------------------------------------- /docs/recipes/transpile-vue-files.md: -------------------------------------------------------------------------------- 1 | # Transpile Vue files 2 | 3 | You can use [rollup-plugin-vue](https://github.com/vuejs/rollup-plugin-vue) to achieve this. 4 | 5 | ```bash 6 | cd my-project 7 | yarn add rollup-plugin-vue --dev 8 | bili --plugin vue 9 | # or with options 10 | bili --plugin vue --vue.css dist/style.css 11 | ``` 12 | 13 | By default `~/dev/vue-final-formmasterbili--formatumd,umd-min--module-nameVueFinalForm--global.final-formfinal-formBundling1file:src/index.jsBundling2files:src/Form.jsBundling3files:node_modules/nano-assign/dist/nano-assign.common.jsBundling4files:src/utils.jsBundling5files:src/Field.jsBuiltin1.23s.┌──────────────────────────────────────────────┐filesizegzipsize│dist/vue-final-form.js4.96KB1.66KB│dist/vue-final-form.min.js2.83KB1.06KB└──────────────────────────────────────────────┘clearclearbili--formatumd,umd-min--module-nameVueFinalForm--global.final-formfinal-form -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import path from 'path' 3 | import EventEmitter from 'events' 4 | import globby from 'globby' 5 | import fs from 'fs-extra' 6 | import chalk from 'chalk' 7 | import { rollup, watch } from 'rollup' 8 | import camelcase from 'camelcase' 9 | import bytes from 'bytes' 10 | import gzipSize from 'gzip-size' 11 | import stringWidth from 'string-width' 12 | import boxen from 'boxen' 13 | import nodeResolvePlugin from 'rollup-plugin-node-resolve' 14 | import commonjsPlugin from 'rollup-plugin-commonjs' 15 | import jsonPlugin from 'rollup-plugin-json' 16 | import uglifyPlugin from 'rollup-plugin-uglify' 17 | import aliasPlugin from 'rollup-plugin-alias' 18 | import replacePlugin from 'rollup-plugin-replace' 19 | import hashbangPlugin from 'rollup-plugin-hashbang' 20 | import isBuiltinModule from 'is-builtin-module' 21 | import textTable from 'text-table' 22 | import resolveFrom from 'resolve-from' 23 | import isCI from 'is-ci' 24 | import virtualModulesPlugin from './virtual-modules-plugin' 25 | import progressPlugin from './progress-plugin' 26 | import template from './template' 27 | import getBanner from './get-banner' 28 | import { getBabelConfig, getBiliConfig } from './get-config' 29 | import BiliError from './bili-error' 30 | import { handleError, getDocRef } from './handle-error' 31 | import logger from './logger' 32 | import emoji from './emoji' 33 | import { relativePath } from './util' 34 | 35 | const FORMATS = ['cjs'] 36 | 37 | const prettyBytes = v => bytes.format(v, { unitSeparator: ' ' }) 38 | 39 | export default class Bili extends EventEmitter { 40 | static async generate(options) { 41 | const bundle = await new Bili(options).bundle({ write: false }) 42 | return bundle 43 | } 44 | 45 | static async write(options) { 46 | const bundler = new Bili(options) 47 | const startTime = Date.now() 48 | try { 49 | await bundler.bundle() 50 | const buildTime = Date.now() - startTime 51 | const time = 52 | buildTime < 1000 ? 53 | `${buildTime}ms` : 54 | `${(buildTime / 1000).toFixed(2)}s` 55 | 56 | if (!options.watch) { 57 | logger.status(emoji.success, chalk.green(`Built in ${time}.`)) 58 | logger.log(await bundler.stats()) 59 | } 60 | return bundler 61 | } catch (err) { 62 | bundler.handleError(err) 63 | } 64 | } 65 | 66 | constructor(options = {}) { 67 | super() 68 | logger.setOptions(options) 69 | this.options = { 70 | outDir: 'dist', 71 | filename: '[name][suffix].js', 72 | cwd: process.cwd(), 73 | target: 'browser', 74 | js: 'babel', 75 | babel: {}, 76 | ...(options.config !== false && getBiliConfig()), 77 | ...options 78 | } 79 | this.babelPresetOptions = { 80 | objectAssign: this.options.objectAssign, 81 | jsx: this.options.jsx, 82 | target: this.options.target, 83 | buble: false 84 | } 85 | this.options.babel = { 86 | ...getBabelConfig( 87 | this.options.cwd, 88 | this.options.babel.babelrc === false, 89 | this.babelPresetOptions 90 | ), 91 | ...this.options.babel 92 | } 93 | this.pkg = readPkg(this.options.cwd) 94 | this.pkgName = this.pkg.name && this.pkg.name.replace(/^@.+\//, '') 95 | this.bundles = {} 96 | this.cssBundles = new Map() 97 | 98 | this.handleError = err => handleError(err) 99 | } 100 | 101 | async stats() { 102 | const { bundles } = this 103 | const { sizeLimit } = this.options 104 | let leading = '' 105 | let sizeExceeded = false 106 | const sizes = await Promise.all(Object.keys(bundles) 107 | .sort() 108 | .map(async filepath => { 109 | const { code, relative, formatFull } = bundles[filepath] 110 | const gzipSizeNumber = await gzipSize(code) 111 | const expectedSize = 112 | sizeLimit && 113 | sizeLimit[formatFull] && 114 | bytes.parse(sizeLimit[formatFull]) 115 | let sizeInfo 116 | if (expectedSize && gzipSizeNumber > expectedSize) { 117 | process.exitCode = 1 118 | sizeExceeded = true 119 | sizeInfo = chalk.red(` threshold: ${prettyBytes(expectedSize)}`) 120 | } else { 121 | sizeInfo = '' 122 | } 123 | return [ 124 | relative, 125 | prettyBytes(code.length), 126 | chalk.green(prettyBytes(gzipSizeNumber)) + sizeInfo 127 | ] 128 | })) 129 | 130 | if (sizeExceeded) { 131 | leading = chalk.red(`${ 132 | emoji.error 133 | } Bundle size exceeded the limit, check below for details.\n`) 134 | } 135 | 136 | await Promise.all(Array.from(this.cssBundles.keys()) 137 | .sort() 138 | .map(async id => { 139 | const bundle = this.cssBundles.get(id) 140 | sizes.push([ 141 | path.relative(process.cwd(), bundle.filepath), 142 | prettyBytes(bundle.code.length), 143 | chalk.green(prettyBytes(await gzipSize(bundle.code))) 144 | ]) 145 | })) 146 | 147 | return ( 148 | leading + 149 | boxen(textTable( 150 | [['file', 'size', 'gzip size'].map(v => chalk.bold(v)), ...sizes], 151 | { 152 | stringLength: stringWidth 153 | } 154 | )) 155 | ) 156 | } 157 | 158 | resolveCwd(...args) { 159 | return path.resolve(this.options.cwd, ...args) 160 | } 161 | 162 | loadUserPlugins({ plugins, filename }) { 163 | // eslint-disable-next-line array-callback-return 164 | return plugins.map(pluginName => { 165 | // In bili.config.js or you're using the API 166 | // You can require rollup plugin directly 167 | if (typeof pluginName === 'object') { 168 | return pluginName 169 | } 170 | 171 | let pluginOptions = this.options[pluginName] 172 | if (pluginName === 'vue') { 173 | pluginOptions = { 174 | include: ['**/*.vue'], 175 | // Let rollup-plugin-postcss handle external CSS dependencies 176 | autoStyles: false, 177 | styleToImports: true, 178 | css: path.resolve( 179 | this.options.outDir, 180 | filename.replace(/\.[^.]+$/, '.css') 181 | ), 182 | ...pluginOptions 183 | } 184 | } 185 | const moduleName = isPath(pluginName) ? 186 | path.resolve(pluginName) : 187 | `rollup-plugin-${pluginName}` 188 | try { 189 | // TODO: 190 | // Local require is always relative to `process.cwd()` 191 | // Instead of `this.options.cwd` 192 | // We need to ensure that which is actually better 193 | return localRequire(moduleName)(pluginOptions) 194 | } catch (err) { 195 | handleLoadPluginError(moduleName, err) 196 | } 197 | }) 198 | } 199 | 200 | async writeCSS() { 201 | await Promise.all(Array.from(this.cssBundles.keys()).map(id => { 202 | const { code, map, filepath } = this.cssBundles.get(id) 203 | return Promise.all([ 204 | fs.writeFile(filepath, code, 'utf8'), 205 | map && fs.writeFile(`${filepath}.map`, map, 'utf8') 206 | ]) 207 | })) 208 | } 209 | 210 | getJsOptions(name, pluginOptions) { 211 | if (name === 'babel') { 212 | return this.options.babel 213 | } 214 | 215 | if (name === 'typescript' || name === 'typescript2') { 216 | let typescript 217 | try { 218 | typescript = localRequire('typescript') 219 | } catch (err) {} 220 | return { 221 | typescript, 222 | ...pluginOptions 223 | } 224 | } 225 | 226 | if (name === 'buble') { 227 | return { 228 | ...pluginOptions, 229 | transforms: { 230 | // Skip transforming for..of 231 | forOf: false, 232 | ...(pluginOptions && pluginOptions.transforms) 233 | } 234 | } 235 | } 236 | 237 | return pluginOptions 238 | } 239 | 240 | // eslint-disable-next-line complexity 241 | async createConfig( 242 | { input, format, formatFull, compress }, 243 | { multipleEntries } 244 | ) { 245 | const options = this.options.extendOptions ? 246 | this.options.extendOptions(this.options, { 247 | input, 248 | format, 249 | compress 250 | }) : 251 | this.options 252 | 253 | logger.debug(chalk.bold(`Bili options for ${input} in ${formatFull}:\n`) + 254 | util.inspect(options, { colors: true })) 255 | 256 | if (typeof options !== 'object') { 257 | throw new BiliError('You must return the options in `extendOptions` method!') 258 | } 259 | 260 | const { 261 | outDir, 262 | filename, 263 | inline = format === 'umd' || format === 'iife' 264 | } = options 265 | 266 | const sourceMap = typeof options.map === 'boolean' ? options.map : compress 267 | 268 | const outFilename = getFilename({ 269 | input, 270 | format, 271 | filename, 272 | compress, 273 | // If it's not bundling multi-entry 274 | // The name can fallback to pkg name 275 | name: options.name || (!multipleEntries && this.pkgName) 276 | }) 277 | // The path to output file 278 | // Relative to `this.options.cwd` 279 | const file = this.resolveCwd(outDir, outFilename) 280 | 281 | const transformJS = options.js !== false 282 | const jsPluginName = transformJS && getJsPluginName(options.js, input) 283 | const jsPlugin = transformJS && getJsPlugin(jsPluginName) 284 | const jsOptions = 285 | transformJS && this.getJsOptions(jsPluginName, options[jsPluginName]) 286 | 287 | const banner = getBanner(options.banner, this.pkg) 288 | 289 | let external = getArrayOption(options, 'external') || [] 290 | external = external.map(e => (e.startsWith('./') ? path.resolve(options.cwd, e) : e)) 291 | let globals = options.globals || options.global 292 | if (typeof globals === 'object') { 293 | external = [...new Set(external.concat(Object.keys(globals)))] 294 | } 295 | 296 | let env = options.env 297 | if (format === 'umd' || format === 'iife') { 298 | env = { 299 | NODE_ENV: compress ? 'production' : 'development', 300 | ...env 301 | } 302 | } 303 | 304 | const inputOptions = { 305 | input, 306 | external, 307 | onwarn: err => { 308 | if (options.quiet) return 309 | 310 | if (typeof err === 'string') { 311 | return logger.warn(err) 312 | } 313 | 314 | const { loc, frame, message, code, source } = err 315 | 316 | if (options.quiet || code === 'THIS_IS_UNDEFINED') { 317 | return 318 | } 319 | // Unresolved modules 320 | // If `inline` is not trusty there will always be this warning 321 | // But we only need this when the module is not installed 322 | // i.e. does not exist on disk 323 | if (code === 'UNRESOLVED_IMPORT' && source) { 324 | if ( 325 | // Skip sub path for now 326 | source.indexOf('/') === -1 && 327 | // Skip built-in modules 328 | !isBuiltinModule(source) && 329 | // Check if the module exists 330 | resolveFrom.silent(process.cwd(), source) === null 331 | ) { 332 | logger.warn(`Module "${source}" was not installed, you may run "${chalk.cyan(`${getPackageManager()} add ${source}`)}" to install it!`) 333 | } 334 | return 335 | } 336 | // print location if applicable 337 | if (loc) { 338 | logger.warn(`${loc.file} (${loc.line}:${loc.column}) ${message}`) 339 | if (frame) logger.warn(chalk.dim(frame)) 340 | } else { 341 | logger.warn(message) 342 | } 343 | }, 344 | plugins: [ 345 | !isCI && 346 | process.stderr.isTTY && 347 | process.env.NODE_ENV !== 'test' && 348 | options.progress !== false && 349 | progressPlugin(), 350 | hashbangPlugin(), 351 | options.virtualModules && 352 | virtualModulesPlugin(options.virtualModules, this.options.cwd), 353 | ...this.loadUserPlugins({ 354 | filename: outFilename, 355 | plugins: getArrayOption(options, 'plugin') || [] 356 | }), 357 | jsonPlugin(), 358 | require('rollup-plugin-postcss')({ 359 | extract: true, 360 | minimize: compress, 361 | sourceMap, 362 | ...options.postcss, 363 | onExtract: getExtracted => { 364 | // Use `z` `a` to ensure the order when we log the stats 365 | const id = `${input}::${compress ? 'z-compressed' : 'a-normal'}` 366 | if (!this.cssBundles.has(id)) { 367 | // Don't really need suffix for format 368 | const filepath = this.resolveCwd( 369 | outDir, 370 | outFilename.replace( 371 | /(\.(iife|cjs|es))?(\.min)?\.js$/, 372 | compress ? '.min.css' : '.css' 373 | ) 374 | ) 375 | const bundle = getExtracted(filepath) 376 | 377 | this.cssBundles.set(id, { 378 | ...bundle, 379 | filepath 380 | }) 381 | } 382 | // We extract CSS but never atually let `rollup-plugin-postcss` write to disk 383 | // To prevent from duplicated css files 384 | return false 385 | } 386 | }), 387 | transformJS && 388 | jsPluginName === 'buble' && 389 | require('rollup-plugin-babel')({ 390 | include: '**/*.js', 391 | exclude: 'node_modules/**', 392 | babelrc: false, 393 | presets: [ 394 | [ 395 | require.resolve('./babel'), 396 | { 397 | ...this.babelPresetOptions, 398 | buble: true 399 | } 400 | ] 401 | ] 402 | }), 403 | transformJS && 404 | jsPlugin({ 405 | exclude: 'node_modules/**', 406 | ...jsOptions 407 | }), 408 | inline && 409 | nodeResolvePlugin({ 410 | module: true, 411 | extensions: ['.js', '.json'], 412 | preferBuiltIns: true, 413 | browser: !options.target.startsWith('node'), 414 | ...options.nodeResolve 415 | }), 416 | commonjsPlugin(options.commonjs), 417 | compress && 418 | uglifyPlugin({ 419 | ...options.uglify, 420 | output: { 421 | ...(options.uglify && options.uglify.output), 422 | // Add banner (if there is) 423 | preamble: banner 424 | } 425 | }), 426 | options.alias && aliasPlugin(options.alias), 427 | options.replace && replacePlugin(options.replace), 428 | { 429 | name: 'bili', 430 | ongenerate: (_, { code }) => { 431 | this.bundles[file] = { 432 | relative: path.relative(process.cwd(), file), 433 | input, 434 | format, 435 | formatFull, 436 | compress, 437 | code 438 | } 439 | } 440 | }, 441 | env && 442 | Object.keys(env).length > 0 && 443 | replacePlugin({ 444 | values: Object.keys(env).reduce((res, key) => { 445 | res[`process.env.${key}`] = JSON.stringify(env[key]) 446 | return res 447 | }, {}) 448 | }) 449 | ].filter(v => Boolean(v)) 450 | } 451 | 452 | const outputOptions = { 453 | format, 454 | globals, 455 | name: this.getModuleName(format), 456 | file, 457 | banner, 458 | exports: options.exports, 459 | sourcemap: sourceMap 460 | } 461 | 462 | return { 463 | inputOptions, 464 | outputOptions 465 | } 466 | } 467 | 468 | async bundle({ write = true } = {}) { 469 | let inputFiles = this.options.input 470 | if (!inputFiles || inputFiles.length === 0) { 471 | inputFiles = ['src/index.js'] 472 | } 473 | if (!Array.isArray(inputFiles)) { 474 | inputFiles = [inputFiles] 475 | } 476 | 477 | const magicPatterns = [] 478 | inputFiles = inputFiles.filter(v => { 479 | if (globby.hasMagic(v)) { 480 | magicPatterns.push(v) 481 | return false 482 | } 483 | return true 484 | }) 485 | 486 | await globby(magicPatterns, { cwd: this.options.cwd }).then(files => { 487 | inputFiles = inputFiles 488 | .concat(files) 489 | .map(v => relativePath(this.resolveCwd(v))) 490 | }) 491 | 492 | if (inputFiles.length === 0) { 493 | throw new BiliError('No matched files to bundle.') 494 | } 495 | 496 | const formats = getArrayOption(this.options, 'format') || FORMATS 497 | 498 | const options = inputFiles.reduce( 499 | (res, input) => [ 500 | ...res, 501 | ...formats.map(format => { 502 | const compress = format.endsWith('-min') 503 | return { 504 | input, 505 | format: format.replace(/-min$/, ''), 506 | formatFull: format, 507 | compress 508 | } 509 | }) 510 | ], 511 | [] 512 | ) 513 | 514 | const multipleEntries = inputFiles.length > 1 515 | const actions = options.map(async option => { 516 | const { inputOptions, outputOptions } = await this.createConfig(option, { 517 | multipleEntries 518 | }) 519 | 520 | logger.debug(chalk.bold(`Rollup input options for bundling ${option.input} in ${ 521 | option.formatFull 522 | }:\n`) + util.inspect(inputOptions, { colors: true })) 523 | 524 | logger.debug(chalk.bold(`Rollup output options for bundling ${option.input} in ${ 525 | option.formatFull 526 | }:\n`) + util.inspect(outputOptions, { colors: true })) 527 | 528 | if (this.options.watch) { 529 | const watcher = watch({ 530 | ...inputOptions, 531 | output: outputOptions, 532 | watch: { 533 | clearScreen: true 534 | } 535 | }) 536 | watcher.on('event', async e => { 537 | if (e.code === 'ERROR' || e.code === 'FATAL') { 538 | handleError(e.error) 539 | } 540 | if (e.code === 'BUNDLE_END') { 541 | process.exitCode = 0 542 | logger.write(await this.stats()) 543 | } 544 | }) 545 | return 546 | } 547 | 548 | const bundle = await rollup(inputOptions) 549 | if (write) return bundle.write(outputOptions) 550 | return bundle.generate(outputOptions) 551 | }) 552 | 553 | await Promise.all(actions) 554 | 555 | // Since we update `this.bundles` in Rollup plugin's `ongenerate` callback 556 | // We have to put follow code into another callback to execute at th end of call stack 557 | await nextTick() 558 | const bundleCount = Object.keys(this.bundles).length 559 | 560 | if (bundleCount < formats.length * inputFiles.length) { 561 | const hasName = this.options.filename.includes('[name]') 562 | const hasSuffix = this.options.filename.includes('[suffix]') 563 | const msg = `Multiple files are emitting to the same path.\nPlease check if ${ 564 | hasName || inputFiles.length === 1 ? 565 | '' : 566 | `${chalk.green('[name]')}${hasSuffix ? '' : ' or '}` 567 | }${hasSuffix ? '' : chalk.green('[suffix]')} is missing in ${chalk.green('filename')} option.\n${getDocRef('api', 'filename')}` 568 | 569 | throw new BiliError(msg) 570 | } 571 | 572 | // Write potential CSS files 573 | await this.writeCSS() 574 | 575 | return this 576 | } 577 | 578 | getModuleName(format) { 579 | if (format !== 'umd' && format !== 'iife') return undefined 580 | return ( 581 | this.options.moduleName || 582 | this.pkg.moduleName || 583 | (this.pkgName ? camelcase(this.pkgName) : undefined) 584 | ) 585 | } 586 | } 587 | 588 | function getSuffix(format) { 589 | let suffix = '' 590 | switch (format) { 591 | case 'cjs': 592 | suffix += '.cjs' 593 | break 594 | case 'umd': 595 | break 596 | case 'es': 597 | suffix += '.es' 598 | break 599 | case 'iife': 600 | suffix += '.iife' 601 | break 602 | default: 603 | throw new Error('unsupported format') 604 | } 605 | return suffix 606 | } 607 | 608 | function getNameFromInput(input) { 609 | return path.basename(input, path.extname(input)) 610 | } 611 | 612 | function getFilename({ input, format, filename, compress, name }) { 613 | name = name || getNameFromInput(input) 614 | const suffix = getSuffix(format) 615 | const res = template(filename, { name, suffix }) 616 | return compress ? 617 | path.basename(res, path.extname(res)) + '.min' + path.extname(res) : 618 | res 619 | } 620 | 621 | function getJsPlugin(name) { 622 | let req 623 | if (isPath(name)) { 624 | req = require 625 | name = path.resolve(name) 626 | } else { 627 | req = name === 'babel' || name === 'buble' ? require : localRequire 628 | name = `rollup-plugin-${name}` 629 | } 630 | try { 631 | return req(name) 632 | } catch (err) { 633 | handleLoadPluginError(name, err) 634 | } 635 | } 636 | 637 | function localRequire(name) { 638 | return require(path.resolve('node_modules', name)) 639 | } 640 | 641 | function isPath(v) { 642 | return /^[./]|(^[a-zA-Z]:)/.test(v) 643 | } 644 | 645 | function handleLoadPluginError(moduleName, err) { 646 | if (err.code === 'MODULE_NOT_FOUND' && err.message.includes(moduleName)) { 647 | let msg = `Cannot find plugin "${chalk.cyan(moduleName)}" in current directory!` 648 | const pm = getPackageManager() 649 | const command = 650 | pm === 'yarn' ? 651 | `yarn add ${moduleName} --dev` : 652 | `npm install -D ${moduleName}` 653 | if (!isPath(moduleName)) { 654 | msg += `\n${chalk.dim(`You may run "${command}" to install it.`)}` 655 | } 656 | throw new BiliError(msg) 657 | } else { 658 | throw err 659 | } 660 | } 661 | 662 | function nextTick() { 663 | return new Promise(resolve => { 664 | process.nextTick(() => { 665 | resolve() 666 | }) 667 | }) 668 | } 669 | 670 | function getArrayOption(options, name) { 671 | const option = options[name] || options[`${name}s`] 672 | if (typeof option === 'string') return option.split(',') 673 | return option 674 | } 675 | 676 | let packageManager 677 | 678 | function getPackageManager() { 679 | if (packageManager) return packageManager 680 | packageManager = fs.existsSync('yarn.lock') ? 'yarn' : 'npm' 681 | return packageManager 682 | } 683 | 684 | function readPkg(cwd = process.cwd()) { 685 | try { 686 | return require(path.resolve(cwd, 'package.json')) 687 | } catch (err) { 688 | if (err.code === 'MODULE_NOT_FOUND') { 689 | return {} 690 | } 691 | throw err 692 | } 693 | } 694 | 695 | function getJsPluginName(name, input) { 696 | if (name) { 697 | return name 698 | } 699 | 700 | if (input.endsWith('.ts')) { 701 | return 'typescript2' 702 | } 703 | 704 | return 'babel' 705 | } 706 | --------------------------------------------------------------------------------