@@ -3,7 +3,7 @@ import { substituteAtApply } from './apply'
33import {
44 atRoot ,
55 comment ,
6- context ,
6+ context as contextNode ,
77 decl ,
88 rule ,
99 toCss ,
@@ -72,22 +72,64 @@ async function parseCss(
7272 loadStylesheet = throwOnLoadStylesheet ,
7373 } : CompileOptions = { } ,
7474) {
75- let ast = [ context ( { base } , CSS . parse ( css ) ) ] as AstNode [ ]
75+ let ast = [ contextNode ( { base } , CSS . parse ( css ) ) ] as AstNode [ ]
7676
7777 await substituteAtImports ( ast , base , loadStylesheet )
7878
79- let important : boolean | null = null
79+ let important = null as boolean | null
8080 let theme = new Theme ( )
8181 let customVariants : ( ( designSystem : DesignSystem ) => void ) [ ] = [ ]
8282 let customUtilities : ( ( designSystem : DesignSystem ) => void ) [ ] = [ ]
83- let firstThemeRule : Rule | null = null
83+ let firstThemeRule = null as Rule | null
84+ let utilitiesNode = null as Rule | null
8485 let globs : { base : string ; pattern : string } [ ] = [ ]
86+ let root :
87+ | null // Unknown root
88+ | 'none' // Explicitly no root specified via `source(none)`
89+ // Specified via `source(…)`, relative to the `base`
90+ | { base : string ; pattern : string } = null
8591
8692 // Handle at-rules
8793 walk ( ast , ( node , { parent, replaceWith, context } ) => {
8894 if ( node . kind !== 'rule' ) return
8995 if ( node . selector [ 0 ] !== '@' ) return
9096
97+ // Find `@tailwind utilities` so that we can later replace it with the
98+ // actual generated utility class CSS.
99+ if (
100+ utilitiesNode === null &&
101+ ( node . selector === '@tailwind utilities' || node . selector . startsWith ( '@tailwind utilities ' ) )
102+ ) {
103+ let params = segment ( node . selector . slice ( 20 ) . trim ( ) , ' ' )
104+ for ( let param of params ) {
105+ if ( param . startsWith ( 'source(' ) ) {
106+ let path = param . slice ( 7 , - 1 )
107+
108+ // Keyword: `source(none)`
109+ if ( path === 'none' ) {
110+ root = path
111+ continue
112+ }
113+
114+ // Explicit path: `source('…')`
115+ if (
116+ ( path [ 0 ] === '"' && path [ path . length - 1 ] !== '"' ) ||
117+ ( path [ 0 ] === "'" && path [ path . length - 1 ] !== "'" ) ||
118+ ( path [ 0 ] !== "'" && path [ 0 ] !== '"' )
119+ ) {
120+ throw new Error ( '`source(…)` paths must be quoted.' )
121+ }
122+
123+ root = {
124+ base : context . sourceBase ?? context . base ,
125+ pattern : path . slice ( 1 , - 1 ) ,
126+ }
127+ }
128+ }
129+
130+ utilitiesNode = node
131+ }
132+
91133 // Collect custom `@utility` at-rules
92134 if ( node . selector . startsWith ( '@utility ' ) ) {
93135 if ( parent !== null ) {
@@ -202,12 +244,26 @@ async function parseCss(
202244 let unknownParams : string [ ] = [ ]
203245
204246 for ( let param of params ) {
247+ // Handle `@media source(…)`
248+ if ( param . startsWith ( 'source(' ) ) {
249+ let path = param . slice ( 7 , - 1 )
250+
251+ walk ( node . nodes , ( child , { replaceWith } ) => {
252+ if ( child . kind !== 'rule' ) return
253+ if ( child . selector === '@tailwind utilities' ) {
254+ child . selector += ` source(${ path } )`
255+ replaceWith ( [ contextNode ( { sourceBase : context . base } , [ child ] ) ] )
256+ return WalkAction . Stop
257+ }
258+ } )
259+ }
260+
205261 // Handle `@media theme(…)`
206262 //
207263 // We support `@import "tailwindcss/theme" theme(reference)` as a way to
208264 // import an external theme file as a reference, which becomes `@media
209265 // theme(reference) { … }` when the `@import` is processed.
210- if ( param . startsWith ( 'theme(' ) ) {
266+ else if ( param . startsWith ( 'theme(' ) ) {
211267 let themeParams = param . slice ( 6 , - 1 )
212268
213269 walk ( node . nodes , ( child ) => {
@@ -389,6 +445,8 @@ async function parseCss(
389445 designSystem,
390446 ast,
391447 globs,
448+ root,
449+ utilitiesNode,
392450 }
393451}
394452
@@ -397,24 +455,13 @@ export async function compile(
397455 opts : CompileOptions = { } ,
398456) : Promise < {
399457 globs : { base : string ; pattern : string } [ ]
458+ root :
459+ | null // Unknown root
460+ | 'none' // Explicitly no root specified via `source(none)`
461+ | { base : string ; pattern : string } // Specified via `source(…)`, relative to the `base`
400462 build ( candidates : string [ ] ) : string
401463} > {
402- let { designSystem, ast, globs } = await parseCss ( css , opts )
403-
404- let tailwindUtilitiesNode : Rule | null = null
405-
406- // Find `@tailwind utilities` so that we can later replace it with the actual
407- // generated utility class CSS.
408- walk ( ast , ( node ) => {
409- if ( node . kind === 'rule' && node . selector === '@tailwind utilities' ) {
410- tailwindUtilitiesNode = node
411-
412- // Stop walking after finding `@tailwind utilities` to avoid walking all
413- // of the generated CSS. This means `@tailwind utilities` can only appear
414- // once per file but that's the intended usage at this point in time.
415- return WalkAction . Stop
416- }
417- } )
464+ let { designSystem, ast, globs, root, utilitiesNode } = await parseCss ( css , opts )
418465
419466 if ( process . env . NODE_ENV !== 'test' ) {
420467 ast . unshift ( comment ( `! tailwindcss v${ version } | MIT License | https://tailwindcss.com ` ) )
@@ -434,6 +481,7 @@ export async function compile(
434481
435482 return {
436483 globs,
484+ root,
437485 build ( newRawCandidates : string [ ] ) {
438486 let didChange = false
439487
@@ -452,7 +500,7 @@ export async function compile(
452500 return compiledCss
453501 }
454502
455- if ( tailwindUtilitiesNode ) {
503+ if ( utilitiesNode ) {
456504 let newNodes = compileCandidates ( allValidCandidates , designSystem , {
457505 onInvalidCandidate,
458506 } ) . astNodes
@@ -466,7 +514,7 @@ export async function compile(
466514
467515 previousAstNodeCount = newNodes . length
468516
469- tailwindUtilitiesNode . nodes = newNodes
517+ utilitiesNode . nodes = newNodes
470518 compiledCss = toCss ( ast )
471519 }
472520
0 commit comments