Skip to content

Commit 7fa2a20

Browse files
Reject invalid custom and arbitrary variants (#8345)
* WIP Still need to write error message * Update error message first pass at something better * Detect invalid variant formats returned by functions * Add proper error message Co-authored-by: Jordan Pittman <[email protected]>
1 parent e41bf3d commit 7fa2a20

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed

src/lib/generateRules.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as sharedState from './sharedState'
99
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
1010
import { asClass } from '../util/nameClass'
1111
import { normalize } from '../util/dataTypes'
12-
import { parseVariant } from './setupContextUtils'
12+
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
1313
import isValidArbitraryValue from '../util/isValidArbitraryValue'
1414
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
1515

@@ -131,6 +131,10 @@ function applyVariant(variant, matches, context) {
131131
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
132132
let selector = normalize(variant.slice(1, -1))
133133

134+
if (!isValidVariantFormatString(selector)) {
135+
return []
136+
}
137+
134138
let fn = parseVariant(selector)
135139

136140
let sort = Array.from(context.variantOrder.values()).pop() << 1n

src/lib/setupContextUtils.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ function withIdentifiers(styles) {
170170
})
171171
}
172172

173+
export function isValidVariantFormatString(format) {
174+
return format.startsWith('@') || format.includes('&')
175+
}
176+
173177
export function parseVariant(variant) {
174178
variant = variant
175179
.replace(/\n+/g, '')
@@ -221,10 +225,24 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
221225
if (typeof variantFunction !== 'string') {
222226
// Safelist public API functions
223227
return ({ modifySelectors, container, separator }) => {
224-
return variantFunction({ modifySelectors, container, separator })
228+
let result = variantFunction({ modifySelectors, container, separator })
229+
230+
if (typeof result === 'string' && !isValidVariantFormatString(result)) {
231+
throw new Error(
232+
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
233+
)
234+
}
235+
236+
return result
225237
}
226238
}
227239

240+
if (!isValidVariantFormatString(variantFunction)) {
241+
throw new Error(
242+
`Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
243+
)
244+
}
245+
228246
return parseVariant(variantFunction)
229247
})
230248

tests/arbitrary-variants.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ test('arbitrary variants with modifiers', () => {
7777
})
7878
})
7979

80+
test('variants without & or an at-rule are ignored', () => {
81+
let config = {
82+
content: [
83+
{
84+
raw: html`
85+
<div class="[div]:underline"></div>
86+
<div class="[:hover]:underline"></div>
87+
<div class="[wtf-bbq]:underline"></div>
88+
<div class="[lol]:hover:underline"></div>
89+
`,
90+
},
91+
],
92+
corePlugins: { preflight: false },
93+
}
94+
95+
let input = css`
96+
@tailwind base;
97+
@tailwind components;
98+
@tailwind utilities;
99+
`
100+
101+
return run(input, config).then((result) => {
102+
expect(result.css).toMatchFormattedCss(css`
103+
${defaults}
104+
`)
105+
})
106+
})
107+
80108
test('arbitrary variants are sorted after other variants', () => {
81109
let config = {
82110
content: [{ raw: html`<div class="[&>*]:underline underline lg:underline"></div>` }],

tests/variants.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,44 @@ describe('custom advanced variants', () => {
206206
`)
207207
})
208208
})
209+
210+
test('variant format string must include at-rule or & (1)', async () => {
211+
let config = {
212+
content: [
213+
{
214+
raw: html` <div class="wtf-bbq:text-center"></div> `,
215+
},
216+
],
217+
plugins: [
218+
function ({ addVariant }) {
219+
addVariant('wtf-bbq', 'lol')
220+
},
221+
],
222+
}
223+
224+
await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
225+
"Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
226+
)
227+
})
228+
229+
test('variant format string must include at-rule or & (2)', async () => {
230+
let config = {
231+
content: [
232+
{
233+
raw: html` <div class="wtf-bbq:text-center"></div> `,
234+
},
235+
],
236+
plugins: [
237+
function ({ addVariant }) {
238+
addVariant('wtf-bbq', () => 'lol')
239+
},
240+
],
241+
}
242+
243+
await expect(run('@tailwind components;@tailwind utilities', config)).rejects.toThrowError(
244+
"Your custom variant `wtf-bbq` has an invalid format string. Make sure it's an at-rule or contains a `&` placeholder."
245+
)
246+
})
209247
})
210248

211249
test('stacked peer variants', async () => {

0 commit comments

Comments
 (0)