Skip to content

Commit

Permalink
feat: build --target web-component (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jan 30, 2018
1 parent 27c4bc3 commit 6db7735
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 81 deletions.
2 changes: 1 addition & 1 deletion packages/@vue/cli-service-global/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ exports.build = (_entry, args) => {
const { context, entry } = resolveEntry(_entry)
const asLib = args.target && args.target !== 'app'
if (asLib) {
args.libEntry = entry
args.entry = entry
}
createService(context, entry, asLib).run('build', args)
}
5 changes: 4 additions & 1 deletion packages/@vue/cli-service-global/lib/createConfigPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ module.exports = function createConfigPlugin (context, entry, asLib) {
.clear()
.end()
.exclude
.add(/node_modules/)
.add(/node_modules|@vue\/cli-service/)
.end()
.uses
.delete('cache-loader')
.end()
.use('babel-loader')
.tap(() => babelOptions)
Expand Down
101 changes: 99 additions & 2 deletions packages/@vue/cli-service/lib/commands/build/entry-web-component.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,103 @@
// TODO
/* global HTMLElement */

import Vue from 'vue'
import Component from '~entry'

new Vue(Component)
// Name to register the custom element as. Must contain a hyphen.
const name = process.env.CUSTOM_ELEMENT_NAME

// Whether to keep the instance alive when element is removed from DOM.
// Default: false.
// - false: the instance is destroyed and recreated when element is removed / reinserted
// - true: the instance is always kept alive
const keepAlive = process.env.CUSTOM_ELEMENT_KEEP_ALIVE

// Whether to use Shadow DOM.
// default: true
const useShadowDOM = process.env.CUSTOM_ELEMENT_USE_SHADOW_DOM

const options = typeof Component === 'function'
? Component.options
: Component

const arrToObj = (arr, defaultValue) => arr.reduce((acc, key) => {
acc[key] = defaultValue
return acc
}, {})

const props = Array.isArray(options.props)
? arrToObj(options.props, {})
: options.props || {}
const propsList = Object.keys(props)

// TODO use ES5 syntax
class CustomElement extends HTMLElement {
static get observedAttributes () {
return propsList
}

constructor () {
super()

const data = arrToObj(propsList)
data._active = false
this._wrapper = new Vue({
data,
render: h => data._active
? h(Component, { props: this._data })
: null
})

this._attached = false
if (useShadowDOM) {
this._shadowRoot = this.attachShadow({ mode: 'open' })
}
}

connectedCallback () {
this._attached = true
if (!this._wrapper._isMounted) {
this._wrapper.$mount()
const el = this._wrapper.$el
if (useShadowDOM) {
this._shadowRoot.appendChild(el)
} else {
this.appendChild(el)
}
}
this._wrapper._data._active = true
}

disconnectedCallback () {
this._attached = false
const destroy = () => {
this._wrapper._data._active = false
}
if (!keepAlive) {
destroy()
} else if (typeof keepAlive === 'number') {
setTimeout(() => {
if (!this._attached) destroy()
}, keepAlive)
}
}

attributeChangedCallback (attrName, oldVal, newVal) {
this._wrapper._data[attrName] = newVal
}
}

propsList.forEach(key => {
Object.defineProperty(CustomElement.prototype, key, {
get () {
return this._wrapper._data[key]
},
set (newVal) {
this._wrapper._data[key] = newVal
},
enumerable: false,
configurable: true
})
})

window.customElements.define(name, CustomElement)
15 changes: 7 additions & 8 deletions packages/@vue/cli-service/lib/commands/build/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const defaults = {
mode: 'production',
target: 'app',
libEntry: 'src/App.vue'
entry: 'src/App.vue',
keepAlive: false,
shadow: true
}

module.exports = (api, options) => {
Expand All @@ -11,8 +13,10 @@ module.exports = (api, options) => {
options: {
'--mode': `specify env mode (default: ${defaults.mode})`,
'--target': `app | lib | web-component (default: ${defaults.target})`,
'--libEntry': `entry for lib or web-component (default: ${defaults.entry})`,
'--libName': `name for lib or web-component (default: "name" in package.json)`
'--entry': `entry for lib or web-component (default: ${defaults.entry})`,
'--name': `name for lib or web-component (default: "name" in package.json or entry filename)`,
'--keepAlive': `keep component alive when web-component is detached? (default: ${defaults.keepAlive})`,
'--shadow': `use shadow DOM when building as web-component? (default: ${defaults.shadow})`
}
}, args => {
for (const key in defaults) {
Expand All @@ -35,11 +39,6 @@ module.exports = (api, options) => {
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}...`)
}

Expand Down
81 changes: 44 additions & 37 deletions packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,63 @@
module.exports = (api, { libEntry, libName }) => {
const genConfig = (format, postfix = format) => {
api.chainWebpack(config => {
libName = libName || api.service.pkg.name || libEntry.replace(/\.(js|vue)$/, '')
module.exports = (api, { entry, name }) => {
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
// setting this disables app-only configs
process.env.VUE_CLI_TARGET = 'lib'
// inline all static asset files since there is no publicPath handling
process.env.VUE_CLI_INLINE_LIMIT = Infinity

api.chainWebpack(config => {
config.output
.filename(`[name].js`)
.library(libName)
.libraryExport('default')

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

function genConfig (format, postfix = format) {
api.chainWebpack(config => {
config.entryPoints.clear()
// set proxy entry for *.vue files
if (/\.vue$/.test(libEntry)) {
if (/\.vue$/.test(entry)) {
config
.entry(`${libName}.${postfix}`)
.add(require.resolve('./entry-lib.js'))
config.resolve
.alias
.set('~entry', api.resolve(libEntry))
.set('~entry', api.resolve(entry))
} else {
config
.entry(`${libName}.${postfix}`)
.add(api.resolve(libEntry))
.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()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,71 @@
module.exports = (api, { libEntry, libName }) => {
const genConfig = postfix => {
api.chainWebpack(config => {
libName = libName || api.service.pkg.name || libEntry.replace(/\.(js|vue)$/, '')
module.exports = (api, { entry, name, keepAlive, shadow }) => {
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
if (libName.indexOf('-') < 0) {
const { log, error } = require('@vue/cli-shared-utils')
log()
error(`--name must contain a hyphen when building as web-component. (got "${libName}")`)
process.exit(1)
}

// setting this disables app-only configs
process.env.VUE_CLI_TARGET = 'web-component'
// inline all static asset files since there is no publicPath handling
process.env.VUE_CLI_INLINE_LIMIT = Infinity

api.chainWebpack(config => {
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'
})

config
.plugin('web-component-options')
.use(require('webpack/lib/DefinePlugin'), [{
'process.env': {
CUSTOM_ELEMENT_NAME: JSON.stringify(libName),
CUSTOM_ELEMENT_KEEP_ALIVE: keepAlive,
CUSTOM_ELEMENT_USE_SHADOW_DOM: shadow
}
}])

// TODO handle CSS (insert in shadow DOM)
})

function genConfig (postfix) {
postfix = postfix ? `.${postfix}` : ``
api.chainWebpack(config => {
config.entryPoints.clear()
// set proxy entry for *.vue files
if (/\.vue$/.test(libEntry)) {
if (/\.vue$/.test(entry)) {
config
.entry(`${libName}.${postfix}`)
.entry(`${libName}${postfix}`)
.add(require.resolve('./entry-web-component.js'))
config.resolve
.alias
.set('~entry', api.resolve(libEntry))
.set('~entry', api.resolve(entry))
} else {
config
.entry(`${libName}.${postfix}`)
.add(api.resolve(libEntry))
.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')
genConfig(''),
genConfig('min')
]
}
4 changes: 3 additions & 1 deletion packages/@vue/cli/bin/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ program
program
.command('build [entry]')
.option('-t, --target <target>', 'Build target (app | lib | web-component, default: app)')
.option('-n, --libName <name>', 'name for lib or web-component')
.option('-n, --name <name>', 'name for lib or web-component (default: entry filename)')
.option('--keepAlive', 'keep component alive when web-component is detached? (default: false)')
.option('--shadow', 'use shadow DOM when building as web-component? (default: true)')
.description('build a .js or .vue file in production mode with zero config')
.action((entry, cmd) => {
loadCommand('build', '@vue/cli-service-global').build(entry, cleanArgs(cmd))
Expand Down

0 comments on commit 6db7735

Please sign in to comment.