From 36fc03b16d0decaaefcaf42d6b3ff2bbe6cc1131 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 12 Aug 2020 19:47:21 -0400 Subject: [PATCH 01/20] Add initial support for applying variants and other complex classes --- __tests__/applyComplexClasses.test.js | 348 +++++++++++++++++++++++++ src/featureFlags.js | 1 + src/flagged/applyComplexClasses.js | 207 +++++++++++++++ src/lib/substituteClassApplyAtRules.js | 7 + 4 files changed, 563 insertions(+) create mode 100644 __tests__/applyComplexClasses.test.js create mode 100644 src/flagged/applyComplexClasses.js diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js new file mode 100644 index 000000000000..ce6a31981a52 --- /dev/null +++ b/__tests__/applyComplexClasses.test.js @@ -0,0 +1,348 @@ +import postcss from 'postcss' +import substituteClassApplyAtRules from '../src/lib/substituteClassApplyAtRules' +import processPlugins from '../src/util/processPlugins' +import resolveConfig from '../src/util/resolveConfig' +import corePlugins from '../src/corePlugins' +import defaultConfig from '../stubs/defaultConfig.stub.js' + +const resolvedDefaultConfig = resolveConfig([defaultConfig]) + +const { utilities: defaultUtilities } = processPlugins( + corePlugins(resolvedDefaultConfig), + resolvedDefaultConfig +) + +function run(input, config = resolvedDefaultConfig, utilities = defaultUtilities) { + config.experimental = { + applyComplexClasses: true, + } + return postcss([substituteClassApplyAtRules(config, utilities)]).process(input, { + from: undefined, + }) +} + +test('it copies class declarations into itself', () => { + const output = '.a { color: red; } .b { color: red; }' + + return run('.a { color: red; } .b { @apply a; }').then(result => { + expect(result.css).toEqual(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('selectors with invalid characters do not need to be manually escaped', () => { + const input = ` + .a\\:1\\/2 { color: red; } + .b { @apply a:1/2; } + ` + + const expected = ` + .a\\:1\\/2 { color: red; } + .b { color: red; } + ` + + return run(input).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('it removes important from applied classes by default', () => { + const input = ` + .a { color: red !important; } + .b { @apply a; } + ` + + const expected = ` + .a { color: red !important; } + .b { color: red; } + ` + + return run(input).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('applied rules can be made !important', () => { + const input = ` + .a { color: red; } + .b { @apply a !important; } + ` + + const expected = ` + .a { color: red; } + .b { color: red !important; } + ` + + return run(input).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('cssnext custom property sets are preserved', () => { + const input = ` + .a { + color: red; + } + .b { + @apply a --custom-property-set; + } + ` + + const expected = ` + .a { + color: red; + } + .b { + color: red; + @apply --custom-property-set; + } + ` + + return run(input).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('it fails if the class does not exist', () => { + return run('.b { @apply a; }').catch(e => { + expect(e).toMatchObject({ name: 'CssSyntaxError' }) + }) +}) + +test('applying classes that are defined in a media query is supported', () => { + const input = ` + @media (min-width: 300px) { + .a { color: blue; } + } + + .b { + @apply a; + } + ` + + const output = ` + @media (min-width: 300px) { + .a { color: blue; } + } + @media (min-width: 300px) { + .b { color: blue; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('applying classes that are used in a media query is supported', () => { + const input = ` + .a { + color: red; + } + + @media (min-width: 300px) { + .a { color: blue; } + } + + .b { + @apply a; + } + ` + + const output = ` + .a { + color: red; + } + + @media (min-width: 300px) { + .a { color: blue; } + } + + .b { + color: red; + } + + @media (min-width: 300px) { + .b { color: blue; } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('it matches classes that include pseudo-selectors', () => { + const input = ` + .a:hover { + color: red; + } + + .b { + @apply a; + } + ` + + const output = ` + .a:hover { + color: red; + } + + .b:hover { + color: red; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('it matches classes that have multiple rules', () => { + const input = ` + .a { + color: red; + } + + .b { + @apply a; + } + + .a { + color: blue; + } + ` + + const output = ` + .a { + color: red; + } + + .b { + color: red; + color: blue; + } + + .a { + color: blue; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { + const input = ` + .foo { @apply mt-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; } + ` + + return run(input).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('you can apply utility classes without using the given prefix', () => { + const input = ` + .foo { @apply .tw-mt-4 .mb-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; margin-bottom: 1rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) + + return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('you can apply utility classes without using the given prefix when using a function for the prefix', () => { + const input = ` + .foo { @apply .tw-mt-4 .mb-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; margin-bottom: 1rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: () => { + return 'tw-' + }, + }, + ]) + + return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('you can apply utility classes without specificity prefix even if important (selector) is used', () => { + const input = ` + .foo { @apply .mt-8 .mb-8; } + ` + + const expected = ` + .foo { margin-top: 2rem; margin-bottom: 2rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + important: '#app', + }, + ]) + + return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test.skip('you can apply utility classes without using the given prefix even if important (selector) is used', () => { + const input = ` + .foo { @apply .tw-mt-4 .mb-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; margin-bottom: 1rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + important: '#app', + }, + ]) + + return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { + expect(result.css).toEqual(expected) + expect(result.warnings().length).toBe(0) + }) +}) diff --git a/src/featureFlags.js b/src/featureFlags.js index 485dc07232a9..3c881e6a1096 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -8,6 +8,7 @@ const featureFlags = { 'extendedSpacingScale', 'defaultLineHeights', 'extendedFontSizeScale', + 'applyComplexClasses', ], } diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js new file mode 100644 index 000000000000..f15be919d705 --- /dev/null +++ b/src/flagged/applyComplexClasses.js @@ -0,0 +1,207 @@ +import _ from 'lodash' +import selectorParser from 'postcss-selector-parser' + +function applyUtility(rule, className, replaceWith) { + const selectors = rule.selectors.map(selector => { + const processor = selectorParser(selectors => { + selectors.walkClasses(c => { + if (c.value === className) { + c.replaceWith(selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__' })) + } + }) + }) + + // You could argue we should make this replacement at the AST level, but if we believe + // the placeholder string is safe from collisions then it is safe to do this is a simple + // string replacement, and much, much faster. + const processedSelector = processor + .processSync(selector) + .replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) + + return processedSelector + }) + + const cloned = rule.clone() + let current = cloned + let parent = rule.parent + + while (parent && parent.type !== 'root') { + const parentClone = parent.clone() + parentClone.removeAll() + parentClone.append(current) + current.parent = parentClone + current = parentClone + parent = parent.parent + } + + cloned.selectors = selectors + return current +} + +function extractUtilityNames(selector) { + const processor = selectorParser(selectors => { + let classes = [] + + selectors.walkClasses(c => { + classes.push(c) + }) + + return classes.map(c => c.value) + }) + + return processor.transformSync(selector) +} + +function buildUtilityMap(css) { + let index = 0 + const utilityMap = {} + + css.walkRules(rule => { + const utilityNames = extractUtilityNames(rule.selector) + + utilityNames.forEach(utilityName => { + if (utilityMap[utilityName] === undefined) { + utilityMap[utilityName] = [] + } + + utilityMap[utilityName].push({ + index, + utilityName, + rule: rule.clone({ parent: rule.parent }), + containsApply: hasInject(rule), + }) + index++ + }) + }) + + return utilityMap +} + +function mergeAdjacentRules(initialRule, rulesToInsert) { + let previousRule = initialRule + + rulesToInsert.forEach(toInsert => { + if ( + toInsert.type === 'rule' && + previousRule.type === 'rule' && + toInsert.selector === previousRule.selector + ) { + previousRule.append(toInsert.nodes) + } else if ( + toInsert.type === 'atrule' && + previousRule.type === 'atrule' && + toInsert.params === previousRule.params + ) { + const merged = mergeAdjacentRules( + previousRule.nodes[previousRule.nodes.length - 1], + toInsert.nodes + ) + + previousRule.append(merged) + } else { + previousRule = toInsert + } + + toInsert.walk(n => { + if (n.nodes && n.nodes.length === 0) { + n.remove() + } + }) + }) + + return rulesToInsert.filter(r => r.nodes.length > 0) +} + +function makeExtractUtilityRules(css) { + const utilityMap = buildUtilityMap(css) + const orderUtilityMap = Object.fromEntries( + Object.entries(utilityMap).flatMap(([utilityName, utilities]) => { + return utilities.map(utility => { + return [utility.index, utility] + }) + }) + ) + return function(utilityNames, rule) { + return utilityNames + .flatMap(utilityName => { + if (utilityMap[utilityName] === undefined) { + throw rule.error( + `The \`${utilityName}\` utility does not exist. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, + { word: utilityName } + ) + } + return utilityMap[utilityName].map(({ index }) => index) + }) + .sort((a, b) => a - b) + .map(i => orderUtilityMap[i]) + } +} + +function hasInject(css) { + let foundInject = false + + css.walkAtRules('apply', () => { + foundInject = true + return false + }) + + return foundInject +} + +export default function applyComplexClasses(css) { + const extractUtilityRules = makeExtractUtilityRules(css) + + while (hasInject(css)) { + css.walkRules(rule => { + const injectRules = [] + + // Only walk direct children to avoid issues with nesting plugins + rule.each(child => { + if (child.type === 'atrule' && child.name === 'apply') { + injectRules.unshift(child) + } + }) + + injectRules.forEach(inject => { + const injectUtilityNames = inject.params.split(' ') + const currentUtilityNames = extractUtilityNames(rule.selector) + + if (_.intersection(injectUtilityNames, currentUtilityNames).length > 0) { + const currentUtilityName = _.intersection(injectUtilityNames, currentUtilityNames)[0] + throw rule.error( + `You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.` + ) + } + + // Extract any post-inject declarations and re-insert them after inject rules + const afterRule = rule.clone({ raws: {} }) + afterRule.nodes = afterRule.nodes.slice(rule.index(inject) + 1) + rule.nodes = rule.nodes.slice(0, rule.index(inject) + 1) + + // Sort injects to match CSS source order + const injects = extractUtilityRules(injectUtilityNames, inject) + + // Get new rules with the utility portion of the selector replaced with the new selector + const rulesToInsert = [ + ...injects.map(injectUtility => { + return applyUtility(injectUtility.rule, injectUtility.utilityName, rule.selector) + }), + afterRule, + ] + + const mergedRules = mergeAdjacentRules(rule, rulesToInsert) + + inject.remove() + rule.after(mergedRules) + }) + + // If the base rule has nothing in it (all injects were pseudo or responsive variants), + // remove the rule fuggit. + if (rule.nodes.length === 0) { + rule.remove() + } + }) + } + + return css +} diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index 59439e87b949..1a3e74e1c1f1 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -4,6 +4,9 @@ import escapeClassName from '../util/escapeClassName' import prefixSelector from '../util/prefixSelector' import increaseSpecificity from '../util/increaseSpecificity' +import { flagEnabled } from '../featureFlags' +import applyComplexClasses from '../flagged/applyComplexClasses' + function buildClassTable(css) { const classTable = {} @@ -54,6 +57,10 @@ function findClass(classToApply, classTable, onError) { } export default function(config, generatedUtilities) { + if (flagEnabled(config, 'applyComplexClasses')) { + return applyComplexClasses + } + return function(css) { const classLookup = buildClassTable(css) const shadowLookup = buildShadowTable(generatedUtilities) From d6e22b944e0d8cce8bd9cdec5f32055555aad238 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 07:44:59 -0400 Subject: [PATCH 02/20] Remove leading dot from apply case in sanity test --- __tests__/fixtures/tailwind-input.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/fixtures/tailwind-input.css b/__tests__/fixtures/tailwind-input.css index 91bb3eebdab4..c5c8767ffe5b 100644 --- a/__tests__/fixtures/tailwind-input.css +++ b/__tests__/fixtures/tailwind-input.css @@ -6,7 +6,7 @@ @responsive { .example { - @apply .font-bold; + @apply font-bold; color: theme('colors.red.500'); } } From e313de63543ef25e3bac1bd05740d0f409f3339d Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 07:45:04 -0400 Subject: [PATCH 03/20] Fix lint issues --- src/flagged/applyComplexClasses.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index f15be919d705..c91c13a42dc2 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -1,8 +1,18 @@ import _ from 'lodash' import selectorParser from 'postcss-selector-parser' +function hasInject(css) { + let foundInject = false + + css.walkAtRules('apply', () => { + foundInject = true + return false + }) + + return foundInject +} function applyUtility(rule, className, replaceWith) { - const selectors = rule.selectors.map(selector => { + const processedSelectors = rule.selectors.map(selector => { const processor = selectorParser(selectors => { selectors.walkClasses(c => { if (c.value === className) { @@ -34,7 +44,7 @@ function applyUtility(rule, className, replaceWith) { parent = parent.parent } - cloned.selectors = selectors + cloned.selectors = processedSelectors return current } @@ -115,7 +125,7 @@ function mergeAdjacentRules(initialRule, rulesToInsert) { function makeExtractUtilityRules(css) { const utilityMap = buildUtilityMap(css) const orderUtilityMap = Object.fromEntries( - Object.entries(utilityMap).flatMap(([utilityName, utilities]) => { + Object.entries(utilityMap).flatMap(([_utilityName, utilities]) => { return utilities.map(utility => { return [utility.index, utility] }) @@ -137,17 +147,6 @@ function makeExtractUtilityRules(css) { } } -function hasInject(css) { - let foundInject = false - - css.walkAtRules('apply', () => { - foundInject = true - return false - }) - - return foundInject -} - export default function applyComplexClasses(css) { const extractUtilityRules = makeExtractUtilityRules(css) From 3d156cc81fe7d218c6e52d40b46d93c0f09858ef Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 07:51:46 -0400 Subject: [PATCH 04/20] Explicitly don't support legacy cssnext `@apply` rules --- __tests__/applyComplexClasses.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index ce6a31981a52..15d452f7ac17 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -81,7 +81,7 @@ test.skip('applied rules can be made !important', () => { }) }) -test.skip('cssnext custom property sets are preserved', () => { +test('cssnext custom property sets are no longer supported', () => { const input = ` .a { color: red; @@ -101,9 +101,8 @@ test.skip('cssnext custom property sets are preserved', () => { } ` - return run(input).then(result => { - expect(result.css).toEqual(expected) - expect(result.warnings().length).toBe(0) + return run(input).catch(e => { + expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) From 1c23b0a9ed522d46ca5845d872b720bd485cb307 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 08:56:27 -0400 Subject: [PATCH 05/20] Use lodash for flatMap --- src/flagged/applyComplexClasses.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index c91c13a42dc2..2be8eec46425 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -125,23 +125,22 @@ function mergeAdjacentRules(initialRule, rulesToInsert) { function makeExtractUtilityRules(css) { const utilityMap = buildUtilityMap(css) const orderUtilityMap = Object.fromEntries( - Object.entries(utilityMap).flatMap(([_utilityName, utilities]) => { + _.flatMap(Object.entries(utilityMap), ([_utilityName, utilities]) => { return utilities.map(utility => { return [utility.index, utility] }) }) ) return function(utilityNames, rule) { - return utilityNames - .flatMap(utilityName => { - if (utilityMap[utilityName] === undefined) { - throw rule.error( - `The \`${utilityName}\` utility does not exist. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, - { word: utilityName } - ) - } - return utilityMap[utilityName].map(({ index }) => index) - }) + return _.flatMap(utilityNames, utilityName => { + if (utilityMap[utilityName] === undefined) { + throw rule.error( + `The \`${utilityName}\` utility does not exist. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, + { word: utilityName } + ) + } + return utilityMap[utilityName].map(({ index }) => index) + }) .sort((a, b) => a - b) .map(i => orderUtilityMap[i]) } From ba9ee0600b475d5ea3c78126976c9b8c9f6c4249 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 09:01:58 -0400 Subject: [PATCH 06/20] Remove unused expected --- __tests__/applyComplexClasses.test.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 15d452f7ac17..b9fbe15af962 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -91,16 +91,6 @@ test('cssnext custom property sets are no longer supported', () => { } ` - const expected = ` - .a { - color: red; - } - .b { - color: red; - @apply --custom-property-set; - } - ` - return run(input).catch(e => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) From 6f1fb5c401d046acdf2b7764dc4f71d392038392 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 09:35:45 -0400 Subject: [PATCH 07/20] Drop entries methods for lodash --- src/flagged/applyComplexClasses.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 2be8eec46425..d481042f601f 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -124,8 +124,8 @@ function mergeAdjacentRules(initialRule, rulesToInsert) { function makeExtractUtilityRules(css) { const utilityMap = buildUtilityMap(css) - const orderUtilityMap = Object.fromEntries( - _.flatMap(Object.entries(utilityMap), ([_utilityName, utilities]) => { + const orderUtilityMap = _.fromPairs( + _.flatMap(_.toPairs(utilityMap), ([_utilityName, utilities]) => { return utilities.map(utility => { return [utility.index, utility] }) From c252e3325483430d4c842b8010af15499749511c Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 12:14:26 -0400 Subject: [PATCH 08/20] Get shadow lookup working-ish with new apply approach --- __tests__/applyComplexClasses.test.js | 37 ++++++++++++++----- src/flagged/applyComplexClasses.js | 51 ++++++++++++++++++++++++-- src/lib/substituteClassApplyAtRules.js | 6 +-- src/processTailwindFeatures.js | 2 +- 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index b9fbe15af962..e8ebf2f4d5c2 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -4,19 +4,33 @@ import processPlugins from '../src/util/processPlugins' import resolveConfig from '../src/util/resolveConfig' import corePlugins from '../src/corePlugins' import defaultConfig from '../stubs/defaultConfig.stub.js' +import cloneNodes from '../src/util/cloneNodes' const resolvedDefaultConfig = resolveConfig([defaultConfig]) -const { utilities: defaultUtilities } = processPlugins( - corePlugins(resolvedDefaultConfig), +const defaultProcessedPlugins = processPlugins( + [...corePlugins(resolvedDefaultConfig), ...resolvedDefaultConfig.plugins], resolvedDefaultConfig ) -function run(input, config = resolvedDefaultConfig, utilities = defaultUtilities) { +const defaultGetProcessedPlugins = function() { + return { + ...defaultProcessedPlugins, + base: cloneNodes(defaultProcessedPlugins.base), + components: cloneNodes(defaultProcessedPlugins.components), + utilities: cloneNodes(defaultProcessedPlugins.utilities), + } +} + +function run( + input, + config = resolvedDefaultConfig, + getProcessedPlugins = defaultGetProcessedPlugins +) { config.experimental = { applyComplexClasses: true, } - return postcss([substituteClassApplyAtRules(config, utilities)]).process(input, { + return postcss([substituteClassApplyAtRules(config, getProcessedPlugins)]).process(input, { from: undefined, }) } @@ -47,7 +61,7 @@ test('selectors with invalid characters do not need to be manually escaped', () }) }) -test.skip('it removes important from applied classes by default', () => { +test('it removes important from applied classes by default', () => { const input = ` .a { color: red !important; } .b { @apply a; } @@ -64,7 +78,7 @@ test.skip('it removes important from applied classes by default', () => { }) }) -test.skip('applied rules can be made !important', () => { +test('applied rules can be made !important', () => { const input = ` .a { color: red; } .b { @apply a !important; } @@ -230,7 +244,7 @@ test('it matches classes that have multiple rules', () => { }) }) -test.skip('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { +test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { const input = ` .foo { @apply mt-4; } ` @@ -293,11 +307,16 @@ test.skip('you can apply utility classes without using the given prefix when usi test.skip('you can apply utility classes without specificity prefix even if important (selector) is used', () => { const input = ` - .foo { @apply .mt-8 .mb-8; } + .foo { + @apply mt-8 mb-8; + } ` const expected = ` - .foo { margin-top: 2rem; margin-bottom: 2rem; } + .foo { + margin-top: 2rem; + margin-bottom: 2rem; + } ` const config = resolveConfig([ diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index d481042f601f..37dd7bbb9be9 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -1,5 +1,12 @@ import _ from 'lodash' import selectorParser from 'postcss-selector-parser' +import postcss from 'postcss' +import substituteTailwindAtRules from '../lib/substituteTailwindAtRules' +import evaluateTailwindFunctions from '../lib/evaluateTailwindFunctions' +import substituteVariantsAtRules from '../lib/substituteVariantsAtRules' +import substituteResponsiveAtRules from '../lib/substituteResponsiveAtRules' +import convertLayerAtRulesToControlComments from '../lib/convertLayerAtRulesToControlComments' +import substituteScreenAtRules from '../lib/substituteScreenAtRules' function hasInject(css) { let foundInject = false @@ -146,8 +153,8 @@ function makeExtractUtilityRules(css) { } } -export default function applyComplexClasses(css) { - const extractUtilityRules = makeExtractUtilityRules(css) +function themagic(css, lookupTree) { + const extractUtilityRules = makeExtractUtilityRules(lookupTree) while (hasInject(css)) { css.walkRules(rule => { @@ -161,7 +168,12 @@ export default function applyComplexClasses(css) { }) injectRules.forEach(inject => { - const injectUtilityNames = inject.params.split(' ') + const [ + importantEntries, + injectUtilityNames, + important = importantEntries.length > 0, + ] = _.partition(inject.params.split(' '), n => n === '!important') + const currentUtilityNames = extractUtilityNames(rule.selector) if (_.intersection(injectUtilityNames, currentUtilityNames).length > 0) { @@ -187,7 +199,11 @@ export default function applyComplexClasses(css) { afterRule, ] - const mergedRules = mergeAdjacentRules(rule, rulesToInsert) + const root = _.tap(postcss.root({ nodes: rulesToInsert }), root => + root.walkDecls(d => (d.important = important)) + ) + + const mergedRules = mergeAdjacentRules(rule, root.nodes) inject.remove() rule.after(mergedRules) @@ -203,3 +219,30 @@ export default function applyComplexClasses(css) { return css } + +export default function applyComplexClasses(config, getProcessedPlugins) { + return function(css) { + return postcss([ + substituteTailwindAtRules(config, getProcessedPlugins()), + evaluateTailwindFunctions(config), + substituteVariantsAtRules(config, getProcessedPlugins()), + substituteResponsiveAtRules(config), + convertLayerAtRulesToControlComments(config), + substituteScreenAtRules(config), + ]) + .process( + ` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + // if css already contains tailwind, css is the lookup tree + const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root)) + + return themagic(css, lookupTree) + }) + } +} diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index 1a3e74e1c1f1..4b0fae75a2ac 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -56,14 +56,14 @@ function findClass(classToApply, classTable, onError) { return match.clone().nodes } -export default function(config, generatedUtilities) { +export default function(config, getProcessedPlugins) { if (flagEnabled(config, 'applyComplexClasses')) { - return applyComplexClasses + return applyComplexClasses(config, getProcessedPlugins) } return function(css) { const classLookup = buildClassTable(css) - const shadowLookup = buildShadowTable(generatedUtilities) + const shadowLookup = buildShadowTable(getProcessedPlugins().utilities) css.walkRules(rule => { rule.walkAtRules('apply', atRule => { diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 7899dee7eac4..729e3ff9c7b1 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -39,7 +39,7 @@ export default function(getConfig) { substituteResponsiveAtRules(config), convertLayerAtRulesToControlComments(config), substituteScreenAtRules(config), - substituteClassApplyAtRules(config, getProcessedPlugins().utilities), + substituteClassApplyAtRules(config, getProcessedPlugins), purgeUnusedStyles(config), ]).process(css, { from: _.get(css, 'source.input.file') }) } From 8646c94dd5fd2073b97d228f7c42458f133eb939 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 20:16:56 -0400 Subject: [PATCH 09/20] Prepend a shadow lookup table when no @tailwind rules are in the tree --- __tests__/applyAtRule.test.js | 6 +++++- __tests__/applyComplexClasses.test.js | 11 +++++++++++ src/flagged/applyComplexClasses.js | 24 +++++++++++++++--------- src/lib/substituteTailwindAtRules.js | 9 +++------ src/processTailwindFeatures.js | 3 +++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index 2de552e82689..18db716a86a0 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -13,7 +13,11 @@ const { utilities: defaultUtilities } = processPlugins( ) function run(input, config = resolvedDefaultConfig, utilities = defaultUtilities) { - return postcss([substituteClassApplyAtRules(config, utilities)]).process(input, { + return postcss([ + substituteClassApplyAtRules(config, () => ({ + utilities, + })), + ]).process(input, { from: undefined, }) } diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index e8ebf2f4d5c2..189c2c541d59 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -259,6 +259,17 @@ test('you can apply utility classes that do not actually exist as long as they w }) }) +test('the shadow lookup is only used if no @tailwind rules were in the source tree', () => { + const input = ` + @tailwind base; + .foo { @apply mt-4; } + ` + + return run(input).catch(e => { + expect(e).toMatchObject({ name: 'CssSyntaxError' }) + }) +}) + test.skip('you can apply utility classes without using the given prefix', () => { const input = ` .foo { @apply .tw-mt-4 .mb-4; } diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 37dd7bbb9be9..a933b0d4128a 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -8,16 +8,17 @@ import substituteResponsiveAtRules from '../lib/substituteResponsiveAtRules' import convertLayerAtRulesToControlComments from '../lib/convertLayerAtRulesToControlComments' import substituteScreenAtRules from '../lib/substituteScreenAtRules' -function hasInject(css) { - let foundInject = false +function hasAtRule(css, atRule) { + let foundAtRule = false - css.walkAtRules('apply', () => { - foundInject = true + css.walkAtRules(atRule, () => { + foundAtRule = true return false }) - return foundInject + return foundAtRule } + function applyUtility(rule, className, replaceWith) { const processedSelectors = rule.selectors.map(selector => { const processor = selectorParser(selectors => { @@ -85,7 +86,7 @@ function buildUtilityMap(css) { index, utilityName, rule: rule.clone({ parent: rule.parent }), - containsApply: hasInject(rule), + containsApply: hasAtRule(rule, 'apply'), }) index++ }) @@ -153,10 +154,10 @@ function makeExtractUtilityRules(css) { } } -function themagic(css, lookupTree) { +function processApplyAtRules(css, lookupTree) { const extractUtilityRules = makeExtractUtilityRules(lookupTree) - while (hasInject(css)) { + while (hasAtRule(css, 'apply')) { css.walkRules(rule => { const injectRules = [] @@ -222,6 +223,11 @@ function themagic(css, lookupTree) { export default function applyComplexClasses(config, getProcessedPlugins) { return function(css) { + // Tree already contains @tailwind rules, don't prepend default Tailwind tree + if (hasAtRule(css, 'tailwind')) { + return processApplyAtRules(css, css) + } + return postcss([ substituteTailwindAtRules(config, getProcessedPlugins()), evaluateTailwindFunctions(config), @@ -242,7 +248,7 @@ export default function applyComplexClasses(config, getProcessedPlugins) { // if css already contains tailwind, css is the lookup tree const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root)) - return themagic(css, lookupTree) + return processApplyAtRules(css, lookupTree) }) } } diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 4354d2847fe7..ef9d36bf6b4a 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -49,18 +49,15 @@ export default function( } if (atRule.params === 'base') { - atRule.before(updateSource(pluginBase, atRule.source)) - atRule.remove() + atRule.after(updateSource(pluginBase, atRule.source)) } if (atRule.params === 'components') { - atRule.before(updateSource(pluginComponents, atRule.source)) - atRule.remove() + atRule.after(updateSource(pluginComponents, atRule.source)) } if (atRule.params === 'utilities') { - atRule.before(updateSource(pluginUtilities, atRule.source)) - atRule.remove() + atRule.after(updateSource(pluginUtilities, atRule.source)) } if (atRule.params === 'screens') { diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 729e3ff9c7b1..7ea80ee8f05e 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -40,6 +40,9 @@ export default function(getConfig) { convertLayerAtRulesToControlComments(config), substituteScreenAtRules(config), substituteClassApplyAtRules(config, getProcessedPlugins), + function(css) { + css.walkAtRules('tailwind', rule => rule.remove()) + }, purgeUnusedStyles(config), ]).process(css, { from: _.get(css, 'source.input.file') }) } From 2cbc8e90af3c2768a548ad2e4595914670c831d3 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 13 Aug 2020 20:46:27 -0400 Subject: [PATCH 10/20] Add tests for all the new scenarios `@apply` now supports --- __tests__/applyComplexClasses.test.js | 381 +++++++++++++++++++++++++- jest/customMatchers.js | 2 +- 2 files changed, 374 insertions(+), 9 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 189c2c541d59..2806485af788 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -56,7 +56,7 @@ test('selectors with invalid characters do not need to be manually escaped', () ` return run(input).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -73,7 +73,7 @@ test('it removes important from applied classes by default', () => { ` return run(input).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -90,7 +90,7 @@ test('applied rules can be made !important', () => { ` return run(input).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -254,7 +254,7 @@ test('you can apply utility classes that do not actually exist as long as they w ` return run(input).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -270,6 +270,371 @@ test('the shadow lookup is only used if no @tailwind rules were in the source tr }) }) +test('you can apply a class that is defined in multiple rules', () => { + const input = ` + .foo { + color: red; + } + .bar { + @apply foo; + } + .foo { + oapcity: .5; + } + ` + const expected = ` + .foo { + color: red; + } + .bar { + color: red; + oapcity: .5; + } + .foo { + oapcity: .5; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply a class that is defined in a media query', () => { + const input = ` + .foo { + @apply sm:text-center; + } + ` + const expected = ` + @media (min-width: 640px) { + .foo { + text-align: center + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply pseudo-class variant utilities', () => { + const input = ` + .foo { + @apply hover:opacity-50; + } + ` + const expected = ` + .foo:hover { + opacity: 0.5 + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply responsive pseudo-class variant utilities', () => { + const input = ` + .foo { + @apply sm:hover:opacity-50; + } + ` + const expected = ` + @media (min-width: 640px) { + .foo:hover { + opacity: 0.5 + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply the container component', () => { + const input = ` + .foo { + @apply container; + } + ` + const expected = ` + .foo { + width: 100%; + } + @media (min-width: 640px) { + .foo { + max-width: 640px; + } + } + @media (min-width: 768px) { + .foo { + max-width: 768px; + } + } + @media (min-width: 1024px) { + .foo { + max-width: 1024px; + } + } + @media (min-width: 1280px) { + .foo { + max-width: 1280px; + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('classes are applied according to CSS source order, not apply order', () => { + const input = ` + .foo { + color: red; + } + .bar { + color: blue; + } + .baz { + @apply bar foo; + } + ` + const expected = ` + .foo { + color: red; + } + .bar { + color: blue; + } + .baz { + color: red; + color: blue; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply utilities with multi-class selectors like group-hover variants', () => { + const input = ` + .foo { + @apply group-hover:bar; + } + .bar { + color: blue; + } + .group:hover .group-hover\\:bar { + color: blue; + } + ` + const expected = ` + .group:hover .foo { + color: blue; + } + .bar { + color: blue; + } + .group:hover .group-hover\\:bar { + color: blue; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply classes recursively', () => { + const input = ` + .foo { + @apply bar; + } + .bar { + @apply baz; + } + .baz { + color: blue; + } + ` + const expected = ` + .foo { + color: blue; + } + .bar { + color: blue; + } + .baz { + color: blue; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('applied classes are always inserted before subsequent declarations in the same rule, even if it means moving those subsequent declarations to a new rule', () => { + const input = ` + .foo { + background: blue; + @apply opacity-50 hover:opacity-100 text-right sm:align-middle; + color: red; + } + ` + const expected = ` + .foo { + background: blue; + opacity: 0.5; + } + .foo:hover { + opacity: 1; + } + .foo { + text-align: right; + } + @media (min-width: 640px) { + .foo { + vertical-align: middle; + } + } + .foo { + color: red; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('adjacent rules are collapsed after being applied', () => { + const input = ` + .foo { + @apply hover:bg-white hover:opacity-50 absolute text-right sm:align-middle sm:text-center; + } + ` + const expected = ` + .foo:hover { + --bg-opacity: 1; + background-color: #fff; + background-color: rgba(255, 255, 255, var(--bg-opacity)); + opacity: 0.5; + } + .foo { + position: absolute; + text-align: right; + } + @media (min-width: 640px) { + .foo { + text-align: center; + vertical-align: middle; + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('applying a class applies all instances of that class, even complex selectors', () => { + const input = ` + h1 > p:hover .banana:first-child * { + @apply bar; + } + .bar { + color: blue; + } + @media (print) { + @supports (display: grid) { + .baz .bar:hover { + text-align: right; + float: left; + } + } + } + ` + const expected = ` + h1 > p:hover .banana:first-child * { + color: blue; + } + @media (print) { + @supports (display: grid) { + .baz h1 > p:hover .banana:first-child *:hover { + text-align: right; + float: left; + } + } + } + .bar { + color: blue; + } + @media (print) { + @supports (display: grid) { + .baz .bar:hover { + text-align: right; + float: left; + } + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply classes to rules within at-rules', () => { + const input = ` + @supports (display: grid) { + .baz .bar { + @apply text-right float-left hover:opacity-50 md:float-right; + } + } + ` + const expected = ` + @supports (display: grid) { + .baz .bar { + float: left; + } + .baz .bar:hover { + opacity: 0.5; + } + .baz .bar { + text-align: right; + } + @media (min-width: 768px) { + .baz .bar { + float: right; + } + } + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + test.skip('you can apply utility classes without using the given prefix', () => { const input = ` .foo { @apply .tw-mt-4 .mb-4; } @@ -287,7 +652,7 @@ test.skip('you can apply utility classes without using the given prefix', () => ]) return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -311,7 +676,7 @@ test.skip('you can apply utility classes without using the given prefix when usi ]) return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -338,7 +703,7 @@ test.skip('you can apply utility classes without specificity prefix even if impo ]) return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) @@ -361,7 +726,7 @@ test.skip('you can apply utility classes without using the given prefix even if ]) return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toEqual(expected) + expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) }) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 23372befcd2a..09fd808c9b88 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -3,7 +3,7 @@ expect.extend({ // This is probably naive but it's fast and works well enough. toMatchCss(received, argument) { function stripped(str) { - return str.replace(/\s/g, '') + return str.replace(/\s/g, '').replace(/;/g, '') } if (stripped(received) === stripped(argument)) { From e03db68ff94b0e539e3d207fb95dadb6f0f18237 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 11:15:17 -0400 Subject: [PATCH 11/20] Fix test typo --- __tests__/applyComplexClasses.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 2806485af788..2d775d766706 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -279,7 +279,7 @@ test('you can apply a class that is defined in multiple rules', () => { @apply foo; } .foo { - oapcity: .5; + opacity: .5; } ` const expected = ` @@ -288,10 +288,10 @@ test('you can apply a class that is defined in multiple rules', () => { } .bar { color: red; - oapcity: .5; + opacity: .5; } .foo { - oapcity: .5; + opacity: .5; } ` From b518dc3e4c0ed3445bf56ed3dfa619158ad0ac74 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 11:15:45 -0400 Subject: [PATCH 12/20] Test applying classes with a prefix configured --- __tests__/applyComplexClasses.test.js | 157 ++++++++++++++++++++------ src/flagged/applyComplexClasses.js | 22 +++- 2 files changed, 139 insertions(+), 40 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 2d775d766706..3f44e341a98c 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -635,49 +635,138 @@ test('you can apply classes to rules within at-rules', () => { }) }) -test.skip('you can apply utility classes without using the given prefix', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` +describe('using apply with the prefix option', () => { + test('applying a class including the prefix', () => { + const input = ` + .foo { @apply tw-mt-4; } + ` - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` + const expected = ` + .foo { margin-top: 1rem; } + ` - const config = resolveConfig([ - { - ...defaultConfig, - prefix: 'tw-', - }, - ]) + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) - return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) + return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) }) -}) -test.skip('you can apply utility classes without using the given prefix when using a function for the prefix', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` + test('applying a class including the prefix when using a prefix function', () => { + const input = ` + .foo { @apply tw-func-mt-4; } + ` - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` + const expected = ` + .foo { margin-top: 1rem; } + ` - const config = resolveConfig([ - { - ...defaultConfig, - prefix: () => { - return 'tw-' + const config = resolveConfig([ + { + ...defaultConfig, + prefix: () => { + return 'tw-func-' + }, }, - }, - ]) + ]) - return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) + return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) + }) + + test('applying a class without the prefix fails', () => { + const input = ` + .foo { @apply mt-4; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) + + return run(input, config, () => processPlugins(corePlugins(config), config)).catch(e => { + expect(e).toMatchObject({ name: 'CssSyntaxError' }) + }) + }) + + test('custom classes with no prefix can be applied', () => { + const input = ` + .foo { @apply mt-4; } + .mt-4 { color: red; } + ` + + const expected = ` + .foo { color: red; } + .mt-4 { color: red; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) + + return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) + }) + + test('built-in prefixed utilities can be extended and applied', () => { + const input = ` + .foo { @apply tw-mt-4; } + .tw-mt-4 { color: red; } + ` + + const expected = ` + .foo { margin-top: 1rem; color: red; } + .tw-mt-4 { color: red; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) + + return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) + }) + + test('a helpful error message is provided if it appears the user forgot to include their prefix', () => { + const input = ` + .foo { @apply mt-4; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + }, + ]) + + expect.assertions(1) + + return run(input, config, () => processPlugins(corePlugins(config), config)).catch(e => { + expect(e).toMatchObject({ + name: 'CssSyntaxError', + reason: 'The `mt-4` class does not exist, but `tw-mt-4` does. Did you forget the prefix?', + }) + }) }) }) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index a933b0d4128a..3cf4395b32bc 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -7,6 +7,7 @@ import substituteVariantsAtRules from '../lib/substituteVariantsAtRules' import substituteResponsiveAtRules from '../lib/substituteResponsiveAtRules' import convertLayerAtRulesToControlComments from '../lib/convertLayerAtRulesToControlComments' import substituteScreenAtRules from '../lib/substituteScreenAtRules' +import prefixSelector from '../util/prefixSelector' function hasAtRule(css, atRule) { let foundAtRule = false @@ -130,7 +131,7 @@ function mergeAdjacentRules(initialRule, rulesToInsert) { return rulesToInsert.filter(r => r.nodes.length > 0) } -function makeExtractUtilityRules(css) { +function makeExtractUtilityRules(css, config) { const utilityMap = buildUtilityMap(css) const orderUtilityMap = _.fromPairs( _.flatMap(_.toPairs(utilityMap), ([_utilityName, utilities]) => { @@ -142,8 +143,17 @@ function makeExtractUtilityRules(css) { return function(utilityNames, rule) { return _.flatMap(utilityNames, utilityName => { if (utilityMap[utilityName] === undefined) { + // Look for prefixed utility in case the user has goofed + const prefixedUtility = prefixSelector(config.prefix, `.${utilityName}`).slice(1) + + if (utilityMap[prefixedUtility] !== undefined) { + throw rule.error( + `The \`${utilityName}\` class does not exist, but \`${prefixedUtility}\` does. Did you forget the prefix?` + ) + } + throw rule.error( - `The \`${utilityName}\` utility does not exist. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, + `The \`${utilityName}\` class does not exist. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, { word: utilityName } ) } @@ -154,8 +164,8 @@ function makeExtractUtilityRules(css) { } } -function processApplyAtRules(css, lookupTree) { - const extractUtilityRules = makeExtractUtilityRules(lookupTree) +function processApplyAtRules(css, lookupTree, config) { + const extractUtilityRules = makeExtractUtilityRules(lookupTree, config) while (hasAtRule(css, 'apply')) { css.walkRules(rule => { @@ -225,7 +235,7 @@ export default function applyComplexClasses(config, getProcessedPlugins) { return function(css) { // Tree already contains @tailwind rules, don't prepend default Tailwind tree if (hasAtRule(css, 'tailwind')) { - return processApplyAtRules(css, css) + return processApplyAtRules(css, css, config) } return postcss([ @@ -248,7 +258,7 @@ export default function applyComplexClasses(config, getProcessedPlugins) { // if css already contains tailwind, css is the lookup tree const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root)) - return processApplyAtRules(css, lookupTree) + return processApplyAtRules(css, lookupTree, config) }) } } From 23ffa25e8aea461928488a03076a49f0fb477d83 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 11:25:55 -0400 Subject: [PATCH 13/20] Avoid accidentally forgetting getProcessedPlugins arg when customizing config --- __tests__/applyComplexClasses.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 3f44e341a98c..ee0d63446590 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -25,7 +25,10 @@ const defaultGetProcessedPlugins = function() { function run( input, config = resolvedDefaultConfig, - getProcessedPlugins = defaultGetProcessedPlugins + getProcessedPlugins = () => + config === resolvedDefaultConfig + ? defaultGetProcessedPlugins() + : processPlugins(corePlugins(config), config) ) { config.experimental = { applyComplexClasses: true, From b527dcf30175e9c38242436e777e650ea65d4506 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 11:26:01 -0400 Subject: [PATCH 14/20] Improve test name --- __tests__/applyComplexClasses.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index ee0d63446590..a211f6f62b99 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -773,7 +773,7 @@ describe('using apply with the prefix option', () => { }) }) -test.skip('you can apply utility classes without specificity prefix even if important (selector) is used', () => { +test.skip('you can apply utility classes when a selector is used for the important option', () => { const input = ` .foo { @apply mt-8 mb-8; From 577f536eeb82a6340bd90bc478837ec2202e54f0 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 12:23:18 -0400 Subject: [PATCH 15/20] Update tests that relied on changed implementation details --- __tests__/processPlugins.test.js | 310 ++++++++++++++++--------------- 1 file changed, 163 insertions(+), 147 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 5ceea53e0a76..da369bde8122 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -979,64 +979,46 @@ test('plugins can add multiple sets of utilities and components', () => { }) test('plugins respect prefix and important options by default when adding utilities', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities({ - '.rotate-90': { - transform: 'rotate(90deg)', - }, - }) - }, - ], - makeConfig({ + return _postcss([ + tailwind({ prefix: 'tw-', important: true, - }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { + corePlugins: [], + plugins: [ + function({ addUtilities }) { + addUtilities({ + '.rotate-90': { + transform: 'rotate(90deg)', + }, + }) + }, + ], + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + const expected = ` .tw-rotate-90 { transform: rotate(90deg) !important } - } - } - `) -}) + ` -test('when important is a selector it is used to scope utilities instead of adding !important', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities({ - '.rotate-90': { - transform: 'rotate(90deg)', - }, - }) - }, - ], - makeConfig({ - important: '#app', + expect(result.css).toMatchCss(expected) }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { - #app .rotate-90 { - transform: rotate(90deg) - } - } - } - `) }) -test('when important contains a class an error is thrown', () => { - expect(() => { - processPlugins( - [ +test('when important is a selector it is used to scope utilities instead of adding !important', () => { + return _postcss([ + tailwind({ + prefix: 'tw-', + important: '#app', + corePlugins: [], + plugins: [ function({ addUtilities }) { addUtilities({ '.rotate-90': { @@ -1045,65 +1027,89 @@ test('when important contains a class an error is thrown', () => { }) }, ], - makeConfig({ - important: '#app .project', - }) + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } ) - }).toThrow() + .then(result => { + const expected = ` + #app .tw-rotate-90 { + transform: rotate(90deg) + } + ` + + expect(result.css).toMatchCss(expected) + }) }) test('when important is a selector it scopes all selectors in a rule, even though defining utilities like this is stupid', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities({ - '.rotate-90, .rotate-1\\/4': { - transform: 'rotate(90deg)', - }, - }) - }, - ], - makeConfig({ + return _postcss([ + tailwind({ important: '#app', - }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { + corePlugins: [], + plugins: [ + function({ addUtilities }) { + addUtilities({ + '.rotate-90, .rotate-1\\/4': { + transform: 'rotate(90deg)', + }, + }) + }, + ], + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + const expected = ` #app .rotate-90, #app .rotate-1\\/4 { transform: rotate(90deg) } - } - } - `) + ` + + expect(result.css).toMatchCss(expected) + }) }) test('important utilities are not made double important when important option is used', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities({ - '.rotate-90': { - transform: 'rotate(90deg) !important', - }, - }) - }, - ], - makeConfig({ + return _postcss([ + tailwind({ important: true, - }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { + corePlugins: [], + plugins: [ + function({ addUtilities }) { + addUtilities({ + '.rotate-90': { + transform: 'rotate(90deg) !important', + }, + }) + }, + ], + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + const expected = ` .rotate-90 { transform: rotate(90deg) !important } - } - } - `) + ` + + expect(result.css).toMatchCss(expected) + }) }) test("component declarations respect the 'prefix' option by default", () => { @@ -1346,69 +1352,79 @@ test("plugins can apply the user's chosen prefix to components manually", () => }) test('prefix can optionally be ignored for utilities', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities( - { - '.rotate-90': { - transform: 'rotate(90deg)', - }, - }, - { - respectPrefix: false, - } - ) - }, - ], - makeConfig({ + return _postcss([ + tailwind({ prefix: 'tw-', - important: true, - }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { + corePlugins: [], + plugins: [ + function({ addUtilities }) { + addUtilities( + { + '.rotate-90': { + transform: 'rotate(90deg)', + }, + }, + { + respectPrefix: false, + } + ) + }, + ], + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + const expected = ` .rotate-90 { - transform: rotate(90deg) !important + transform: rotate(90deg) } - } - } - `) + ` + + expect(result.css).toMatchCss(expected) + }) }) test('important can optionally be ignored for utilities', () => { - const { utilities } = processPlugins( - [ - function({ addUtilities }) { - addUtilities( - { - '.rotate-90': { - transform: 'rotate(90deg)', - }, - }, - { - respectImportant: false, - } - ) - }, - ], - makeConfig({ - prefix: 'tw-', + return _postcss([ + tailwind({ important: true, - }) - ) - - expect(css(utilities)).toMatchCss(` - @layer utilities { - @variants { - .tw-rotate-90 { + corePlugins: [], + plugins: [ + function({ addUtilities }) { + addUtilities( + { + '.rotate-90': { + transform: 'rotate(90deg)', + }, + }, + { + respectImportant: false, + } + ) + }, + ], + }), + ]) + .process( + ` + @tailwind utilities; + `, + { from: undefined } + ) + .then(result => { + const expected = ` + .rotate-90 { transform: rotate(90deg) } - } - } - `) + ` + + expect(result.css).toMatchCss(expected) + }) }) test('variants can still be specified when ignoring prefix and important options', () => { From cef0b84abff055c7bf59d81871a527e02af785f2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 12:23:28 -0400 Subject: [PATCH 16/20] Reorganize prototype code --- __tests__/applyComplexClasses.test.js | 85 ++++++++++++++++---------- src/featureFlags.js | 9 +-- src/flagged/applyComplexClasses.js | 4 +- src/lib/applyImportantConfiguration.js | 19 ++++++ src/lib/purgeUnusedStyles.js | 9 +-- src/processTailwindFeatures.js | 5 +- src/util/processPlugins.js | 27 ++------ 7 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 src/lib/applyImportantConfiguration.js diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index a211f6f62b99..1793ffd8439e 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -655,7 +655,7 @@ describe('using apply with the prefix option', () => { }, ]) - return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + return run(input, config).then(result => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) @@ -679,7 +679,7 @@ describe('using apply with the prefix option', () => { }, ]) - return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + return run(input, config).then(result => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) @@ -697,7 +697,7 @@ describe('using apply with the prefix option', () => { }, ]) - return run(input, config, () => processPlugins(corePlugins(config), config)).catch(e => { + return run(input, config).catch(e => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) @@ -720,7 +720,7 @@ describe('using apply with the prefix option', () => { }, ]) - return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + return run(input, config).then(result => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) @@ -744,7 +744,7 @@ describe('using apply with the prefix option', () => { }, ]) - return run(input, config, () => processPlugins(corePlugins(config), config)).then(result => { + return run(input, config).then(result => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) @@ -764,16 +764,62 @@ describe('using apply with the prefix option', () => { expect.assertions(1) - return run(input, config, () => processPlugins(corePlugins(config), config)).catch(e => { + return run(input, config).catch(e => { expect(e).toMatchObject({ name: 'CssSyntaxError', reason: 'The `mt-4` class does not exist, but `tw-mt-4` does. Did you forget the prefix?', }) }) }) + + test('you can apply classes with important and a prefix enabled', () => { + const input = ` + .foo { @apply tw-mt-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + important: true, + }, + ]) + + return run(input, config).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) + }) + + test('you can apply classes with an important selector and a prefix enabled', () => { + const input = ` + .foo { @apply tw-mt-4; } + ` + + const expected = ` + .foo { margin-top: 1rem; } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + important: '#app', + }, + ]) + + return run(input, config).then(result => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) + }) }) -test.skip('you can apply utility classes when a selector is used for the important option', () => { +test('you can apply utility classes when a selector is used for the important option', () => { const input = ` .foo { @apply mt-8 mb-8; @@ -794,30 +840,7 @@ test.skip('you can apply utility classes when a selector is used for the importa }, ]) - return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test.skip('you can apply utility classes without using the given prefix even if important (selector) is used', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` - - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` - - const config = resolveConfig([ - { - ...defaultConfig, - prefix: 'tw-', - important: '#app', - }, - ]) - - return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => { + return run(input, config).then(result => { expect(result.css).toMatchCss(expected) expect(result.warnings().length).toBe(0) }) diff --git a/src/featureFlags.js b/src/featureFlags.js index 3c881e6a1096..9e8895045096 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -70,8 +70,7 @@ export function issueFlagNotices(config) { .map(s => chalk.cyan(s)) .join(', ') - console.log() - log.info(`You have opted-in to future-facing breaking changes: ${changes}`) + log.info(`\nYou have opted-in to future-facing breaking changes: ${changes}`) log.info( 'These changes are stable and will be the default behavior in the next major version of Tailwind.' ) @@ -82,8 +81,7 @@ export function issueFlagNotices(config) { .map(s => chalk.yellow(s)) .join(', ') - console.log() - log.warn(`You have enabled experimental features: ${changes}`) + log.warn(`\nYou have enabled experimental features: ${changes}`) log.warn( 'Experimental features are not covered by semver, may introduce breaking changes, and can change at any time.' ) @@ -94,8 +92,7 @@ export function issueFlagNotices(config) { .map(s => chalk.magenta(s)) .join(', ') - console.log() - log.risk(`There are upcoming breaking changes: ${changes}`) + log.risk(`\nThere are upcoming breaking changes: ${changes}`) log.risk( 'We highly recommend opting-in to these changes now to simplify upgrading Tailwind in the future.' ) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 3cf4395b32bc..f4764988453e 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -210,11 +210,11 @@ function processApplyAtRules(css, lookupTree, config) { afterRule, ] - const root = _.tap(postcss.root({ nodes: rulesToInsert }), root => + const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root => root.walkDecls(d => (d.important = important)) ) - const mergedRules = mergeAdjacentRules(rule, root.nodes) + const mergedRules = mergeAdjacentRules(rule, nodes) inject.remove() rule.after(mergedRules) diff --git a/src/lib/applyImportantConfiguration.js b/src/lib/applyImportantConfiguration.js new file mode 100644 index 000000000000..7a2cb2d2c29b --- /dev/null +++ b/src/lib/applyImportantConfiguration.js @@ -0,0 +1,19 @@ +export default function applyImportantConfiguration(_config) { + return function(css) { + css.walkRules(rule => { + const important = rule.__tailwind ? rule.__tailwind.important : false + + if (!important) { + return + } + + if (typeof important === 'string') { + rule.selectors = rule.selectors.map(selector => { + return `${rule.__tailwind.important} ${selector}` + }) + } else { + rule.walkDecls(decl => (decl.important = true)) + } + }) + } +} diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js index e496b14e5edb..296715b485c3 100644 --- a/src/lib/purgeUnusedStyles.js +++ b/src/lib/purgeUnusedStyles.js @@ -5,7 +5,8 @@ import chalk from 'chalk' import { log } from '../cli/utils' import * as emoji from '../cli/emoji' -function removeTailwindComments(css) { +function removeTailwindMarkers(css) { + css.walkAtRules('tailwind', rule => rule.remove()) css.walkComments(comment => { switch (comment.text.trim()) { case 'tailwind start components': @@ -28,7 +29,7 @@ export default function purgeUnusedUtilities(config) { ) if (!purgeEnabled) { - return removeTailwindComments + return removeTailwindMarkers } // Skip if `purge: []` since that's part of the default config @@ -48,7 +49,7 @@ export default function purgeUnusedUtilities(config) { log( chalk.white('\n https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css') ) - return removeTailwindComments + return removeTailwindMarkers } return postcss([ @@ -73,7 +74,7 @@ export default function purgeUnusedUtilities(config) { }) } }, - removeTailwindComments, + removeTailwindMarkers, purgecss({ content: Array.isArray(config.purge) ? config.purge : config.purge.content, defaultExtractor: content => { diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 7ea80ee8f05e..91ea35fd2d0a 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -8,6 +8,7 @@ import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import convertLayerAtRulesToControlComments from './lib/convertLayerAtRulesToControlComments' import substituteScreenAtRules from './lib/substituteScreenAtRules' import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' +import applyImportantConfiguration from './lib/applyImportantConfiguration' import purgeUnusedStyles from './lib/purgeUnusedStyles' import corePlugins from './corePlugins' @@ -40,9 +41,7 @@ export default function(getConfig) { convertLayerAtRulesToControlComments(config), substituteScreenAtRules(config), substituteClassApplyAtRules(config, getProcessedPlugins), - function(css) { - css.walkAtRules('tailwind', rule => rule.remove()) - }, + applyImportantConfiguration(config), purgeUnusedStyles(config), ]).process(css, { from: _.get(css, 'source.input.file') }) } diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index d147d9a4d283..e6effae3b36c 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -9,8 +9,6 @@ import parseObjectStyles from '../util/parseObjectStyles' import prefixSelector from '../util/prefixSelector' import wrapWithVariants from '../util/wrapWithVariants' import cloneNodes from '../util/cloneNodes' -import increaseSpecificity from '../util/increaseSpecificity' -import selectorParser from 'postcss-selector-parser' function parseStyles(styles) { if (!Array.isArray(styles)) { @@ -20,14 +18,6 @@ function parseStyles(styles) { return _.flatMap(styles, style => (style instanceof Node ? style : parseObjectStyles(style))) } -function containsClass(value) { - return selectorParser(selectors => { - let classFound = false - selectors.walkClasses(() => (classFound = true)) - return classFound - }).transformSync(value) -} - function wrapWithLayer(rules, layer) { return postcss .atRule({ @@ -102,19 +92,10 @@ export default function(plugins, config) { rule.selector = applyConfiguredPrefix(rule.selector) } - if (options.respectImportant && _.get(config, 'important')) { - if (config.important === true) { - rule.walkDecls(decl => (decl.important = true)) - } else if (typeof config.important === 'string') { - if (containsClass(config.important)) { - throw rule.error( - `Classes are not allowed when using the \`important\` option with a string argument. Please use an ID instead.` - ) - } - - rule.selectors = rule.selectors.map(selector => { - return increaseSpecificity(config.important, selector) - }) + if (options.respectImportant && config.important) { + rule.__tailwind = { + ...rule.__tailwind, + important: config.important, } } }) From 6fe745b90804aa3cb5cd38774ff4b17b47c73f01 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 13:26:14 -0400 Subject: [PATCH 17/20] Improve comment --- src/flagged/applyComplexClasses.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index f4764988453e..6fe0232e6e69 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -255,9 +255,8 @@ export default function applyComplexClasses(config, getProcessedPlugins) { { from: undefined } ) .then(result => { - // if css already contains tailwind, css is the lookup tree + // Prepend Tailwind's generated classes to the tree so they are available for `@apply` const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root)) - return processApplyAtRules(css, lookupTree, config) }) } From e37b665b6094196337a89c1798c2f2aa353053f7 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 14 Aug 2020 19:12:45 -0400 Subject: [PATCH 18/20] Support applying classes that occur multiple times in a single selector --- __tests__/applyComplexClasses.test.js | 32 +++++++++++++++++++++++++++ src/flagged/applyComplexClasses.js | 11 +++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 1793ffd8439e..c4dd5ce486b4 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -247,6 +247,38 @@ test('it matches classes that have multiple rules', () => { }) }) +test('applying a class that appears multiple times in one selector', () => { + const input = ` + .a + .a > .a { + color: red; + } + + .b { + @apply a; + } + ` + + const output = ` + .a + .a > .a { + color: red; + } + .b + .a > .a { + color: red; + } + .a + .b > .a { + color: red; + } + .a + .a > .b { + color: red; + } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { const input = ` .foo { @apply mt-4; } diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 6fe0232e6e69..d31355dc16ec 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -20,13 +20,15 @@ function hasAtRule(css, atRule) { return foundAtRule } -function applyUtility(rule, className, replaceWith) { +function applyUtility({ rule, utilityName: className, classPosition }, replaceWith) { const processedSelectors = rule.selectors.map(selector => { const processor = selectorParser(selectors => { + let i = 0 selectors.walkClasses(c => { - if (c.value === className) { + if (c.value === className && classPosition === i) { c.replaceWith(selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__' })) } + i++ }) }) @@ -78,7 +80,7 @@ function buildUtilityMap(css) { css.walkRules(rule => { const utilityNames = extractUtilityNames(rule.selector) - utilityNames.forEach(utilityName => { + utilityNames.forEach((utilityName, i) => { if (utilityMap[utilityName] === undefined) { utilityMap[utilityName] = [] } @@ -86,6 +88,7 @@ function buildUtilityMap(css) { utilityMap[utilityName].push({ index, utilityName, + classPosition: i, rule: rule.clone({ parent: rule.parent }), containsApply: hasAtRule(rule, 'apply'), }) @@ -205,7 +208,7 @@ function processApplyAtRules(css, lookupTree, config) { // Get new rules with the utility portion of the selector replaced with the new selector const rulesToInsert = [ ...injects.map(injectUtility => { - return applyUtility(injectUtility.rule, injectUtility.utilityName, rule.selector) + return applyUtility(injectUtility, rule.selector) }), afterRule, ] From 9d257a986c302a7a35b7a711c98d79e4479360a6 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 15 Aug 2020 15:31:26 -0400 Subject: [PATCH 19/20] Rename inject instances to apply --- src/flagged/applyComplexClasses.js | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index d31355dc16ec..de97b166b65b 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -20,7 +20,7 @@ function hasAtRule(css, atRule) { return foundAtRule } -function applyUtility({ rule, utilityName: className, classPosition }, replaceWith) { +function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) { const processedSelectors = rule.selectors.map(selector => { const processor = selectorParser(selectors => { let i = 0 @@ -172,43 +172,43 @@ function processApplyAtRules(css, lookupTree, config) { while (hasAtRule(css, 'apply')) { css.walkRules(rule => { - const injectRules = [] + const applyRules = [] // Only walk direct children to avoid issues with nesting plugins rule.each(child => { if (child.type === 'atrule' && child.name === 'apply') { - injectRules.unshift(child) + applyRules.unshift(child) } }) - injectRules.forEach(inject => { + applyRules.forEach(applyRule => { const [ importantEntries, - injectUtilityNames, + applyUtilityNames, important = importantEntries.length > 0, - ] = _.partition(inject.params.split(' '), n => n === '!important') + ] = _.partition(applyRule.params.split(' '), n => n === '!important') const currentUtilityNames = extractUtilityNames(rule.selector) - if (_.intersection(injectUtilityNames, currentUtilityNames).length > 0) { - const currentUtilityName = _.intersection(injectUtilityNames, currentUtilityNames)[0] + if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) { + const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0] throw rule.error( `You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.` ) } - // Extract any post-inject declarations and re-insert them after inject rules + // Extract any post-apply declarations and re-insert them after apply rules const afterRule = rule.clone({ raws: {} }) - afterRule.nodes = afterRule.nodes.slice(rule.index(inject) + 1) - rule.nodes = rule.nodes.slice(0, rule.index(inject) + 1) + afterRule.nodes = afterRule.nodes.slice(rule.index(applyRule) + 1) + rule.nodes = rule.nodes.slice(0, rule.index(applyRule) + 1) - // Sort injects to match CSS source order - const injects = extractUtilityRules(injectUtilityNames, inject) + // Sort applys to match CSS source order + const applys = extractUtilityRules(applyUtilityNames, applyRule) // Get new rules with the utility portion of the selector replaced with the new selector const rulesToInsert = [ - ...injects.map(injectUtility => { - return applyUtility(injectUtility, rule.selector) + ...applys.map(applyUtility => { + return generateRulesFromApply(applyUtility, rule.selector) }), afterRule, ] @@ -219,11 +219,11 @@ function processApplyAtRules(css, lookupTree, config) { const mergedRules = mergeAdjacentRules(rule, nodes) - inject.remove() + applyRule.remove() rule.after(mergedRules) }) - // If the base rule has nothing in it (all injects were pseudo or responsive variants), + // If the base rule has nothing in it (all applys were pseudo or responsive variants), // remove the rule fuggit. if (rule.nodes.length === 0) { rule.remove() From 6b32635f2c84f7efd6d9b026f879c9a43e6a4492 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 15 Aug 2020 15:39:39 -0400 Subject: [PATCH 20/20] Explain other code branch when processing `@apply` rules --- src/flagged/applyComplexClasses.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index de97b166b65b..9b1d7e8f706d 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -241,6 +241,8 @@ export default function applyComplexClasses(config, getProcessedPlugins) { return processApplyAtRules(css, css, config) } + // Tree contains no @tailwind rules, so generate all of Tailwind's styles and + // prepend them to the user's CSS. Important for