@@ -54,7 +54,7 @@ import {
5454 SPECIAL_QUERY_RE ,
5555} from '../constants'
5656import type { ResolvedConfig } from '../config'
57- import type { Plugin } from '../plugin'
57+ import type { CustomPluginOptionsVite , Plugin } from '../plugin'
5858import { checkPublicFile } from '../publicDir'
5959import {
6060 arraify ,
@@ -439,12 +439,69 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
439439 }
440440}
441441
442+ const createStyleContentMap = ( ) => {
443+ const contents = new Map < string , string > ( ) // css id -> css content
444+ const scopedIds = new Set < string > ( ) // ids of css that are scoped
445+ const relations = new Map <
446+ /* the id of the target for which css is scoped to */ string ,
447+ Array < {
448+ /** css id */ id : string
449+ /** export name */ exp : string | undefined
450+ } >
451+ > ( )
452+
453+ return {
454+ putContent (
455+ id : string ,
456+ content : string ,
457+ scopeTo : CustomPluginOptionsVite [ 'cssScopeTo' ] | undefined ,
458+ ) {
459+ contents . set ( id , content )
460+ if ( scopeTo ) {
461+ const [ scopedId , exp ] = scopeTo
462+ if ( ! relations . has ( scopedId ) ) {
463+ relations . set ( scopedId , [ ] )
464+ }
465+ relations . get ( scopedId ) ! . push ( { id, exp } )
466+ scopedIds . add ( id )
467+ }
468+ } ,
469+ hasContentOfNonScoped ( id : string ) {
470+ return ! scopedIds . has ( id ) && contents . has ( id )
471+ } ,
472+ getContentOfNonScoped ( id : string ) {
473+ if ( scopedIds . has ( id ) ) return
474+ return contents . get ( id )
475+ } ,
476+ hasContentsScopedTo ( id : string ) {
477+ return ( relations . get ( id ) ?? [ ] ) ?. length > 0
478+ } ,
479+ getContentsScopedTo ( id : string , importedIds : readonly string [ ] ) {
480+ const values = ( relations . get ( id ) ?? [ ] ) . map (
481+ ( { id, exp } ) =>
482+ [
483+ id ,
484+ {
485+ content : contents . get ( id ) ?? '' ,
486+ exp,
487+ } ,
488+ ] as const ,
489+ )
490+ const styleIdToValue = new Map ( values )
491+ // get a sorted output by import order to make output deterministic
492+ return importedIds
493+ . filter ( ( id ) => styleIdToValue . has ( id ) )
494+ . map ( ( id ) => styleIdToValue . get ( id ) ! )
495+ } ,
496+ }
497+ }
498+
442499/**
443500 * Plugin applied after user plugins
444501 */
445502export function cssPostPlugin ( config : ResolvedConfig ) : Plugin {
446503 // styles initialization in buildStart causes a styling loss in watch
447- const styles : Map < string , string > = new Map < string , string > ( )
504+ const styles = createStyleContentMap ( )
448505 // queue to emit css serially to guarantee the files are emitted in a deterministic order
449506 let codeSplitEmitQueue = createSerialPromiseQueue < string > ( )
450507 const urlEmitQueue = createSerialPromiseQueue < unknown > ( )
@@ -588,9 +645,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
588645
589646 // build CSS handling ----------------------------------------------------
590647
648+ const cssScopeTo = (
649+ this . getModuleInfo ( id ) ?. meta ?. vite as
650+ | CustomPluginOptionsVite
651+ | undefined
652+ ) ?. cssScopeTo
653+
591654 // record css
592655 if ( ! inlined ) {
593- styles . set ( id , css )
656+ styles . putContent ( id , css , cssScopeTo )
594657 }
595658
596659 let code : string
@@ -612,7 +675,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
612675 map : { mappings : '' } ,
613676 // avoid the css module from being tree-shaken so that we can retrieve
614677 // it in renderChunk()
615- moduleSideEffects : modulesCode || inlined ? false : 'no-treeshake' ,
678+ moduleSideEffects :
679+ modulesCode || inlined || cssScopeTo ? false : 'no-treeshake' ,
616680 }
617681 } ,
618682
@@ -623,15 +687,28 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
623687 let isPureCssChunk = chunk . exports . length === 0
624688 const ids = Object . keys ( chunk . modules )
625689 for ( const id of ids ) {
626- if ( styles . has ( id ) ) {
690+ if ( styles . hasContentOfNonScoped ( id ) ) {
627691 // ?transform-only is used for ?url and shouldn't be included in normal CSS chunks
628692 if ( ! transformOnlyRE . test ( id ) ) {
629- chunkCSS += styles . get ( id )
693+ chunkCSS += styles . getContentOfNonScoped ( id )
630694 // a css module contains JS, so it makes this not a pure css chunk
631695 if ( cssModuleRE . test ( id ) ) {
632696 isPureCssChunk = false
633697 }
634698 }
699+ } else if ( styles . hasContentsScopedTo ( id ) ) {
700+ const renderedExports = chunk . modules [ id ] ! . renderedExports
701+ const importedIds = this . getModuleInfo ( id ) ?. importedIds ?? [ ]
702+ // If this module has scoped styles, check for the rendered exports
703+ // and include the corresponding CSS.
704+ for ( const { exp, content } of styles . getContentsScopedTo (
705+ id ,
706+ importedIds ,
707+ ) ) {
708+ if ( exp === undefined || renderedExports . includes ( exp ) ) {
709+ chunkCSS += content
710+ }
711+ }
635712 } else if ( ! isJsChunkEmpty ) {
636713 // if the module does not have a style, then it's not a pure css chunk.
637714 // this is true because in the `transform` hook above, only modules
@@ -726,13 +803,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
726803 path . basename ( originalFileName ) ,
727804 '.css' ,
728805 )
729- if ( ! styles . has ( id ) ) {
806+ if ( ! styles . hasContentOfNonScoped ( id ) ) {
730807 throw new Error (
731808 `css content for ${ JSON . stringify ( id ) } was not found` ,
732809 )
733810 }
734811
735- let cssContent = styles . get ( id ) !
812+ let cssContent = styles . getContentOfNonScoped ( id ) !
736813
737814 cssContent = resolveAssetUrlsInCss ( cssContent , cssAssetName )
738815
0 commit comments