Skip to content

Commit 8fe6f48

Browse files
Don't emit utilities containing invalid theme fn keys (#9319)
* Don't emit utilities containing invalid theme keys * Update changelog
1 parent 527031d commit 8fe6f48

File tree

5 files changed

+145
-8
lines changed

5 files changed

+145
-8
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ isolate*.log
1717

1818
# Generated files
1919
/src/corePluginList.js
20+
21+
# Generated files during tests
22+
/tests/evaluate-tailwind-functions.test.html

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Fix ordering of parallel variants ([#9282](https://github.com/tailwindlabs/tailwindcss/pull/9282))
3333
- Handle variants in utility selectors using `:where()` and `:has()` ([#9309](https://github.com/tailwindlabs/tailwindcss/pull/9309))
3434
- Improve data type analyses for arbitrary values ([#9320](https://github.com/tailwindlabs/tailwindcss/pull/9320))
35+
- Don't emit generated utilities with invalid uses of theme functions ([#9319](https://github.com/tailwindlabs/tailwindcss/pull/9319))
3536

3637
## [3.1.8] - 2022-08-05
3738

src/lib/evaluateTailwindFunctions.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import buildMediaQuery from '../util/buildMediaQuery'
77
import { toPath } from '../util/toPath'
88
import { withAlphaValue } from '../util/withAlphaVariable'
99
import { parseColorFormat } from '../util/pluginUtils'
10+
import log from '../util/log'
1011

1112
function isObject(input) {
1213
return typeof input === 'object' && input !== null
@@ -196,7 +197,9 @@ function resolvePath(config, path, defaultValue) {
196197
return results.find((result) => result.isValid) ?? results[0]
197198
}
198199

199-
export default function ({ tailwindConfig: config }) {
200+
export default function (context) {
201+
let config = context.tailwindConfig
202+
200203
let functions = {
201204
theme: (node, path, ...defaultValue) => {
202205
let { isValid, value, error, alpha } = resolvePath(
@@ -206,6 +209,24 @@ export default function ({ tailwindConfig: config }) {
206209
)
207210

208211
if (!isValid) {
212+
let parentNode = node.parent
213+
let candidate = parentNode?.raws.tailwind?.candidate
214+
215+
if (parentNode && candidate !== undefined) {
216+
// Remove this utility from any caches
217+
context.markInvalidUtilityNode(parentNode)
218+
219+
// Remove the CSS node from the markup
220+
parentNode.remove()
221+
222+
// Show a warning
223+
log.warn('invalid-theme-key-in-class', [
224+
`The utility \`${candidate}\` contains an invalid theme value and was not generated.`,
225+
])
226+
227+
return
228+
}
229+
209230
throw node.error(error)
210231
}
211232

src/lib/setupContextUtils.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,52 @@ function registerPlugins(plugins, context) {
856856
}
857857
}
858858

859+
/**
860+
* Mark as class as retroactively invalid
861+
*
862+
*
863+
* @param {string} candidate
864+
*/
865+
function markInvalidUtilityCandidate(context, candidate) {
866+
if (!context.classCache.has(candidate)) {
867+
return
868+
}
869+
870+
// Mark this as not being a real utility
871+
context.notClassCache.add(candidate)
872+
873+
// Remove it from any candidate-specific caches
874+
context.classCache.delete(candidate)
875+
context.applyClassCache.delete(candidate)
876+
context.candidateRuleMap.delete(candidate)
877+
context.candidateRuleCache.delete(candidate)
878+
879+
// Ensure the stylesheet gets rebuilt
880+
context.stylesheetCache = null
881+
}
882+
883+
/**
884+
* Mark as class as retroactively invalid
885+
*
886+
* @param {import('postcss').Node} node
887+
*/
888+
function markInvalidUtilityNode(context, node) {
889+
let candidate = node.raws.tailwind.candidate
890+
891+
if (!candidate) {
892+
return
893+
}
894+
895+
for (const entry of context.ruleCache) {
896+
if (entry[1].raws.tailwind.candidate === candidate) {
897+
context.ruleCache.delete(entry)
898+
// context.postCssNodeCache.delete(node)
899+
}
900+
}
901+
902+
markInvalidUtilityCandidate(context, candidate)
903+
}
904+
859905
export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) {
860906
let context = {
861907
disposables: [],
@@ -870,6 +916,9 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
870916
changedContent: changedContent,
871917
variantMap: new Map(),
872918
stylesheetCache: null,
919+
920+
markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate),
921+
markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node),
873922
}
874923

875924
let resolvedPlugins = resolvePlugins(context, root)

tests/evaluateTailwindFunctions.test.js

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import fs from 'fs'
2+
import path from 'path'
13
import postcss from 'postcss'
24
import plugin from '../src/lib/evaluateTailwindFunctions'
3-
import tailwind from '../src/index'
4-
import { css } from './util/run'
5+
import { run as runFull, css, html } from './util/run'
56

67
function run(input, opts = {}) {
7-
return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined })
8-
}
9-
10-
function runFull(input, config) {
11-
return postcss([tailwind(config)]).process(input, { from: undefined })
8+
return postcss([
9+
plugin({
10+
tailwindConfig: opts,
11+
markInvalidUtilityNode() {
12+
// no op
13+
},
14+
}),
15+
]).process(input, { from: undefined })
1216
}
1317

1418
test('it looks up values in the theme using dot notation', () => {
@@ -1222,3 +1226,62 @@ it('can find values with slashes in the theme key while still allowing for alpha
12221226
`)
12231227
})
12241228
})
1229+
1230+
describe('context dependent', () => {
1231+
let configPath = path.resolve(__dirname, './evaluate-tailwind-functions.tailwind.config.js')
1232+
let filePath = path.resolve(__dirname, './evaluate-tailwind-functions.test.html')
1233+
let config = {
1234+
content: [filePath],
1235+
corePlugins: { preflight: false },
1236+
}
1237+
1238+
// Rebuild the config file for each test
1239+
beforeEach(() => fs.promises.writeFile(configPath, `module.exports = ${JSON.stringify(config)};`))
1240+
afterEach(() => fs.promises.unlink(configPath))
1241+
1242+
let warn
1243+
1244+
beforeEach(() => {
1245+
warn = jest.spyOn(require('../src/util/log').default, 'warn')
1246+
})
1247+
1248+
afterEach(() => warn.mockClear())
1249+
1250+
it('should not generate when theme fn doesnt resolve', async () => {
1251+
await fs.promises.writeFile(
1252+
filePath,
1253+
html`
1254+
<div class="underline [--box-shadow:theme('boxShadow.doesnotexist')]"></div>
1255+
<div class="bg-[theme('boxShadow.doesnotexist')]"></div>
1256+
`
1257+
)
1258+
1259+
// TODO: We need a way to reuse the context in our test suite without requiring writing to files
1260+
// It should be an explicit thing tho — like we create a context and pass it in or something
1261+
let result = await runFull('@tailwind utilities', configPath)
1262+
1263+
// 1. On first run it should work because it's been removed from the class cache
1264+
expect(result.css).toMatchCss(css`
1265+
.underline {
1266+
text-decoration-line: underline;
1267+
}
1268+
`)
1269+
1270+
// 2. But we get a warning in the console
1271+
expect(warn).toHaveBeenCalledTimes(1)
1272+
expect(warn.mock.calls.map((x) => x[0])).toEqual(['invalid-theme-key-in-class'])
1273+
1274+
// 3. The second run should work fine because it's been removed from the class cache
1275+
result = await runFull('@tailwind utilities', configPath)
1276+
1277+
expect(result.css).toMatchCss(css`
1278+
.underline {
1279+
text-decoration-line: underline;
1280+
}
1281+
`)
1282+
1283+
// 4. But we've not received any further logs about it
1284+
expect(warn).toHaveBeenCalledTimes(1)
1285+
expect(warn.mock.calls.map((x) => x[0])).toEqual(['invalid-theme-key-in-class'])
1286+
})
1287+
})

0 commit comments

Comments
 (0)