Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-use existing entries in the rule cache #9208

Merged
merged 3 commits into from Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
6 changes: 1 addition & 5 deletions src/lib/expandTailwindAtRules.js
Expand Up @@ -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')
Expand Down
81 changes: 47 additions & 34 deletions src/lib/generateRules.js
Expand Up @@ -649,16 +649,49 @@ 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)) {
continue
}

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
}

Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/setupContextUtils.js
Expand Up @@ -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(),
Expand Down
74 changes: 73 additions & 1 deletion 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 = {
Expand Down Expand Up @@ -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`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)

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`
<div class="ml-2"></div>
<div class="ml-6"></div>
`
)

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)
})
})