Skip to content

Commit 3bd0221

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 5191a76 commit 3bd0221

File tree

5 files changed

+303
-8
lines changed

5 files changed

+303
-8
lines changed

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

Lines changed: 232 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,32 @@ 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+
if (data.layers) {
26+
let meta = { layers: data.layers }
27+
let parent = await Stylesheet.fromString('.placeholder {}')
28+
29+
stylesheet.parents.add({ item: parent, meta })
30+
parent.children.add({ item: stylesheet, meta })
31+
}
32+
}
1433

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

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 layers = stylesheet.layers()
271+
let isUtilityStylesheet = layers.has('utilities') || 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: 14 additions & 3 deletions
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 { migrateMediaScreen } from './codemods/migrate-media-screen'
@@ -29,7 +30,7 @@ export async function migrateContents(
2930
return postcss()
3031
.use(migrateAtApply(options))
3132
.use(migrateMediaScreen(options))
32-
.use(migrateAtLayerUtilities())
33+
.use(migrateAtLayerUtilities(stylesheet))
3334
.use(migrateMissingLayers())
3435
.use(migrateTailwindDirectives(options))
3536
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
@@ -91,10 +92,20 @@ export async function analyze(stylesheets: Stylesheet[]) {
9192
? stylesheetsByFile.get(node.source.input.file)
9293
: undefined
9394

95+
let layers: string[] = []
96+
97+
for (let part of segment(node.params, ' ')) {
98+
if (!part.startsWith('layer(')) continue
99+
if (!part.endsWith(')')) continue
100+
101+
layers.push(part.slice(6, -1).trim())
102+
}
103+
94104
// Connect sheets together in a dependency graph
95105
if (parent) {
96-
stylesheet.parents.add(parent)
97-
parent.children.add(stylesheet)
106+
let meta = { layers }
107+
stylesheet.parents.add({ item: parent, meta })
108+
parent.children.add({ item: stylesheet, meta })
98109
}
99110
},
100111
},

0 commit comments

Comments
 (0)