From 9824a79a0ef740b15da4b2be738f19bba304fdf7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 12:24:03 +0200 Subject: [PATCH 1/9] allow to return an array of format strings from matchVariant or addVariant --- src/lib/generateRules.js | 12 ++++++++++++ src/lib/setupContextUtils.js | 4 ++++ tests/match-variants.test.js | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index d1f216dd4d9b..2b3b8d9d182a 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -216,6 +216,18 @@ function applyVariant(variant, matches, context) { args, }) + // It can happen that a list of format strings is returned from within the function. In that + // case, we have to process them as well. We can use the existing `variantSort`. + if (Array.isArray(ruleWithVariant)) { + for (let variantFunction of ruleWithVariant) { + // This is a little bit scary since we are pushing to an array of items that we are + // currently looping over. However, you can also think of it like a processing queue + // where you keep handling jobs until everything is done. + variantFunctionTuples.push([variantSort, variantFunction]) + } + continue + } + if (typeof ruleWithVariant === 'string') { collectedFormats.push(ruleWithVariant) } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 5f0a3203ede7..004b8f9756cb 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -463,6 +463,10 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs ) } + if (Array.isArray(result)) { + return result.map((variant) => parseVariant(variant)) + } + // result may be undefined with legacy variants that use APIs like `modifySelectors` return result && parseVariant(result)(api) } diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js index 1f0252ce8178..528a63e9ff94 100644 --- a/tests/match-variants.test.js +++ b/tests/match-variants.test.js @@ -206,3 +206,41 @@ test('matched variant values maintain the sort order they are registered in', () `) }) }) + +test('matchVariant can return an array of format strings from the function', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchVariant }) => { + matchVariant({ + test: (selector) => selector.split(',').map((selector) => `&.${selector} > *`), + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .test-\[a\2c b\2c c\]\:underline.a > * { + text-decoration-line: underline; + } + + .test-\[a\2c b\2c c\]\:underline.b > * { + text-decoration-line: underline; + } + + .test-\[a\2c b\2c c\]\:underline.c > * { + text-decoration-line: underline; + } + `) + }) +}) From 6b15caa39c7b7268190c18e12845cae0d63cd24b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 12:27:57 +0200 Subject: [PATCH 2/9] add parallel variant with function test --- tests/parallel-variants.test.js | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index 94cb1bc13f90..95d985a400e7 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -42,3 +42,46 @@ test('basic parallel variants', async () => { `) }) }) + +test('parallel variants can be generated using a function that returns parallel variants', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + plugins: [ + function test({ addVariant }) { + addVariant('test', ['& *::test', '&::test']) + }, + ], + } + + return run('@tailwind utilities', config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .font-normal { + font-weight: 400; + } + .test\:font-bold *::test { + font-weight: 700; + } + .test\:font-medium *::test { + font-weight: 500; + } + .hover\:test\:font-black *:hover::test { + font-weight: 900; + } + .test\:font-bold::test { + font-weight: 700; + } + .test\:font-medium::test { + font-weight: 500; + } + .hover\:test\:font-black:hover::test { + font-weight: 900; + } + `) + }) +}) From f5369ca466df83cbccbc44274002d37895e9fa17 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 12:28:19 +0200 Subject: [PATCH 3/9] upgrade test to use a function call --- tests/parallel-variants.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index 95d985a400e7..f1656892ac1a 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -54,7 +54,7 @@ test('parallel variants can be generated using a function that returns parallel ], plugins: [ function test({ addVariant }) { - addVariant('test', ['& *::test', '&::test']) + addVariant('test', () => ['& *::test', '&::test']) }, ], } From e1006f847a7c702aa2855df75085228073a0e240 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 14:38:59 +0200 Subject: [PATCH 4/9] 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. --- src/lib/generateRules.js | 10 +++++++--- tests/parallel-variants.test.js | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 2b3b8d9d182a..915573bdb252 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -219,11 +219,15 @@ function applyVariant(variant, matches, context) { // It can happen that a list of format strings is returned from within the function. In that // case, we have to process them as well. We can use the existing `variantSort`. if (Array.isArray(ruleWithVariant)) { - for (let variantFunction of ruleWithVariant) { + for (let [idx, variantFunction] of ruleWithVariant.entries()) { // This is a little bit scary since we are pushing to an array of items that we are // currently looping over. However, you can also think of it like a processing queue - // where you keep handling jobs until everything is done. - variantFunctionTuples.push([variantSort, variantFunction]) + // where you keep handling jobs until everything is done and each job can queue more + // jobs if needed. + variantFunctionTuples.push([ + variantSort | BigInt(idx << ruleWithVariant.length), + variantFunction, + ]) } continue } diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index f1656892ac1a..f1be86832884 100644 --- a/tests/parallel-variants.test.js +++ b/tests/parallel-variants.test.js @@ -70,15 +70,15 @@ test('parallel variants can be generated using a function that returns parallel .test\:font-medium *::test { font-weight: 500; } - .hover\:test\:font-black *:hover::test { - font-weight: 900; - } .test\:font-bold::test { font-weight: 700; } .test\:font-medium::test { font-weight: 500; } + .hover\:test\:font-black *:hover::test { + font-weight: 900; + } .hover\:test\:font-black:hover::test { font-weight: 900; } From f94311bff74c0fd87bdfeffe2acc322c9ca9670f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 14:39:39 +0200 Subject: [PATCH 5/9] prevent crash if `.toMatchFormattedCss()` receives undefined --- jest/customMatchers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 3f1ea2d83d50..385faf7bdda5 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -100,7 +100,7 @@ expect.extend({ expect.extend({ // Compare two CSS strings with all whitespace removed // This is probably naive but it's fast and works well enough. - toMatchFormattedCss(received, argument) { + toMatchFormattedCss(received = '', argument = '') { function format(input) { return prettier.format(input.replace(/\n/g, ''), { parser: 'css', From be09accc2539d0bd8f2b66ff85ec06ae7def52ac Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 14:41:03 +0200 Subject: [PATCH 6/9] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb797c11ca34..a9a8e1e95387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create tailwind.config.cjs file in ESM package when running init ([#8363](https://github.com/tailwindlabs/tailwindcss/pull/8363)) - Fix `matchVariants` that use at-rules and placeholders ([#8392](https://github.com/tailwindlabs/tailwindcss/pull/8392)) - Improve types of the `tailwindcss/plugin` ([#8400](https://github.com/tailwindlabs/tailwindcss/pull/8400)) +- Allow returning parallel variants from variant function ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455)) ### Changed From fbeaca82193a579164b1be36ff5a605181142468 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 16:14:34 +0200 Subject: [PATCH 7/9] 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. --- src/lib/generateRules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 915573bdb252..63fe63be0ac2 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -152,7 +152,7 @@ function applyVariant(variant, matches, context) { } if (context.variantMap.has(variant)) { - let variantFunctionTuples = context.variantMap.get(variant) + let variantFunctionTuples = context.variantMap.get(variant).slice() let result = [] for (let [meta, rule] of matches) { From dae24b7f4ea0557fb476d1911be6990f9cbf6e77 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 27 May 2022 16:17:43 +0200 Subject: [PATCH 8/9] add little warning regarding potential clashes --- src/lib/generateRules.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 63fe63be0ac2..c5f82d8c391c 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -225,6 +225,10 @@ function applyVariant(variant, matches, context) { // where you keep handling jobs until everything is done and each job can queue more // jobs if needed. variantFunctionTuples.push([ + // TODO: This could have potential bugs if we shift the sort order from variant A far + // enough into the sort space of variant B. The chances are low, but if this happens + // then this might be the place too look at. One potential solution to this problem is + // reserving additional X places for these 'unknown' variants in between. variantSort | BigInt(idx << ruleWithVariant.length), variantFunction, ]) From 641400a079cd7d9b521455e70844cb6de3e7fe13 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 31 May 2022 15:00:03 +0200 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a8e1e95387..72537c9141ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create tailwind.config.cjs file in ESM package when running init ([#8363](https://github.com/tailwindlabs/tailwindcss/pull/8363)) - Fix `matchVariants` that use at-rules and placeholders ([#8392](https://github.com/tailwindlabs/tailwindcss/pull/8392)) - Improve types of the `tailwindcss/plugin` ([#8400](https://github.com/tailwindlabs/tailwindcss/pull/8400)) -- Allow returning parallel variants from variant function ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455)) +- Allow returning parallel variants from `addVariant` or `matchVariant` callback functions ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455)) ### Changed