Skip to content

Commit 6e9b2d7

Browse files
committed
Migrate utilities in layered stylesheets
When a stylesheet is imported with `@import “…” layer(utilities)` that means that all classes in that stylesheet and any of its imported stylesheets become candidates for `@utility` conversion
1 parent 5fbcf0c commit 6e9b2d7

File tree

5 files changed

+282
-6
lines changed

5 files changed

+282
-6
lines changed

packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts

Lines changed: 228 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,28 @@ import { migrateAtLayerUtilities } from './migrate-at-layer-utilities'
77

88
const css = dedent
99

10-
async function migrate(data: string) {
10+
async function migrate(
11+
data:
12+
| string
13+
| {
14+
root: postcss.Root
15+
layers?: string[]
16+
},
17+
) {
1118
let stylesheet: Stylesheet
1219

13-
stylesheet = await Stylesheet.fromString(data)
20+
if (typeof data === 'string') {
21+
stylesheet = await Stylesheet.fromString(data)
22+
} else {
23+
stylesheet = await Stylesheet.fromRoot(data.root)
24+
25+
for (let layer of data.layers ?? []) {
26+
stylesheet.layers.add(layer)
27+
}
28+
}
1429

1530
return postcss()
16-
.use(migrateAtLayerUtilities())
31+
.use(migrateAtLayerUtilities(stylesheet))
1732
.use(formatNodes())
1833
.process(stylesheet.root!, { from: expect.getState().testPath })
1934
.then((result) => result.css)
@@ -825,3 +840,213 @@ it('should not lose attribute selectors', async () => {
825840
}"
826841
`)
827842
})
843+
844+
describe('layered stylesheets', () => {
845+
it('should transform classes to utilities inside a layered stylesheet (utilities)', async () => {
846+
expect(
847+
await migrate({
848+
root: postcss.parse(css`
849+
/* Utility #1 */
850+
.foo {
851+
/* Declarations: */
852+
color: red;
853+
}
854+
`),
855+
layers: ['utilities'],
856+
}),
857+
).toMatchInlineSnapshot(`
858+
"@utility foo {
859+
/* Utility #1 */
860+
/* Declarations: */
861+
color: red;
862+
}"
863+
`)
864+
})
865+
866+
it('should transform classes to utilities inside a layered stylesheet (components)', async () => {
867+
expect(
868+
await migrate({
869+
root: postcss.parse(css`
870+
/* Utility #1 */
871+
.foo {
872+
/* Declarations: */
873+
color: red;
874+
}
875+
`),
876+
layers: ['components'],
877+
}),
878+
).toMatchInlineSnapshot(`
879+
"@utility foo {
880+
/* Utility #1 */
881+
/* Declarations: */
882+
color: red;
883+
}"
884+
`)
885+
})
886+
887+
it('should NOT transform classes to utilities inside a non-utility, layered stylesheet', async () => {
888+
expect(
889+
await migrate({
890+
root: postcss.parse(css`
891+
/* Utility #1 */
892+
.foo {
893+
/* Declarations: */
894+
color: red;
895+
}
896+
`),
897+
layers: ['foo'],
898+
}),
899+
).toMatchInlineSnapshot(`
900+
"/* Utility #1 */
901+
.foo {
902+
/* Declarations: */
903+
color: red;
904+
}"
905+
`)
906+
})
907+
908+
it('should handle non-classes in utility-layered stylesheets', async () => {
909+
expect(
910+
await migrate({
911+
root: postcss.parse(css`
912+
/* Utility #1 */
913+
.foo {
914+
/* Declarations: */
915+
color: red;
916+
}
917+
#main {
918+
color: red;
919+
}
920+
`),
921+
layers: ['utilities'],
922+
}),
923+
).toMatchInlineSnapshot(`
924+
"
925+
#main {
926+
color: red;
927+
}
928+
929+
@utility foo {
930+
/* Utility #1 */
931+
/* Declarations: */
932+
color: red;
933+
}"
934+
`)
935+
})
936+
937+
it('should handle non-classes in utility-layered stylesheets', async () => {
938+
expect(
939+
await migrate({
940+
root: postcss.parse(css`
941+
@layer utilities {
942+
@layer utilities {
943+
/* Utility #1 */
944+
.foo {
945+
/* Declarations: */
946+
color: red;
947+
}
948+
}
949+
950+
/* Utility #2 */
951+
.bar {
952+
/* Declarations: */
953+
color: red;
954+
}
955+
956+
#main {
957+
color: red;
958+
}
959+
}
960+
961+
/* Utility #3 */
962+
.baz {
963+
/* Declarations: */
964+
color: red;
965+
}
966+
967+
#secondary {
968+
color: red;
969+
}
970+
`),
971+
layers: ['utilities'],
972+
}),
973+
).toMatchInlineSnapshot(`
974+
"@layer utilities {
975+
976+
#main {
977+
color: red;
978+
}
979+
}
980+
981+
#secondary {
982+
color: red;
983+
}
984+
985+
@utility foo {
986+
@layer utilities {
987+
@layer utilities {
988+
/* Utility #1 */
989+
/* Declarations: */
990+
color: red;
991+
}
992+
}
993+
}
994+
995+
@utility bar {
996+
@layer utilities {
997+
/* Utility #2 */
998+
/* Declarations: */
999+
color: red;
1000+
}
1001+
}
1002+
1003+
@utility baz {
1004+
/* Utility #3 */
1005+
/* Declarations: */
1006+
color: red;
1007+
}"
1008+
`)
1009+
})
1010+
1011+
it('imports are preserved in layered stylesheets', async () => {
1012+
expect(
1013+
await migrate({
1014+
root: postcss.parse(css`
1015+
@import 'thing';
1016+
1017+
.foo {
1018+
color: red;
1019+
}
1020+
`),
1021+
layers: ['utilities'],
1022+
}),
1023+
).toMatchInlineSnapshot(`
1024+
"@import 'thing';
1025+
1026+
@utility foo {
1027+
color: red;
1028+
}"
1029+
`)
1030+
})
1031+
1032+
it('charset is preserved in layered stylesheets', async () => {
1033+
expect(
1034+
await migrate({
1035+
root: postcss.parse(css`
1036+
@charset "utf-8";
1037+
1038+
.foo {
1039+
color: red;
1040+
}
1041+
`),
1042+
layers: ['utilities'],
1043+
}),
1044+
).toMatchInlineSnapshot(`
1045+
"@charset "utf-8";
1046+
1047+
@utility foo {
1048+
color: red;
1049+
}"
1050+
`)
1051+
})
1052+
})

packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss'
22
import SelectorParser from 'postcss-selector-parser'
33
import { segment } from '../../../tailwindcss/src/utils/segment'
4+
import { Stylesheet } from '../stylesheet'
45
import { walk, WalkAction, walkDepth } from '../utils/walk'
56

6-
export function migrateAtLayerUtilities(): Plugin {
7+
export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
78
function migrate(atRule: AtRule) {
89
// Only migrate `@layer utilities` and `@layer components`.
910
if (atRule.params !== 'utilities' && atRule.params !== 'components') return
@@ -86,6 +87,12 @@ export function migrateAtLayerUtilities(): Plugin {
8687
clones.push(clone)
8788

8889
walk(clone, (node) => {
90+
if (node.type === 'atrule') {
91+
if (!node.nodes || node.nodes?.length === 0) {
92+
node.remove()
93+
}
94+
}
95+
8996
if (node.type !== 'rule') return
9097

9198
// Fan out each utility into its own rule.
@@ -259,7 +266,16 @@ export function migrateAtLayerUtilities(): Plugin {
259266

260267
return {
261268
postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities',
262-
OnceExit: (root) => {
269+
OnceExit: (root, { atRule }) => {
270+
let isUtilityStylesheet =
271+
stylesheet.layers?.has('utilities') || stylesheet.layers?.has('components')
272+
273+
if (isUtilityStylesheet) {
274+
let rule = atRule({ name: 'layer', params: 'utilities' })
275+
rule.append(root.nodes)
276+
root.append(rule)
277+
}
278+
263279
// Migrate `@layer utilities` and `@layer components` into `@utility`.
264280
// Using this instead of the visitor API in case we want to use
265281
// postcss-nesting in the future.
@@ -282,6 +298,17 @@ export function migrateAtLayerUtilities(): Plugin {
282298
}
283299
})
284300
}
301+
302+
// If the stylesheet is inside a layered import then we can remove the top-level layer directive we added
303+
if (isUtilityStylesheet) {
304+
root.each((node) => {
305+
if (node.type !== 'atrule') return
306+
if (node.name !== 'layer') return
307+
if (node.params !== 'utilities') return
308+
309+
node.replaceWith(node.nodes ?? [])
310+
})
311+
}
285312
},
286313
}
287314
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export function migrateMissingLayers(): Plugin {
7171
if (node.name === 'import') {
7272
if (lastLayer !== '' && !node.params.includes('layer(')) {
7373
node.params += ` layer(${lastLayer})`
74+
node.raws.tailwind_injected_layer = true
7475
}
7576

7677
if (bucket.length > 0) {

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'node:path'
22
import postcss from 'postcss'
33
import type { Config } from 'tailwindcss'
44
import type { DesignSystem } from '../../tailwindcss/src/design-system'
5+
import { segment } from '../../tailwindcss/src/utils/segment'
56
import { migrateAtApply } from './codemods/migrate-at-apply'
67
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
78
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
@@ -27,7 +28,7 @@ export async function migrateContents(
2728

2829
return postcss()
2930
.use(migrateAtApply(options))
30-
.use(migrateAtLayerUtilities())
31+
.use(migrateAtLayerUtilities(stylesheet))
3132
.use(migrateMissingLayers())
3233
.use(migrateTailwindDirectives(options))
3334
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
@@ -94,6 +95,13 @@ export async function analyze(stylesheets: Stylesheet[]) {
9495
stylesheet.parents.add(parent)
9596
parent.children.add(stylesheet)
9697
}
98+
99+
for (let part of segment(node.params, ' ')) {
100+
if (!part.startsWith('layer(')) continue
101+
if (!part.endsWith(')')) continue
102+
103+
stylesheet.layers.add(part.slice(6, -1).trim())
104+
}
97105
},
98106
},
99107
},
@@ -104,4 +112,13 @@ export async function analyze(stylesheets: Stylesheet[]) {
104112

105113
await processor.process(sheet.root, { from: sheet.file })
106114
}
115+
116+
// Step 2: Analyze the AST so each stylesheet can know what layers it is inside
117+
for (let sheet of stylesheets) {
118+
for (let ancestor of sheet.ancestors) {
119+
for (let layer of ancestor.layers) {
120+
sheet.layers.add(layer)
121+
}
122+
}
123+
}
107124
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export class Stylesheet {
3535
*/
3636
children = new Set<Stylesheet>()
3737

38+
/**
39+
* The layers this stylesheet is in, even transitive layers from parents.
40+
*/
41+
layers = new Set<string>()
42+
3843
static async load(filepath: string) {
3944
filepath = path.resolve(process.cwd(), filepath)
4045

@@ -72,6 +77,7 @@ export class Stylesheet {
7277
return {
7378
...this,
7479
root: this.root.toString(),
80+
layers: Array.from(this.layers),
7581
parents: Array.from(this.parents, (s) => s.id),
7682
children: Array.from(this.children, (s) => s.id),
7783
}

0 commit comments

Comments
 (0)