feat: inject styles under shadow root in web component mode

This commit is contained in:
Evan You
2018-01-31 00:07:57 -05:00
parent 6db7735c74
commit 98afd07017
8 changed files with 71 additions and 79 deletions

View File

@@ -12,10 +12,6 @@ const name = process.env.CUSTOM_ELEMENT_NAME
// - 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
@@ -30,6 +26,10 @@ const props = Array.isArray(options.props)
: options.props || {}
const propsList = Object.keys(props)
// CSS injection function exposed by vue-loader & vue-style-loader
const styleInjectors = window[options.__shadowInjectId]
const injectStyle = root => styleInjectors.forEach(inject => inject(root))
// TODO use ES5 syntax
class CustomElement extends HTMLElement {
static get observedAttributes () {
@@ -49,21 +49,15 @@ class CustomElement extends HTMLElement {
})
this._attached = false
if (useShadowDOM) {
this._shadowRoot = this.attachShadow({ mode: 'open' })
}
this._shadowRoot = this.attachShadow({ mode: 'open' })
injectStyle(this._shadowRoot)
}
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._shadowRoot.appendChild(this._wrapper.$el)
}
this._wrapper._data._active = true
}

View File

@@ -2,8 +2,7 @@ const defaults = {
mode: 'production',
target: 'app',
entry: 'src/App.vue',
keepAlive: false,
shadow: true
keepAlive: false
}
module.exports = (api, options) => {
@@ -12,16 +11,19 @@ module.exports = (api, options) => {
usage: 'vue-cli-service build [options]',
options: {
'--mode': `specify env mode (default: ${defaults.mode})`,
'--dest': `specify output directory (default: ${options.outputDir})`,
'--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 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})`
'--keepAlive': `keep component alive when web-component is detached? (default: ${defaults.keepAlive})`
}
}, args => {
for (const key in defaults) {
if (args[key] == null) args[key] = defaults[key]
}
if (args.dest == null) {
args.dest = options.outputDir
}
api.setMode(args.mode)
const chalk = require('chalk')
@@ -43,16 +45,16 @@ module.exports = (api, options) => {
}
return new Promise((resolve, reject) => {
const targetDir = api.resolve(options.outputDir)
const targetDir = api.resolve(args.dest)
rimraf(targetDir, err => {
if (err) {
return reject(err)
}
let webpackConfig
if (args.target === 'lib') {
webpackConfig = require('./resolveLibConfig')(api, args)
webpackConfig = require('./resolveLibConfig')(api, args, options)
} else if (args.target === 'web-component') {
webpackConfig = require('./resolveWebComponentConfig')(api, args)
webpackConfig = require('./resolveWebComponentConfig')(api, args, options)
} else {
webpackConfig = api.resolveWebpackConfig()
}
@@ -66,7 +68,7 @@ module.exports = (api, options) => {
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: api.hasPlugin('typescript'),
children: api.hasPlugin('typescript') || args.target !== 'app',
chunks: false,
chunkModules: false
}) + '\n\n')

View File

@@ -1,4 +1,4 @@
module.exports = (api, { entry, name }) => {
module.exports = (api, { entry, name, dest }, options) => {
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
// setting this disables app-only configs
process.env.VUE_CLI_TARGET = 'lib'
@@ -7,17 +7,20 @@ module.exports = (api, { entry, name }) => {
api.chainWebpack(config => {
config.output
.path(api.resolve(dest))
.filename(`[name].js`)
.library(libName)
.libraryExport('default')
// adjust css output name
config
.plugin('extract-css')
.tap(args => {
args[0].filename = `${libName}.css`
return args
})
if (options.css.extract !== false) {
config
.plugin('extract-css')
.tap(args => {
args[0].filename = `${libName}.css`
return args
})
}
// only minify min entry
config

View File

@@ -1,4 +1,4 @@
module.exports = (api, { entry, name, keepAlive, shadow }) => {
module.exports = (api, { entry, name, dest, keepAlive }) => {
const libName = name || api.service.pkg.name || entry.replace(/\.(js|vue)$/, '')
if (libName.indexOf('-') < 0) {
const { log, error } = require('@vue/cli-shared-utils')
@@ -11,18 +11,28 @@ module.exports = (api, { entry, name, keepAlive, shadow }) => {
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
// Disable CSS extraction and turn on CSS shadow mode for vue-style-loader
process.env.VUE_CLI_CSS_SHADOW_MODE = true
api.chainWebpack(config => {
config.output
.filename(`[name].js`)
config.entryPoints.clear()
// set proxy entry for *.vue files
if (/\.vue$/.test(entry)) {
config
.entry(libName)
.add(require.resolve('./entry-web-component.js'))
config.resolve
.alias
.set('~entry', api.resolve(entry))
} else {
config
.entry(libName)
.add(api.resolve(entry))
}
// only minify min entry
config
.plugin('uglify')
.tap(args => {
args[0].include = /\.min\.js$/
return args
})
config.output
.path(api.resolve(dest))
.filename(`[name].js`)
// externalize Vue in case user imports it
config
@@ -35,37 +45,19 @@ module.exports = (api, { entry, name, keepAlive, shadow }) => {
.use(require('webpack/lib/DefinePlugin'), [{
'process.env': {
CUSTOM_ELEMENT_NAME: JSON.stringify(libName),
CUSTOM_ELEMENT_KEEP_ALIVE: keepAlive,
CUSTOM_ELEMENT_USE_SHADOW_DOM: shadow
CUSTOM_ELEMENT_KEEP_ALIVE: keepAlive
}
}])
// TODO handle CSS (insert in shadow DOM)
// enable shadow mode in vue-loader
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.shadowMode = true
return options
})
})
function genConfig (postfix) {
postfix = postfix ? `.${postfix}` : ``
api.chainWebpack(config => {
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))
}
})
return api.resolveWebpackConfig()
}
return [
genConfig(''),
genConfig('min')
]
return api.resolveWebpackConfig()
}

View File

@@ -29,7 +29,7 @@ module.exports = class CSSLoaderResolver {
this.cssLoader = 'css-loader'
this.fallbackLoader = 'vue-style-loader'
this.sourceMap = sourceMap
this.extract = extract
this.extract = extract && !process.env.VUE_CLI_CSS_SHADOW_MODE
this.minimize = minimize
this.modules = modules
this.postcss = postcss
@@ -83,6 +83,7 @@ module.exports = class CSSLoaderResolver {
}) : [{
loader: this.fallbackLoader,
options: {
shadowMode: !!process.env.VUE_CLI_CSS_SHADOW_MODE,
sourceMap: this.sourceMap
}
}, ...use]

View File

@@ -55,7 +55,7 @@
"thread-loader": "^1.1.2",
"uglifyjs-webpack-plugin": "^1.1.6",
"url-loader": "^0.6.2",
"vue-loader": "^13.7.0",
"vue-loader": "^14.0.0",
"vue-style-loader": "^3.1.1",
"vue-template-compiler": "^2.5.13",
"webpack": "^3.10.0",

View File

@@ -66,8 +66,8 @@ program
.command('build [entry]')
.option('-t, --target <target>', 'Build target (app | lib | web-component, default: app)')
.option('-n, --name <name>', 'name for lib or web-component (default: entry filename)')
.option('-d, --dest <dir>', 'output directory (default: dist)')
.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))

View File

@@ -10077,9 +10077,9 @@ vue-jest@^2.0.0:
tsconfig "^7.0.0"
vue-template-es2015-compiler "^1.5.3"
vue-loader@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-13.7.0.tgz#4d6a35b169c2a0a488842fb95c85052105fa9729"
vue-loader@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.0.0.tgz#47175b739309b25d3b1f6b48466e72332093f533"
dependencies:
consolidate "^0.14.0"
hash-sum "^1.0.2"
@@ -10092,7 +10092,7 @@ vue-loader@^13.7.0:
resolve "^1.4.0"
source-map "^0.6.1"
vue-hot-reload-api "^2.2.0"
vue-style-loader "^3.0.0"
vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0"
vue-parser@^1.1.5:
@@ -10112,16 +10112,16 @@ vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
vue-style-loader@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.3.tgz#623658f81506aef9d121cdc113a4f5c9cac32df7"
vue-style-loader@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.1.1.tgz#74fdef91a81d38bc0125746a1b5505e62d69e32c"
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-style-loader@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.1.1.tgz#74fdef91a81d38bc0125746a1b5505e62d69e32c"
vue-style-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.0.1.tgz#252300d32eb97e83c1a1cb5b2029e2d8c3adcf9f"
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"