Skip to content

Commit 4079059

Browse files
authored
Add missing layer(…) to imports above Tailwind directives (#14982)
This PR fixes an issue where imports above Tailwind directives didn't get a `layer(…)` argument. Given this CSS: ```css @import "./typography.css"; @tailwind base; @tailwind components; @tailwind utilities; ``` It was migrated to: ```css @import "./typography.css"; @import "tailwindcss"; ``` But to ensure that the typography styles end up in the correct location, it requires the `layer(…)` argument. This PR now migrates the input to: ```css @import "./typography.css" layer(base); @import "tailwindcss"; ``` Test plan: --- Added an integration test where an import receives the `layer(…)`, but an import that eventually contains `@utility` does not receive the `layer(…)` argument. This is necessary otherwise the `@utility` will be nested when we are processing the inlined CSS. Running this on the Commit template, we do have a proper `layer(…)` <img width="585" alt="image" src="https://github.com/user-attachments/assets/538055e6-a9ac-490d-981f-41065a6b59f9">
1 parent 8538ad8 commit 4079059

File tree

4 files changed

+185
-18
lines changed

4 files changed

+185
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
2020
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
2121
- _Upgrade (experimental)_: Ensure `@config` is injected in nearest common ancestor stylesheet ([#14989](https://github.com/tailwindlabs/tailwindcss/pull/14989))
22+
- _Upgrade (experimental)_: Add missing `layer(…)` to imports above Tailwind directives ([#14982](https://github.com/tailwindlabs/tailwindcss/pull/14982))
2223

2324
## [4.0.0-alpha.33] - 2024-11-11
2425

integrations/upgrade/index.test.ts

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,132 @@ test(
453453
},
454454
)
455455

456+
test(
457+
'migrate imports with `layer(…)`',
458+
{
459+
fs: {
460+
'package.json': json`
461+
{
462+
"dependencies": {
463+
"tailwindcss": "workspace:^",
464+
"@tailwindcss/upgrade": "workspace:^"
465+
}
466+
}
467+
`,
468+
'tailwind.config.js': js`module.exports = {}`,
469+
'src/index.css': css`
470+
@import './base.css';
471+
@import './components.css';
472+
@import './utilities.css';
473+
@import './mix.css';
474+
475+
@tailwind base;
476+
@tailwind components;
477+
@tailwind utilities;
478+
`,
479+
'src/base.css': css`
480+
html {
481+
color: red;
482+
}
483+
`,
484+
'src/components.css': css`
485+
@layer components {
486+
.foo {
487+
color: red;
488+
}
489+
}
490+
`,
491+
'src/utilities.css': css`
492+
@layer utilities {
493+
.bar {
494+
color: red;
495+
}
496+
}
497+
`,
498+
'src/mix.css': css`
499+
html {
500+
color: blue;
501+
}
502+
503+
@layer components {
504+
.foo-mix {
505+
color: red;
506+
}
507+
}
508+
509+
@layer utilities {
510+
.bar-mix {
511+
color: red;
512+
}
513+
}
514+
`,
515+
},
516+
},
517+
async ({ fs, exec }) => {
518+
await exec('npx @tailwindcss/upgrade')
519+
520+
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
521+
"
522+
--- ./src/index.css ---
523+
@import './base.css' layer(base);
524+
@import './components.css';
525+
@import './utilities.css';
526+
@import './mix.css' layer(base);
527+
@import './mix.utilities.css';
528+
529+
@import 'tailwindcss';
530+
531+
/*
532+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
533+
so we've added these compatibility styles to make sure everything still
534+
looks the same as it did with Tailwind CSS v3.
535+
536+
If we ever want to remove these styles, we need to add an explicit border
537+
color utility to any element that depends on these defaults.
538+
*/
539+
@layer base {
540+
*,
541+
::after,
542+
::before,
543+
::backdrop,
544+
::file-selector-button {
545+
border-color: var(--color-gray-200, currentColor);
546+
}
547+
}
548+
549+
--- ./src/base.css ---
550+
html {
551+
color: red;
552+
}
553+
554+
--- ./src/components.css ---
555+
@utility foo {
556+
color: red;
557+
}
558+
559+
--- ./src/mix.css ---
560+
html {
561+
color: blue;
562+
}
563+
564+
--- ./src/mix.utilities.css ---
565+
@utility foo-mix {
566+
color: red;
567+
}
568+
569+
@utility bar-mix {
570+
color: red;
571+
}
572+
573+
--- ./src/utilities.css ---
574+
@utility bar {
575+
color: red;
576+
}
577+
"
578+
`)
579+
},
580+
)
581+
456582
test(
457583
'migrates a simple postcss setup',
458584
{
@@ -1571,7 +1697,7 @@ test(
15711697
}
15721698
15731699
--- ./src/components.css ---
1574-
@import './typography.css';
1700+
@import './typography.css' layer(components);
15751701
15761702
@utility foo {
15771703
color: red;
@@ -1706,7 +1832,7 @@ test(
17061832
}
17071833
17081834
--- ./src/components.css ---
1709-
@import './typography.css';
1835+
@import './typography.css' layer(components);
17101836
17111837
@utility foo {
17121838
color: red;

packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ export function migrateMissingLayers(): Plugin {
7575

7676
// Add layer to `@import` at-rules
7777
if (node.name === 'import') {
78-
if (lastLayer !== '' && !node.params.includes('layer(')) {
79-
node.params += ` layer(${lastLayer})`
80-
node.raws.tailwind_injected_layer = true
81-
}
82-
8378
if (bucket.length > 0) {
8479
buckets.push([lastLayer, bucket.splice(0)])
8580
}
81+
82+
// Create new bucket just for the import. This way every import exists
83+
// in its own layer which allows us to add the `layer(…)` parameter
84+
// later on.
85+
buckets.push([lastLayer, [node]])
8686
return
8787
}
8888
}
@@ -102,7 +102,6 @@ export function migrateMissingLayers(): Plugin {
102102
bucket.push(node)
103103
})
104104

105-
// Wrap each bucket in an `@layer` at-rule
106105
for (let [layerName, nodes] of buckets) {
107106
let targetLayerName = layerName || firstLayerName || ''
108107
if (targetLayerName === '') {
@@ -114,6 +113,20 @@ export function migrateMissingLayers(): Plugin {
114113
continue
115114
}
116115

116+
// Add `layer(…)` to `@import` at-rules
117+
if (nodes.every((node) => node.type === 'atrule' && node.name === 'import')) {
118+
for (let node of nodes) {
119+
if (node.type !== 'atrule' || node.name !== 'import') continue
120+
121+
if (!node.params.includes('layer(')) {
122+
node.params += ` layer(${targetLayerName})`
123+
node.raws.tailwind_injected_layer = true
124+
}
125+
}
126+
continue
127+
}
128+
129+
// Wrap each bucket in an `@layer` at-rule
117130
let target = nodes[0]
118131
let layerNode = new AtRule({
119132
name: 'layer',

packages/@tailwindcss-upgrade/src/migrate.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,21 +402,48 @@ export async function split(stylesheets: Stylesheet[]) {
402402
}
403403

404404
// Keep track of sheets that contain `@utility` rules
405-
let containsUtilities = new Set<Stylesheet>()
405+
let requiresSplit = new Set<Stylesheet>()
406406

407407
for (let sheet of stylesheets) {
408-
let layers = sheet.layers()
409-
let isLayered = layers.has('utilities') || layers.has('components')
410-
if (!isLayered) continue
408+
// Root files don't need to be split
409+
if (sheet.isTailwindRoot) continue
410+
411+
let containsUtility = false
412+
let containsUnsafe = sheet.layers().size > 0
411413

412414
walk(sheet.root, (node) => {
413-
if (node.type !== 'atrule') return
414-
if (node.name !== 'utility') return
415+
if (node.type === 'atrule' && node.name === 'utility') {
416+
containsUtility = true
417+
}
418+
419+
// Safe to keep without splitting
420+
else if (
421+
// An `@import "…" layer(…)` is safe
422+
(node.type === 'atrule' && node.name === 'import' && node.params.includes('layer(')) ||
423+
// @layer blocks are safe
424+
(node.type === 'atrule' && node.name === 'layer') ||
425+
// Comments are safe
426+
node.type === 'comment'
427+
) {
428+
return WalkAction.Skip
429+
}
415430

416-
containsUtilities.add(sheet)
431+
// Everything else is not safe, and requires a split
432+
else {
433+
containsUnsafe = true
434+
}
435+
436+
// We already know we need to split this sheet
437+
if (containsUtility && containsUnsafe) {
438+
return WalkAction.Stop
439+
}
417440

418-
return WalkAction.Stop
441+
return WalkAction.Skip
419442
})
443+
444+
if (containsUtility && containsUnsafe) {
445+
requiresSplit.add(sheet)
446+
}
420447
}
421448

422449
// Split every imported stylesheet into two parts
@@ -429,8 +456,8 @@ export async function split(stylesheets: Stylesheet[]) {
429456

430457
// Skip stylesheets that don't have utilities
431458
// and don't have any children that have utilities
432-
if (!containsUtilities.has(sheet)) {
433-
if (!Array.from(sheet.descendants()).some((child) => containsUtilities.has(child))) {
459+
if (!requiresSplit.has(sheet)) {
460+
if (!Array.from(sheet.descendants()).some((child) => requiresSplit.has(child))) {
434461
continue
435462
}
436463
}

0 commit comments

Comments
 (0)