mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-05-01 09:21:03 -05:00
feat: build --target web-component (WIP)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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) {
|
||||
@@ -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}...`)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user