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">
This commit is contained in:
Robin Malfait 2024-11-14 18:05:14 +01:00 committed by GitHub
parent 8538ad859c
commit 4079059420
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 185 additions and 18 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
- _Upgrade (experimental)_: Ensure `@config` is injected in nearest common ancestor stylesheet ([#14989](https://github.com/tailwindlabs/tailwindcss/pull/14989))
- _Upgrade (experimental)_: Add missing `layer(…)` to imports above Tailwind directives ([#14982](https://github.com/tailwindlabs/tailwindcss/pull/14982))
## [4.0.0-alpha.33] - 2024-11-11

View File

@ -453,6 +453,132 @@ test(
},
)
test(
'migrate imports with `layer(…)`',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`module.exports = {}`,
'src/index.css': css`
@import './base.css';
@import './components.css';
@import './utilities.css';
@import './mix.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'src/base.css': css`
html {
color: red;
}
`,
'src/components.css': css`
@layer components {
.foo {
color: red;
}
}
`,
'src/utilities.css': css`
@layer utilities {
.bar {
color: red;
}
}
`,
'src/mix.css': css`
html {
color: blue;
}
@layer components {
.foo-mix {
color: red;
}
}
@layer utilities {
.bar-mix {
color: red;
}
}
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
"
--- ./src/index.css ---
@import './base.css' layer(base);
@import './components.css';
@import './utilities.css';
@import './mix.css' layer(base);
@import './mix.utilities.css';
@import 'tailwindcss';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
--- ./src/base.css ---
html {
color: red;
}
--- ./src/components.css ---
@utility foo {
color: red;
}
--- ./src/mix.css ---
html {
color: blue;
}
--- ./src/mix.utilities.css ---
@utility foo-mix {
color: red;
}
@utility bar-mix {
color: red;
}
--- ./src/utilities.css ---
@utility bar {
color: red;
}
"
`)
},
)
test(
'migrates a simple postcss setup',
{
@ -1571,7 +1697,7 @@ test(
}
--- ./src/components.css ---
@import './typography.css';
@import './typography.css' layer(components);
@utility foo {
color: red;
@ -1706,7 +1832,7 @@ test(
}
--- ./src/components.css ---
@import './typography.css';
@import './typography.css' layer(components);
@utility foo {
color: red;

View File

@ -75,14 +75,14 @@ export function migrateMissingLayers(): Plugin {
// Add layer to `@import` at-rules
if (node.name === 'import') {
if (lastLayer !== '' && !node.params.includes('layer(')) {
node.params += ` layer(${lastLayer})`
node.raws.tailwind_injected_layer = true
}
if (bucket.length > 0) {
buckets.push([lastLayer, bucket.splice(0)])
}
// Create new bucket just for the import. This way every import exists
// in its own layer which allows us to add the `layer(…)` parameter
// later on.
buckets.push([lastLayer, [node]])
return
}
}
@ -102,7 +102,6 @@ export function migrateMissingLayers(): Plugin {
bucket.push(node)
})
// Wrap each bucket in an `@layer` at-rule
for (let [layerName, nodes] of buckets) {
let targetLayerName = layerName || firstLayerName || ''
if (targetLayerName === '') {
@ -114,6 +113,20 @@ export function migrateMissingLayers(): Plugin {
continue
}
// Add `layer(…)` to `@import` at-rules
if (nodes.every((node) => node.type === 'atrule' && node.name === 'import')) {
for (let node of nodes) {
if (node.type !== 'atrule' || node.name !== 'import') continue
if (!node.params.includes('layer(')) {
node.params += ` layer(${targetLayerName})`
node.raws.tailwind_injected_layer = true
}
}
continue
}
// Wrap each bucket in an `@layer` at-rule
let target = nodes[0]
let layerNode = new AtRule({
name: 'layer',

View File

@ -402,21 +402,48 @@ export async function split(stylesheets: Stylesheet[]) {
}
// Keep track of sheets that contain `@utility` rules
let containsUtilities = new Set<Stylesheet>()
let requiresSplit = new Set<Stylesheet>()
for (let sheet of stylesheets) {
let layers = sheet.layers()
let isLayered = layers.has('utilities') || layers.has('components')
if (!isLayered) continue
// Root files don't need to be split
if (sheet.isTailwindRoot) continue
let containsUtility = false
let containsUnsafe = sheet.layers().size > 0
walk(sheet.root, (node) => {
if (node.type !== 'atrule') return
if (node.name !== 'utility') return
if (node.type === 'atrule' && node.name === 'utility') {
containsUtility = true
}
containsUtilities.add(sheet)
// Safe to keep without splitting
else if (
// An `@import "…" layer(…)` is safe
(node.type === 'atrule' && node.name === 'import' && node.params.includes('layer(')) ||
// @layer blocks are safe
(node.type === 'atrule' && node.name === 'layer') ||
// Comments are safe
node.type === 'comment'
) {
return WalkAction.Skip
}
return WalkAction.Stop
// Everything else is not safe, and requires a split
else {
containsUnsafe = true
}
// We already know we need to split this sheet
if (containsUtility && containsUnsafe) {
return WalkAction.Stop
}
return WalkAction.Skip
})
if (containsUtility && containsUnsafe) {
requiresSplit.add(sheet)
}
}
// Split every imported stylesheet into two parts
@ -429,8 +456,8 @@ export async function split(stylesheets: Stylesheet[]) {
// Skip stylesheets that don't have utilities
// and don't have any children that have utilities
if (!containsUtilities.has(sheet)) {
if (!Array.from(sheet.descendants()).some((child) => containsUtilities.has(child))) {
if (!requiresSplit.has(sheet)) {
if (!Array.from(sheet.descendants()).some((child) => requiresSplit.has(child))) {
continue
}
}