Skip to content

Commit 6db7735

Browse files
committed
feat: build --target web-component (WIP)
1 parent 27c4bc3 commit 6db7735

File tree

7 files changed

+212
-81
lines changed

7 files changed

+212
-81
lines changed

packages/@vue/cli-service-global/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ exports.build = (_entry, args) => {
5858
const { context, entry } = resolveEntry(_entry)
5959
const asLib = args.target && args.target !== 'app'
6060
if (asLib) {
61-
args.libEntry = entry
61+
args.entry = entry
6262
}
6363
createService(context, entry, asLib).run('build', args)
6464
}

packages/@vue/cli-service-global/lib/createConfigPlugin.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ module.exports = function createConfigPlugin (context, entry, asLib) {
7171
.clear()
7272
.end()
7373
.exclude
74-
.add(/node_modules/)
74+
.add(/node_modules|@vue\/cli-service/)
75+
.end()
76+
.uses
77+
.delete('cache-loader')
7578
.end()
7679
.use('babel-loader')
7780
.tap(() => babelOptions)
Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,103 @@
1-
// TODO
1+
/* global HTMLElement */
22

33
import Vue from 'vue'
44
import Component from '~entry'
55

6-
new Vue(Component)
6+
// Name to register the custom element as. Must contain a hyphen.
7+
const name = process.env.CUSTOM_ELEMENT_NAME
8+
9+
// Whether to keep the instance alive when element is removed from DOM.
10+
// Default: false.
11+
// - false: the instance is destroyed and recreated when element is removed / reinserted
12+
// - true: the instance is always kept alive
13+
const keepAlive = process.env.CUSTOM_ELEMENT_KEEP_ALIVE
14+
15+
// Whether to use Shadow DOM.
16+
// default: true
17+
const useShadowDOM = process.env.CUSTOM_ELEMENT_USE_SHADOW_DOM
18+
19+
const options = typeof Component === 'function'
20+
? Component.options
21+
: Component
22+
23+
const arrToObj = (arr, defaultValue) => arr.reduce((acc, key) => {
24+
acc[key] = defaultValue
25+
return acc
26+
}, {})
27+
28+
const props = Array.isArray(options.props)
29+
? arrToObj(options.props, {})
30+
: options.props || {}
31+
const propsList = Object.keys(props)
32+
33+
// TODO use ES5 syntax
34+
class CustomElement extends HTMLElement {
35+
static get observedAttributes () {
36+
return propsList
37+
}
38+
39+
constructor () {
40+
super()
41+
42+
const data = arrToObj(propsList)
43+
data._active = false
44+
this._wrapper = new Vue({
45+
data,
46+
render: h => data._active
47+
? h(Component, { props: this._data })
48+
: null
49+
})
50+
51+
this._attached = false
52+
if (useShadowDOM) {
53+
this._shadowRoot = this.attachShadow({ mode: 'open' })
54+
}
55+
}
56+
57+
connectedCallback () {
58+
this._attached = true
59+
if (!this._wrapper._isMounted) {
60+
this._wrapper.$mount()
61+
const el = this._wrapper.$el
62+
if (useShadowDOM) {
63+
this._shadowRoot.appendChild(el)
64+
} else {
65+
this.appendChild(el)
66+
}
67+
}
68+
this._wrapper._data._active = true
69+
}
70+
71+
disconnectedCallback () {
72+
this._attached = false
73+
const destroy = () => {
74+
this._wrapper._data._active = false
75+
}
76+
if (!keepAlive) {
77+
destroy()
78+
} else if (typeof keepAlive === 'number') {
79+
setTimeout(() => {
80+
if (!this._attached) destroy()
81+
}, keepAlive)
82+
}
83+
}
84+
85+
attributeChangedCallback (attrName, oldVal, newVal) {
86+
this._wrapper._data[attrName] = newVal
87+
}
88+
}
89+
90+
propsList.forEach(key => {
91+
Object.defineProperty(CustomElement.prototype, key, {
92+
get () {
93+
return this._wrapper._data[key]
94+
},
95+
set (newVal) {
96+
this._wrapper._data[key] = newVal
97+
},
98+
enumerable: false,
99+
configurable: true
100+
})
101+
})
102+
103+
window.customElements.define(name, CustomElement)

packages/@vue/cli-service/lib/commands/build/index.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const defaults = {
22
mode: 'production',
33
target: 'app',
4-
libEntry: 'src/App.vue'
4+
entry: 'src/App.vue',
5+
keepAlive: false,
6+
shadow: true
57
}
68

79
module.exports = (api, options) => {
@@ -11,8 +13,10 @@ module.exports = (api, options) => {
1113
options: {
1214
'--mode': `specify env mode (default: ${defaults.mode})`,
1315
'--target': `app | lib | web-component (default: ${defaults.target})`,
14-
'--libEntry': `entry for lib or web-component (default: ${defaults.entry})`,
15-
'--libName': `name for lib or web-component (default: "name" in package.json)`
16+
'--entry': `entry for lib or web-component (default: ${defaults.entry})`,
17+
'--name': `name for lib or web-component (default: "name" in package.json or entry filename)`,
18+
'--keepAlive': `keep component alive when web-component is detached? (default: ${defaults.keepAlive})`,
19+
'--shadow': `use shadow DOM when building as web-component? (default: ${defaults.shadow})`
1620
}
1721
}, args => {
1822
for (const key in defaults) {
@@ -35,11 +39,6 @@ module.exports = (api, options) => {
3539
if (args.target === 'app') {
3640
logWithSpinner(`Building for production...`)
3741
} else {
38-
// setting this disables app-only configs
39-
process.env.VUE_CLI_TARGET = args.target
40-
// when building as a lib, inline all static asset files
41-
// since there is no publicPath handling
42-
process.env.VUE_CLI_INLINE_LIMIT = Infinity
4342
logWithSpinner(`Building for production as ${args.target}...`)
4443
}
4544

packages/@vue/cli-service/lib/commands/build/resolveLibConfig.js

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,63 @@
1-
module.exports = (api, { libEntry, libName }) => {
2-
const genConfig = (format, postfix = format) => {
3-
api.chainWebpack(config => {
4-
libName = libName || api.service.pkg.name || libEntry.replace(/\.(js|vue)$/, '')
1+
module.exports = (api, { entry, name }) => {
2+
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
3+
// setting this disables app-only configs
4+
process.env.VUE_CLI_TARGET = 'lib'
5+
// inline all static asset files since there is no publicPath handling
6+
process.env.VUE_CLI_INLINE_LIMIT = Infinity
7+
8+
api.chainWebpack(config => {
9+
config.output
10+
.filename(`[name].js`)
11+
.library(libName)
12+
.libraryExport('default')
13+
14+
// adjust css output name
15+
config
16+
.plugin('extract-css')
17+
.tap(args => {
18+
args[0].filename = `${libName}.css`
19+
return args
20+
})
21+
22+
// only minify min entry
23+
config
24+
.plugin('uglify')
25+
.tap(args => {
26+
args[0].include = /\.min\.js$/
27+
return args
28+
})
29+
30+
// externalize Vue in case user imports it
31+
config
32+
.externals({
33+
vue: {
34+
commonjs: 'vue',
35+
commonjs2: 'vue',
36+
root: 'Vue'
37+
}
38+
})
39+
})
540

41+
function genConfig (format, postfix = format) {
42+
api.chainWebpack(config => {
643
config.entryPoints.clear()
744
// set proxy entry for *.vue files
8-
if (/\.vue$/.test(libEntry)) {
45+
if (/\.vue$/.test(entry)) {
946
config
1047
.entry(`${libName}.${postfix}`)
1148
.add(require.resolve('./entry-lib.js'))
1249
config.resolve
1350
.alias
14-
.set('~entry', api.resolve(libEntry))
51+
.set('~entry', api.resolve(entry))
1552
} else {
1653
config
1754
.entry(`${libName}.${postfix}`)
18-
.add(api.resolve(libEntry))
55+
.add(api.resolve(entry))
1956
}
2057

2158
config.output
22-
.filename(`[name].js`)
23-
.library(libName)
24-
.libraryExport('default')
2559
.libraryTarget(format)
26-
27-
// adjust css output name
28-
config
29-
.plugin('extract-css')
30-
.tap(args => {
31-
args[0].filename = `${libName}.css`
32-
return args
33-
})
34-
35-
// only minify min entry
36-
config
37-
.plugin('uglify')
38-
.tap(args => {
39-
args[0].include = /\.min\.js$/
40-
return args
41-
})
42-
43-
// externalize Vue in case user imports it
44-
config
45-
.externals({
46-
vue: {
47-
commonjs: 'vue',
48-
commonjs2: 'vue',
49-
root: 'Vue'
50-
}
51-
})
5260
})
53-
5461
return api.resolveWebpackConfig()
5562
}
5663

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,71 @@
1-
module.exports = (api, { libEntry, libName }) => {
2-
const genConfig = postfix => {
3-
api.chainWebpack(config => {
4-
libName = libName || api.service.pkg.name || libEntry.replace(/\.(js|vue)$/, '')
1+
module.exports = (api, { entry, name, keepAlive, shadow }) => {
2+
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
3+
if (libName.indexOf('-') < 0) {
4+
const { log, error } = require('@vue/cli-shared-utils')
5+
log()
6+
error(`--name must contain a hyphen when building as web-component. (got "${libName}")`)
7+
process.exit(1)
8+
}
9+
10+
// setting this disables app-only configs
11+
process.env.VUE_CLI_TARGET = 'web-component'
12+
// inline all static asset files since there is no publicPath handling
13+
process.env.VUE_CLI_INLINE_LIMIT = Infinity
14+
15+
api.chainWebpack(config => {
16+
config.output
17+
.filename(`[name].js`)
18+
19+
// only minify min entry
20+
config
21+
.plugin('uglify')
22+
.tap(args => {
23+
args[0].include = /\.min\.js$/
24+
return args
25+
})
26+
27+
// externalize Vue in case user imports it
28+
config
29+
.externals({
30+
vue: 'Vue'
31+
})
532

33+
config
34+
.plugin('web-component-options')
35+
.use(require('webpack/lib/DefinePlugin'), [{
36+
'process.env': {
37+
CUSTOM_ELEMENT_NAME: JSON.stringify(libName),
38+
CUSTOM_ELEMENT_KEEP_ALIVE: keepAlive,
39+
CUSTOM_ELEMENT_USE_SHADOW_DOM: shadow
40+
}
41+
}])
42+
43+
// TODO handle CSS (insert in shadow DOM)
44+
})
45+
46+
function genConfig (postfix) {
47+
postfix = postfix ? `.${postfix}` : ``
48+
api.chainWebpack(config => {
649
config.entryPoints.clear()
750
// set proxy entry for *.vue files
8-
if (/\.vue$/.test(libEntry)) {
51+
if (/\.vue$/.test(entry)) {
952
config
10-
.entry(`${libName}.${postfix}`)
53+
.entry(`${libName}${postfix}`)
1154
.add(require.resolve('./entry-web-component.js'))
1255
config.resolve
1356
.alias
14-
.set('~entry', api.resolve(libEntry))
57+
.set('~entry', api.resolve(entry))
1558
} else {
1659
config
17-
.entry(`${libName}.${postfix}`)
18-
.add(api.resolve(libEntry))
60+
.entry(`${libName}${postfix}`)
61+
.add(api.resolve(entry))
1962
}
20-
21-
config.output
22-
.filename(`[name].js`)
23-
24-
// only minify min entry
25-
config
26-
.plugin('uglify')
27-
.tap(args => {
28-
args[0].include = /\.min\.js$/
29-
return args
30-
})
31-
32-
// externalize Vue in case user imports it
33-
config
34-
.externals({
35-
vue: 'Vue'
36-
})
37-
38-
// TODO handle CSS (insert in shadow DOM)
3963
})
40-
4164
return api.resolveWebpackConfig()
4265
}
4366

4467
return [
45-
genConfig('web-component'),
46-
genConfig('web-component.min')
68+
genConfig(''),
69+
genConfig('min')
4770
]
4871
}

packages/@vue/cli/bin/vue.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ program
6565
program
6666
.command('build [entry]')
6767
.option('-t, --target <target>', 'Build target (app | lib | web-component, default: app)')
68-
.option('-n, --libName <name>', 'name for lib or web-component')
68+
.option('-n, --name <name>', 'name for lib or web-component (default: entry filename)')
69+
.option('--keepAlive', 'keep component alive when web-component is detached? (default: false)')
70+
.option('--shadow', 'use shadow DOM when building as web-component? (default: true)')
6971
.description('build a .js or .vue file in production mode with zero config')
7072
.action((entry, cmd) => {
7173
loadCommand('build', '@vue/cli-service-global').build(entry, cleanArgs(cmd))

0 commit comments

Comments
 (0)