Skip to content

Commit 1c4943b

Browse files
committed
feat: improve build lib/web-component
1 parent 5ad8fae commit 1c4943b

16 files changed

+176
-150
lines changed

packages/@vue/cli-service-global/__tests__/globalServiceBuildLib.spec.js

Whitespace-only changes.

packages/@vue/cli-service-global/__tests__/globalServiceBuildWebComponent.spec.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
jest.setTimeout(30000)
2+
3+
const path = require('path')
4+
const portfinder = require('portfinder')
5+
const { createServer } = require('http-server')
6+
const { defaultPreset } = require('@vue/cli/lib/options')
7+
const create = require('@vue/cli-test-utils/createTestProject')
8+
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
9+
10+
let server, browser, page
11+
test('build as lib', async () => {
12+
const project = await create('e2e-build-lib', defaultPreset)
13+
14+
const { stdout } = await project.run('vue-cli-service build --traget lib --name testLib')
15+
expect(stdout).toMatch('Build complete.')
16+
17+
expect(project.has('dist/index.html')).toBe(true)
18+
expect(project.has('dist/favicon.ico')).toBe(true)
19+
expect(project.has('dist/js')).toBe(true)
20+
expect(project.has('dist/css')).toBe(true)
21+
22+
const index = await project.read('dist/index.html')
23+
// should preload app.js & vendor.js
24+
expect(index).toMatch(/<link rel=preload [^>]+app[^>]+\.js>/)
25+
expect(index).toMatch(/<link rel=preload [^>]+vendor[^>]+\.js>/)
26+
27+
const vendorFile = index.match(/<link rel=preload [^>]+(vendor[^>]+\.js)>/)[1]
28+
const vendor = await project.read(`dist/js/${vendorFile}`)
29+
expect(vendor).toMatch(`router-link`)
30+
expect(vendor).toMatch(`vuex`)
31+
32+
const port = await portfinder.getPortPromise()
33+
server = createServer({ root: path.join(project.dir, 'dist') })
34+
35+
await new Promise((resolve, reject) => {
36+
server.listen(port, err => {
37+
if (err) return reject(err)
38+
resolve()
39+
})
40+
})
41+
42+
const launched = await launchPuppeteer(`http://localhost:${port}/`)
43+
browser = launched.browser
44+
page = launched.page
45+
46+
const h1Text = await page.evaluate(() => {
47+
return document.querySelector('h1').textContent
48+
})
49+
50+
expect(h1Text).toMatch('Welcome to Your Vue.js App')
51+
})
52+
53+
afterAll(async () => {
54+
await browser.close()
55+
server.close()
56+
})

packages/@vue/cli-service/__tests__/buildWebComponent.spec.js

Whitespace-only changes.

packages/@vue/cli-service/lib/PluginAPI.js

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ module.exports = class PluginAPI {
5151
return this.service.resolveWebpackConfig()
5252
}
5353

54+
resolveChainableWebpackConfig () {
55+
return this.service.resolveChainableWebpackConfig()
56+
}
57+
5458
configureDevServer (fn) {
5559
this.service.devServerConfigFns.push(fn)
5660
}

packages/@vue/cli-service/lib/Service.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ module.exports = class Service {
1616
constructor (context, { plugins, pkg, projectOptions, useBuiltIn } = {}) {
1717
process.VUE_CLI_SERVICE = this
1818
this.context = context
19-
this.webpackConfig = new Config()
2019
this.webpackChainFns = []
2120
this.webpackRawConfigFns = []
2221
this.devServerConfigFns = []
@@ -132,11 +131,16 @@ module.exports = class Service {
132131
return Promise.resolve(fn(args, rawArgv))
133132
}
134133

135-
resolveWebpackConfig () {
134+
resolveChainableWebpackConfig () {
135+
const chainableConfig = new Config()
136136
// apply chains
137-
this.webpackChainFns.forEach(fn => fn(this.webpackConfig))
138-
// to raw config
139-
let config = this.webpackConfig.toConfig()
137+
this.webpackChainFns.forEach(fn => fn(chainableConfig))
138+
return chainableConfig
139+
}
140+
141+
resolveWebpackConfig () {
142+
// get raw config
143+
let config = this.resolveChainableWebpackConfig().toConfig()
140144
// apply raw config fns
141145
this.webpackRawConfigFns.forEach(fn => {
142146
if (typeof fn === 'function') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<title><%- htmlWebpackPlugin.options.libName %> demo</title>
2+
<script src="https://unpkg.com/vue"></script>
3+
<script src="./<%- htmlWebpackPlugin.options.libName %>.umd.js"></script>
4+
<link rel="stylesheet" href="./<%- htmlWebpackPlugin.options.libName %>.css">
5+
6+
<div id="app">
7+
<demo></demo>
8+
</div>
9+
10+
<script>
11+
new Vue({
12+
components: {
13+
demo: <%- htmlWebpackPlugin.options.libName %>
14+
}
15+
}).$mount('#app')
16+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<title><%- htmlWebpackPlugin.options.libName %> demo</title>
2+
<script src="https://unpkg.com/vue"></script>
3+
<script src="./<%- htmlWebpackPlugin.options.libName %>.js"></script>
4+
5+
<<%= htmlWebpackPlugin.options.libName %>></<%= htmlWebpackPlugin.options.libName %>>
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,12 @@
1-
/* global HTMLElement */
2-
31
import Vue from 'vue'
42
import Component from '~entry'
3+
import wrap from '@vue/web-component-wrapper'
54

6-
// Name to register the custom element as. Must contain a hyphen.
75
const name = process.env.CUSTOM_ELEMENT_NAME
86

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-
const options = typeof Component === 'function'
16-
? Component.options
17-
: Component
18-
19-
const arrToObj = (arr, defaultValue) => arr.reduce((acc, key) => {
20-
acc[key] = defaultValue
21-
return acc
22-
}, {})
23-
24-
const props = Array.isArray(options.props)
25-
? arrToObj(options.props, {})
26-
: options.props || {}
27-
const propsList = Object.keys(props)
28-
297
// CSS injection function exposed by vue-loader & vue-style-loader
8+
const options = typeof Component === 'function' ? Component.options : Component
309
const styleInjectors = window[options.__shadowInjectId]
31-
const injectStyle = root => styleInjectors.forEach(inject => inject(root))
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-
this._shadowRoot = this.attachShadow({ mode: 'open' })
53-
injectStyle(this._shadowRoot)
54-
}
55-
56-
connectedCallback () {
57-
this._attached = true
58-
if (!this._wrapper._isMounted) {
59-
this._wrapper.$mount()
60-
this._shadowRoot.appendChild(this._wrapper.$el)
61-
}
62-
this._wrapper._data._active = true
63-
}
64-
65-
disconnectedCallback () {
66-
this._attached = false
67-
const destroy = () => {
68-
this._wrapper._data._active = false
69-
}
70-
if (!keepAlive) {
71-
destroy()
72-
} else if (typeof keepAlive === 'number') {
73-
setTimeout(() => {
74-
if (!this._attached) destroy()
75-
}, keepAlive)
76-
}
77-
}
78-
79-
attributeChangedCallback (attrName, oldVal, newVal) {
80-
this._wrapper._data[attrName] = newVal
81-
}
82-
}
83-
84-
propsList.forEach(key => {
85-
Object.defineProperty(CustomElement.prototype, key, {
86-
get () {
87-
return this._wrapper._data[key]
88-
},
89-
set (newVal) {
90-
this._wrapper._data[key] = newVal
91-
},
92-
enumerable: false,
93-
configurable: true
94-
})
95-
})
10+
const onMounted = root => styleInjectors.forEach(inject => inject(root))
9611

97-
window.customElements.define(name, CustomElement)
12+
wrap(Vue, name, Component, onMounted)

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
const defaults = {
22
mode: 'production',
33
target: 'app',
4-
entry: 'src/App.vue',
5-
keepAlive: false
4+
entry: 'src/App.vue'
65
}
76

87
module.exports = (api, options) => {
@@ -14,8 +13,7 @@ module.exports = (api, options) => {
1413
'--dest': `specify output directory (default: ${options.outputDir})`,
1514
'--target': `app | lib | web-component (default: ${defaults.target})`,
1615
'--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})`
16+
'--name': `name for lib or web-component (default: "name" in package.json or entry filename)`
1917
}
2018
}, args => {
2119
for (const key in defaults) {
@@ -53,7 +51,7 @@ module.exports = (api, options) => {
5351
let webpackConfig
5452
if (args.target === 'lib') {
5553
webpackConfig = require('./resolveLibConfig')(api, args, options)
56-
} else if (args.target === 'web-component') {
54+
} else if (args.target === 'web-component' || args.target === 'wc') {
5755
webpackConfig = require('./resolveWebComponentConfig')(api, args, options)
5856
} else {
5957
webpackConfig = api.resolveWebpackConfig()
@@ -65,7 +63,10 @@ module.exports = (api, options) => {
6563
}
6664

6765
if (!args.silent) {
66+
// TODO polish output
6867
process.stdout.write(stats.toString({
68+
hash: false,
69+
timings: false,
6970
colors: true,
7071
modules: false,
7172
children: api.hasPlugin('typescript') || args.target !== 'app',
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
1+
const path = require('path')
2+
13
module.exports = (api, { entry, name, dest }, options) => {
24
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
35
// setting this disables app-only configs
46
process.env.VUE_CLI_TARGET = 'lib'
57
// inline all static asset files since there is no publicPath handling
68
process.env.VUE_CLI_INLINE_LIMIT = Infinity
79

8-
api.chainWebpack(config => {
10+
function genConfig (format, postfix = format, genHTML) {
11+
const config = api.resolveChainableWebpackConfig()
12+
13+
config.entryPoints.clear()
14+
// set proxy entry for *.vue files
15+
if (/\.vue$/.test(entry)) {
16+
config
17+
.entry(`${libName}.${postfix}`)
18+
.add(require.resolve('./entry-lib.js'))
19+
config.resolve
20+
.alias
21+
.set('~entry', api.resolve(entry))
22+
} else {
23+
config
24+
.entry(`${libName}.${postfix}`)
25+
.add(api.resolve(entry))
26+
}
27+
928
config.output
1029
.path(api.resolve(dest))
1130
.filename(`[name].js`)
1231
.library(libName)
1332
.libraryExport('default')
33+
.libraryTarget(format)
1434

15-
// adjust css output name
35+
// adjust css output name so they write to the same file
1636
if (options.css.extract !== false) {
1737
config
1838
.plugin('extract-css')
@@ -23,12 +43,9 @@ module.exports = (api, { entry, name, dest }, options) => {
2343
}
2444

2545
// only minify min entry
26-
config
27-
.plugin('uglify')
28-
.tap(args => {
29-
args[0].include = /\.min\.js$/
30-
return args
31-
})
46+
if (!/\.min/.test(postfix)) {
47+
config.plugins.delete('uglify')
48+
}
3249

3350
// externalize Vue in case user imports it
3451
config
@@ -39,34 +56,25 @@ module.exports = (api, { entry, name, dest }, options) => {
3956
root: 'Vue'
4057
}
4158
})
42-
})
4359

44-
function genConfig (format, postfix = format) {
45-
api.chainWebpack(config => {
46-
config.entryPoints.clear()
47-
// set proxy entry for *.vue files
48-
if (/\.vue$/.test(entry)) {
49-
config
50-
.entry(`${libName}.${postfix}`)
51-
.add(require.resolve('./entry-lib.js'))
52-
config.resolve
53-
.alias
54-
.set('~entry', api.resolve(entry))
55-
} else {
56-
config
57-
.entry(`${libName}.${postfix}`)
58-
.add(api.resolve(entry))
59-
}
60+
// inject demo page for umd
61+
if (genHTML) {
62+
config
63+
.plugin('demo-html')
64+
.use(require('html-webpack-plugin'), [{
65+
template: path.resolve(__dirname, './demo-lib.html'),
66+
inject: false,
67+
filename: 'demo.html',
68+
libName
69+
}])
70+
}
6071

61-
config.output
62-
.libraryTarget(format)
63-
})
64-
return api.resolveWebpackConfig()
72+
return config.toConfig()
6573
}
6674

6775
return [
6876
genConfig('commonjs2', 'common'),
69-
genConfig('umd'),
77+
genConfig('umd', undefined, true),
7078
genConfig('umd', 'umd.min')
7179
]
7280
}

0 commit comments

Comments
 (0)