@@ -54,7 +54,7 @@ import {
54
54
SPECIAL_QUERY_RE ,
55
55
} from '../constants'
56
56
import type { ResolvedConfig } from '../config'
57
- import type { Plugin } from '../plugin'
57
+ import type { CustomPluginOptionsVite , Plugin } from '../plugin'
58
58
import { checkPublicFile } from '../publicDir'
59
59
import {
60
60
arraify ,
@@ -439,12 +439,69 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
439
439
}
440
440
}
441
441
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
+
442
499
/**
443
500
* Plugin applied after user plugins
444
501
*/
445
502
export function cssPostPlugin ( config : ResolvedConfig ) : Plugin {
446
503
// styles initialization in buildStart causes a styling loss in watch
447
- const styles : Map < string , string > = new Map < string , string > ( )
504
+ const styles = createStyleContentMap ( )
448
505
// queue to emit css serially to guarantee the files are emitted in a deterministic order
449
506
let codeSplitEmitQueue = createSerialPromiseQueue < string > ( )
450
507
const urlEmitQueue = createSerialPromiseQueue < unknown > ( )
@@ -588,9 +645,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
588
645
589
646
// build CSS handling ----------------------------------------------------
590
647
648
+ const cssScopeTo = (
649
+ this . getModuleInfo ( id ) ?. meta ?. vite as
650
+ | CustomPluginOptionsVite
651
+ | undefined
652
+ ) ?. cssScopeTo
653
+
591
654
// record css
592
655
if ( ! inlined ) {
593
- styles . set ( id , css )
656
+ styles . putContent ( id , css , cssScopeTo )
594
657
}
595
658
596
659
let code : string
@@ -612,7 +675,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
612
675
map : { mappings : '' } ,
613
676
// avoid the css module from being tree-shaken so that we can retrieve
614
677
// it in renderChunk()
615
- moduleSideEffects : modulesCode || inlined ? false : 'no-treeshake' ,
678
+ moduleSideEffects :
679
+ modulesCode || inlined || cssScopeTo ? false : 'no-treeshake' ,
616
680
}
617
681
} ,
618
682
@@ -623,15 +687,28 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
623
687
let isPureCssChunk = chunk . exports . length === 0
624
688
const ids = Object . keys ( chunk . modules )
625
689
for ( const id of ids ) {
626
- if ( styles . has ( id ) ) {
690
+ if ( styles . hasContentOfNonScoped ( id ) ) {
627
691
// ?transform-only is used for ?url and shouldn't be included in normal CSS chunks
628
692
if ( ! transformOnlyRE . test ( id ) ) {
629
- chunkCSS += styles . get ( id )
693
+ chunkCSS += styles . getContentOfNonScoped ( id )
630
694
// a css module contains JS, so it makes this not a pure css chunk
631
695
if ( cssModuleRE . test ( id ) ) {
632
696
isPureCssChunk = false
633
697
}
634
698
}
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
+ }
635
712
} else if ( ! isJsChunkEmpty ) {
636
713
// if the module does not have a style, then it's not a pure css chunk.
637
714
// this is true because in the `transform` hook above, only modules
@@ -726,13 +803,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
726
803
path . basename ( originalFileName ) ,
727
804
'.css' ,
728
805
)
729
- if ( ! styles . has ( id ) ) {
806
+ if ( ! styles . hasContentOfNonScoped ( id ) ) {
730
807
throw new Error (
731
808
`css content for ${ JSON . stringify ( id ) } was not found` ,
732
809
)
733
810
}
734
811
735
- let cssContent = styles . get ( id ) !
812
+ let cssContent = styles . getContentOfNonScoped ( id ) !
736
813
737
814
cssContent = resolveAssetUrlsInCss ( cssContent , cssAssetName )
738
815
0 commit comments