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 blocks in Vue components.
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions(config),