@@ -27,7 +27,12 @@ import type { RawSourceMap } from '@ampproject/remapping'
27
27
import { getCodeWithSourcemap , injectSourcesContent } from '../server/sourcemap'
28
28
import type { ModuleNode } from '../server/moduleGraph'
29
29
import type { ResolveFn , ViteDevServer } from '../'
30
- import { resolveUserExternal , toOutputFilePathInCss } from '../build'
30
+ import {
31
+ createToImportMetaURLBasedRelativeRuntime ,
32
+ resolveUserExternal ,
33
+ toOutputFilePathInCss ,
34
+ toOutputFilePathInJS ,
35
+ } from '../build'
31
36
import {
32
37
CLIENT_PUBLIC_PATH ,
33
38
CSS_LANGS_RE ,
@@ -42,10 +47,12 @@ import {
42
47
asyncReplace ,
43
48
cleanUrl ,
44
49
combineSourcemaps ,
50
+ createSerialPromiseQueue ,
45
51
emptyCssComments ,
46
52
generateCodeFrame ,
47
53
getHash ,
48
54
getPackageManagerCommand ,
55
+ injectQuery ,
49
56
isDataUrl ,
50
57
isExternalUrl ,
51
58
isObject ,
@@ -54,10 +61,12 @@ import {
54
61
parseRequest ,
55
62
processSrcSet ,
56
63
removeDirectQuery ,
64
+ removeUrlQuery ,
57
65
requireResolveFromRootWithFallback ,
58
66
slash ,
59
67
stripBase ,
60
68
stripBomTag ,
69
+ urlRE ,
61
70
} from '../utils'
62
71
import type { Logger } from '../logger'
63
72
import { addToHTMLProxyTransformResult } from './html'
@@ -167,6 +176,7 @@ const inlineRE = /[?&]inline\b/
167
176
const inlineCSSRE = / [ ? & ] i n l i n e - c s s \b /
168
177
const styleAttrRE = / [ ? & ] s t y l e - a t t r \b /
169
178
const functionCallRE = / ^ [ A - Z _ ] [ \w - ] * \( / i
179
+ const transformOnlyRE = / [ ? & ] t r a n s f o r m - o n l y \b /
170
180
const nonEscapedDoubleQuoteRe = / (?< ! \\ ) ( " ) / g
171
181
172
182
const cssBundleName = 'style.css'
@@ -220,10 +230,13 @@ function encodePublicUrlsInCSS(config: ResolvedConfig) {
220
230
return config . command === 'build'
221
231
}
222
232
233
+ const cssUrlAssetRE = / _ _ V I T E _ C S S _ U R L _ _ ( [ \d a - f ] + ) _ _ / g
234
+
223
235
/**
224
236
* Plugin applied before user plugins
225
237
*/
226
238
export function cssPlugin ( config : ResolvedConfig ) : Plugin {
239
+ const isBuild = config . command === 'build'
227
240
let server : ViteDevServer
228
241
let moduleCache : Map < string , Record < string , string > >
229
242
@@ -253,6 +266,32 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
253
266
removedPureCssFilesCache . set ( config , new Map < string , RenderedChunk > ( ) )
254
267
} ,
255
268
269
+ async load ( id ) {
270
+ if ( ! isCSSRequest ( id ) ) return
271
+
272
+ if ( urlRE . test ( id ) ) {
273
+ if ( isModuleCSSRequest ( id ) ) {
274
+ throw new Error (
275
+ `?url is not supported with CSS modules. (tried to import ${ JSON . stringify (
276
+ id ,
277
+ ) } )`,
278
+ )
279
+ }
280
+
281
+ // *.css?url
282
+ // in dev, it's handled by assets plugin.
283
+ if ( isBuild ) {
284
+ id = injectQuery ( removeUrlQuery ( id ) , 'transform-only' )
285
+ return (
286
+ `import ${ JSON . stringify ( id ) } ;` +
287
+ `export default "__VITE_CSS_URL__${ Buffer . from ( id ) . toString (
288
+ 'hex' ,
289
+ ) } __"`
290
+ )
291
+ }
292
+ }
293
+ } ,
294
+
256
295
async transform ( raw , id , options ) {
257
296
if (
258
297
! isCSSRequest ( id ) ||
@@ -374,8 +413,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
374
413
export function cssPostPlugin ( config : ResolvedConfig ) : Plugin {
375
414
// styles initialization in buildStart causes a styling loss in watch
376
415
const styles : Map < string , string > = new Map < string , string > ( )
377
- // list of css emit tasks to guarantee the files are emitted in a deterministic order
378
- let emitTasks : Promise < void > [ ] = [ ]
416
+ // queue to emit css serially to guarantee the files are emitted in a deterministic order
417
+ let codeSplitEmitQueue = createSerialPromiseQueue < string > ( )
418
+ const urlEmitQueue = createSerialPromiseQueue < unknown > ( )
379
419
let pureCssChunks : Set < RenderedChunk >
380
420
381
421
// when there are multiple rollup outputs and extracting CSS, only emit once,
@@ -414,7 +454,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
414
454
pureCssChunks = new Set < RenderedChunk > ( )
415
455
hasEmitted = false
416
456
chunkCSSMap = new Map ( )
417
- emitTasks = [ ]
457
+ codeSplitEmitQueue = createSerialPromiseQueue ( )
418
458
} ,
419
459
420
460
async transform ( css , id , options ) {
@@ -530,10 +570,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
530
570
const ids = Object . keys ( chunk . modules )
531
571
for ( const id of ids ) {
532
572
if ( styles . has ( id ) ) {
533
- chunkCSS += styles . get ( id )
534
- // a css module contains JS, so it makes this not a pure css chunk
535
- if ( cssModuleRE . test ( id ) ) {
536
- isPureCssChunk = false
573
+ // ?transform-only is used for ?url and shouldn't be included in normal CSS chunks
574
+ if ( ! transformOnlyRE . test ( id ) ) {
575
+ chunkCSS += styles . get ( id )
576
+ // a css module contains JS, so it makes this not a pure css chunk
577
+ if ( cssModuleRE . test ( id ) ) {
578
+ isPureCssChunk = false
579
+ }
537
580
}
538
581
} else {
539
582
// if the module does not have a style, then it's not a pure css chunk.
@@ -543,10 +586,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
543
586
}
544
587
}
545
588
546
- if ( ! chunkCSS ) {
547
- return null
548
- }
549
-
550
589
const publicAssetUrlMap = publicAssetUrlCache . get ( config ) !
551
590
552
591
// resolve asset URL placeholders to their built file URLs
@@ -608,6 +647,98 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
608
647
)
609
648
}
610
649
650
+ let s : MagicString | undefined
651
+ const urlEmitTasks : Array < {
652
+ cssAssetName : string
653
+ originalFilename : string
654
+ content : string
655
+ start : number
656
+ end : number
657
+ } > = [ ]
658
+
659
+ if ( code . includes ( '__VITE_CSS_URL__' ) ) {
660
+ let match : RegExpExecArray | null
661
+ cssUrlAssetRE . lastIndex = 0
662
+ while ( ( match = cssUrlAssetRE . exec ( code ) ) ) {
663
+ const [ full , idHex ] = match
664
+ const id = Buffer . from ( idHex , 'hex' ) . toString ( )
665
+ const originalFilename = cleanUrl ( id )
666
+ const cssAssetName = ensureFileExt (
667
+ path . basename ( originalFilename ) ,
668
+ '.css' ,
669
+ )
670
+ if ( ! styles . has ( id ) ) {
671
+ throw new Error (
672
+ `css content for ${ JSON . stringify ( id ) } was not found` ,
673
+ )
674
+ }
675
+
676
+ let cssContent = styles . get ( id ) !
677
+
678
+ cssContent = resolveAssetUrlsInCss ( cssContent , cssAssetName )
679
+
680
+ urlEmitTasks . push ( {
681
+ cssAssetName,
682
+ originalFilename,
683
+ content : cssContent ,
684
+ start : match . index ,
685
+ end : match . index + full . length ,
686
+ } )
687
+ }
688
+ }
689
+
690
+ // should await even if this chunk does not include __VITE_CSS_URL__
691
+ // so that code after this line runs in the same order
692
+ await urlEmitQueue . run ( async ( ) =>
693
+ Promise . all (
694
+ urlEmitTasks . map ( async ( info ) => {
695
+ info . content = await finalizeCss ( info . content , true , config )
696
+ } ) ,
697
+ ) ,
698
+ )
699
+ if ( urlEmitTasks . length > 0 ) {
700
+ const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime (
701
+ opts . format ,
702
+ config . isWorker ,
703
+ )
704
+ s ||= new MagicString ( code )
705
+
706
+ for ( const {
707
+ cssAssetName,
708
+ originalFilename,
709
+ content,
710
+ start,
711
+ end,
712
+ } of urlEmitTasks ) {
713
+ const referenceId = this . emitFile ( {
714
+ name : cssAssetName ,
715
+ type : 'asset' ,
716
+ source : content ,
717
+ } )
718
+ generatedAssets
719
+ . get ( config ) !
720
+ . set ( referenceId , { originalName : originalFilename } )
721
+
722
+ const replacement = toOutputFilePathInJS (
723
+ this . getFileName ( referenceId ) ,
724
+ 'asset' ,
725
+ chunk . fileName ,
726
+ 'js' ,
727
+ config ,
728
+ toRelativeRuntime ,
729
+ )
730
+ const replacementString =
731
+ typeof replacement === 'string'
732
+ ? JSON . stringify ( replacement ) . slice ( 1 , - 1 )
733
+ : `"+${ replacement . runtime } +"`
734
+ s . update ( start , end , replacementString )
735
+ }
736
+ }
737
+
738
+ if ( ! chunkCSS && ! s ) {
739
+ return null
740
+ }
741
+
611
742
if ( config . build . cssCodeSplit ) {
612
743
if ( opts . format === 'es' || opts . format === 'cjs' ) {
613
744
if ( isPureCssChunk ) {
@@ -633,22 +764,11 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
633
764
634
765
chunkCSS = resolveAssetUrlsInCss ( chunkCSS , cssAssetName )
635
766
636
- const previousTask = emitTasks [ emitTasks . length - 1 ]
637
- // finalizeCss is async which makes `emitFile` non-deterministic, so
638
- // we use a `.then` to wait for previous tasks before finishing this
639
- const thisTask = finalizeCss ( chunkCSS , true , config ) . then ( ( css ) => {
640
- chunkCSS = css
641
- // make sure the previous task is also finished, this works recursively
642
- return previousTask
767
+ // wait for previous tasks as well
768
+ chunkCSS = await codeSplitEmitQueue . run ( async ( ) => {
769
+ return finalizeCss ( chunkCSS , true , config )
643
770
} )
644
771
645
- // push this task so the next task can wait for this one
646
- emitTasks . push ( thisTask )
647
- const emitTasksLength = emitTasks . length
648
-
649
- // wait for this and previous tasks to finish
650
- await thisTask
651
-
652
772
// emit corresponding css file
653
773
const referenceId = this . emitFile ( {
654
774
name : cssAssetName ,
@@ -659,11 +779,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
659
779
. get ( config ) !
660
780
. set ( referenceId , { originalName : originalFilename , isEntry } )
661
781
chunk . viteMetadata ! . importedCss . add ( this . getFileName ( referenceId ) )
662
-
663
- if ( emitTasksLength === emitTasks . length ) {
664
- // this is the last task, clear `emitTasks` to free up memory
665
- emitTasks = [ ]
666
- }
667
782
} else if ( ! config . build . ssr ) {
668
783
// legacy build and inline css
669
784
@@ -697,24 +812,27 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
697
812
const insertMark = "'use strict';"
698
813
injectionPoint = code . indexOf ( insertMark ) + insertMark . length
699
814
}
700
- const s = new MagicString ( code )
815
+ s || = new MagicString ( code )
701
816
s . appendRight ( injectionPoint , injectCode )
702
- if ( config . build . sourcemap ) {
703
- // resolve public URL from CSS paths, we need to use absolute paths
704
- return {
705
- code : s . toString ( ) ,
706
- map : s . generateMap ( { hires : 'boundary' } ) ,
707
- }
708
- } else {
709
- return { code : s . toString ( ) }
710
- }
711
817
}
712
818
} else {
819
+ // resolve public URL from CSS paths, we need to use absolute paths
713
820
chunkCSS = resolveAssetUrlsInCss ( chunkCSS , cssBundleName )
714
821
// finalizeCss is called for the aggregated chunk in generateBundle
715
822
716
823
chunkCSSMap . set ( chunk . fileName , chunkCSS )
717
824
}
825
+
826
+ if ( s ) {
827
+ if ( config . build . sourcemap ) {
828
+ return {
829
+ code : s . toString ( ) ,
830
+ map : s . generateMap ( { hires : 'boundary' } ) ,
831
+ }
832
+ } else {
833
+ return { code : s . toString ( ) }
834
+ }
835
+ }
718
836
return null
719
837
} ,
720
838
0 commit comments