diff --git a/CHANGELOG.md b/CHANGELOG.md index 070c8dba3d98..933fa5f9ab49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove invalid `outline-hidden` utility ([#9147](https://github.com/tailwindlabs/tailwindcss/pull/9147)) - Honor the `hidden` attribute on elements in preflight ([#9174](https://github.com/tailwindlabs/tailwindcss/pull/9174)) - Don't stop watching atomically renamed files ([#9173](https://github.com/tailwindlabs/tailwindcss/pull/9173)) +- Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208)) +- Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208)) ## [3.1.8] - 2022-08-05 diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index a0c1635a0fba..30b470800626 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -177,16 +177,12 @@ export default function expandTailwindAtRules(context) { let classCacheCount = context.classCache.size env.DEBUG && console.time('Generate rules') - let rules = generateRules(candidates, context) + generateRules(candidates, context) env.DEBUG && console.timeEnd('Generate rules') // We only ever add to the classCache, so if it didn't grow, there is nothing new. env.DEBUG && console.time('Build stylesheet') if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) { - for (let rule of rules) { - context.ruleCache.add(rule) - } - context.stylesheetCache = buildStylesheet([...context.ruleCache], context) } env.DEBUG && console.timeEnd('Build stylesheet') diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 0f441fb18018..c99826e88971 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -649,8 +649,37 @@ function inKeyframes(rule) { return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes' } +function getImportantStrategy(important) { + if (important === true) { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.walkDecls((d) => { + if (d.parent.type === 'rule' && !inKeyframes(d.parent)) { + d.important = true + } + }) + } + } + + if (typeof important === 'string') { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return `${important} ${selector}` + }) + } + } +} + function generateRules(candidates, context) { let allRules = [] + let strategy = getImportantStrategy(context.tailwindConfig.important) for (let candidate of candidates) { if (context.notClassCache.has(candidate)) { @@ -658,7 +687,11 @@ function generateRules(candidates, context) { } if (context.classCache.has(candidate)) { - allRules.push(context.classCache.get(candidate)) + continue + } + + if (context.candidateRuleCache.has(candidate)) { + allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate))) continue } @@ -670,47 +703,27 @@ function generateRules(candidates, context) { } context.classCache.set(candidate, matches) - allRules.push(matches) - } - // Strategy based on `tailwindConfig.important` - let strategy = ((important) => { - if (important === true) { - return (rule) => { - rule.walkDecls((d) => { - if (d.parent.type === 'rule' && !inKeyframes(d.parent)) { - d.important = true - } - }) - } - } + let rules = context.candidateRuleCache.get(candidate) ?? new Set() + context.candidateRuleCache.set(candidate, rules) - if (typeof important === 'string') { - return (rule) => { - rule.selectors = rule.selectors.map((selector) => { - return `${important} ${selector}` - }) - } - } - })(context.tailwindConfig.important) + for (const match of matches) { + let [{ sort, layer, options }, rule] = match - return allRules.flat(1).map(([{ sort, layer, options }, rule]) => { - if (options.respectImportant) { - if (strategy) { + if (options.respectImportant && strategy) { let container = postcss.root({ nodes: [rule.clone()] }) - container.walkRules((r) => { - if (inKeyframes(r)) { - return - } - - strategy(r) - }) + container.walkRules(strategy) rule = container.nodes[0] } + + let newEntry = [sort | context.layerOrder[layer], rule] + rules.add(newEntry) + context.ruleCache.add(newEntry) + allRules.push(newEntry) } + } - return [sort | context.layerOrder[layer], rule] - }) + return allRules } function isArbitraryValue(input) { diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index b0552b05d50c..1fd72e301c4e 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -873,6 +873,7 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs let context = { disposables: [], ruleCache: new Set(), + candidateRuleCache: new Map(), classCache: new Map(), applyClassCache: new Map(), notClassCache: new Set(), diff --git a/tests/important-boolean.test.js b/tests/important-boolean.test.js index 20fc4d1ee598..48a7c10f89f8 100644 --- a/tests/important-boolean.test.js +++ b/tests/important-boolean.test.js @@ -1,7 +1,8 @@ import fs from 'fs' import path from 'path' +import * as sharedState from '../src/lib/sharedState' -import { run, css } from './util/run' +import { run, css, html } from './util/run' test('important boolean', () => { let config = { @@ -63,3 +64,74 @@ test('important boolean', () => { expect(result.css).toMatchFormattedCss(expected) }) }) + +// This is in a describe block so we can use `afterEach` :) +describe('duplicate elision', () => { + let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html') + + afterEach(async () => await fs.promises.unlink(filePath)) + + test('important rules are not duplicated when rebuilding', async () => { + let config = { + important: true, + content: [filePath], + } + + await fs.promises.writeFile( + config.content[0], + html` +
+
+ ` + ) + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + let allContexts = Array.from(sharedState.contextMap.values()) + + let context = allContexts[allContexts.length - 1] + + let ruleCacheSize1 = context.ruleCache.size + + expect(result.css).toMatchFormattedCss(css` + .ml-2 { + margin-left: 0.5rem !important; + } + .ml-4 { + margin-left: 1rem !important; + } + `) + + await fs.promises.writeFile( + config.content[0], + html` +
+
+ ` + ) + + result = await run(input, config) + + let ruleCacheSize2 = context.ruleCache.size + + expect(result.css).toMatchFormattedCss(css` + .ml-2 { + margin-left: 0.5rem !important; + } + .ml-4 { + margin-left: 1rem !important; + } + .ml-6 { + margin-left: 1.5rem !important; + } + `) + + // The rule cache was effectively doubling in size previously + // because the rule cache was never de-duped + // This ensures this behavior doesn't return + expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10) + }) +})