fix(vite): extract styles for shared chunks (#25455)

This commit is contained in:
Daniel Roe 2024-01-28 21:25:42 +00:00 committed by GitHub
parent 80b1c7077f
commit c446602529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 60 additions and 21 deletions

View File

@ -8,7 +8,7 @@ import type { Component } from '@nuxt/schema'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { findStaticImports } from 'mlly' import { findStaticImports } from 'mlly'
import { isCSS } from '../utils' import { isCSS, isVue } from '../utils'
interface SSRStylePluginOptions { interface SSRStylePluginOptions {
srcDir: string srcDir: string
@ -107,25 +107,31 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
}) })
}, },
renderChunk (_code, chunk) { renderChunk (_code, chunk) {
if (!chunk.facadeModuleId) { return null } const isEntry = chunk.facadeModuleId === options.entry
if (isEntry) {
// 'Teleport' CSS chunks that made it into the bundle on the client side options.clientCSSMap[chunk.facadeModuleId!] ||= new Set()
// to be inlined on server rendering
if (options.mode === 'client') {
options.clientCSSMap[chunk.facadeModuleId] ||= new Set()
for (const id of chunk.moduleIds) {
if (isCSS(id)) {
options.clientCSSMap[chunk.facadeModuleId].add(id)
}
}
return
} }
for (const moduleId of [chunk.facadeModuleId, ...chunk.moduleIds].filter(Boolean) as string[]) {
// 'Teleport' CSS chunks that made it into the bundle on the client side
// to be inlined on server rendering
if (options.mode === 'client') {
options.clientCSSMap[moduleId] ||= new Set()
if (isCSS(moduleId)) {
// Vue files can (also) be their own entrypoints as they are tracked separately
if (isVue(moduleId)) {
options.clientCSSMap[moduleId].add(moduleId)
}
// This is required to track CSS in entry chunk
if (isEntry) {
options.clientCSSMap[chunk.facadeModuleId!].add(moduleId)
}
}
continue
}
const id = relativeToSrcDir(chunk.facadeModuleId) const relativePath = relativeToSrcDir(moduleId)
for (const file in chunk.modules) {
const relativePath = relativeToSrcDir(file)
if (relativePath in cssMap) { if (relativePath in cssMap) {
cssMap[relativePath].inBundle = cssMap[relativePath].inBundle ?? !!id cssMap[relativePath].inBundle = cssMap[relativePath].inBundle ?? ((isVue(moduleId) && relativeToSrcDir(moduleId)) || isEntry)
} }
} }

View File

@ -1,5 +1,7 @@
import { hash } from 'ohash' import { hash } from 'ohash'
export { isVue } from '../../../nuxt/src/core/utils/plugins'
export function uniq<T> (arr: T[]): T[] { export function uniq<T> (arr: T[]): T[] {
return Array.from(new Set(arr)) return Array.from(new Set(arr))
} }

View File

@ -112,7 +112,7 @@ describe('pages', () => {
// should apply attributes to client-only components // should apply attributes to client-only components
expect(html).toContain('<div style="color:red;" class="client-only"></div>') expect(html).toContain('<div style="color:red;" class="client-only"></div>')
// should render server-only components // should render server-only components
expect(html.replace(/ data-island-uid="[^"]*"/, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component </div>') expect(html.replace(/ data-island-uid="[^"]*"/, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component <div> server-only component child (non-server-only) </div></div>')
// should register global components automatically // should register global components automatically
expect(html).toContain('global component registered automatically') expect(html).toContain('global component registered automatically')
expect(html).toContain('global component via suffix') expect(html).toContain('global component via suffix')
@ -1382,6 +1382,8 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
'{--assets:"assets"}', // <script> '{--assets:"assets"}', // <script>
'{--postcss:"postcss"}', // <style lang=postcss> '{--postcss:"postcss"}', // <style lang=postcss>
'{--scoped:"scoped"}', // <style lang=css> '{--scoped:"scoped"}', // <style lang=css>
'{--shared-component:"shared-component"}', // styles in a chunk shared between pages
'{--server-only-child:"server-only-child"}', // child of a server-only component
'{--server-only:"server-only"}' // server-only component not in client build '{--server-only:"server-only"}' // server-only component not in client build
// TODO: ideally both client/server components would have inlined css when used // TODO: ideally both client/server components would have inlined css when used
// '{--client-only:"client-only"}', // client-only component not in server build // '{--client-only:"client-only"}', // client-only component not in server build
@ -1392,7 +1394,7 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
it('should inline styles', async () => { it('should inline styles', async () => {
const html = await $fetch('/styles') const html = await $fetch('/styles')
for (const style of inlinedCSS) { for (const style of inlinedCSS) {
expect(html).toContain(style) expect.soft(html).toContain(style)
} }
}) })
@ -1403,7 +1405,7 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
] ]
const html = await $fetch('/route-rules/spa') const html = await $fetch('/route-rules/spa')
for (const style of globalCSS) { for (const style of globalCSS) {
expect(html).toContain(style) expect.soft(html).toContain(style)
} }
}) })
@ -1414,7 +1416,7 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
expect(files.map(m => m.replace(/\.\w+(\.\w+)$/, '$1'))).toContain('css-only-asset.svg') expect(files.map(m => m.replace(/\.\w+(\.\w+)$/, '$1'))).toContain('css-only-asset.svg')
}) })
it('should not include inlined CSS in generated CSS file', async () => { it('should not include inlined CSS in generated CSS file', async () => {
const html: string = await $fetch('/styles') const html: string = await $fetch('/styles')
const cssFiles = new Set([...html.matchAll(/<link [^>]*href="([^"]*\.css)">/g)].map(m => m[1])) const cssFiles = new Set([...html.matchAll(/<link [^>]*href="([^"]*\.css)">/g)].map(m => m[1]))
let css = '' let css = ''
@ -1950,6 +1952,11 @@ describe('component islands', () => {
expect(result.head).toMatchInlineSnapshot(` expect(result.head).toMatchInlineSnapshot(`
{ {
"link": [ "link": [
{
"href": "/_nuxt/components/SharedComponent.vue?vue&type=style&index=0&scoped=3ee84738&lang.css",
"key": "island-link",
"rel": "stylesheet",
},
{ {
"href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css", "href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css",
"key": "island-link", "key": "island-link",

View File

@ -5,6 +5,7 @@ prerenderRoutes(['/some/url/from/server-only/component'])
<template> <template>
<div> <div>
server-only component server-only component
<ServerOnlyComponentChild />
</div> </div>
</template> </template>

View File

@ -0,0 +1,11 @@
<template>
<div>
server-only component child (non-server-only)
</div>
</template>
<style>
:root {
--server-only-child: 'server-only-child';
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<span class="shared-component" />
</template>
<style scoped>
.shared-component {
--shared-component: 'shared-component';
}
</style>

View File

@ -3,6 +3,7 @@
<ClientOnlyScript /> <ClientOnlyScript />
<FunctionalComponent /> <FunctionalComponent />
<ServerOnlyComponent /> <ServerOnlyComponent />
<SharedComponent />
</div> </div>
</template> </template>

View File

@ -21,5 +21,7 @@ useLegacyVueUseHead()
<template> <template>
<div> <div>
<h1>VueUse head polyfill test</h1> <h1>VueUse head polyfill test</h1>
<!-- This component is only here to make it a shared chunk for test in `styles.vue` -->
<SharedComponent />
</div> </div>
</template> </template>