diff --git a/src/lib/cacheInvalidation.js b/src/lib/cacheInvalidation.js new file mode 100644 index 000000000000..95a49ea4f39d --- /dev/null +++ b/src/lib/cacheInvalidation.js @@ -0,0 +1,36 @@ +import crypto from 'crypto' +import * as sharedState from './sharedState' + +/** + * + * @param {string} str + */ +function getHash(str) { + try { + return crypto.createHash('md5').update(str, 'utf-8').digest('binary') + } catch (err) { + return '' + } +} + +/** + * @param {string} sourcePath + * @param {import('postcss').Node} root + */ +export function hasContentChanged(sourcePath, root) { + let css = root.toString() + + // We only care about files with @tailwind directives + // Other files use an existing context + if (!css.includes('@tailwind')) { + return false + } + + let existingHash = sharedState.sourceHashMap.get(sourcePath) + let rootHash = getHash(css) + let didChange = existingHash !== rootHash + + sharedState.sourceHashMap.set(sourcePath, rootHash) + + return didChange +} diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index e35edcc14145..56986edc9815 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -20,6 +20,7 @@ import log from '../util/log' import negateValue from '../util/negateValue' import isValidArbitraryValue from '../util/isValidArbitraryValue' import { generateRules } from './generateRules' +import { hasContentChanged } from './cacheInvalidation.js' function prefix(context, selector) { let prefix = context.tailwindConfig.prefix @@ -790,6 +791,8 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs let resolvedPlugins = resolvePlugins(context, root) registerPlugins(resolvedPlugins, context) + sharedState.contextInvalidationCount++ + return context } @@ -822,6 +825,8 @@ export function getContext( existingContext = context } + let cssDidChange = hasContentChanged(sourcePath, root) + // If there's already a context in the cache and we don't need to // reset the context, return the cached context. if (existingContext) { @@ -829,7 +834,7 @@ export function getContext( [...contextDependencies], getFileModifiedMap(existingContext) ) - if (!contextDependenciesChanged) { + if (!contextDependenciesChanged && !cssDidChange) { return [existingContext, false] } } diff --git a/src/lib/sharedState.js b/src/lib/sharedState.js index e0cfe9f3f653..c9c34a518c43 100644 --- a/src/lib/sharedState.js +++ b/src/lib/sharedState.js @@ -5,6 +5,13 @@ export const env = { export const contextMap = new Map() export const configContextMap = new Map() export const contextSourcesMap = new Map() +/** + * A map of source files to their sizes / hashes + * + * @type {Map} + */ +export const sourceHashMap = new Map() +export const contextInvalidationCount = 0 export const NOT_ON_DEMAND = new String('*') export function resolveDebug(debug) { diff --git a/tests/context-reuse.test.js b/tests/context-reuse.test.js index caa5861c4790..f0ceb81815e7 100644 --- a/tests/context-reuse.test.js +++ b/tests/context-reuse.test.js @@ -85,3 +85,37 @@ it('a build re-uses the context across multiple files with the same config', asy // And none of this should have resulted in multiple contexts being created expect(sharedState.contextSourcesMap.size).toBe(1) }) + +fit('passing in different css invalidates the context if it contains @tailwind directives', async () => { + let from = path.resolve(__filename) + + // Save the file a handful of times with no changes + // This builds the context at most once + for (let n = 0; n < 5; n++) { + await run(`@tailwind utilities;`, configPath, `${from}?id=1`) + } + + expect(sharedState.contextInvalidationCount).toBe(1) + + // Save the file twice with a change + // This should rebuild the context again but only once + await run(`@tailwind utilities; .foo {}`, configPath, `${from}?id=1`) + await run(`@tailwind utilities; .foo {}`, configPath, `${from}?id=1`) + + expect(sharedState.contextInvalidationCount).toBe(2) + + // Save the file twice with a content but not length change + // This should rebuild the context two more times + await run(`@tailwind utilities; .bar {}`, configPath, `${from}?id=1`) + await run(`@tailwind utilities; .baz {}`, configPath, `${from}?id=1`) + + expect(sharedState.contextInvalidationCount).toBe(4) + + // Save a file with a change that does not affect the context + // No invalidation should occur + await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`) + await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`) + await run(`.foo { @apply mb-1; }`, configPath, `${from}?id=2`) + + expect(sharedState.contextInvalidationCount).toBe(4) +})