feat: build --target lib/wc

This commit is contained in:
Evan You
2018-01-30 01:04:11 -05:00
parent 120d5c5256
commit faadadf5ad
11 changed files with 361 additions and 218 deletions

View File

@@ -97,7 +97,8 @@ module.exports = class Service {
'./config/base',
'./config/css',
'./config/dev',
'./config/prod'
'./config/prod',
'./config/app'
].map(idToPlugin)
if (inlinePlugins) {

View File

@@ -0,0 +1,3 @@
import Component from '~entry'
export default Component

View File

@@ -0,0 +1,6 @@
// TODO
import Vue from 'vue'
import Component from '~entry'
new Vue(Component)

View File

@@ -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.`)

View File

@@ -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')
]
}

View File

@@ -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')
]
}

View File

@@ -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', '.*']
}]])
}
})
}

View File

@@ -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]
}])
})
}

View File

@@ -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(

View File

@@ -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' })
}
})
}

View File

@@ -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
})