Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Next] shadow root style injection #1850

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cssModules.ts
Expand Up @@ -10,7 +10,8 @@ export function genCSSModulesCode(

// inject variable
const name = typeof moduleName === 'string' ? moduleName : '$style'
code += `\ncssModules["${name}"] = ${styleVar}`
code += `\ncssModules["${name}"] = ${styleVar}.locals`
code += `\ncssBlocks["${styleVar}"] = ${styleVar}`

if (needsHotReload) {
code += `
Expand Down
48 changes: 34 additions & 14 deletions src/index.ts
Expand Up @@ -25,7 +25,6 @@ import {
} from '@vue/compiler-sfc'
import { selectBlock } from './select'
import { genHotReloadCode } from './hotReload'
import { genCSSModulesCode } from './cssModules'
import { formatError } from './formatError'

import VueLoaderPlugin from './plugin'
Expand Down Expand Up @@ -178,36 +177,57 @@ export default function loader(

// styles
let stylesCode = ``
let hasCSSModules = false
stylesCode += `\nconst cssModules = script.__cssModules = {}`
stylesCode += `\nconst cssBlocks = script.__cssBlocks = {}`

const nonWhitespaceRE = /\S+/
if (descriptor.styles.length) {
descriptor.styles
.filter((style) => style.src || nonWhitespaceRE.test(style.content))
.forEach((style: SFCStyleBlock, i: number) => {
style.attrs.module = true

const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
// make sure to only pass id when necessary so that we don't inject
// duplicate tags when multiple components import the same css file
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)

const styleVar = `style${i}`
const styleId = style.src && !style.scoped ? style.src : `${id}-${i}`

stylesCode += `\nimport ${styleVar} from ${styleRequest}`
stylesCode += `\n${styleVar}.id = "${styleId}"`
stylesCode += `\ncssBlocks['${styleVar}'] = ${styleVar}`

if (style.module) {
if (!hasCSSModules) {
stylesCode += `\nconst cssModules = script.__cssModules = {}`
hasCSSModules = true
const name =
typeof style.module === 'string' ? style.module : '$style'
stylesCode += `\ncssModules["${name}"] = ${styleVar}.locals`

if (needsHotReload) {
stylesCode += `
if (module.hot) {
module.hot.accept(${styleRequest}, () => {
cssModules["${name}"] = ${styleVar}
__VUE_HMR_RUNTIME__.rerender("${id}")
})
}
`
}
stylesCode += genCSSModulesCode(
id,
i,
styleRequest,
style.module,
needsHotReload
)
} else {
stylesCode += `\nimport ${styleRequest}`
}

// TODO SSR critical CSS collection
})

// Inject the styles
const styleInjectionPath = stringifyRequest(
path.join(__dirname, 'styleInjection.js')
)
stylesCode += `\nimport addStyleInjectionCode from ${styleInjectionPath}`
stylesCode += `\naddStyleInjectionCode(script)`
}

let code = [
Expand Down
6 changes: 5 additions & 1 deletion src/pitcher.ts
Expand Up @@ -15,6 +15,7 @@ interface Loader {
}

const isESLintLoader = (l: Loader) => /(\/|\\|@)eslint-loader/.test(l.path)
const isStyleLoader = (l: Loader) => /(\/|\\|@)style-loader/.test(l.path)
const isNullLoader = (l: Loader) => /(\/|\\|@)null-loader/.test(l.path)
const isCSSLoader = (l: Loader) => /(\/|\\|@)css-loader/.test(l.path)
const isCacheLoader = (l: Loader) => /(\/|\\|@)cache-loader/.test(l.path)
Expand Down Expand Up @@ -60,8 +61,11 @@ export const pitch = function () {
}
})

// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
// Remove the style-loader, we'll handle style injection ourselves
loaders = loaders.filter((loader) => !isStyleLoader(loader))

// Inject style-post-loader before css-loader for scoped CSS and trimming
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
Expand Down
41 changes: 41 additions & 0 deletions src/styleInjection.ts
@@ -0,0 +1,41 @@
interface ComponentOptions {
beforeMount?(): void
__cssBlocks: Record<string, CSSBlock>
shadowRoot?: HTMLElement
}

interface ComponentInstance {
$options: ComponentOptions
$root: ComponentInstance
}

interface CSSBlock {
id: string
}

function getStyleElement(id: string, parent: HTMLElement) {
var existing = parent.querySelector("[data-style-id='" + id + "']")
if (existing) return existing

var styleElement = document.createElement('style')
styleElement.setAttribute('data-style-id', id)
styleElement.setAttribute('type', 'text/css')
parent.appendChild(styleElement)
return styleElement
}

function injectStyles(component: ComponentInstance) {
const parent = component.$root.$options.shadowRoot || document.head
Object.values(component.$options.__cssBlocks).forEach(function (cssBlock) {
var styleElement = getStyleElement(cssBlock.id, parent)
styleElement.innerHTML = cssBlock.toString()
})
}

export default function addStyleInjectionCode(script: ComponentOptions) {
var existing = script.beforeMount
script.beforeMount = function beforeMount() {
injectStyles(this)
existing && existing()
}
}
4 changes: 2 additions & 2 deletions test/core.spec.ts
Expand Up @@ -92,9 +92,9 @@ test('style import for a same file twice', async () => {
expect(styles[0].textContent).toContain('h1 { color: red;\n}')

// import with scoped
const id = 'data-v-' + genId('style-import-twice-sub.vue')
const id = 'data-v-' + genId('style-import-twice.vue')
expect(styles[1].textContent).toContain('h1[' + id + '] { color: green;\n}')
const id2 = 'data-v-' + genId('style-import-twice.vue')
const id2 = 'data-v-' + genId('style-import-twice-sub.vue')
expect(styles[2].textContent).toContain('h1[' + id2 + '] { color: green;\n}')
})

Expand Down
14 changes: 12 additions & 2 deletions test/fixtures/duplicate-cssm.js
@@ -1,7 +1,17 @@
import { createApp } from 'vue'

import values from './duplicate-cssm.css'
import Comp from './duplicate-cssm.vue'
import Component from './duplicate-cssm.vue'

if (typeof window !== 'undefined') {
window.componentModule = Component

const app = createApp(Component)
const container = window.document.createElement('div')
window.instance = app.mount(container)
}

export { values }
export default Comp
export default Component

window.exports = values
15 changes: 15 additions & 0 deletions test/fixtures/shadow-root-injection.js
@@ -0,0 +1,15 @@
import { createApp } from 'vue'

import Component from './basic.vue'

if (typeof window !== 'undefined') {
const container = window.document.getElementById('#app')
const shadowRoot = container.attachShadow({ mode: 'open' })

Component.shadowRoot = shadowRoot

const app = createApp(Component)
window.instance = app.mount(shadowRoot)
}

export default Component
2 changes: 1 addition & 1 deletion test/fixtures/style-import-twice-sub.vue
@@ -1,3 +1,3 @@
<template><div></div></template>
<style src="./style-import.css"></style>
<style src="./style-import-scoped.css" scoped></style>
<style src="./style-import.css"></style>
16 changes: 15 additions & 1 deletion test/style.spec.ts
Expand Up @@ -189,8 +189,22 @@ test('CSS Modules Extend', async () => {
})

expect(instance.$el.className).toBe(instance.$style.red)
const style = window.document.querySelectorAll('style')![1]!.textContent
const style = window.document.querySelector('style')!.textContent
expect(style).toContain(`.${instance.$style.red} {\n color: #FF0000;\n}`)
})

test('shadow root injection', async () => {
const { window, instance } = await mockBundleAndRun({
entry: './test/fixtures/shadow-root-injection.js',
})

const headStyles = window.document.head.querySelectorAll('style')
expect(headStyles.length).toBe(0)

const shadowStyles = instance.$options.shadowRoot.querySelectorAll('style')
expect(shadowStyles.length).toBe(1)

expect(shadowStyles[0].innerHTML).toContain('comp-a h2 {\n color: #f00;\n}')
})

test.todo('experimental <style vars>')