Skip to content

Commit d2865c3

Browse files
authored
Remove layer(utilities) if imports contain @utility (#14738)
We have a migration that adds the `layer(…)` next to the `@import` depending on the order of original values. For example: ```css @import "tailwindcss/utilities": @import "./foo.css": @import "tailwindcss/components": ``` Will be turned into: ```css @import "tailwindcss": @import "./foo.css" layer(utilities): ``` Because it used to exist between `utilities` and `components`. Without this it would be _after_ `components`. This results in an issue if an import has (deeply) nested `@utility` at-rules after migrations. This is because if this is generated: ```css /* ./src/index.css */ @import "tailwindcss"; @import "./foo.css" layer(utilities); /* ./src/foo.css */ @Utility foo { color: red; } ``` Once we interpret this (and thus flatten it), the final CSS would look like: ```css @layer utilities { @Utility foo { color: red; } } ``` This means that `@utility` is not top-level and an error would occur. This fixes that by removing the `layer(…)` from the import if the imported file (or any of its children) contains an `@utility`. This is to ensure that once everything is imported and flattened, that all `@utility` at-rules are top-level.
1 parent 19de557 commit d2865c3

File tree

5 files changed

+111
-14
lines changed

5 files changed

+111
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
2222
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
2323
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
24+
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))
2425

2526
### Changed
2627

integrations/upgrade/index.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,60 @@ test(
638638
},
639639
)
640640

641+
test(
642+
'migrate utilities in an imported file and keep @utility top-level',
643+
{
644+
fs: {
645+
'package.json': json`
646+
{
647+
"dependencies": {
648+
"tailwindcss": "workspace:^",
649+
"@tailwindcss/upgrade": "workspace:^"
650+
}
651+
}
652+
`,
653+
'tailwind.config.js': js`module.exports = {}`,
654+
'src/index.css': css`
655+
@import 'tailwindcss/utilities';
656+
@import './utilities.css';
657+
@import 'tailwindcss/components';
658+
`,
659+
'src/utilities.css': css`
660+
@layer utilities {
661+
.no-scrollbar::-webkit-scrollbar {
662+
display: none;
663+
}
664+
665+
.no-scrollbar {
666+
-ms-overflow-style: none;
667+
scrollbar-width: none;
668+
}
669+
}
670+
`,
671+
},
672+
},
673+
async ({ fs, exec }) => {
674+
await exec('npx @tailwindcss/upgrade --force')
675+
676+
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
677+
"
678+
--- ./src/index.css ---
679+
@import 'tailwindcss/utilities' layer(utilities);
680+
@import './utilities.css';
681+
682+
--- ./src/utilities.css ---
683+
@utility no-scrollbar {
684+
&::-webkit-scrollbar {
685+
display: none;
686+
}
687+
-ms-overflow-style: none;
688+
scrollbar-width: none;
689+
}
690+
"
691+
`)
692+
},
693+
)
694+
641695
test(
642696
'migrate utilities in deep import trees',
643697
{
@@ -737,7 +791,7 @@ test(
737791
@import './a.1.css' layer(utilities);
738792
@import './a.1.utilities.1.css';
739793
@import './b.1.css';
740-
@import './c.1.css' layer(utilities);
794+
@import './c.1.css';
741795
@import './c.1.utilities.css';
742796
@import './d.1.css';
743797

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,47 @@ async function run() {
149149
error(`${e}`)
150150
}
151151

152+
// Cleanup `@import "…" layer(utilities)`
153+
for (let sheet of stylesheets) {
154+
// If the `@import` contains an injected `layer(…)` we need to remove it
155+
if (!Array.from(sheet.importRules).some((node) => node.raws.tailwind_injected_layer)) {
156+
continue
157+
}
158+
159+
let hasAtUtility = false
160+
161+
// Only remove the `layer(…)` next to the import, if any of the children
162+
// contains an `@utility`. Otherwise the `@utility` will not be top-level.
163+
{
164+
sheet.root.walkAtRules('utility', () => {
165+
hasAtUtility = true
166+
return false
167+
})
168+
169+
if (!hasAtUtility) {
170+
for (let child of sheet.descendants()) {
171+
child.root.walkAtRules('utility', () => {
172+
hasAtUtility = true
173+
return false
174+
})
175+
176+
if (hasAtUtility) {
177+
break
178+
}
179+
}
180+
}
181+
}
182+
183+
// No `@utility` found, we can keep the `layer(…)` next to the import
184+
if (!hasAtUtility) continue
185+
186+
for (let importNode of sheet.importRules) {
187+
if (importNode.raws.tailwind_injected_layer) {
188+
importNode.params = importNode.params.replace(/ layer\([^)]+\)/, '').trim()
189+
}
190+
}
191+
}
192+
152193
// Format nodes
153194
for (let sheet of stylesheets) {
154195
await postcss([formatNodes()]).process(sheet.root!, { from: sheet.file! })

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,20 +159,20 @@ export async function analyze(stylesheets: Stylesheet[]) {
159159
for (let sheet of stylesheets) {
160160
if (!sheet.file) continue
161161

162-
let { convertablePaths, nonConvertablePaths } = sheet.analyzeImportPaths()
163-
let isAmbiguous = convertablePaths.length > 0 && nonConvertablePaths.length > 0
162+
let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths()
163+
let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0
164164

165165
if (!isAmbiguous) continue
166166

167167
sheet.canMigrate = false
168168

169169
let filePath = sheet.file.replace(commonPath, '')
170170

171-
for (let path of convertablePaths) {
171+
for (let path of convertiblePaths) {
172172
lines.push(`- ${filePath} <- ${pathToString(path)}`)
173173
}
174174

175-
for (let path of nonConvertablePaths) {
175+
for (let path of nonConvertiblePaths) {
176176
lines.push(`- ${filePath} <- ${pathToString(path)}`)
177177
}
178178
}
@@ -197,7 +197,7 @@ export async function split(stylesheets: Stylesheet[]) {
197197
}
198198
}
199199

200-
// Keep track of sheets that contain `@utillity` rules
200+
// Keep track of sheets that contain `@utility` rules
201201
let containsUtilities = new Set<Stylesheet>()
202202

203203
for (let sheet of stylesheets) {
@@ -324,6 +324,7 @@ export async function split(stylesheets: Stylesheet[]) {
324324
params: `${quote}${newFile}${quote}`,
325325
raws: {
326326
after: '\n\n',
327+
tailwind_injected_layer: node.raws.tailwind_injected_layer,
327328
tailwind_original_params: `${quote}${id}${quote}`,
328329
tailwind_destination_sheet_id: utilityDestination.id,
329330
},

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,26 +197,26 @@ export class Stylesheet {
197197
* adjusting imports which is a non-trivial task.
198198
*/
199199
analyzeImportPaths() {
200-
let convertablePaths: StylesheetConnection[][] = []
201-
let nonConvertablePaths: StylesheetConnection[][] = []
200+
let convertiblePaths: StylesheetConnection[][] = []
201+
let nonConvertiblePaths: StylesheetConnection[][] = []
202202

203203
for (let path of this.pathsToRoot()) {
204-
let isConvertable = false
204+
let isConvertible = false
205205

206206
for (let { meta } of path) {
207207
for (let layer of meta.layers) {
208-
isConvertable ||= layer === 'utilities' || layer === 'components'
208+
isConvertible ||= layer === 'utilities' || layer === 'components'
209209
}
210210
}
211211

212-
if (isConvertable) {
213-
convertablePaths.push(path)
212+
if (isConvertible) {
213+
convertiblePaths.push(path)
214214
} else {
215-
nonConvertablePaths.push(path)
215+
nonConvertiblePaths.push(path)
216216
}
217217
}
218218

219-
return { convertablePaths, nonConvertablePaths }
219+
return { convertiblePaths, nonConvertiblePaths }
220220
}
221221

222222
[util.inspect.custom]() {

0 commit comments

Comments
 (0)