diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index 149f211a4..95ac51526 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -97,7 +97,8 @@ module.exports = class Service { './config/base', './config/css', './config/dev', - './config/prod' + './config/prod', + './config/app' ].map(idToPlugin) if (inlinePlugins) { diff --git a/packages/@vue/cli-service/lib/commands/build/entry-lib.js b/packages/@vue/cli-service/lib/commands/build/entry-lib.js new file mode 100644 index 000000000..a24421718 --- /dev/null +++ b/packages/@vue/cli-service/lib/commands/build/entry-lib.js @@ -0,0 +1,3 @@ +import Component from '~entry' + +export default Component diff --git a/packages/@vue/cli-service/lib/commands/build/entry-web-component.js b/packages/@vue/cli-service/lib/commands/build/entry-web-component.js new file mode 100644 index 000000000..cc750ddc3 --- /dev/null +++ b/packages/@vue/cli-service/lib/commands/build/entry-web-component.js @@ -0,0 +1,6 @@ +// TODO + +import Vue from 'vue' +import Component from '~entry' + +new Vue(Component) diff --git a/packages/@vue/cli-service/lib/commands/build.js b/packages/@vue/cli-service/lib/commands/build/index.js similarity index 61% rename from packages/@vue/cli-service/lib/commands/build.js rename to packages/@vue/cli-service/lib/commands/build/index.js index 2ace94d68..4ea19a569 100644 --- a/packages/@vue/cli-service/lib/commands/build.js +++ b/packages/@vue/cli-service/lib/commands/build/index.js @@ -1,6 +1,7 @@ const defaults = { mode: 'production', - target: 'app' + target: 'app', + entry: 'src/App.vue' } module.exports = (api, options) => { @@ -8,27 +9,37 @@ module.exports = (api, options) => { description: 'build for production', usage: 'vue-cli-service build [options]', options: { - '--mode': `specify env mode (default: ${defaults.mode})` - // TODO build target - // '--target': `app | lib | web-component (default: ${defaults.target})`, - // '--format': `How the lib is exposed (esm, umd, cjs, amd). Default: esm` - // '--name': `Library name for umd/iife export` + '--mode': `specify env mode (default: ${defaults.mode})`, + '--target': `app | lib | web-component (default: ${defaults.target})`, + '--entry': `entry for lib or web-component (default: ${defaults.entry})`, + '--name': `name for lib or web-component (default: "name" in package.json)` } }, args => { - api.setMode(args.mode || defaults.mode) + args = Object.assign({}, defaults, args) + api.setMode(args.mode) const chalk = require('chalk') const rimraf = require('rimraf') const webpack = require('webpack') const { + log, done, info, logWithSpinner, stopSpinner } = require('@vue/cli-shared-utils') - console.log() - logWithSpinner(`Building for production...`) + log() + if (args.target === 'app') { + logWithSpinner(`Building for production...`) + } else { + // setting this disables app-only configs + process.env.VUE_CLI_TARGET = args.target + // when building as a lib, inline all static asset files + // since there is no publicPath handling + process.env.VUE_CLI_INLINE_LIMIT = Infinity + logWithSpinner(`Building for production as ${args.target}...`) + } return new Promise((resolve, reject) => { const targetDir = api.resolve(options.outputDir) @@ -36,7 +47,14 @@ module.exports = (api, options) => { if (err) { return reject(err) } - const webpackConfig = api.resolveWebpackConfig() + let webpackConfig + if (args.target === 'lib') { + webpackConfig = require('./resolveLibConfig')(api, args) + } else if (args.target === 'web-component') { + webpackConfig = require('./resolveWebComponentConfig')(api, args) + } else { + webpackConfig = api.resolveWebpackConfig() + } webpack(webpackConfig, (err, stats) => { stopSpinner(false) if (err) { @@ -57,7 +75,7 @@ module.exports = (api, options) => { return reject(`Build failed with errors.`) } - if (!args.silent) { + if (!args.silent && args.target === 'app') { done(`Build complete. The ${chalk.cyan(options.outputDir)} directory is ready to be deployed.\n`) if (options.baseUrl === '/') { info(`The app is built assuming that it will be deployed at the root of a domain.`) diff --git a/packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js b/packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js new file mode 100644 index 000000000..7641319d5 --- /dev/null +++ b/packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js @@ -0,0 +1,62 @@ +module.exports = (api, { entry, name }) => { + const genConfig = (format, postfix = format) => { + api.chainWebpack(config => { + const libName = name || api.service.pkg.name + + config.entryPoints.clear() + // set proxy entry for *.vue files + if (/\.vue$/.test(entry)) { + config + .entry(`${libName}.${postfix}`) + .add(require.resolve('./entry-lib.js')) + config.resolve + .alias + .set('~entry', api.resolve(entry)) + } else { + config + .entry(`${libName}.${postfix}`) + .add(api.resolve(entry)) + } + + config.output + .filename(`[name].js`) + .library(libName) + .libraryExport('default') + .libraryTarget(format) + + // adjust css output name + config + .plugin('extract-css') + .tap(args => { + args[0].filename = `${libName}.css` + return args + }) + + // only minify min entry + config + .plugin('uglify') + .tap(args => { + args[0].include = /\.min\.js$/ + return args + }) + + // externalize Vue in case user imports it + config + .externals({ + vue: { + commonjs: 'vue', + commonjs2: 'vue', + root: 'Vue' + } + }) + }) + + return api.resolveWebpackConfig() + } + + return [ + genConfig('commonjs2', 'common'), + genConfig('umd'), + genConfig('umd', 'umd.min') + ] +} diff --git a/packages/@vue/cli-service/lib/commands/build/resolveWebComponentConfig.js b/packages/@vue/cli-service/lib/commands/build/resolveWebComponentConfig.js new file mode 100644 index 000000000..5efaa064f --- /dev/null +++ b/packages/@vue/cli-service/lib/commands/build/resolveWebComponentConfig.js @@ -0,0 +1,48 @@ +module.exports = (api, { entry, name }) => { + const genConfig = postfix => { + api.chainWebpack(config => { + const libName = name || api.service.pkg.name + + config.entryPoints.clear() + // set proxy entry for *.vue files + if (/\.vue$/.test(entry)) { + config + .entry(`${libName}.${postfix}`) + .add(require.resolve('./entry-web-component.js')) + config.resolve + .alias + .set('~entry', api.resolve(entry)) + } else { + config + .entry(`${libName}.${postfix}`) + .add(api.resolve(entry)) + } + + config.output + .filename(`[name].js`) + + // only minify min entry + config + .plugin('uglify') + .tap(args => { + args[0].include = /\.min\.js$/ + return args + }) + + // externalize Vue in case user imports it + config + .externals({ + vue: 'Vue' + }) + + // TODO handle CSS (insert in shadow DOM) + }) + + return api.resolveWebpackConfig() + } + + return [ + genConfig('web-component'), + genConfig('web-component.min') + ] +} diff --git a/packages/@vue/cli-service/lib/config/app.js b/packages/@vue/cli-service/lib/config/app.js new file mode 100644 index 000000000..5e72c6c81 --- /dev/null +++ b/packages/@vue/cli-service/lib/config/app.js @@ -0,0 +1,156 @@ +// config that are specific to --target app + +module.exports = (api, options) => { + api.chainWebpack(webpackConfig => { + // only apply when there's no alternative target + if (process.env.VUE_CLI_TARGET) { + return + } + + // inject preload/prefetch to HTML + const PreloadPlugin = require('../webpack/PreloadPlugin') + webpackConfig + .plugin('preload') + .use(PreloadPlugin, [{ + rel: 'preload', + include: 'initial', + fileBlacklist: [/\.map$/, /hot-update\.js$/] + }]) + + webpackConfig + .plugin('prefetch') + .use(PreloadPlugin, [{ + rel: 'prefetch', + include: 'asyncChunks' + }]) + + // HTML plugin + const fs = require('fs') + const htmlPath = api.resolve('public/index.html') + const resolveClientEnv = require('../util/resolveClientEnv') + webpackConfig + .plugin('html') + .use(require('html-webpack-plugin'), [ + Object.assign( + fs.existsSync(htmlPath) ? { template: htmlPath } : {}, + // expose client env to html template + { env: resolveClientEnv(options.baseUrl, true /* raw */) } + ) + ]) + + if (process.env.NODE_ENV === 'production') { + // minify HTML + webpackConfig + .plugin('html') + .tap(([options]) => [Object.assign(options, { + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true + // more options: + // https://github.com/kangax/html-minifier#options-quick-reference + }, + // necessary to consistently work with multiple chunks via CommonsChunkPlugin + chunksSortMode: 'dependency' + })]) + + // Code splitting configs for better long-term caching + // This needs to be updated when upgrading to webpack 4 + const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin') + + if (!options.dll) { + // extract vendor libs into its own chunk for better caching, since they + // are more likely to stay the same. + webpackConfig + .plugin('split-vendor') + .use(CommonsChunkPlugin, [{ + name: 'vendor', + minChunks (module) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf(`node_modules`) > -1 + ) + } + }]) + + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + webpackConfig + .plugin('split-manifest') + .use(CommonsChunkPlugin, [{ + name: 'manifest', + minChunks: Infinity + }]) + + // inline the manifest chunk into HTML + webpackConfig + .plugin('inline-manifest') + .use(require('../webpack/InlineSourcePlugin'), [{ + include: /manifest\..*\.js$/ + }]) + + // since manifest is inlined, don't preload it anymore + webpackConfig + .plugin('preload') + .tap(([options]) => { + options.fileBlacklist.push(/manifest\..*\.js$/) + return [options] + }) + } + + // This CommonsChunkPlugin instance extracts shared chunks from async + // chunks and bundles them in a separate chunk, similar to the vendor chunk + // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk + webpackConfig + .plugin('split-vendor-async') + .use(CommonsChunkPlugin, [{ + name: 'app', + async: 'vendor-async', + children: true, + minChunks: 3 + }]) + + // DLL + if (options.dll) { + const webpack = require('webpack') + const UglifyPlugin = require('uglifyjs-webpack-plugin') + const getUglifyOptions = require('./uglifyOptions') + const dllEntries = Array.isArray(options.dll) + ? options.dll + : Object.keys(api.service.pkg.dependencies) + + webpackConfig + .plugin('dll') + .use(require('autodll-webpack-plugin'), [{ + inject: true, + inherit: true, + path: 'js/', + context: api.resolve('.'), + filename: '[name].[hash:8].js', + entry: { + 'vendor': [ + ...dllEntries, + 'vue-loader/lib/component-normalizer' + ] + }, + plugins: [ + new webpack.DefinePlugin(resolveClientEnv(options.baseUrl)), + new UglifyPlugin(getUglifyOptions(options)) + ] + }]) + .after('preload') + } + + // copy static assets in public/ + webpackConfig + .plugin('copy') + .use(require('copy-webpack-plugin'), [[{ + from: api.resolve('public'), + to: api.resolve(options.outputDir), + ignore: ['index.html', '.*'] + }]]) + } + }) +} diff --git a/packages/@vue/cli-service/lib/config/base.js b/packages/@vue/cli-service/lib/config/base.js index d49f0a1e5..b3253e74e 100644 --- a/packages/@vue/cli-service/lib/config/base.js +++ b/packages/@vue/cli-service/lib/config/base.js @@ -1,8 +1,7 @@ module.exports = (api, options) => { api.chainWebpack(webpackConfig => { - const fs = require('fs') const resolveLocal = require('../util/resolveLocal') - const resolveClientEnv = require('../util/resolveClientEnv') + const inlineLimit = process.env.VUE_CLI_INLINE_LIMIT || 1000 webpackConfig .context(api.service.context) @@ -53,7 +52,7 @@ module.exports = (api, options) => { .use('url-loader') .loader('url-loader') .options({ - limit: 10000, + limit: inlineLimit, name: `img/[name].[hash:8].[ext]` }) @@ -74,7 +73,7 @@ module.exports = (api, options) => { .use('url-loader') .loader('url-loader') .options({ - limit: 10000, + limit: inlineLimit, name: `media/[name].[hash:8].[ext]` }) @@ -84,7 +83,7 @@ module.exports = (api, options) => { .use('url-loader') .loader('url-loader') .options({ - limit: 10000, + limit: inlineLimit, name: `fonts/[name].[hash:8].[ext]` }) @@ -102,34 +101,7 @@ module.exports = (api, options) => { child_process: 'empty' }) - // inject preload/prefetch to HTML - const PreloadPlugin = require('../webpack/PreloadPlugin') - webpackConfig - .plugin('preload') - .use(PreloadPlugin, [{ - rel: 'preload', - include: 'initial', - fileBlacklist: [/\.map$/, /hot-update\.js$/] - }]) - - webpackConfig - .plugin('prefetch') - .use(PreloadPlugin, [{ - rel: 'prefetch', - include: 'asyncChunks' - }]) - - const htmlPath = api.resolve('public/index.html') - webpackConfig - .plugin('html') - .use(require('html-webpack-plugin'), [ - Object.assign( - fs.existsSync(htmlPath) ? { template: htmlPath } : {}, - // expose client env to html template - { env: resolveClientEnv(options.baseUrl, true /* raw */) } - ) - ]) - + const resolveClientEnv = require('../util/resolveClientEnv') webpackConfig .plugin('define') .use(require('webpack/lib/DefinePlugin'), [ @@ -143,5 +115,15 @@ module.exports = (api, options) => { webpackConfig .plugin('case-sensitive-paths') .use(require('case-sensitive-paths-webpack-plugin')) + + // friendly error plugin displays very confusing errors when webpack + // fails to resolve a loader, so we provide custom handlers to improve it + const { transformer, formatter } = require('../webpack/resolveLoaderError') + webpackConfig + .plugin('firendly-errors') + .use(require('friendly-errors-webpack-plugin'), [{ + additionalTransformers: [transformer], + additionalFormatters: [formatter] + }]) }) } diff --git a/packages/@vue/cli-service/lib/config/dev.js b/packages/@vue/cli-service/lib/config/dev.js index 94a6d0825..37970270c 100644 --- a/packages/@vue/cli-service/lib/config/dev.js +++ b/packages/@vue/cli-service/lib/config/dev.js @@ -18,16 +18,6 @@ module.exports = api => { .plugin('no-emit-on-errors') .use(require('webpack/lib/NoEmitOnErrorsPlugin')) - // friendly error plugin displays very confusing errors when webpack - // fails to resolve a loader, so we provide custom handlers to improve it - const { transformer, formatter } = require('../webpack/resolveLoaderError') - webpackConfig - .plugin('firendly-errors') - .use(require('friendly-errors-webpack-plugin'), [{ - additionalTransformers: [transformer], - additionalFormatters: [formatter] - }]) - webpackConfig .plugin('watch-missing') .use( diff --git a/packages/@vue/cli-service/lib/config/prod.js b/packages/@vue/cli-service/lib/config/prod.js index 0abc5f774..551e07afc 100644 --- a/packages/@vue/cli-service/lib/config/prod.js +++ b/packages/@vue/cli-service/lib/config/prod.js @@ -17,21 +17,6 @@ module.exports = (api, options) => { .plugin('module-concatenation') .use(require('webpack/lib/optimize/ModuleConcatenationPlugin')) - // minify HTML - webpackConfig - .plugin('html') - .tap(([options]) => [Object.assign(options, { - minify: { - removeComments: true, - collapseWhitespace: true, - removeAttributeQuotes: true - // more options: - // https://github.com/kangax/html-minifier#options-quick-reference - }, - // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency' - })]) - // optimize CSS (dedupe) webpackConfig .plugin('optimize-css') @@ -43,159 +28,13 @@ module.exports = (api, options) => { // minify JS const UglifyPlugin = require('uglifyjs-webpack-plugin') - const uglifyPluginOptions = { - uglifyOptions: { - compress: { - // turn off flags with small gains to speed up minification - arrows: false, - collapse_vars: false, // 0.3kb - comparisons: false, - computed_props: false, - hoist_funs: false, - hoist_props: false, - hoist_vars: false, - inline: false, - loops: false, - negate_iife: false, - properties: false, - reduce_funcs: false, - reduce_vars: false, - switches: false, - toplevel: false, - typeofs: false, - - // a few flags with noticable gains/speed ratio - // numbers based on out of the box vendor bundle - booleans: true, // 0.7kb - if_return: true, // 0.4kb - sequences: true, // 0.7kb - unused: true, // 2.3kb - - // required features to drop conditional branches - conditionals: true, - dead_code: true, - evaluate: true - } - }, - sourceMap: options.productionSourceMap, - cache: true, - parallel: true - } + const getUglifyOptions = require('./uglifyOptions') // disable during tests to speed things up if (!process.env.VUE_CLI_TEST) { webpackConfig .plugin('uglify') - .use(UglifyPlugin, [uglifyPluginOptions]) + .use(UglifyPlugin, [getUglifyOptions(options)]) } - - const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin') - - // Chunk splits - if (!options.dll) { - // extract vendor libs into its own chunk for better caching, since they - // are more likely to stay the same. - webpackConfig - .plugin('split-vendor') - .use(CommonsChunkPlugin, [{ - name: 'vendor', - minChunks (module) { - // any required modules inside node_modules are extracted to vendor - return ( - module.resource && - /\.js$/.test(module.resource) && - module.resource.indexOf(`node_modules`) > -1 - ) - } - }]) - - // extract webpack runtime and module manifest to its own file in order to - // prevent vendor hash from being updated whenever app bundle is updated - webpackConfig - .plugin('split-manifest') - .use(CommonsChunkPlugin, [{ - name: 'manifest', - minChunks: Infinity - }]) - - // inline the manifest chunk into HTML - webpackConfig - .plugin('inline-manifest') - .use(require('../webpack/InlineSourcePlugin'), [{ - include: /manifest\..*\.js$/ - }]) - - // since manifest is inlined, don't preload it anymore - webpackConfig - .plugin('preload') - .tap(([options]) => { - options.fileBlacklist.push(/manifest\..*\.js$/) - return [options] - }) - } - - // This CommonsChunkPlugin instance extracts shared chunks from async - // chunks and bundles them in a separate chunk, similar to the vendor chunk - // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk - webpackConfig - .plugin('split-vendor-async') - .use(CommonsChunkPlugin, [{ - name: 'app', - async: 'vendor-async', - children: true, - minChunks: 3 - }]) - - // DLL - if (options.dll) { - const webpack = require('webpack') - const resolveClientEnv = require('../util/resolveClientEnv') - const dllEntries = Array.isArray(options.dll) - ? options.dll - : Object.keys(api.service.pkg.dependencies) - - webpackConfig - .plugin('dll') - .use(require('autodll-webpack-plugin'), [{ - inject: true, - inherit: true, - path: 'js/', - context: api.resolve('.'), - filename: '[name].[hash:8].js', - entry: { - 'vendor': [ - ...dllEntries, - 'vue-loader/lib/component-normalizer' - ] - }, - plugins: [ - new webpack.DefinePlugin(resolveClientEnv(options.baseUrl)), - new UglifyPlugin(uglifyPluginOptions) - ] - }]) - .after('preload') - } - - // copy static assets in public/ - webpackConfig - .plugin('copy') - .use(require('copy-webpack-plugin'), [[{ - from: api.resolve('public'), - to: api.resolve(options.outputDir), - ignore: ['index.html', '.*'] - }]]) - - // TODO parallelazation - // thread-loader doesn't seem to have obvious effect because vue-loader - // offloads most of the work to other loaders. We may need to re-think - // vue-loader implementation in order to better take advantage of - // parallelazation - - // webpackConfig.module - // .rule('vue') - // .use('thread-loader') - // .before('vue-loader') - // .loader('thread-loader') - // .options({ name: 'vue' }) } }) } diff --git a/packages/@vue/cli-service/lib/config/uglifyOptions.js b/packages/@vue/cli-service/lib/config/uglifyOptions.js new file mode 100644 index 000000000..29ea84f2f --- /dev/null +++ b/packages/@vue/cli-service/lib/config/uglifyOptions.js @@ -0,0 +1,38 @@ +module.exports = options => ({ + uglifyOptions: { + compress: { + // turn off flags with small gains to speed up minification + arrows: false, + collapse_vars: false, // 0.3kb + comparisons: false, + computed_props: false, + hoist_funs: false, + hoist_props: false, + hoist_vars: false, + inline: false, + loops: false, + negate_iife: false, + properties: false, + reduce_funcs: false, + reduce_vars: false, + switches: false, + toplevel: false, + typeofs: false, + + // a few flags with noticable gains/speed ratio + // numbers based on out of the box vendor bundle + booleans: true, // 0.7kb + if_return: true, // 0.4kb + sequences: true, // 0.7kb + unused: true, // 2.3kb + + // required features to drop conditional branches + conditionals: true, + dead_code: true, + evaluate: true + } + }, + sourceMap: options.productionSourceMap, + cache: true, + parallel: true +})