Skip to content

Commit f99302c

Browse files
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 <[email protected]>
1 parent 4142b3f commit f99302c

File tree

4 files changed

+196
-4
lines changed

4 files changed

+196
-4
lines changed

src/lib/expandApplyAtRules.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function processApply(root, context) {
9797
for (let util of candidates) {
9898
applyCandidates.add(util)
9999
}
100+
100101
applies.push(rule)
101102
})
102103

@@ -210,7 +211,12 @@ function processApply(root, context) {
210211
})
211212
}
212213

213-
siblings.push([meta, root.nodes[0]])
214+
// Insert it
215+
siblings.push([
216+
// Ensure that when we are sorting, that we take the layer order into account
217+
{ ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
218+
root.nodes[0],
219+
])
214220
}
215221
}
216222

src/lib/expandTailwindAtRules.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ function buildStylesheet(rules, context) {
109109
components: new Set(),
110110
utilities: new Set(),
111111
variants: new Set(),
112+
113+
// All the CSS that is not Tailwind related can be put in this bucket. This
114+
// will make it easier to later use this information when we want to
115+
// `@apply` for example. The main reason we do this here is because we
116+
// still need to make sure the order is correct. Last but not least, we
117+
// will make sure to always re-inject this section into the css, even if
118+
// certain rules were not used. This means that it will look like a no-op
119+
// from the user's perspective, but we gathered all the useful information
120+
// we need.
121+
user: new Set(),
112122
}
113123

114124
for (let [sort, rule] of sortedRules) {
@@ -131,6 +141,11 @@ function buildStylesheet(rules, context) {
131141
returnValue.utilities.add(rule)
132142
continue
133143
}
144+
145+
if (sort & context.layerOrder.user) {
146+
returnValue.user.add(rule)
147+
continue
148+
}
134149
}
135150

136151
return returnValue

src/lib/setupContextUtils.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
201201

202202
return getConfigValue(['variants', path], defaultValue)
203203
},
204+
addUserCss(userCss) {
205+
for (let [identifier, rule] of withIdentifiers(userCss)) {
206+
let offset = offsets.user++
207+
208+
if (!context.candidateRuleMap.has(identifier)) {
209+
context.candidateRuleMap.set(identifier, [])
210+
}
211+
212+
context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
213+
}
214+
},
204215
addBase(base) {
205216
for (let [identifier, rule] of withIdentifiers(base)) {
206217
let prefixedIdentifier = prefixIdentifier(identifier, {})
@@ -404,6 +415,15 @@ function collectLayerPlugins(root) {
404415
}
405416
})
406417

418+
root.walkRules((rule) => {
419+
// At this point it is safe to include all the left-over css from the
420+
// user's css file. This is because the `@tailwind` and `@layer` directives
421+
// will already be handled and will be removed from the css tree.
422+
layerPlugins.push(function ({ addUserCss }) {
423+
addUserCss(rule, { respectPrefix: false })
424+
})
425+
})
426+
407427
return layerPlugins
408428
}
409429

@@ -448,6 +468,7 @@ function registerPlugins(plugins, context) {
448468
base: 0n,
449469
components: 0n,
450470
utilities: 0n,
471+
user: 0n,
451472
}
452473

453474
let pluginApi = buildPluginApi(context.tailwindConfig, context, {
@@ -470,16 +491,18 @@ function registerPlugins(plugins, context) {
470491
offsets.base,
471492
offsets.components,
472493
offsets.utilities,
494+
offsets.user,
473495
])
474496
let reservedBits = BigInt(highestOffset.toString(2).length)
475497

476498
context.layerOrder = {
477499
base: (1n << reservedBits) << 0n,
478500
components: (1n << reservedBits) << 1n,
479501
utilities: (1n << reservedBits) << 2n,
502+
user: (1n << reservedBits) << 3n,
480503
}
481504

482-
reservedBits += 3n
505+
reservedBits += 4n
483506

484507
let offset = 0
485508
context.variantOrder = new Map(

tests/apply.test.js

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs'
22
import path from 'path'
33

4-
import { run, css } from './util/run'
4+
import { run, html, css } from './util/run'
55

66
test('@apply', () => {
77
let config = {
@@ -236,7 +236,7 @@ test('@apply error when using a prefixed .group utility', async () => {
236236
let config = {
237237
prefix: 'tw-',
238238
darkMode: 'class',
239-
content: [{ raw: '<div class="foo"></div>' }],
239+
content: [{ raw: html`<div class="foo"></div>` }],
240240
}
241241

242242
let input = css`
@@ -254,3 +254,151 @@ test('@apply error when using a prefixed .group utility', async () => {
254254
`@apply should not be used with the 'tw-group' utility`
255255
)
256256
})
257+
258+
test('@apply classes from outside a @layer', async () => {
259+
let config = {
260+
content: [{ raw: html`<div class="font-bold foo bar baz"></div>` }],
261+
}
262+
263+
let input = css`
264+
@tailwind components;
265+
@tailwind utilities;
266+
267+
.foo {
268+
@apply font-bold;
269+
}
270+
271+
.bar {
272+
@apply foo text-red-500 hover:text-green-500;
273+
}
274+
275+
.baz {
276+
@apply bar underline;
277+
}
278+
279+
.keep-me-even-though-I-am-not-used-in-content {
280+
color: green;
281+
}
282+
`
283+
284+
await run(input, config).then((result) => {
285+
return expect(result.css).toMatchFormattedCss(css`
286+
.font-bold {
287+
font-weight: 700;
288+
}
289+
290+
.foo {
291+
font-weight: 700;
292+
}
293+
294+
.bar {
295+
--tw-text-opacity: 1;
296+
color: rgba(239, 68, 68, var(--tw-text-opacity));
297+
font-weight: 700;
298+
}
299+
300+
.bar:hover {
301+
--tw-text-opacity: 1;
302+
color: rgba(34, 197, 94, var(--tw-text-opacity));
303+
}
304+
305+
.baz {
306+
text-decoration: underline;
307+
--tw-text-opacity: 1;
308+
color: rgba(239, 68, 68, var(--tw-text-opacity));
309+
font-weight: 700;
310+
}
311+
312+
.baz:hover {
313+
--tw-text-opacity: 1;
314+
color: rgba(34, 197, 94, var(--tw-text-opacity));
315+
}
316+
317+
.keep-me-even-though-I-am-not-used-in-content {
318+
color: green;
319+
}
320+
`)
321+
})
322+
})
323+
324+
test('@applying classes from outside a @layer respects the source order', async () => {
325+
let config = {
326+
content: [{ raw: html`<div class="container font-bold foo bar baz"></div>` }],
327+
}
328+
329+
let input = css`
330+
.baz {
331+
@apply bar underline;
332+
}
333+
334+
@tailwind components;
335+
336+
.keep-me-even-though-I-am-not-used-in-content {
337+
color: green;
338+
}
339+
340+
@tailwind utilities;
341+
342+
.foo {
343+
@apply font-bold;
344+
}
345+
346+
.bar {
347+
@apply no-underline;
348+
}
349+
`
350+
351+
await run(input, config).then((result) => {
352+
return expect(result.css).toMatchFormattedCss(css`
353+
.baz {
354+
text-decoration: underline;
355+
text-decoration: none;
356+
}
357+
358+
.container {
359+
width: 100%;
360+
}
361+
@media (min-width: 640px) {
362+
.container {
363+
max-width: 640px;
364+
}
365+
}
366+
@media (min-width: 768px) {
367+
.container {
368+
max-width: 768px;
369+
}
370+
}
371+
@media (min-width: 1024px) {
372+
.container {
373+
max-width: 1024px;
374+
}
375+
}
376+
@media (min-width: 1280px) {
377+
.container {
378+
max-width: 1280px;
379+
}
380+
}
381+
@media (min-width: 1536px) {
382+
.container {
383+
max-width: 1536px;
384+
}
385+
}
386+
387+
.keep-me-even-though-I-am-not-used-in-content {
388+
color: green;
389+
}
390+
391+
.font-bold {
392+
font-weight: 700;
393+
}
394+
395+
.foo {
396+
font-weight: 700;
397+
}
398+
399+
.bar {
400+
text-decoration: none;
401+
}
402+
`)
403+
})
404+
})

0 commit comments

Comments
 (0)