Merge branch 'apply-complex-classes'
This commit is contained in:
commit
8a066c27a2
13 changed files with 1371 additions and 199 deletions
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
879
__tests__/applyComplexClasses.test.js
Normal file
879
__tests__/applyComplexClasses.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
@responsive {
|
||||
.example {
|
||||
@apply .font-bold;
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
)
|
||||
|
|
|
|||
268
src/flagged/applyComplexClasses.js
Normal file
268
src/flagged/applyComplexClasses.js
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
19
src/lib/applyImportantConfiguration.js
Normal file
19
src/lib/applyImportantConfiguration.js
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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') })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue