From 09688568c8c251b79d29d82cbc004ad730a5f131 Mon Sep 17 00:00:00 2001 From: Tom Nijmeijer Date: Tue, 6 Jul 2021 11:18:12 +0200 Subject: [PATCH 1/4] Proof of concept for doing our own style injection --- src/cssModules.ts | 3 ++- src/index.ts | 21 ++++++++++++++------- src/pitcher.ts | 6 +++++- src/styleInjection.ts | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 src/styleInjection.ts diff --git a/src/cssModules.ts b/src/cssModules.ts index e95a27e9..5c788873 100644 --- a/src/cssModules.ts +++ b/src/cssModules.ts @@ -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 += ` diff --git a/src/index.ts b/src/index.ts index 8a51d165..ff0a4d41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ import { formatError } from './formatError' import VueLoaderPlugin from './plugin' import { canInlineTemplate } from './resolveScript' import { setDescriptor } from './descriptorCache' +import { addStyleInjectionCode } from './styleInjection' export { VueLoaderPlugin } @@ -178,36 +179,42 @@ 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) => { const src = style.src || resourcePath + style.attrs.module = true 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) + if (style.module) { - if (!hasCSSModules) { - stylesCode += `\nconst cssModules = script.__cssModules = {}` - hasCSSModules = true - } stylesCode += genCSSModulesCode( id, i, styleRequest, - style.module, + style.module || true, needsHotReload ) } else { - stylesCode += `\nimport ${styleRequest}` + const styleVar = `style${i}` + stylesCode += `\nimport ${styleVar} from ${styleRequest}` + stylesCode += `\ncssBlocks['${styleVar}'] = ${styleVar}` } + // TODO SSR critical CSS collection }) + + // Inject the styles + stylesCode += addStyleInjectionCode } let code = [ diff --git a/src/pitcher.ts b/src/pitcher.ts index 135e72e6..d35d51f8 100644 --- a/src/pitcher.ts +++ b/src/pitcher.ts @@ -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) @@ -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) diff --git a/src/styleInjection.ts b/src/styleInjection.ts new file mode 100644 index 00000000..6a5ce5fe --- /dev/null +++ b/src/styleInjection.ts @@ -0,0 +1,18 @@ +export const addStyleInjectionCode = ` +var options = typeof script === 'function' ? script.options : script + +function injectStyles() { + Object.values(cssBlocks).forEach(function(cssBlock) { + var styleElement = document.createElement('style') + styleElement.type = 'text/css' + styleElement.innerHTML = cssBlock.toString() + document.head.appendChild(styleElement) + }) +} + +var existing = options.beforeMount +options.beforeMount = function() { + injectStyles() + existing && existing() +} +` From f966c81709274f80fd897b38e8949cbf80205019 Mon Sep 17 00:00:00 2001 From: Tom Nijmeijer Date: Wed, 7 Jul 2021 11:19:56 +0200 Subject: [PATCH 2/4] Do not inject the same style block more than once --- src/index.ts | 36 +++++++++++++++--------- src/styleInjection.ts | 20 +++++++++---- test/core.spec.ts | 4 +-- test/fixtures/duplicate-cssm.js | 14 +++++++-- test/fixtures/style-import-twice-sub.vue | 2 +- 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index ff0a4d41..36d8cb37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' @@ -187,8 +186,9 @@ export default function loader( descriptor.styles .filter((style) => style.src || nonWhitespaceRE.test(style.content)) .forEach((style: SFCStyleBlock, i: number) => { - const src = style.src || resourcePath 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 @@ -196,18 +196,28 @@ export default function loader( 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) { - stylesCode += genCSSModulesCode( - id, - i, - styleRequest, - style.module || true, - needsHotReload - ) - } else { - const styleVar = `style${i}` - stylesCode += `\nimport ${styleVar} from ${styleRequest}` - stylesCode += `\ncssBlocks['${styleVar}'] = ${styleVar}` + 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}") + }) + } + ` + } } // TODO SSR critical CSS collection diff --git a/src/styleInjection.ts b/src/styleInjection.ts index 6a5ce5fe..1588ad23 100644 --- a/src/styleInjection.ts +++ b/src/styleInjection.ts @@ -1,18 +1,26 @@ export const addStyleInjectionCode = ` var options = typeof script === 'function' ? script.options : script +function getStyleElement(id) { + var existing = document.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") + document.head.appendChild(styleElement) + return styleElement +} function injectStyles() { Object.values(cssBlocks).forEach(function(cssBlock) { - var styleElement = document.createElement('style') - styleElement.type = 'text/css' + var styleElement = getStyleElement(cssBlock.id) styleElement.innerHTML = cssBlock.toString() - document.head.appendChild(styleElement) }) } -var existing = options.beforeMount -options.beforeMount = function() { +var beforeCreate = options.beforeCreate +options.beforeCreate = function beforeCreate() { injectStyles() - existing && existing() + beforeCreate && beforeCreate() } ` diff --git a/test/core.spec.ts b/test/core.spec.ts index 1fffaaa3..69031db8 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -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}') }) diff --git a/test/fixtures/duplicate-cssm.js b/test/fixtures/duplicate-cssm.js index 20c5905e..099d5531 100644 --- a/test/fixtures/duplicate-cssm.js +++ b/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 diff --git a/test/fixtures/style-import-twice-sub.vue b/test/fixtures/style-import-twice-sub.vue index 6ae3d717..a59212e3 100644 --- a/test/fixtures/style-import-twice-sub.vue +++ b/test/fixtures/style-import-twice-sub.vue @@ -1,3 +1,3 @@ - + From 73d418f41fcf534006925f6f0906a1bbc14e30b3 Mon Sep 17 00:00:00 2001 From: Tom Nijmeijer Date: Wed, 7 Jul 2021 15:14:57 +0200 Subject: [PATCH 3/4] Convert the style injection to typescript, instead of a template literal --- src/index.ts | 7 +++++-- src/styleInjection.ts | 36 ++++++++++++++++++++++++------------ test/style.spec.ts | 2 +- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 36d8cb37..b0889b1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,6 @@ import { formatError } from './formatError' import VueLoaderPlugin from './plugin' import { canInlineTemplate } from './resolveScript' import { setDescriptor } from './descriptorCache' -import { addStyleInjectionCode } from './styleInjection' export { VueLoaderPlugin } @@ -224,7 +223,11 @@ export default function loader( }) // Inject the styles - stylesCode += addStyleInjectionCode + const styleInjectionPath = stringifyRequest( + path.join(__dirname, 'styleInjection.js') + ) + stylesCode += `\nimport addStyleInjectionCode from ${styleInjectionPath}` + stylesCode += `\naddStyleInjectionCode(script)` } let code = [ diff --git a/src/styleInjection.ts b/src/styleInjection.ts index 1588ad23..dbbac3f0 100644 --- a/src/styleInjection.ts +++ b/src/styleInjection.ts @@ -1,26 +1,38 @@ -export const addStyleInjectionCode = ` -var options = typeof script === 'function' ? script.options : script +interface ComponentOptions { + beforeMount?(): void + __cssBlocks: Record +} + +interface ComponentInstance { + $options: ComponentOptions +} -function getStyleElement(id) { +interface CSSBlock { + id: string +} + +function getStyleElement(id: string) { var existing = document.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") + styleElement.setAttribute('data-style-id', id) + styleElement.setAttribute('type', 'text/css') document.head.appendChild(styleElement) return styleElement } -function injectStyles() { - Object.values(cssBlocks).forEach(function(cssBlock) { + +function injectStyles(component: ComponentInstance) { + Object.values(component.$options.__cssBlocks).forEach(function (cssBlock) { var styleElement = getStyleElement(cssBlock.id) styleElement.innerHTML = cssBlock.toString() }) } -var beforeCreate = options.beforeCreate -options.beforeCreate = function beforeCreate() { - injectStyles() - beforeCreate && beforeCreate() +export default function addStyleInjectionCode(script: ComponentOptions) { + var existing = script.beforeMount + script.beforeMount = function beforeMount() { + injectStyles(this) + existing && existing() + } } -` diff --git a/test/style.spec.ts b/test/style.spec.ts index 94a04eb0..bc685a74 100644 --- a/test/style.spec.ts +++ b/test/style.spec.ts @@ -189,7 +189,7 @@ 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}`) }) From 9deb088fdf9ea90d4439792609c4736b0f87bdaf Mon Sep 17 00:00:00 2001 From: Tom Nijmeijer Date: Wed, 7 Jul 2021 15:58:15 +0200 Subject: [PATCH 4/4] Add the actual support for injecting styles into a shadowRoot --- src/styleInjection.ts | 11 +++++++---- test/fixtures/shadow-root-injection.js | 15 +++++++++++++++ test/style.spec.ts | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/shadow-root-injection.js diff --git a/src/styleInjection.ts b/src/styleInjection.ts index dbbac3f0..22bdffcf 100644 --- a/src/styleInjection.ts +++ b/src/styleInjection.ts @@ -1,30 +1,33 @@ interface ComponentOptions { beforeMount?(): void __cssBlocks: Record + shadowRoot?: HTMLElement } interface ComponentInstance { $options: ComponentOptions + $root: ComponentInstance } interface CSSBlock { id: string } -function getStyleElement(id: string) { - var existing = document.querySelector("[data-style-id='" + id + "']") +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') - document.head.appendChild(styleElement) + 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) + var styleElement = getStyleElement(cssBlock.id, parent) styleElement.innerHTML = cssBlock.toString() }) } diff --git a/test/fixtures/shadow-root-injection.js b/test/fixtures/shadow-root-injection.js new file mode 100644 index 00000000..4ff78733 --- /dev/null +++ b/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 diff --git a/test/style.spec.ts b/test/style.spec.ts index bc685a74..298439a8 100644 --- a/test/style.spec.ts +++ b/test/style.spec.ts @@ -193,4 +193,18 @@ test('CSS Modules Extend', async () => { 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