Skip to content

Commit 430836f

Browse files
authored
Ensure layer(…) on @import is only removed when @utility is present (tailwindlabs#14783)
This PR fixes an issue where `layer(…)` next to imports were removed where they shouldn't have been removed. The issue exists if _any_ of the `@import` nodes in a file contains `@utility`, if that's the case then we removed the `layer(…)` next to _all_ `@import` nodes. Before we were checking if the current sheet contained `@utility` or in any of its children (sub-`@import` nodes). This fixes that by looping over the `@import` nodes in the current sheet, and looking for the `@utility` in the associated/imported file. This way we update each node individually. Test plan: --- Added a dedicated integration test to make sure all codemods together result in the correct result. Input: https://github.com/tailwindlabs/tailwindcss/blob/96e890837809487fecdd713695294cb9961cc1d6/integrations/upgrade/index.test.ts#L2076-L2108 Output: https://github.com/tailwindlabs/tailwindcss/blob/96e890837809487fecdd713695294cb9961cc1d6/integrations/upgrade/index.test.ts#L2116-L2126
1 parent 148d870 commit 430836f

File tree

5 files changed

+144
-39
lines changed

5 files changed

+144
-39
lines changed

CHANGELOG.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added `not-*` versions of all builtin media query and supports variants ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743))
1313
- Improved support for custom variants with `group-*`, `peer-*`, `has-*`, and `not-*` ([#14743](https://github.com/tailwindlabs/tailwindcss/pull/14743))
1414

15-
### Changed
16-
17-
- Don't convert underscores in the first argument to `var()` and `theme()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776), [#14781](https://github.com/tailwindlabs/tailwindcss/pull/14781))
18-
1915
### Fixed
2016

2117
- Ensure individual logical property utilities are sorted later than left/right pair utilities ([#14777](https://github.com/tailwindlabs/tailwindcss/pull/14777))
@@ -26,6 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2622
- _Upgrade (experimental)_: Ensure `@import` statements for relative CSS files are actually migrated to use relative path syntax ([#14769](https://github.com/tailwindlabs/tailwindcss/pull/14769))
2723
- _Upgrade (experimental)_: Only generate Preflight compatibility styles when Preflight is used ([#14773](https://github.com/tailwindlabs/tailwindcss/pull/14773))
2824
- _Upgrade (experimental)_: Don't escape underscores when printing theme values migrated to CSS variables in arbitrary values (e.g. `m-[var(--spacing-1_5)]` instead of `m-[var(--spacing-1\_5)]`) ([#14778](https://github.com/tailwindlabs/tailwindcss/pull/14778))
25+
- _Upgrade (experimental)_: Ensure `layer(…)` on `@import` is only removed when `@utility` is present ([#14783](https://github.com/tailwindlabs/tailwindcss/pull/14783))
26+
27+
### Changed
28+
29+
- Don't convert underscores in the first argument to `var()` and `theme()` to spaces ([#14776](https://github.com/tailwindlabs/tailwindcss/pull/14776), [#14781](https://github.com/tailwindlabs/tailwindcss/pull/14781))
2930

3031
## [4.0.0-alpha.29] - 2024-10-23
3132

integrations/upgrade/index.test.ts

+85-1
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ test(
10241024
@import './a.1.css' layer(utilities);
10251025
@import './a.1.utilities.1.css';
10261026
@import './b.1.css';
1027-
@import './c.1.css';
1027+
@import './c.1.css' layer(utilities);
10281028
@import './c.1.utilities.css';
10291029
@import './d.1.css';
10301030
@@ -1545,3 +1545,87 @@ test(
15451545
`)
15461546
},
15471547
)
1548+
1549+
test(
1550+
'that it attaches the correct layers to the imported files',
1551+
{
1552+
fs: {
1553+
'package.json': json`
1554+
{
1555+
"dependencies": {
1556+
"tailwindcss": "workspace:^",
1557+
"@tailwindcss/upgrade": "workspace:^"
1558+
}
1559+
}
1560+
`,
1561+
'tailwind.config.js': js`module.exports = {}`,
1562+
'src/index.css': css`
1563+
@import 'tailwindcss/utilities';
1564+
1565+
/* No layer expected */
1566+
@import './my-components.css';
1567+
1568+
/* No layer expected */
1569+
@import './my-utilities.css';
1570+
1571+
/* Expecting a layer */
1572+
@import './my-other.css';
1573+
1574+
@import 'tailwindcss/components';
1575+
`,
1576+
'src/my-components.css': css`
1577+
@layer components {
1578+
.foo {
1579+
color: red;
1580+
}
1581+
}
1582+
`,
1583+
'src/my-utilities.css': css`
1584+
@layer utilities {
1585+
.css {
1586+
color: red;
1587+
}
1588+
}
1589+
`,
1590+
'src/my-other.css': css`
1591+
/* All my fonts! */
1592+
@font-face {
1593+
}
1594+
`,
1595+
},
1596+
},
1597+
async ({ fs, exec }) => {
1598+
await exec('npx @tailwindcss/upgrade --force')
1599+
1600+
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
1601+
"
1602+
--- ./src/index.css ---
1603+
@import 'tailwindcss/utilities' layer(utilities);
1604+
1605+
/* No layer expected */
1606+
@import './my-components.css';
1607+
1608+
/* No layer expected */
1609+
@import './my-utilities.css';
1610+
1611+
/* Expecting a layer */
1612+
@import './my-other.css' layer(utilities);
1613+
1614+
--- ./src/my-components.css ---
1615+
@utility foo {
1616+
color: red;
1617+
}
1618+
1619+
--- ./src/my-other.css ---
1620+
/* All my fonts! */
1621+
@font-face {
1622+
}
1623+
1624+
--- ./src/my-utilities.css ---
1625+
@utility css {
1626+
color: red;
1627+
}
1628+
"
1629+
`)
1630+
},
1631+
)

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

+16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ it('should not migrate already migrated `@import` at-rules', async () => {
2222
).toMatchInlineSnapshot(`"@import 'tailwindcss';"`)
2323
})
2424

25+
it('should add missing `layer(…)` to imported files', async () => {
26+
expect(
27+
await migrate(css`
28+
@import 'tailwindcss/utilities'; /* Expected no layer */
29+
@import './foo.css'; /* Expected layer(utilities) */
30+
@import './bar.css'; /* Expected layer(utilities) */
31+
@import 'tailwindcss/components'; /* Expected no layer */
32+
`),
33+
).toMatchInlineSnapshot(`
34+
"@import 'tailwindcss/utilities'; /* Expected no layer */
35+
@import './foo.css' layer(utilities); /* Expected layer(utilities) */
36+
@import './bar.css' layer(utilities); /* Expected layer(utilities) */
37+
@import 'tailwindcss/components'; /* Expected no layer */"
38+
`)
39+
})
40+
2541
it('should not migrate anything if no `@tailwind` directives (or imports) are found', async () => {
2642
expect(
2743
await migrate(css`

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

+15-34
Original file line numberDiff line numberDiff line change
@@ -151,42 +151,23 @@ async function run() {
151151

152152
// Cleanup `@import "…" layer(utilities)`
153153
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-
}
154+
for (let importRule of sheet.importRules) {
155+
if (!importRule.raws.tailwind_injected_layer) continue
156+
let importedSheet = stylesheets.find(
157+
(sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id,
158+
)
159+
if (!importedSheet) continue
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+
if (
164+
!importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility')
165+
) {
166+
continue
180167
}
181-
}
182168

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-
}
169+
// Make sure to remove the `layer(…)` from the `@import` at-rule
170+
importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim()
190171
}
191172
}
192173

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

+23
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,29 @@ export class Stylesheet {
219219
return { convertiblePaths, nonConvertiblePaths }
220220
}
221221

222+
containsRule(cb: (rule: postcss.AnyNode) => boolean) {
223+
let contains = false
224+
225+
this.root.walk((rule) => {
226+
if (cb(rule)) {
227+
contains = true
228+
return false
229+
}
230+
})
231+
232+
if (contains) {
233+
return true
234+
}
235+
236+
for (let child of this.children) {
237+
if (child.item.containsRule(cb)) {
238+
return true
239+
}
240+
}
241+
242+
return false
243+
}
244+
222245
[util.inspect.custom]() {
223246
return {
224247
...this,

0 commit comments

Comments
 (0)