From d2fdf9eb09bff5cca8cc5190b06f1217793741a4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 31 May 2022 15:03:06 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + jest/customMatchers.js | 2 +- src/lib/generateRules.js | 22 ++++++++++++++++- src/lib/setupContextUtils.js | 4 +++ tests/match-variants.test.js | 38 +++++++++++++++++++++++++++++ tests/parallel-variants.test.js | 43 +++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7b0f38a..8af96b47e 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 `addVariant` or `matchVariant` callback functions ([#8455](https://github.com/tailwindlabs/tailwindcss/pull/8455)) ### Changed diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 3f1ea2d83..385faf7bd 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', diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index d1f216dd4..c5f82d8c3 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) { @@ -216,6 +216,26 @@ 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 [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 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, + ]) + } + continue + } + if (typeof ruleWithVariant === 'string') { collectedFormats.push(ruleWithVariant) } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 5f0a3203e..004b8f975 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 1f0252ce8..528a63e9f 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; + } + `) + }) +}) diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js index 94cb1bc13..f1be86832 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; + } + .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; + } + `) + }) +})