diff --git a/src/cssModules.ts b/src/cssModules.ts index e95a27e91..5c7888735 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 8a51d1655..b0889b1f1 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' @@ -178,12 +177,16 @@ 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 @@ -191,23 +194,40 @@ export default function loader( 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 = [ diff --git a/src/pitcher.ts b/src/pitcher.ts index 135e72e62..d35d51f82 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 000000000..22bdffcf1 --- /dev/null +++ b/src/styleInjection.ts @@ -0,0 +1,41 @@ +interface ComponentOptions { + beforeMount?(): void + __cssBlocks: Record + 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() + } +} diff --git a/test/core.spec.ts b/test/core.spec.ts index 1fffaaa32..69031db8b 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 20c5905e3..099d5531d 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/shadow-root-injection.js b/test/fixtures/shadow-root-injection.js new file mode 100644 index 000000000..4ff787330 --- /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/fixtures/style-import-twice-sub.vue b/test/fixtures/style-import-twice-sub.vue index 6ae3d7173..a59212e39 100644 --- a/test/fixtures/style-import-twice-sub.vue +++ b/test/fixtures/style-import-twice-sub.vue @@ -1,3 +1,3 @@ - + diff --git a/test/style.spec.ts b/test/style.spec.ts index 94a04eb0e..298439a8c 100644 --- a/test/style.spec.ts +++ b/test/style.spec.ts @@ -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