mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
8538ad859c
commit
4079059420
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user