Skip to content

Commit ee7e02b

Browse files
authored
Add initial codemod tooling (#14434)
This PR adds some initial tooling for codemods. We are currently only interested in migrating CSS files, so we will be using PostCSS under the hood to do this. This PR also implements the "migrate `@apply`" codemod from #14412. The usage will look like this: ```sh npx @tailwindcss/upgrade ``` You can pass in CSS files to transform as arguments: ```sh npx @tailwindcss/upgrade src/**/*.css ``` But, if none are provided, it will search for CSS files in the current directory and its subdirectories. ``` ≈ tailwindcss v4.0.0-alpha.24 │ No files provided. Searching for CSS files in the current │ directory and its subdirectories… │ Migration complete. Verify the changes and commit them to │ your repository. ``` The tooling also requires the Git repository to be in a clean state. This is a common convention to ensure that everything is undo-able. If we detect that the git repository is dirty, we will abort the migration. ``` ≈ tailwindcss v4.0.0-alpha.24 │ Git directory is not clean. Please stash or commit your │ changes before migrating. │ You may use the `--force` flag to override this safety │ check. ``` --- This PR alsoo adds CSS codemods for migrating existing `@apply` directives to the new version. This PR has the ability to migrate the following cases: --- In v4, the convention is to put the important modifier `!` at the end of the utility class instead of right before it. This makes it easier to reason about, especially when you are variants. Input: ```css .foo { @apply !flex flex-col! hover:!items-start items-center; } ``` Output: ```css .foo { @apply flex! flex-col! hover:items-start! items-center; } ``` --- In v4 we don't support `!important` as a marker at the end of `@apply` directives. Instead, you can append the `!` to each utility class to make it `!important`. Input: ```css .foo { @apply flex flex-col !important; } ``` Output: ```css .foo { @apply flex! flex-col!; } ```
1 parent a51b63a commit ee7e02b

File tree

20 files changed

+1153
-3
lines changed

20 files changed

+1153
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add support for `aria`, `supports`, and `data` variants defined in JS config files ([#14407](https://github.com/tailwindlabs/tailwindcss/pull/14407))
13+
- Add `@tailwindcss/upgrade` tooling ([#14434](https://github.com/tailwindlabs/tailwindcss/pull/14434))
1314

1415
### Fixed
1516

integrations/cli/upgrade.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { css, json, test } from '../utils'
2+
3+
test(
4+
'migrate @apply',
5+
{
6+
fs: {
7+
'package.json': json`
8+
{
9+
"dependencies": {
10+
"tailwindcss": "workspace:^",
11+
"@tailwindcss/upgrade": "workspace:^"
12+
}
13+
}
14+
`,
15+
'src/index.css': css`
16+
@import 'tailwindcss';
17+
18+
.a {
19+
@apply flex;
20+
}
21+
22+
.b {
23+
@apply !flex;
24+
}
25+
26+
.c {
27+
@apply !flex flex-col! items-center !important;
28+
}
29+
`,
30+
},
31+
},
32+
async ({ fs, exec }) => {
33+
await exec('npx @tailwindcss/upgrade')
34+
35+
await fs.expectFileToContain(
36+
'src/index.css',
37+
css`
38+
.a {
39+
@apply flex;
40+
}
41+
42+
.b {
43+
@apply flex!;
44+
}
45+
46+
.c {
47+
@apply flex! flex-col! items-center!;
48+
}
49+
`,
50+
)
51+
},
52+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<p align="center">
2+
<a href="https://tailwindcss.com" target="_blank">
3+
<picture>
4+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-dark.svg">
5+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg">
6+
<img alt="Tailwind CSS" src="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
7+
</picture>
8+
</a>
9+
</p>
10+
11+
<p align="center">
12+
A utility-first CSS framework for rapidly building custom user interfaces.
13+
</p>
14+
15+
<p align="center">
16+
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=next" alt="Build Status"></a>
17+
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
18+
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
19+
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
20+
</p>
21+
22+
---
23+
24+
## Documentation
25+
26+
For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
27+
28+
## Community
29+
30+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
31+
32+
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
33+
34+
For chatting with others using the framework:
35+
36+
[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
37+
38+
## Contributing
39+
40+
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@tailwindcss/upgrade",
3+
"version": "4.0.0-alpha.24",
4+
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/tailwindlabs/tailwindcss.git",
9+
"directory": "packages/@tailwindcss-cli"
10+
},
11+
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
12+
"homepage": "https://tailwindcss.com",
13+
"scripts": {
14+
"lint": "tsc --noEmit",
15+
"build": "tsup-node",
16+
"dev": "pnpm run build -- --watch"
17+
},
18+
"bin": "./dist/index.mjs",
19+
"exports": {
20+
"./package.json": "./package.json"
21+
},
22+
"files": [
23+
"dist"
24+
],
25+
"publishConfig": {
26+
"provenance": true,
27+
"access": "public"
28+
},
29+
"dependencies": {
30+
"enhanced-resolve": "^5.17.1",
31+
"globby": "^14.0.2",
32+
"mri": "^1.2.0",
33+
"picocolors": "^1.0.1",
34+
"postcss": "^8.4.41",
35+
"postcss-import": "^16.1.0",
36+
"tailwindcss": "workspace:^"
37+
},
38+
"devDependencies": {
39+
"@types/node": "catalog:",
40+
"@types/postcss-import": "^14.0.3",
41+
"dedent": "1.5.3"
42+
}
43+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import dedent from 'dedent'
2+
import postcss from 'postcss'
3+
import { expect, it } from 'vitest'
4+
import { migrateAtApply } from './migrate-at-apply'
5+
6+
const css = dedent
7+
8+
function migrate(input: string) {
9+
return postcss()
10+
.use(migrateAtApply())
11+
.process(input, { from: expect.getState().testPath })
12+
.then((result) => result.css)
13+
}
14+
15+
it('should not migrate `@apply`, when there are no issues', async () => {
16+
expect(
17+
await migrate(css`
18+
.foo {
19+
@apply flex flex-col items-center;
20+
}
21+
`),
22+
).toMatchInlineSnapshot(`
23+
".foo {
24+
@apply flex flex-col items-center;
25+
}"
26+
`)
27+
})
28+
29+
it('should append `!` to each utility, when using `!important`', async () => {
30+
expect(
31+
await migrate(css`
32+
.foo {
33+
@apply flex flex-col !important;
34+
}
35+
`),
36+
).toMatchInlineSnapshot(`
37+
".foo {
38+
@apply flex! flex-col!;
39+
}"
40+
`)
41+
})
42+
43+
// TODO: Handle SCSS syntax
44+
it.skip('should append `!` to each utility, when using `#{!important}`', async () => {
45+
expect(
46+
await migrate(css`
47+
.foo {
48+
@apply flex flex-col #{!important};
49+
}
50+
`),
51+
).toMatchInlineSnapshot(`
52+
".foo {
53+
@apply flex! flex-col!;
54+
}"
55+
`)
56+
})
57+
58+
it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => {
59+
expect(
60+
await migrate(css`
61+
.foo {
62+
@apply !flex flex-col! hover:!items-start items-center;
63+
}
64+
`),
65+
).toMatchInlineSnapshot(`
66+
".foo {
67+
@apply flex! flex-col! hover:items-start! items-center;
68+
}"
69+
`)
70+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { AtRule, Plugin } from 'postcss'
2+
import { segment } from '../../../tailwindcss/src/utils/segment'
3+
4+
export function migrateAtApply(): Plugin {
5+
function migrate(atRule: AtRule) {
6+
let utilities = atRule.params.split(/(\s+)/)
7+
let important =
8+
utilities[utilities.length - 1] === '!important' ||
9+
utilities[utilities.length - 1] === '#{!important}' // Sass/SCSS
10+
11+
if (important) utilities.pop() // Remove `!important`
12+
13+
let params = utilities.map((part) => {
14+
// Keep whitespace
15+
if (part.trim() === '') return part
16+
17+
let variants = segment(part, ':')
18+
let utility = variants.pop()!
19+
20+
// Apply the important modifier to all the rules if necessary
21+
if (important && utility[0] !== '!' && utility[utility.length - 1] !== '!') {
22+
utility += '!'
23+
}
24+
25+
// Migrate the important modifier to the end of the utility
26+
if (utility[0] === '!') {
27+
utility = `${utility.slice(1)}!`
28+
}
29+
30+
// Reconstruct the utility with the variants
31+
return [...variants, utility].join(':')
32+
})
33+
34+
atRule.params = params.join('').trim()
35+
}
36+
37+
return {
38+
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
39+
AtRule: {
40+
apply: migrate,
41+
},
42+
}
43+
}

0 commit comments

Comments
 (0)