Support @apply for classes outside of a @layer (#5378)

* support `@apply` for classes outside of a `@layer`

* Add failing test for respecting source order

* sort rules when using `@apply`

The `layer` was not taken into account yet when we resolved the rules
from the applyCache. This is because we set the `classCache` to the
`matches` inside of the `generateRules` function. You can think of them
as "raw" rules I guess. However, it is later in that function that we
apply the `layerOrder` to the `sort`.

This does mean that when you `@apply font-bold text-red-500` that the
rules inside the `.target {}` will be in order of the "normal" css as
well.

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2021-09-08 03:03:46 +02:00 • committed by GitHub
parent 4142b3fbaf
commit f99302c626
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 196 additions and 4 deletions

View file

@ -97,6 +97,7 @@ function processApply(root, context) {
for (let util of candidates) {
applyCandidates.add(util)
}
applies.push(rule)
})
@ -210,7 +211,12 @@ function processApply(root, context) {
})
}
siblings.push([meta, root.nodes[0]])
// Insert it
siblings.push([
// Ensure that when we are sorting, that we take the layer order into account
{ ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
root.nodes[0],
])
}
}

View file

@ -109,6 +109,16 @@ function buildStylesheet(rules, context) {
components: new Set(),
utilities: new Set(),
variants: new Set(),
// All the CSS that is not Tailwind related can be put in this bucket. This
// will make it easier to later use this information when we want to
// `@apply` for example. The main reason we do this here is because we
// still need to make sure the order is correct. Last but not least, we
// will make sure to always re-inject this section into the css, even if
// certain rules were not used. This means that it will look like a no-op
// from the user's perspective, but we gathered all the useful information
// we need.
user: new Set(),
}
for (let [sort, rule] of sortedRules) {
@ -131,6 +141,11 @@ function buildStylesheet(rules, context) {
returnValue.utilities.add(rule)
continue
}
if (sort & context.layerOrder.user) {
returnValue.user.add(rule)
continue
}
}
return returnValue

View file

@ -201,6 +201,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
return getConfigValue(['variants', path], defaultValue)
},
addUserCss(userCss) {
for (let [identifier, rule] of withIdentifiers(userCss)) {
let offset = offsets.user++
if (!context.candidateRuleMap.has(identifier)) {
context.candidateRuleMap.set(identifier, [])
}
context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
}
},
addBase(base) {
for (let [identifier, rule] of withIdentifiers(base)) {
let prefixedIdentifier = prefixIdentifier(identifier, {})
@ -404,6 +415,15 @@ function collectLayerPlugins(root) {
}
})
root.walkRules((rule) => {
// At this point it is safe to include all the left-over css from the
// user's css file. This is because the `@tailwind` and `@layer` directives
// will already be handled and will be removed from the css tree.
layerPlugins.push(function ({ addUserCss }) {
addUserCss(rule, { respectPrefix: false })
})
})
return layerPlugins
}
@ -448,6 +468,7 @@ function registerPlugins(plugins, context) {
base: 0n,
components: 0n,
utilities: 0n,
user: 0n,
}
let pluginApi = buildPluginApi(context.tailwindConfig, context, {
@ -470,6 +491,7 @@ function registerPlugins(plugins, context) {
offsets.base,
offsets.components,
offsets.utilities,
offsets.user,
])
let reservedBits = BigInt(highestOffset.toString(2).length)
@ -477,9 +499,10 @@ function registerPlugins(plugins, context) {
base: (1n << reservedBits) << 0n,
components: (1n << reservedBits) << 1n,
utilities: (1n << reservedBits) << 2n,
user: (1n << reservedBits) << 3n,
}
reservedBits += 3n
reservedBits += 4n
let offset = 0
context.variantOrder = new Map(

View file

@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { run, css } from './util/run'
import { run, html, css } from './util/run'
test('@apply', () => {
let config = {
@ -236,7 +236,7 @@ test('@apply error when using a prefixed .group utility', async () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
content: [{ raw: '<div class="foo"></div>' }],
content: [{ raw: html`<div class="foo"></div>` }],
}
let input = css`
@ -254,3 +254,151 @@ test('@apply error when using a prefixed .group utility', async () => {
`@apply should not be used with the 'tw-group' utility`
)
})
test('@apply classes from outside a @layer', async () => {
let config = {
content: [{ raw: html`<div class="font-bold foo bar baz"></div>` }],
}
let input = css`
@tailwind components;
@tailwind utilities;
.foo {
@apply font-bold;
}
.bar {
@apply foo text-red-500 hover:text-green-500;
}
.baz {
@apply bar underline;
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
`
await run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.foo {
font-weight: 700;
}
.bar {
--tw-text-opacity: 1;
color: rgba(239, 68, 68, var(--tw-text-opacity));
font-weight: 700;
}
.bar:hover {
--tw-text-opacity: 1;
color: rgba(34, 197, 94, var(--tw-text-opacity));
}
.baz {
text-decoration: underline;
--tw-text-opacity: 1;
color: rgba(239, 68, 68, var(--tw-text-opacity));
font-weight: 700;
}
.baz:hover {
--tw-text-opacity: 1;
color: rgba(34, 197, 94, var(--tw-text-opacity));
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
`)
})
})
test('@applying classes from outside a @layer respects the source order', async () => {
let config = {
content: [{ raw: html`<div class="container font-bold foo bar baz"></div>` }],
}
let input = css`
.baz {
@apply bar underline;
}
@tailwind components;
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
@tailwind utilities;
.foo {
@apply font-bold;
}
.bar {
@apply no-underline;
}
`
await run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.baz {
text-decoration: underline;
text-decoration: none;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.keep-me-even-though-I-am-not-used-in-content {
color: green;
}
.font-bold {
font-weight: 700;
}
.foo {
font-weight: 700;
}
.bar {
text-decoration: none;
}
`)
})
})