Skip to content

Commit 7d8eb21

Browse files
Don't prefix classes in arbitrary variants (#10214)
* Add tests * Refactor refactor * Allow `prefixSelector` to take an AST * Consider multiple formats in `finalizeSelector` The functions `finalizeSelector` and `formatVariantSelector` together were using a mix for AST and string-based parsing. This now does the full transformation using the selector AST. This also parses the format strings AST as early as possible and is set up to parse them only once for a given set of rules. All of this will allow considering metadata per format string. For instance, we now know if the format string `.foo &` was produced by a normal variant or by an arbitrary variant. We use this information to control the prefixing behavior for individual format strings. * Update changelog * Cleanup code a bit
1 parent 2b885ef commit 7d8eb21

7 files changed

+487
-249
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Update list of length units ([#10100](https://github.com/tailwindlabs/tailwindcss/pull/10100))
3232
- Fix not matching arbitrary properties when closely followed by square brackets ([#10212](https://github.com/tailwindlabs/tailwindcss/pull/10212))
3333
- Allow direct nesting in `root` or `@layer` nodes ([#10229](https://github.com/tailwindlabs/tailwindcss/pull/10229))
34+
- Don't prefix classes in arbitrary variants ([#10214](https://github.com/tailwindlabs/tailwindcss/pull/10214))
3435

3536
### Changed
3637

src/lib/generateRules.js

+74-42
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function applyVariant(variant, matches, context) {
201201
}
202202

203203
if (context.variantMap.has(variant)) {
204+
let isArbitraryVariant = isArbitraryValue(variant)
204205
let variantFunctionTuples = context.variantMap.get(variant).slice()
205206
let result = []
206207

@@ -262,7 +263,10 @@ function applyVariant(variant, matches, context) {
262263
clone.append(wrapper)
263264
},
264265
format(selectorFormat) {
265-
collectedFormats.push(selectorFormat)
266+
collectedFormats.push({
267+
format: selectorFormat,
268+
isArbitraryVariant,
269+
})
266270
},
267271
args,
268272
})
@@ -288,7 +292,10 @@ function applyVariant(variant, matches, context) {
288292
}
289293

290294
if (typeof ruleWithVariant === 'string') {
291-
collectedFormats.push(ruleWithVariant)
295+
collectedFormats.push({
296+
format: ruleWithVariant,
297+
isArbitraryVariant,
298+
})
292299
}
293300

294301
if (ruleWithVariant === null) {
@@ -329,7 +336,10 @@ function applyVariant(variant, matches, context) {
329336
// modified (by plugin): .foo .foo\\:markdown > p
330337
// rebuiltBase (internal): .foo\\:markdown > p
331338
// format: .foo &
332-
collectedFormats.push(modified.replace(rebuiltBase, '&'))
339+
collectedFormats.push({
340+
format: modified.replace(rebuiltBase, '&'),
341+
isArbitraryVariant,
342+
})
333343
rule.selector = before
334344
})
335345
}
@@ -349,7 +359,6 @@ function applyVariant(variant, matches, context) {
349359
Object.assign(args, context.variantOptions.get(variant))
350360
),
351361
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
352-
isArbitraryVariant: isArbitraryValue(variant),
353362
},
354363
clone.nodes[0],
355364
]
@@ -733,48 +742,15 @@ function* resolveMatches(candidate, context, original = candidate) {
733742
}
734743

735744
for (let match of matches) {
736-
let isValid = true
737-
738745
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
739746

740747
// Apply final format selector
741-
if (match[0].collectedFormats) {
742-
let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
743-
let container = postcss.root({ nodes: [match[1].clone()] })
744-
container.walkRules((rule) => {
745-
if (inKeyframes(rule)) return
746-
747-
let selectorOptions = {
748-
selector: rule.selector,
749-
candidate: original,
750-
base: candidate
751-
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
752-
.pop(),
753-
isArbitraryVariant: match[0].isArbitraryVariant,
754-
755-
context,
756-
}
757-
758-
try {
759-
rule.selector = finalizeSelector(finalFormat, selectorOptions)
760-
} catch {
761-
// The selector we produced is invalid
762-
// This could be because:
763-
// - A bug exists
764-
// - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
765-
// - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
766-
// Either way the build will fail because of this
767-
// We would rather that the build pass "silently" given that this could
768-
// happen because of picking up invalid things when scanning content
769-
// So we'll throw out the candidate instead
770-
isValid = false
771-
return false
772-
}
773-
})
774-
match[1] = container.nodes[0]
775-
}
748+
match = applyFinalFormat(match, { context, candidate, original })
776749

777-
if (!isValid) {
750+
// Skip rules with invalid selectors
751+
// This will cause the candidate to be added to the "not class"
752+
// cache skipping it entirely for future builds
753+
if (match === null) {
778754
continue
779755
}
780756

@@ -783,6 +759,62 @@ function* resolveMatches(candidate, context, original = candidate) {
783759
}
784760
}
785761

762+
function applyFinalFormat(match, { context, candidate, original }) {
763+
if (!match[0].collectedFormats) {
764+
return match
765+
}
766+
767+
let isValid = true
768+
let finalFormat
769+
770+
try {
771+
finalFormat = formatVariantSelector(match[0].collectedFormats, {
772+
context,
773+
candidate,
774+
})
775+
} catch {
776+
// The format selector we produced is invalid
777+
// This could be because:
778+
// - A bug exists
779+
// - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
780+
// - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
781+
// Either way the build will fail because of this
782+
// We would rather that the build pass "silently" given that this could
783+
// happen because of picking up invalid things when scanning content
784+
// So we'll throw out the candidate instead
785+
786+
return null
787+
}
788+
789+
let container = postcss.root({ nodes: [match[1].clone()] })
790+
791+
container.walkRules((rule) => {
792+
if (inKeyframes(rule)) {
793+
return
794+
}
795+
796+
try {
797+
rule.selector = finalizeSelector(rule.selector, finalFormat, {
798+
candidate: original,
799+
context,
800+
})
801+
} catch {
802+
// If this selector is invalid we also want to skip it
803+
// But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
804+
isValid = false
805+
return false
806+
}
807+
})
808+
809+
if (!isValid) {
810+
return null
811+
}
812+
813+
match[1] = container.nodes[0]
814+
815+
return match
816+
}
817+
786818
function inKeyframes(rule) {
787819
return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
788820
}

src/lib/setupContextUtils.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -1080,20 +1080,38 @@ function registerPlugins(plugins, context) {
10801080
})
10811081
}
10821082

1083-
let result = formatStrings.map((formatString) =>
1084-
finalizeSelector(formatVariantSelector('&', ...formatString), {
1085-
selector: `.${candidate}`,
1086-
candidate,
1087-
context,
1088-
isArbitraryVariant: !(value in (options.values ?? {})),
1089-
})
1083+
let isArbitraryVariant = !(value in (options.values ?? {}))
1084+
1085+
formatStrings = formatStrings.map((format) =>
1086+
format.map((str) => ({
1087+
format: str,
1088+
isArbitraryVariant,
1089+
}))
1090+
)
1091+
1092+
manualFormatStrings = manualFormatStrings.map((format) => ({
1093+
format,
1094+
isArbitraryVariant,
1095+
}))
1096+
1097+
let opts = {
1098+
candidate,
1099+
context,
1100+
}
1101+
1102+
let result = formatStrings.map((formats) =>
1103+
finalizeSelector(`.${candidate}`, formatVariantSelector(formats, opts), opts)
10901104
.replace(`.${candidate}`, '&')
10911105
.replace('{ & }', '')
10921106
.trim()
10931107
)
10941108

10951109
if (manualFormatStrings.length > 0) {
1096-
result.push(formatVariantSelector('&', ...manualFormatStrings))
1110+
result.push(
1111+
formatVariantSelector(manualFormatStrings, opts)
1112+
.toString()
1113+
.replace(`.${candidate}`, '&')
1114+
)
10971115
}
10981116

10991117
return result

0 commit comments

Comments
 (0)