Skip to content

Commit 83d7e9e

Browse files
committed
Invalidate context when CSS changes
1 parent 89c4f68 commit 83d7e9e

File tree

4 files changed

+85
-1
lines changed

4 files changed

+85
-1
lines changed

src/lib/cacheInvalidation.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import crypto from 'crypto'
2+
import * as sharedState from './sharedState'
3+
4+
/**
5+
*
6+
* @param {string} str
7+
*/
8+
function getHash(str) {
9+
try {
10+
return crypto.createHash('md5').update(str, 'utf-8').digest('binary')
11+
} catch (err) {
12+
return ''
13+
}
14+
}
15+
16+
/**
17+
* @param {string} sourcePath
18+
* @param {import('postcss').Node} root
19+
*/
20+
export function hasContentChanged(sourcePath, root) {
21+
let css = root.toString()
22+
23+
// We only care about files with @tailwind directives
24+
// Other files use an existing context
25+
if (!css.includes('@tailwind')) {
26+
return false
27+
}
28+
29+
let existingHash = sharedState.sourceHashMap.get(sourcePath)
30+
let rootHash = getHash(css)
31+
let didChange = existingHash !== rootHash
32+
33+
sharedState.sourceHashMap.set(sourcePath, rootHash)
34+
35+
return didChange
36+
}

src/lib/setupContextUtils.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import log from '../util/log'
2020
import negateValue from '../util/negateValue'
2121
import isValidArbitraryValue from '../util/isValidArbitraryValue'
2222
import { generateRules } from './generateRules'
23+
import { hasContentChanged } from './cacheInvalidation.js'
2324

2425
function prefix(context, selector) {
2526
let prefix = context.tailwindConfig.prefix
@@ -790,6 +791,8 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
790791
let resolvedPlugins = resolvePlugins(context, root)
791792
registerPlugins(resolvedPlugins, context)
792793

794+
sharedState.contextInvalidationCount++
795+
793796
return context
794797
}
795798

@@ -822,14 +825,16 @@ export function getContext(
822825
existingContext = context
823826
}
824827

828+
let cssDidChange = hasContentChanged(sourcePath, root)
829+
825830
// If there's already a context in the cache and we don't need to
826831
// reset the context, return the cached context.
827832
if (existingContext) {
828833
let contextDependenciesChanged = trackModified(
829834
[...contextDependencies],
830835
getFileModifiedMap(existingContext)
831836
)
832-
if (!contextDependenciesChanged) {
837+
if (!contextDependenciesChanged && !cssDidChange) {
833838
return [existingContext, false]
834839
}
835840
}

src/lib/sharedState.js

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export const env = {
55
export const contextMap = new Map()
66
export const configContextMap = new Map()
77
export const contextSourcesMap = new Map()
8+
/**
9+
* A map of source files to their sizes / hashes
10+
*
11+
* @type {Map<string, string>}
12+
*/
13+
export const sourceHashMap = new Map()
14+
export const contextInvalidationCount = 0
815
export const NOT_ON_DEMAND = new String('*')
916

1017
export function resolveDebug(debug) {

tests/context-reuse.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,39 @@ it('a build re-uses the context across multiple files with the same config', asy
8585
// And none of this should have resulted in multiple contexts being created
8686
expect(sharedState.contextSourcesMap.size).toBe(1)
8787
})
88+
89+
it('passing in different css invalidates the context if it contains @tailwind directives', async () => {
90+
sharedState.contextInvalidationCount = 0
91+
92+
let from = path.resolve(__filename)
93+
94+
// Save the file a handful of times with no changes
95+
// This builds the context at most once
96+
for (let n = 0; n < 5; n++) {
97+
await run(`@tailwind utilities;`, configPath, `${from}?id=1`)
98+
}
99+
100+
expect(sharedState.contextInvalidationCount).toBe(1)
101+
102+
// Save the file twice with a change
103+
// This should rebuild the context again but only once
104+
await run(`@tailwind utilities; .foo {}`, configPath, `${from}?id=1`)
105+
await run(`@tailwind utilities; .foo {}`, configPath, `${from}?id=1`)
106+
107+
expect(sharedState.contextInvalidationCount).toBe(2)
108+
109+
// Save the file twice with a content but not length change
110+
// This should rebuild the context two more times
111+
await run(`@tailwind utilities; .bar {}`, configPath, `${from}?id=1`)
112+
await run(`@tailwind utilities; .baz {}`, configPath, `${from}?id=1`)
113+
114+
expect(sharedState.contextInvalidationCount).toBe(4)
115+
116+
// Save a file with a change that does not affect the context
117+
// No invalidation should occur
118+
await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`)
119+
await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`)
120+
await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`)
121+
122+
expect(sharedState.contextInvalidationCount).toBe(4)
123+
})

0 commit comments

Comments
 (0)