1
1
const fs = require ( 'fs' )
2
+ const url = require ( 'url' )
2
3
const os = require ( 'os' )
3
4
const path = require ( 'path' )
4
5
const crypto = require ( 'crypto' )
5
6
const chokidar = require ( 'chokidar' )
6
7
const postcss = require ( 'postcss' )
8
+ const hash = require ( 'object-hash' )
7
9
const dlv = require ( 'dlv' )
8
10
const selectorParser = require ( 'postcss-selector-parser' )
9
11
const LRU = require ( 'quick-lru' )
@@ -21,6 +23,8 @@ const { isPlainObject } = require('./utils')
21
23
const { isBuffer } = require ( 'util' )
22
24
23
25
let contextMap = sharedState . contextMap
26
+ let configContextMap = sharedState . configContextMap
27
+ let contextSourcesMap = sharedState . contextSourcesMap
24
28
let env = sharedState . env
25
29
26
30
// Earmarks a directory for our touch files.
@@ -148,27 +152,29 @@ function getTailwindConfig(configOrPath) {
148
152
let userConfigPath = resolveConfigPath ( configOrPath )
149
153
150
154
if ( userConfigPath !== null ) {
151
- let [ prevConfig , prevModified = - Infinity ] = configPathCache . get ( userConfigPath ) ?? [ ]
155
+ let [ prevConfig , prevModified = - Infinity , prevConfigHash ] =
156
+ configPathCache . get ( userConfigPath ) ?? [ ]
152
157
let modified = fs . statSync ( userConfigPath ) . mtimeMs
153
158
154
159
// It hasn't changed (based on timestamp)
155
160
if ( modified <= prevModified ) {
156
- return [ prevConfig , userConfigPath ]
161
+ return [ prevConfig , userConfigPath , prevConfigHash ]
157
162
}
158
163
159
164
// It has changed (based on timestamp), or first run
160
165
delete require . cache [ userConfigPath ]
161
166
let newConfig = resolveConfig ( require ( userConfigPath ) )
162
- configPathCache . set ( userConfigPath , [ newConfig , modified ] )
163
- return [ newConfig , userConfigPath ]
167
+ let newHash = hash ( newConfig )
168
+ configPathCache . set ( userConfigPath , [ newConfig , modified , newHash ] )
169
+ return [ newConfig , userConfigPath , newHash ]
164
170
}
165
171
166
172
// It's a plain object, not a path
167
173
let newConfig = resolveConfig (
168
174
configOrPath . config === undefined ? configOrPath : configOrPath . config
169
175
)
170
176
171
- return [ newConfig , null ]
177
+ return [ newConfig , null , hash ( newConfig ) ]
172
178
}
173
179
174
180
let fileModifiedMap = new Map ( )
@@ -177,7 +183,8 @@ function trackModified(files) {
177
183
let changed = false
178
184
179
185
for ( let file of files ) {
180
- let newModified = fs . statSync ( file ) . mtimeMs
186
+ let pathname = url . parse ( file ) . pathname
187
+ let newModified = fs . statSync ( pathname ) . mtimeMs
181
188
182
189
if ( ! fileModifiedMap . has ( file ) || newModified > fileModifiedMap . get ( file ) ) {
183
190
changed = true
@@ -537,32 +544,68 @@ function cleanupContext(context) {
537
544
// plugins) then return it
538
545
function setupContext ( configOrPath ) {
539
546
return ( result , root ) => {
547
+ let foundTailwind = false
548
+
549
+ root . walkAtRules ( 'tailwind' , ( rule ) => {
550
+ foundTailwind = true
551
+ } )
552
+
540
553
let sourcePath = result . opts . from
541
- let [ tailwindConfig , userConfigPath ] = getTailwindConfig ( configOrPath )
554
+ let [ tailwindConfig , userConfigPath , tailwindConfigHash ] = getTailwindConfig ( configOrPath )
542
555
543
556
let contextDependencies = new Set ( )
544
- contextDependencies . add ( sourcePath )
545
557
546
- if ( userConfigPath !== null ) {
547
- contextDependencies . add ( userConfigPath )
558
+ // If there are no @tailwind rules, we don't consider this CSS file or it's dependencies
559
+ // to be dependencies of the context. Can reuse the context even if they change.
560
+ // We may want to think about `@layer` being part of this trigger too, but it's tough
561
+ // because it's impossible for a layer in one file to end up in the actual @tailwind rule
562
+ // in another file since independent sources are effectively isolated.
563
+ if ( foundTailwind ) {
564
+ contextDependencies . add ( sourcePath )
565
+ for ( let message of result . messages ) {
566
+ if ( message . type === 'dependency' ) {
567
+ contextDependencies . add ( message . file )
568
+ }
569
+ }
548
570
}
549
571
550
- for ( let message of result . messages ) {
551
- if ( message . type === 'dependency' ) {
552
- contextDependencies . add ( message . file )
553
- }
572
+ if ( userConfigPath !== null ) {
573
+ contextDependencies . add ( userConfigPath )
554
574
}
555
575
556
576
let contextDependenciesChanged =
557
577
trackModified ( [ ...contextDependencies ] ) || userConfigPath === null
558
578
559
579
process . env . DEBUG && console . log ( 'Source path:' , sourcePath )
580
+
581
+ // If this file already has a context in the cache and we don't need to
582
+ // reset the context, return the cached context.
560
583
if ( contextMap . has ( sourcePath ) && ! contextDependenciesChanged ) {
561
584
return contextMap . get ( sourcePath )
562
585
}
563
586
587
+ // If the config file used already exists in the cache, return that.
588
+ if ( ! contextDependenciesChanged && configContextMap . has ( tailwindConfigHash ) ) {
589
+ let context = configContextMap . get ( tailwindConfigHash )
590
+ contextSourcesMap . get ( context ) . add ( sourcePath )
591
+ contextMap . set ( sourcePath , context )
592
+ return context
593
+ }
594
+
595
+ // If this source is in the context map, get the old context.
596
+ // Remove this source from the context sources for the old context,
597
+ // and clean up that context if no one else is using it. This can be
598
+ // called by many processes in rapid succession, so we check for presence
599
+ // first because the first process to run this code will wipe it out first.
564
600
if ( contextMap . has ( sourcePath ) ) {
565
- cleanupContext ( contextMap . get ( sourcePath ) )
601
+ let oldContext = contextMap . get ( sourcePath )
602
+ if ( contextSourcesMap . has ( oldContext ) ) {
603
+ contextSourcesMap . get ( oldContext ) . remove ( sourcePath )
604
+ if ( contextSourcesMap . get ( oldContext ) . size === 0 ) {
605
+ contextSourcesMap . delete ( oldContext )
606
+ cleanupContext ( oldContext )
607
+ }
608
+ }
566
609
}
567
610
568
611
process . env . DEBUG && console . log ( 'Setting up new context...' )
@@ -579,7 +622,6 @@ function setupContext(configOrPath) {
579
622
newPostCssNodeCache : new Map ( ) ,
580
623
candidateRuleMap : new Map ( ) ,
581
624
configPath : userConfigPath ,
582
- sourcePath : sourcePath ,
583
625
tailwindConfig : tailwindConfig ,
584
626
configDependencies : new Set ( ) ,
585
627
candidateFiles : Array . isArray ( tailwindConfig . purge )
@@ -588,8 +630,22 @@ function setupContext(configOrPath) {
588
630
variantMap : new Map ( ) ,
589
631
stylesheetCache : null ,
590
632
}
633
+
634
+ // ---
635
+
636
+ // Update all context tracking state
637
+
638
+ configContextMap . set ( tailwindConfigHash , context )
591
639
contextMap . set ( sourcePath , context )
592
640
641
+ if ( ! contextSourcesMap . has ( context ) ) {
642
+ contextSourcesMap . set ( context , new Set ( ) )
643
+ }
644
+
645
+ contextSourcesMap . get ( context ) . add ( sourcePath )
646
+
647
+ // ---
648
+
593
649
if ( userConfigPath !== null ) {
594
650
for ( let dependency of getModuleDependencies ( userConfigPath ) ) {
595
651
if ( dependency . file === userConfigPath ) {
0 commit comments