Skip to content

Commit a3a94ab

Browse files
sapphi-redbluwy
andauthored
fix(css): scoped css order with non-scoped css (#19678)
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
1 parent 9827df2 commit a3a94ab

15 files changed

Lines changed: 80 additions & 140 deletions

File tree

packages/vite/src/node/plugins/css.ts

Lines changed: 46 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
OutputAsset,
1111
OutputChunk,
1212
RenderedChunk,
13+
RenderedModule,
1314
RollupError,
1415
SourceMapInput,
1516
} from 'rollup'
@@ -452,69 +453,12 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
452453
return plugin
453454
}
454455

455-
const createStyleContentMap = () => {
456-
const contents = new Map<string, string>() // css id -> css content
457-
const scopedIds = new Set<string>() // ids of css that are scoped
458-
const relations = new Map<
459-
/* the id of the target for which css is scoped to */ string,
460-
Array<{
461-
/** css id */ id: string
462-
/** export name */ exp: string | undefined
463-
}>
464-
>()
465-
466-
return {
467-
putContent(
468-
id: string,
469-
content: string,
470-
scopeTo: CustomPluginOptionsVite['cssScopeTo'] | undefined,
471-
) {
472-
contents.set(id, content)
473-
if (scopeTo) {
474-
const [scopedId, exp] = scopeTo
475-
if (!relations.has(scopedId)) {
476-
relations.set(scopedId, [])
477-
}
478-
relations.get(scopedId)!.push({ id, exp })
479-
scopedIds.add(id)
480-
}
481-
},
482-
hasContentOfNonScoped(id: string) {
483-
return !scopedIds.has(id) && contents.has(id)
484-
},
485-
getContentOfNonScoped(id: string) {
486-
if (scopedIds.has(id)) return
487-
return contents.get(id)
488-
},
489-
hasContentsScopedTo(id: string) {
490-
return (relations.get(id) ?? [])?.length > 0
491-
},
492-
getContentsScopedTo(id: string, importedIds: readonly string[]) {
493-
const values = (relations.get(id) ?? []).map(
494-
({ id, exp }) =>
495-
[
496-
id,
497-
{
498-
content: contents.get(id) ?? '',
499-
exp,
500-
},
501-
] as const,
502-
)
503-
const styleIdToValue = new Map(values)
504-
// get a sorted output by import order to make output deterministic
505-
return importedIds
506-
.filter((id) => styleIdToValue.has(id))
507-
.map((id) => styleIdToValue.get(id)!)
508-
},
509-
}
510-
}
511-
512456
/**
513457
* Plugin applied after user plugins
514458
*/
515459
export function cssPostPlugin(config: ResolvedConfig): Plugin {
516460
// styles initialization in buildStart causes a styling loss in watch
517-
const styles = createStyleContentMap()
461+
const styles = new Map<string, string>()
518462
// queue to emit css serially to guarantee the files are emitted in a deterministic order
519463
let codeSplitEmitQueue = createSerialPromiseQueue<string>()
520464
const urlEmitQueue = createSerialPromiseQueue<unknown>()
@@ -663,19 +607,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
663607

664608
// build CSS handling ----------------------------------------------------
665609

666-
const cssScopeTo =
667-
// NOTE: `this.getModuleInfo` can be undefined when the plugin is called directly
668-
// adding `?.` temporary to avoid unocss from breaking
669-
// TODO: remove `?.` after `this.getModuleInfo` in Vite 7
670-
(
671-
this.getModuleInfo?.(id)?.meta?.vite as
672-
| CustomPluginOptionsVite
673-
| undefined
674-
)?.cssScopeTo
675-
676610
// record css
677611
if (!inlined) {
678-
styles.putContent(id, css, cssScopeTo)
612+
styles.set(id, css)
679613
}
680614

681615
let code: string
@@ -697,41 +631,49 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
697631
map: { mappings: '' },
698632
// avoid the css module from being tree-shaken so that we can retrieve
699633
// it in renderChunk()
700-
moduleSideEffects:
701-
modulesCode || inlined || cssScopeTo ? false : 'no-treeshake',
634+
moduleSideEffects: modulesCode || inlined ? false : 'no-treeshake',
702635
}
703636
},
704637
},
705638

706-
async renderChunk(code, chunk, opts) {
639+
async renderChunk(code, chunk, opts, meta) {
707640
let chunkCSS = ''
641+
const renderedModules = Object.fromEntries(
642+
Object.values(meta.chunks).flatMap((chunk) =>
643+
Object.entries(chunk.modules),
644+
),
645+
)
708646
// the chunk is empty if it's a dynamic entry chunk that only contains a CSS import
709647
const isJsChunkEmpty = code === '' && !chunk.isEntry
710648
let isPureCssChunk = chunk.exports.length === 0
711649
const ids = Object.keys(chunk.modules)
712650
for (const id of ids) {
713-
if (styles.hasContentOfNonScoped(id)) {
651+
if (styles.has(id)) {
714652
// ?transform-only is used for ?url and shouldn't be included in normal CSS chunks
715-
if (!transformOnlyRE.test(id)) {
716-
chunkCSS += styles.getContentOfNonScoped(id)
717-
// a css module contains JS, so it makes this not a pure css chunk
718-
if (cssModuleRE.test(id)) {
719-
isPureCssChunk = false
720-
}
653+
if (transformOnlyRE.test(id)) {
654+
continue
721655
}
722-
} else if (styles.hasContentsScopedTo(id)) {
723-
const renderedExports = chunk.modules[id]!.renderedExports
724-
const importedIds = this.getModuleInfo(id)?.importedIds ?? []
725-
// If this module has scoped styles, check for the rendered exports
726-
// and include the corresponding CSS.
727-
for (const { exp, content } of styles.getContentsScopedTo(
728-
id,
729-
importedIds,
730-
)) {
731-
if (exp === undefined || renderedExports.includes(exp)) {
732-
chunkCSS += content
733-
}
656+
657+
// If this CSS is scoped to its importers exports, check if those importers exports
658+
// are rendered in the chunks. If they are not, we can skip bundling this CSS.
659+
const cssScopeTo = (
660+
this.getModuleInfo(id)?.meta?.vite as
661+
| CustomPluginOptionsVite
662+
| undefined
663+
)?.cssScopeTo
664+
if (
665+
cssScopeTo &&
666+
!isCssScopeToRendered(cssScopeTo, renderedModules)
667+
) {
668+
continue
669+
}
670+
671+
// a css module contains JS, so it makes this not a pure css chunk
672+
if (cssModuleRE.test(id)) {
673+
isPureCssChunk = false
734674
}
675+
676+
chunkCSS += styles.get(id)
735677
} else if (!isJsChunkEmpty) {
736678
// if the module does not have a style, then it's not a pure css chunk.
737679
// this is true because in the `transform` hook above, only modules
@@ -826,13 +768,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
826768
path.basename(originalFileName),
827769
'.css',
828770
)
829-
if (!styles.hasContentOfNonScoped(id)) {
771+
if (!styles.has(id)) {
830772
throw new Error(
831773
`css content for ${JSON.stringify(id)} was not found`,
832774
)
833775
}
834776

835-
let cssContent = styles.getContentOfNonScoped(id)!
777+
let cssContent = styles.get(id)!
836778

837779
cssContent = resolveAssetUrlsInCss(cssContent, cssAssetName)
838780

@@ -1201,6 +1143,17 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
12011143
}
12021144
}
12031145

1146+
function isCssScopeToRendered(
1147+
cssScopeTo: Exclude<CustomPluginOptionsVite['cssScopeTo'], undefined>,
1148+
renderedModules: Record<string, RenderedModule | undefined>,
1149+
) {
1150+
const [importerId, exp] = cssScopeTo
1151+
const importer = renderedModules[importerId]
1152+
return (
1153+
importer && (exp === undefined || importer.renderedExports.includes(exp))
1154+
)
1155+
}
1156+
12041157
/**
12051158
* Create a replacer function that takes code and replaces given pure CSS chunk imports
12061159
* @param pureCssChunkNames The chunks that only contain pure CSS and should be replaced

playground/css/__tests__/css.spec.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
editFile,
66
findAssetFile,
77
getBg,
8+
getBgColor,
89
getColor,
910
isBuild,
1011
page,
@@ -506,16 +507,8 @@ test.runIf(isBuild)('Scoped CSS via cssScopeTo should be treeshaken', () => {
506507
expect(css).not.toMatch(/\btreeshake-scoped-c\b/)
507508
})
508509

509-
test.runIf(isBuild)(
510-
'Scoped CSS via cssScopeTo should be bundled separately',
511-
() => {
512-
const scopedIndexCss = findAssetFile(/treeshakeScoped-[-\w]{8}\.css$/)
513-
expect(scopedIndexCss).toContain('treeshake-scoped-barrel-a')
514-
expect(scopedIndexCss).not.toContain('treeshake-scoped-barrel-b')
515-
const scopedAnotherCss = findAssetFile(
516-
/treeshakeScopedAnother-[-\w]{8}\.css$/,
517-
)
518-
expect(scopedAnotherCss).toContain('treeshake-scoped-barrel-b')
519-
expect(scopedAnotherCss).not.toContain('treeshake-scoped-barrel-a')
520-
},
521-
)
510+
test('Scoped CSS should have a correct order', async () => {
511+
await page.goto(viteTestUrl + '/treeshake-scoped/')
512+
expect(await getColor('.treeshake-scoped-order')).toBe('red')
513+
expect(await getBgColor('.treeshake-scoped-order')).toBe('blue')
514+
})

playground/css/treeshake-scoped/another.html

Lines changed: 0 additions & 7 deletions
This file was deleted.

playground/css/treeshake-scoped/barrel/a-scoped.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

playground/css/treeshake-scoped/barrel/a.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

playground/css/treeshake-scoped/barrel/b-scoped.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

playground/css/treeshake-scoped/barrel/b.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

playground/css/treeshake-scoped/barrel/index.js

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<h1>treeshake-scoped</h1>
22
<p class="scoped-index">Imported scoped CSS</p>
3+
<p class="treeshake-scoped-order">
4+
scoped CSS order (this should be red text with blue background)
5+
</p>
36

47
<script type="module">
58
import { d } from './index.js'
6-
import { a } from './barrel/index.js'
7-
document.querySelector('.scoped-index').classList.add(d(), a())
9+
import order from './order/a.js'
10+
document.querySelector('.scoped-index').classList.add(d())
11+
document.querySelector('.treeshake-scoped-order').classList.add(order())
812
</script>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.treeshake-scoped-order {
2+
color: red;
3+
background: red;
4+
}

0 commit comments

Comments
 (0)