UNPKG

vbuild

Version:

Refined webpack development experience for Vue.js

405 lines (370 loc) 12 kB
const chalk = require('chalk') const webpack = require('webpack') const co = require('co') const fs = require('mz/fs') const HtmlPlugin = require('html-webpack-plugin') const PostCompilePlugin = require('post-compile-webpack-plugin') const ExtractTextPlugin = require('extract-text-webpack-plugin') const CopyPlugin = require('copy-webpack-plugin') const isYarn = require('installed-by-yarn-globally') const webpackMerge = require('webpack-merge') const dotenv = require('dotenv') const loadPostcssConfig = require('postcss-load-config') const {cwd, ownDir, getPublicPath, ensureEntry, inferHTML, readPkg} = require('./utils') const run = require('./run') const loaders = require('./loaders') const loadConfig = require('./load-config') const AppError = require('./app-error') const getConfig = co.wrap(function * (cliOptions = {}) { // eslint-disable-line complexity const userConfig = yield loadConfig(cliOptions) let defaultPostcssOptions = {} try { defaultPostcssOptions = yield loadPostcssConfig() .then(res => Object.assign({plugins: res.plugins}, res.options)) } catch (err) { if (err.message.indexOf('No PostCSS Config found') === -1) { throw err } } const pkg = readPkg() let options = Object.assign({ entry: pkg.main || 'index.js', dist: 'dist', homepage: pkg.homepage, html: {}, babel: { babelrc: true, cacheDirectory: true, presets: [require.resolve('babel-preset-vue-app')] }, postcss: defaultPostcssOptions, stats: { chunks: false, children: false, modules: false, colors: true }, copy: fs.existsSync('./static') ? [{ from: './static', dist: './' }] : [] }, userConfig, cliOptions) if (options.dev) { options = Object.assign({ host: 'localhost', port: 4000, hot: true, hmrEntry: ['client'], sourceMap: 'eval-source-map' }, options) } else { options = Object.assign({ vendor: true, minimize: true, sourceMap: 'source-map', extract: true, cleanDist: true }, options) } const postcssOptions = options.postcss if (options.autoprefixer !== false) { const autoprefixerPlugin = require('autoprefixer')(Object.assign({ browsers: ['ie > 8', 'last 4 versions'] }, options.autoprefixer)) // `postcss` is set and `autoprefixer` is not disabled // then we add `autoprefixer` to it // only consider this when it's `Array` or `Object` if (Array.isArray(postcssOptions)) { postcssOptions.push(autoprefixerPlugin) } else if (typeof postcssOptions === 'object' && postcssOptions.plugins) { postcssOptions.plugins.push(autoprefixerPlugin) } } // load env variables from let env = {} const stringifiedEnv = {} if (options.env !== false) { if (fs.existsSync('.env')) { console.log('> Using .env file') env = dotenv.parse(yield fs.readFile('.env', 'utf8')) } if (typeof options.env === 'object') { Object.assign(env, options.env) } } env.NODE_ENV = options.dev ? 'development' : 'production' for (const key in env) { stringifiedEnv[key] = JSON.stringify(env[key]) // we're not going to apply this to process.env in node.js process if (key !== 'NODE_ENV') { process.env[key] = env[key] } } if (options.entry === 'index.js' && !fs.existsSync(options.entry)) { throw new AppError(`Entry file ${chalk.yellow(options.entry)} does not exist, did you forget to create one?`) } const filename = getFilenames(options) const cssOptions = { extract: options.extract, sourceMap: Boolean(options.sourceMap), cssModules: options.cssModules } let webpackConfig = { entry: {client: []}, devtool: options.sourceMap, output: { path: cwd(options.dist), publicPath: getPublicPath(options.homepage, options.dev), filename: filename.js }, performance: { hints: false }, resolve: { extensions: ['.js', '.json', '.vue', '.css'], modules: [ cwd(), cwd('node_modules'), ownDir('node_modules') ] }, resolveLoader: { modules: [ cwd('node_modules'), ownDir('node_modules') ] }, module: { rules: [ { test: /\.js$/, loader: 'babel-loader', include(filepath) { // for anything outside node_modules if (filepath.split(/[/\\]/).indexOf('node_modules') === -1) { return true } // specific modules that need to be transpiled by babel if (options.transpileModules) { for (const name of options.transpileModules) { if (filepath.indexOf(`/node_modules/${name}/`) !== -1) { return true } } } } }, { test: /\.es6$/, loader: 'babel-loader' }, { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.(ico|jpg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/, loader: 'file-loader', query: { name: filename.static } }, { test: /\.(svg)(\?.*)?$/, loader: 'file-loader', query: { name: filename.static } } ].concat(loaders.styleLoaders(cssOptions)) }, plugins: [ new PostCompilePlugin(stats => { process.stdout.write('\x1Bc') if (options.dev && !options.watch) { if (stats.hasErrors() || stats.hasWarnings()) { console.log(stats.toString('errors-only')) console.log() console.log(chalk.bgRed.black(' ERROR '), 'Compiling failed!') } else { console.log(stats.toString(options.stats)) if (webpackConfig.target === 'electron-renderer') { console.log(chalk.bold(`\n> Open Electron in another tab\n`)) } else { console.log(chalk.bold(`\n> Open http://localhost:${options.port}\n`)) } console.log(chalk.bgGreen.black(' DONE '), 'Compiled successfully!') } console.log() } }), new webpack.DefinePlugin(Object.assign({ process: { env: stringifiedEnv } }, options.define)), new webpack.LoaderOptionsPlugin({ minimize: !options.dev && options.minimize, options: { context: cwd(), babel: options.babel, postcss: postcssOptions, vue: { postcss: postcssOptions, loaders: loaders.cssLoaders(cssOptions), cssModules: { localIndentName: '[name]__[local]___[hash:base64:5]' } } } }) ] } if (typeof options.entry === 'string') { webpackConfig.entry.client.push(options.entry) } else if (Array.isArray(options.entry)) { webpackConfig.entry.client = options.entry } else if (typeof options.entry === 'object') { webpackConfig.entry = ensureEntry(options.entry) } if (options.format === 'cjs') { webpackConfig.output.libraryTarget = 'commonjs2' webpackConfig.externals = [ // the modules in $cwd/node_modules require('webpack-node-externals')(), // modules that might be loaded from vbuild/node_modules 'vue', 'babel-runtime' ] } else if (options.format === 'umd') { webpackConfig.output.libraryTarget = 'umd' if (options.moduleName) { webpackConfig.output.library = options.moduleName } else { throw new AppError('> `moduleName` is required when bundling in `umd` format') } } if (options.html) { const htmlDefaults = inferHTML(options) if (Array.isArray(options.html)) { webpackConfig.plugins = webpackConfig.plugins.concat(options.html.map(html => new HtmlPlugin(Object.assign({ env }, htmlDefaults, html)))) } else { webpackConfig.plugins.push(new HtmlPlugin(Object.assign({ env }, htmlDefaults, options.html))) } } if (options.eslint) { webpackConfig.module.rules.push({ test: /\.(vue|js)$/, loader: 'eslint-loader', enforce: 'pre', exclude: [/node_modules/], options: Object.assign({ configFile: require.resolve('eslint-config-vue-app'), useEslintrc: false, fix: true }, options.eslintConfig) }) } if (options.extract) { webpackConfig.plugins.push(new ExtractTextPlugin(filename.css)) } // If `copy` is an array, copy specific folder // by default it copies ./static/** to ./dist/** if (Array.isArray(options.copy) && options.copy.length > 0) { webpackConfig.plugins.push(new CopyPlugin(options.copy)) } // installed by `yarn global add` if (isYarn(__dirname)) { // modules in yarn global node_modules // because of yarn's flat node_modules structure webpackConfig.resolve.modules.push(ownDir('..')) // loaders in yarn global node_modules webpackConfig.resolveLoader.modules.push(ownDir('..')) } if (cliOptions.dev) { if (options.hot && !options.watch) { const hmrEntry = options.hmrEntry for (const entry of hmrEntry) { webpackConfig.entry[entry] = Array.isArray(webpackConfig.entry[entry]) ? webpackConfig.entry[entry] : [webpackConfig.entry[entry]] webpackConfig.entry[entry].unshift(ownDir('lib/dev-client.es6')) } webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) } } else { const ProgressPlugin = require('webpack/lib/ProgressPlugin') const NoEmitOnErrorsPlugin = require('webpack/lib/NoEmitOnErrorsPlugin') webpackConfig.plugins.push( new ProgressPlugin(), new NoEmitOnErrorsPlugin() ) // minimize is `true` by default in production mode if (options.minimize) { webpackConfig.plugins.push( /* eslint-disable camelcase */ new webpack.optimize.UglifyJsPlugin({ sourceMap: Boolean(options.sourceMap), compressor: { warnings: false, conditionals: true, unused: true, comparisons: true, sequences: true, dead_code: true, evaluate: true, if_return: true, join_vars: true, negate_iife: false }, output: { comments: false } /* eslint-enable camelcase */ }) ) } // Do not split vendor in `cjs` and `umd` format if (options.vendor && !options.format) { webpackConfig.plugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: module => { return module.resource && /\.(js|css|es6)$/.test(module.resource) && module.resource.indexOf('node_modules') !== -1 } }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' }) ) } } // merge webpack config if (typeof options.webpack === 'function') { webpackConfig = options.webpack(webpackConfig) } else if (typeof options.webpack === 'object') { webpackConfig = webpackMerge.smart(webpackConfig, options.webpack) } return { webpackConfig, options } }) function getFilenames(options) { const excludeHash = options.dev || options.format return Object.assign({ js: excludeHash ? '[name].js' : '[name].[chunkhash:8].js', css: excludeHash ? '[name].css' : '[name].[contenthash:8].css', static: excludeHash ? 'static/[name].[ext]' : 'static/[name].[hash:8].[ext]' }, options.filename) } const vbuild = module.exports = co.wrap(function * (cliOptions) { console.log(chalk.bold('> Starting...')) const {webpackConfig, options} = yield getConfig(cliOptions) return run(webpackConfig, options) }) vbuild.getConfig = getConfig