Merge branch 'apply-complex-classes'

This commit is contained in:
Adam Wathan 2020-08-15 15:39:48 -04:00
commit 8a066c27a2
13 changed files with 1371 additions and 199 deletions

View file

@ -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,
})
}

View file

@ -0,0 +1,879 @@
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'
import cloneNodes from '../src/util/cloneNodes'
const resolvedDefaultConfig = resolveConfig([defaultConfig])
const defaultProcessedPlugins = processPlugins(
[...corePlugins(resolvedDefaultConfig), ...resolvedDefaultConfig.plugins],
resolvedDefaultConfig
)
const defaultGetProcessedPlugins = function() {
return {
...defaultProcessedPlugins,
base: cloneNodes(defaultProcessedPlugins.base),
components: cloneNodes(defaultProcessedPlugins.components),
utilities: cloneNodes(defaultProcessedPlugins.utilities),
}
}
function run(
input,
config = resolvedDefaultConfig,
getProcessedPlugins = () =>
config === resolvedDefaultConfig
? defaultGetProcessedPlugins()
: processPlugins(corePlugins(config), config)
) {
config.experimental = {
applyComplexClasses: true,
}
return postcss([substituteClassApplyAtRules(config, getProcessedPlugins)]).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).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('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).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('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).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('cssnext custom property sets are no longer supported', () => {
const input = `
.a {
color: red;
}
.b {
@apply a --custom-property-set;
}
`
return run(input).catch(e => {
expect(e).toMatchObject({ name: 'CssSyntaxError' })
})
})
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('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; }
`
const expected = `
.foo { margin-top: 1rem; }
`
return run(input).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
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('you can apply a class that is defined in multiple rules', () => {
const input = `
.foo {
color: red;
}
.bar {
@apply foo;
}
.foo {
opacity: .5;
}
`
const expected = `
.foo {
color: red;
}
.bar {
color: red;
opacity: .5;
}
.foo {
opacity: .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)
})
})
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; }
`
const config = resolveConfig([
{
...defaultConfig,
prefix: 'tw-',
},
])
return run(input, config).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
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; }
`
const config = resolveConfig([
{
...defaultConfig,
prefix: () => {
return 'tw-func-'
},
},
])
return run(input, 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).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).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).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).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('you can apply utility classes when a selector is used for the important option', () => {
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).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

View file

@ -6,7 +6,7 @@
@responsive {
.example {
@apply .font-bold;
@apply font-bold;
color: theme('colors.red.500');
}
}

View file

@ -979,64 +979,12 @@ 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 {
.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(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
#app .rotate-90 {
transform: rotate(90deg)
}
}
}
`)
})
test('when important contains a class an error is thrown', () => {
expect(() => {
processPlugins(
[
corePlugins: [],
plugins: [
function({ addUtilities }) {
addUtilities({
'.rotate-90': {
@ -1045,65 +993,123 @@ test('when important contains a class an error is thrown', () => {
})
},
],
makeConfig({
important: '#app .project',
})
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
}).toThrow()
.then(result => {
const expected = `
.tw-rotate-90 {
transform: rotate(90deg) !important
}
`
expect(result.css).toMatchCss(expected)
})
})
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': {
transform: 'rotate(90deg)',
},
})
},
],
}),
])
.process(
`
@tailwind utilities;
`,
{ from: undefined }
)
.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', () => {

View file

@ -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)) {

View file

@ -8,6 +8,7 @@ const featureFlags = {
'extendedSpacingScale',
'defaultLineHeights',
'extendedFontSizeScale',
'applyComplexClasses',
],
}
@ -69,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.'
)
@ -81,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.'
)
@ -93,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.'
)

View file

@ -0,0 +1,268 @@
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'
import prefixSelector from '../util/prefixSelector'
function hasAtRule(css, atRule) {
let foundAtRule = false
css.walkAtRules(atRule, () => {
foundAtRule = true
return false
})
return foundAtRule
}
function generateRulesFromApply({ 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 && classPosition === i) {
c.replaceWith(selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__' }))
}
i++
})
})
// 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 = processedSelectors
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, i) => {
if (utilityMap[utilityName] === undefined) {
utilityMap[utilityName] = []
}
utilityMap[utilityName].push({
index,
utilityName,
classPosition: i,
rule: rule.clone({ parent: rule.parent }),
containsApply: hasAtRule(rule, 'apply'),
})
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, config) {
const utilityMap = buildUtilityMap(css)
const orderUtilityMap = _.fromPairs(
_.flatMap(_.toPairs(utilityMap), ([_utilityName, utilities]) => {
return utilities.map(utility => {
return [utility.index, utility]
})
})
)
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}\` 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 }
)
}
return utilityMap[utilityName].map(({ index }) => index)
})
.sort((a, b) => a - b)
.map(i => orderUtilityMap[i])
}
}
function processApplyAtRules(css, lookupTree, config) {
const extractUtilityRules = makeExtractUtilityRules(lookupTree, config)
while (hasAtRule(css, 'apply')) {
css.walkRules(rule => {
const applyRules = []
// Only walk direct children to avoid issues with nesting plugins
rule.each(child => {
if (child.type === 'atrule' && child.name === 'apply') {
applyRules.unshift(child)
}
})
applyRules.forEach(applyRule => {
const [
importantEntries,
applyUtilityNames,
important = importantEntries.length > 0,
] = _.partition(applyRule.params.split(' '), n => n === '!important')
const currentUtilityNames = extractUtilityNames(rule.selector)
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-apply declarations and re-insert them after apply rules
const afterRule = rule.clone({ raws: {} })
afterRule.nodes = afterRule.nodes.slice(rule.index(applyRule) + 1)
rule.nodes = rule.nodes.slice(0, rule.index(applyRule) + 1)
// 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 = [
...applys.map(applyUtility => {
return generateRulesFromApply(applyUtility, rule.selector)
}),
afterRule,
]
const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root =>
root.walkDecls(d => (d.important = important))
)
const mergedRules = mergeAdjacentRules(rule, nodes)
applyRule.remove()
rule.after(mergedRules)
})
// 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()
}
})
}
return css
}
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, config)
}
// Tree contains no @tailwind rules, so generate all of Tailwind's styles and
// prepend them to the user's CSS. Important for <style> blocks in Vue components.
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 => {
// 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)
})
}
}

View file

@ -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))
}
})
}
}

View file

@ -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 => {

View file

@ -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 = {}
@ -53,10 +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(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 => {

View file

@ -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') {

View file

@ -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'
@ -39,7 +40,8 @@ export default function(getConfig) {
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
substituteClassApplyAtRules(config, getProcessedPlugins().utilities),
substituteClassApplyAtRules(config, getProcessedPlugins),
applyImportantConfiguration(config),
purgeUnusedStyles(config),
]).process(css, { from: _.get(css, 'source.input.file') })
}

View file

@ -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,
}
}
})