Skip to content

Commit 65240c9

Browse files
Template migrations: Migrate v3 prefixes to v4 (#14557)
This PR adds a new migration that can migrate Tailwind CSS v3 style prefixes into Tailwind CSS v4. The migration is split into three separate pieces of work: 1. Firstly, we need to read the full JavaScript config to get the _old_ prefix option. This is necessary because in v4, we will not allow things like custom-separators for the prefix. From this option we will then try and compute a new prefix (in 90% of the cases this is going to just remove the trailing `-` but it can also work in more complex cases). 2. Then we migrate all Candidates. The important thing here is that we need to operate on the raw candidate string because by relying on `parseCandidate` (which we do for all other migrations) would not work, as the candidates are not valid in v4 syntax. More on that in a bit. 3. Lastly we also make sure to update the CSS config to include the new prefix. This is done by prepending the prefix option like so: ```css @import "tailwindcss" prefix(tw); ``` ### Migrating candidates The main difference between v3 prefixes and v4 prefixes is that in v3, the prefix was _part of the utility_ where as in v4 it is _always in front of the CSS class. So, for example, this candidate in v3: ``` hover:-tw-mr-4 ``` Would be converted to the following in v4: ``` tw:hover:-mr-4 ``` Since the first example _won't parse as a valid Candidate in v4, as the `tw-mr` utility does not exist, we have to operate on the raw candidate string first. To do this I created a fork of the `parseCandidate` function _without any validation of utilities or variants_. This is used to identify part of the candidate that is the `base` and then ensuring the `base` starts with the old prefix. We then remove this to create an "unprefixed" candidate that we validate against a version of the DesignSystem _with no prefixes configured_. If the variant is valid this way, we can then print it again with the `DesignSystem` that has the new prefix to get the migrated version. Since we set up the `DesignSystem` to include the new prefix, we can also be certain that migrations that happen afterwards would still disqualify candidates that aren't valid according to the new prefix policy. This does mean we need to have the prefix fixup be the first step in our pipeline. One interesting bit is that in v3, arbitrary properties did not require prefixes where as in v4 they do. So the following candidate: ``` [color:red] ``` Will be converted to: ``` tw:[color:red] ```
1 parent 3f85b74 commit 65240c9

23 files changed

+504
-86
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
1414
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
1515
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
16-
- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526))
1716
- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
17+
- _Experimental_: Add template codemods for migrating prefixes ([#14557](https://github.com/tailwindlabs/tailwindcss/pull/14557]))
18+
- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526))
1819
- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
1920

2021
### Fixed

integrations/upgrade/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,50 @@ test(
4343
},
4444
)
4545

46+
test(
47+
`upgrades a v3 project with prefixes to v4`,
48+
{
49+
fs: {
50+
'package.json': json`
51+
{
52+
"dependencies": {
53+
"@tailwindcss/upgrade": "workspace:^"
54+
}
55+
}
56+
`,
57+
'tailwind.config.js': js`
58+
/** @type {import('tailwindcss').Config} */
59+
module.exports = {
60+
content: ['./src/**/*.{html,js}'],
61+
prefix: 'tw__',
62+
}
63+
`,
64+
'src/index.html': html`
65+
<h1>🤠👋</h1>
66+
<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex [color:red]"></div>
67+
`,
68+
'src/input.css': css`
69+
@tailwind base;
70+
@tailwind components;
71+
@tailwind utilities;
72+
`,
73+
},
74+
},
75+
async ({ exec, fs }) => {
76+
await exec('npx @tailwindcss/upgrade -c tailwind.config.js')
77+
78+
await fs.expectFileToContain(
79+
'src/index.html',
80+
html`
81+
<h1>🤠👋</h1>
82+
<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red]"></div>
83+
`,
84+
)
85+
86+
await fs.expectFileToContain('src/input.css', css`@import 'tailwindcss' prefix(tw);`)
87+
},
88+
)
89+
4690
test(
4791
'migrate @apply',
4892
{

packages/@tailwindcss-node/src/compile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function __unstable__loadDesignSystem(css: string, { base }: { base
3737
})
3838
}
3939

40-
async function loadModule(id: string, base: string, onDependency: (path: string) => void) {
40+
export async function loadModule(id: string, base: string, onDependency: (path: string) => void) {
4141
if (id[0] !== '.') {
4242
let resolvedPath = await resolveJsId(id, base)
4343
if (!resolvedPath) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Module from 'node:module'
22
import { pathToFileURL } from 'node:url'
33
import * as env from './env'
4-
export * from './compile'
4+
export { __unstable__loadDesignSystem, compile } from './compile'
55
export * from './normalize-path'
66
export { env }
77

packages/@tailwindcss-upgrade/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@tailwindcss/oxide": "workspace:^",
3232
"enhanced-resolve": "^5.17.1",
3333
"globby": "^14.0.2",
34+
"jiti": "^2.0.0-beta.3",
3435
"mri": "^1.2.0",
3536
"picocolors": "^1.0.1",
3637
"postcss": "^8.4.41",

packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { migrateTailwindDirectives } from './migrate-tailwind-directives'
66

77
const css = dedent
88

9-
function migrate(input: string) {
9+
function migrate(input: string, options: { newPrefix?: string } = {}) {
1010
return postcss()
11-
.use(migrateTailwindDirectives())
11+
.use(migrateTailwindDirectives(options))
1212
.use(formatNodes())
1313
.process(input, { from: expect.getState().testPath })
1414
.then((result) => result.css)
@@ -24,6 +24,21 @@ it("should not migrate `@import 'tailwindcss'`", async () => {
2424
`)
2525
})
2626

27+
it("should append a prefix to `@import 'tailwindcss'`", async () => {
28+
expect(
29+
await migrate(
30+
css`
31+
@import 'tailwindcss';
32+
`,
33+
{
34+
newPrefix: 'tw',
35+
},
36+
),
37+
).toEqual(css`
38+
@import 'tailwindcss' prefix(tw);
39+
`)
40+
})
41+
2742
it('should migrate the tailwind.css import', async () => {
2843
expect(
2944
await migrate(css`
@@ -34,6 +49,21 @@ it('should migrate the tailwind.css import', async () => {
3449
`)
3550
})
3651

52+
it('should migrate the tailwind.css import with a prefix', async () => {
53+
expect(
54+
await migrate(
55+
css`
56+
@import 'tailwindcss/tailwind.css';
57+
`,
58+
{
59+
newPrefix: 'tw',
60+
},
61+
),
62+
).toEqual(css`
63+
@import 'tailwindcss' prefix(tw);
64+
`)
65+
})
66+
3767
it('should migrate the default @tailwind directives to a single import', async () => {
3868
expect(
3969
await migrate(css`
@@ -46,6 +76,23 @@ it('should migrate the default @tailwind directives to a single import', async (
4676
`)
4777
})
4878

79+
it('should migrate the default @tailwind directives to a single import with a prefix', async () => {
80+
expect(
81+
await migrate(
82+
css`
83+
@tailwind base;
84+
@tailwind components;
85+
@tailwind utilities;
86+
`,
87+
{
88+
newPrefix: 'tw',
89+
},
90+
),
91+
).toEqual(css`
92+
@import 'tailwindcss' prefix(tw);
93+
`)
94+
})
95+
4996
it('should migrate the default @tailwind directives as imports to a single import', async () => {
5097
expect(
5198
await migrate(css`
@@ -64,7 +111,7 @@ it('should migrate the default @tailwind directives to a single import in a vali
64111
@charset "UTF-8";
65112
@layer foo, bar, baz;
66113
67-
/**!
114+
/**!
68115
* License header
69116
*/
70117
@@ -84,7 +131,7 @@ it('should migrate the default @tailwind directives to a single import in a vali
84131
@charset "UTF-8";
85132
@layer foo, bar, baz;
86133
87-
/**!
134+
/**!
88135
* License header
89136
*/
90137
@@ -102,7 +149,7 @@ it('should migrate the default @tailwind directives as imports to a single impor
102149
@charset "UTF-8";
103150
@layer foo, bar, baz;
104151
105-
/**!
152+
/**!
106153
* License header
107154
*/
108155
@@ -114,14 +161,31 @@ it('should migrate the default @tailwind directives as imports to a single impor
114161
@charset "UTF-8";
115162
@layer foo, bar, baz;
116163
117-
/**!
164+
/**!
118165
* License header
119166
*/
120167
121168
@import 'tailwindcss';
122169
`)
123170
})
124171

172+
it('should migrate the default @tailwind directives as imports to a single import with a prefix', async () => {
173+
expect(
174+
await migrate(
175+
css`
176+
@import 'tailwindcss/base';
177+
@import 'tailwindcss/components';
178+
@import 'tailwindcss/utilities';
179+
`,
180+
{
181+
newPrefix: 'tw',
182+
},
183+
),
184+
).toEqual(css`
185+
@import 'tailwindcss' prefix(tw);
186+
`)
187+
})
188+
125189
it.each([
126190
[
127191
// The default order
@@ -213,6 +277,22 @@ it('should migrate `@tailwind base` to theme and preflight imports', async () =>
213277
`)
214278
})
215279

280+
it('should migrate `@tailwind base` to theme and preflight imports with a prefix', async () => {
281+
expect(
282+
await migrate(
283+
css`
284+
@tailwind base;
285+
`,
286+
{
287+
newPrefix: 'tw',
288+
},
289+
),
290+
).toEqual(css`
291+
@import 'tailwindcss/theme' layer(theme) prefix(tw);
292+
@import 'tailwindcss/preflight' layer(base);
293+
`)
294+
})
295+
216296
it('should migrate `@import "tailwindcss/base"` to theme and preflight imports', async () => {
217297
expect(
218298
await migrate(css`
@@ -224,6 +304,22 @@ it('should migrate `@import "tailwindcss/base"` to theme and preflight imports',
224304
`)
225305
})
226306

307+
it('should migrate `@import "tailwindcss/base"` to theme and preflight imports with a prefix', async () => {
308+
expect(
309+
await migrate(
310+
css`
311+
@import 'tailwindcss/base';
312+
`,
313+
{
314+
newPrefix: 'tw',
315+
},
316+
),
317+
).toEqual(css`
318+
@import 'tailwindcss/theme' layer(theme) prefix(tw);
319+
@import 'tailwindcss/preflight' layer(base);
320+
`)
321+
})
322+
227323
it('should migrate `@tailwind utilities` to an import', async () => {
228324
expect(
229325
await migrate(css`
@@ -273,6 +369,22 @@ it('should migrate `@tailwind base` and `@tailwind utilities` to a single import
273369
`)
274370
})
275371

372+
it('should migrate `@tailwind base` and `@tailwind utilities` to a single import with a prefix', async () => {
373+
expect(
374+
await migrate(
375+
css`
376+
@import 'tailwindcss/base';
377+
@import 'tailwindcss/utilities';
378+
`,
379+
{
380+
newPrefix: 'tw',
381+
},
382+
),
383+
).toEqual(css`
384+
@import 'tailwindcss' prefix(tw);
385+
`)
386+
})
387+
276388
it('should drop `@tailwind screens;`', async () => {
277389
expect(
278390
await migrate(css`

packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss'
22

33
const DEFAULT_LAYER_ORDER = ['theme', 'base', 'components', 'utilities']
44

5-
export function migrateTailwindDirectives(): Plugin {
5+
export function migrateTailwindDirectives(options: { newPrefix?: string }): Plugin {
6+
let prefixParams = options.newPrefix ? ` prefix(${options.newPrefix})` : ''
7+
68
function migrate(root: Root) {
79
let baseNode = null as AtRule | null
810
let utilitiesNode = null as AtRule | null
@@ -21,6 +23,11 @@ export function migrateTailwindDirectives(): Plugin {
2123
node.params = node.params.replace('tailwindcss/tailwind.css', 'tailwindcss')
2224
}
2325

26+
// Append any new prefix() param to existing `@import 'tailwindcss'` directives
27+
if (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) {
28+
node.params += prefixParams
29+
}
30+
2431
// Track old imports and directives
2532
else if (
2633
(node.name === 'tailwind' && node.params === 'base') ||
@@ -52,7 +59,9 @@ export function migrateTailwindDirectives(): Plugin {
5259
// Insert default import if all directives are present
5360
if (baseNode !== null && utilitiesNode !== null) {
5461
if (!defaultImportNode) {
55-
findTargetNode(orderedNodes).before(new AtRule({ name: 'import', params: "'tailwindcss'" }))
62+
findTargetNode(orderedNodes).before(
63+
new AtRule({ name: 'import', params: `'tailwindcss'${prefixParams}` }),
64+
)
5665
}
5766
baseNode?.remove()
5867
utilitiesNode?.remove()
@@ -69,7 +78,7 @@ export function migrateTailwindDirectives(): Plugin {
6978
} else if (baseNode !== null) {
7079
if (!themeImportNode) {
7180
findTargetNode(orderedNodes).before(
72-
new AtRule({ name: 'import', params: "'tailwindcss/theme' layer(theme)" }),
81+
new AtRule({ name: 'import', params: `'tailwindcss/theme' layer(theme)${prefixParams}` }),
7382
)
7483
}
7584

0 commit comments

Comments
 (0)