Skip to content

Commit d2fdf9e

Browse files
authored
Allow returning parallel variants from addVariant or matchVariant callback functions (#8455)
* allow to return an array of format strings from matchVariant or addVariant * add parallel variant with function test * upgrade test to use a function call * allow to return parallel variants from variant function Caveat: this now belongs to the same plugin and is not registered as separate variants which means that sort order can differ. * prevent crash if `.toMatchFormattedCss()` receives undefined * update changelog * ensure that we use a local list of variant functions Now that a variant function can return a list of variant functions from within the plugin, we have to make sure to executed and register those functions as well. However, we need to make sure that this list is local for the variant and not "globally" registered otherwise we keep add a dynamic function to the global list which results in duplicate output becaus multiple duplicate variants will be registered. * add little warning regarding potential clashes * Update CHANGELOG.md
1 parent 2dffc87 commit d2fdf9e

6 files changed

+108
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Create tailwind.config.cjs file in ESM package when running init ([#8363](https://github.com/tailwindlabs/tailwindcss/pull/8363))
2626
- Fix `matchVariants` that use at-rules and placeholders ([#8392](https://github.com/tailwindlabs/tailwindcss/pull/8392))
2727
- Improve types of the `tailwindcss/plugin` ([#8400](https://github.com/tailwindlabs/tailwindcss/pull/8400))
28+
- Allow returning parallel variants from `addVariant` or `matchVariant` callback functions ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455))
2829

2930
### Changed
3031

jest/customMatchers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ expect.extend({
100100
expect.extend({
101101
// Compare two CSS strings with all whitespace removed
102102
// This is probably naive but it's fast and works well enough.
103-
toMatchFormattedCss(received, argument) {
103+
toMatchFormattedCss(received = '', argument = '') {
104104
function format(input) {
105105
return prettier.format(input.replace(/\n/g, ''), {
106106
parser: 'css',

src/lib/generateRules.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function applyVariant(variant, matches, context) {
152152
}
153153

154154
if (context.variantMap.has(variant)) {
155-
let variantFunctionTuples = context.variantMap.get(variant)
155+
let variantFunctionTuples = context.variantMap.get(variant).slice()
156156
let result = []
157157

158158
for (let [meta, rule] of matches) {
@@ -216,6 +216,26 @@ function applyVariant(variant, matches, context) {
216216
args,
217217
})
218218

219+
// It can happen that a list of format strings is returned from within the function. In that
220+
// case, we have to process them as well. We can use the existing `variantSort`.
221+
if (Array.isArray(ruleWithVariant)) {
222+
for (let [idx, variantFunction] of ruleWithVariant.entries()) {
223+
// This is a little bit scary since we are pushing to an array of items that we are
224+
// currently looping over. However, you can also think of it like a processing queue
225+
// where you keep handling jobs until everything is done and each job can queue more
226+
// jobs if needed.
227+
variantFunctionTuples.push([
228+
// TODO: This could have potential bugs if we shift the sort order from variant A far
229+
// enough into the sort space of variant B. The chances are low, but if this happens
230+
// then this might be the place too look at. One potential solution to this problem is
231+
// reserving additional X places for these 'unknown' variants in between.
232+
variantSort | BigInt(idx << ruleWithVariant.length),
233+
variantFunction,
234+
])
235+
}
236+
continue
237+
}
238+
219239
if (typeof ruleWithVariant === 'string') {
220240
collectedFormats.push(ruleWithVariant)
221241
}

src/lib/setupContextUtils.js

+4
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
463463
)
464464
}
465465

466+
if (Array.isArray(result)) {
467+
return result.map((variant) => parseVariant(variant))
468+
}
469+
466470
// result may be undefined with legacy variants that use APIs like `modifySelectors`
467471
return result && parseVariant(result)(api)
468472
}

tests/match-variants.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,41 @@ test('matched variant values maintain the sort order they are registered in', ()
206206
`)
207207
})
208208
})
209+
210+
test('matchVariant can return an array of format strings from the function', () => {
211+
let config = {
212+
content: [
213+
{
214+
raw: html`<div class="test-[a,b,c]:underline"></div>`,
215+
},
216+
],
217+
corePlugins: { preflight: false },
218+
plugins: [
219+
({ matchVariant }) => {
220+
matchVariant({
221+
test: (selector) => selector.split(',').map((selector) => `&.${selector} > *`),
222+
})
223+
},
224+
],
225+
}
226+
227+
let input = css`
228+
@tailwind utilities;
229+
`
230+
231+
return run(input, config).then((result) => {
232+
expect(result.css).toMatchFormattedCss(css`
233+
.test-\[a\2c b\2c c\]\:underline.a > * {
234+
text-decoration-line: underline;
235+
}
236+
237+
.test-\[a\2c b\2c c\]\:underline.b > * {
238+
text-decoration-line: underline;
239+
}
240+
241+
.test-\[a\2c b\2c c\]\:underline.c > * {
242+
text-decoration-line: underline;
243+
}
244+
`)
245+
})
246+
})

tests/parallel-variants.test.js

+43
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,46 @@ test('basic parallel variants', async () => {
4242
`)
4343
})
4444
})
45+
46+
test('parallel variants can be generated using a function that returns parallel variants', async () => {
47+
let config = {
48+
content: [
49+
{
50+
raw: html`<div
51+
class="hover:test:font-black test:font-bold test:font-medium font-normal"
52+
></div>`,
53+
},
54+
],
55+
plugins: [
56+
function test({ addVariant }) {
57+
addVariant('test', () => ['& *::test', '&::test'])
58+
},
59+
],
60+
}
61+
62+
return run('@tailwind utilities', config).then((result) => {
63+
expect(result.css).toMatchFormattedCss(css`
64+
.font-normal {
65+
font-weight: 400;
66+
}
67+
.test\:font-bold *::test {
68+
font-weight: 700;
69+
}
70+
.test\:font-medium *::test {
71+
font-weight: 500;
72+
}
73+
.test\:font-bold::test {
74+
font-weight: 700;
75+
}
76+
.test\:font-medium::test {
77+
font-weight: 500;
78+
}
79+
.hover\:test\:font-black *:hover::test {
80+
font-weight: 900;
81+
}
82+
.hover\:test\:font-black:hover::test {
83+
font-weight: 900;
84+
}
85+
`)
86+
})
87+
})

0 commit comments

Comments
 (0)