Add CSS codemods for migrating @tailwind directives (#14411)
This PR adds CSS codemods for migrating existing `@tailwind` directives to the new alternatives. This PR has the ability to migrate the following cases: --- Typical default usage of `@tailwind` directives in v3. Input: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` Output: ```css @import 'tailwindcss'; ``` --- Similar as above, but always using `@import` instead of `@import` directly. Input: ```css @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; ``` Output: ```css @import 'tailwindcss'; ``` --- When you are _only_ using `@tailwind base`: Input: ```css @tailwind base; ``` Output: ```css @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/preflight' layer(base); ``` --- When you are _only_ using `@tailwind utilities`: Input: ```css @tailwind utilities; ``` Output: ```css @import 'tailwindcss/utilities' layer(utilities); ``` --- If the default order changes (aka, `@tailwind utilities` was defined _before_ `@tailwind base`), then an additional `@layer` will be added to the top to re-define the default order. Input: ```css @tailwind utilities; @tailwind base; ``` Output: ```css @layer theme, components, utilities, base; @import 'tailwindcss'; ``` --- When you are _only_ using `@tailwind base; @tailwind utilities;`: Input: ```css @tailwind base; @tailwind utilities; ``` Output: ```css @import 'tailwindcss'; ``` We currently don't have a concept of `@tailwind components` in v4, so if you are not using `@tailwind components`, we can expand to the default `@import 'tailwindcss';` instead of the individual imports. --- `@tailwind screens` and `@tailwind variants` are not supported/necessary in v4, so we can safely remove them. Input: ```css @tailwind screens; @tailwind variants; ``` Output: ```css ```
This commit is contained in:
parent
2ddb715abd
commit
67d1849f34
5 changed files with 334 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Add support for `aria`, `supports`, and `data` variants defined in JS config files ([#14407](https://github.com/tailwindlabs/tailwindcss/pull/14407))
|
||||
- Add `@tailwindcss/upgrade` tooling ([#14434](https://github.com/tailwindlabs/tailwindcss/pull/14434))
|
||||
- Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411))
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
|||
|
|
@ -50,3 +50,29 @@ test(
|
|||
)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrate @tailwind directives',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css` @import 'tailwindcss'; `)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
import dedent from 'dedent'
|
||||
import postcss from 'postcss'
|
||||
import { expect, it } from 'vitest'
|
||||
import { migrateTailwindDirectives } from './migrate-tailwind-directives'
|
||||
|
||||
const css = dedent
|
||||
|
||||
function migrate(input: string) {
|
||||
return postcss()
|
||||
.use(migrateTailwindDirectives())
|
||||
.process(input, { from: expect.getState().testPath })
|
||||
.then((result) => result.css)
|
||||
}
|
||||
|
||||
it("should not migrate `@import 'tailwindcss'`", async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@import 'tailwindcss';
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss';
|
||||
`)
|
||||
})
|
||||
|
||||
it('should migrate the default @tailwind directives to a single import', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss';
|
||||
`)
|
||||
})
|
||||
|
||||
it('should migrate the default @tailwind directives as imports to a single import', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss';
|
||||
`)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
// The default order
|
||||
css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
css`
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
|
||||
// @tailwind components moved, but has no effect in v4. Therefore `base` and
|
||||
// `utilities` are still in the correct order.
|
||||
[
|
||||
css`
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
@tailwind components;
|
||||
`,
|
||||
css`
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
|
||||
// Same as previous comment
|
||||
[
|
||||
css`
|
||||
@tailwind components;
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
css`
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
|
||||
// `base` and `utilities` swapped order, thus the `@layer` directives are
|
||||
// needed. The `components` directive is still ignored.
|
||||
[
|
||||
css`
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind base;
|
||||
`,
|
||||
css`
|
||||
@layer theme, components, utilities, base;
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
[
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
`,
|
||||
css`
|
||||
@layer theme, components, utilities, base;
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
[
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@tailwind components;
|
||||
@tailwind base;
|
||||
`,
|
||||
css`
|
||||
@layer theme, components, utilities, base;
|
||||
@import 'tailwindcss';
|
||||
`,
|
||||
],
|
||||
])(
|
||||
'should migrate the default directives (but in different order) to a single import, order %#',
|
||||
async (input, expected) => {
|
||||
expect(await migrate(input)).toEqual(expected)
|
||||
},
|
||||
)
|
||||
|
||||
it('should migrate `@tailwind base` to theme and preflight imports', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind base;
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
`)
|
||||
})
|
||||
|
||||
it('should migrate `@import "tailwindcss/base"` to theme and preflight imports', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@import 'tailwindcss/base';
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
`)
|
||||
})
|
||||
|
||||
it('should migrate `@tailwind utilities` to an import', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind utilities;
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
`)
|
||||
})
|
||||
|
||||
it('should migrate `@import "tailwindcss/utilities"` to an import', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@import 'tailwindcss/utilities';
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss/utilities' layer(utilities);
|
||||
`)
|
||||
})
|
||||
|
||||
it('should not migrate existing imports using a custom layer', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@import 'tailwindcss/utilities' layer(my-utilities);
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss/utilities' layer(my-utilities);
|
||||
`)
|
||||
})
|
||||
|
||||
// We don't have a `@layer components` anymore, so omitting it should result
|
||||
// in the full import as well. Alternatively, we could expand to:
|
||||
//
|
||||
// ```css
|
||||
// @import 'tailwindcss/theme' layer(theme);
|
||||
// @import 'tailwindcss/preflight' layer(base);
|
||||
// @import 'tailwindcss/utilities' layer(utilities);
|
||||
// ```
|
||||
it('should migrate `@tailwind base` and `@tailwind utilities` to a single import', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
`),
|
||||
).toEqual(css`
|
||||
@import 'tailwindcss';
|
||||
`)
|
||||
})
|
||||
|
||||
it('should drop `@tailwind screens;`', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind screens;
|
||||
`),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
it('should drop `@tailwind variants;`', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@tailwind variants;
|
||||
`),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { AtRule, type Plugin, type Root } from 'postcss'
|
||||
|
||||
const DEFAULT_LAYER_ORDER = ['theme', 'base', 'components', 'utilities']
|
||||
|
||||
export function migrateTailwindDirectives(): Plugin {
|
||||
function migrate(root: Root) {
|
||||
let baseNode: AtRule | null = null
|
||||
let utilitiesNode: AtRule | null = null
|
||||
|
||||
let defaultImportNode: AtRule | null = null
|
||||
let utilitiesImportNode: AtRule | null = null
|
||||
let preflightImportNode: AtRule | null = null
|
||||
let themeImportNode: AtRule | null = null
|
||||
|
||||
let layerOrder: string[] = []
|
||||
|
||||
root.walkAtRules((node) => {
|
||||
// Track old imports and directives
|
||||
if (
|
||||
(node.name === 'tailwind' && node.params === 'base') ||
|
||||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']$/))
|
||||
) {
|
||||
layerOrder.push('base')
|
||||
baseNode = node
|
||||
node.remove()
|
||||
} else if (
|
||||
(node.name === 'tailwind' && node.params === 'utilities') ||
|
||||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']$/))
|
||||
) {
|
||||
layerOrder.push('utilities')
|
||||
utilitiesNode = node
|
||||
node.remove()
|
||||
}
|
||||
|
||||
// Remove directives that are not needed anymore
|
||||
else if (
|
||||
(node.name === 'tailwind' && node.params === 'components') ||
|
||||
(node.name === 'tailwind' && node.params === 'screens') ||
|
||||
(node.name === 'tailwind' && node.params === 'variants') ||
|
||||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/components["']$/))
|
||||
) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// Insert default import if all directives are present
|
||||
if (baseNode !== null && utilitiesNode !== null) {
|
||||
if (!defaultImportNode) {
|
||||
root.prepend(new AtRule({ name: 'import', params: "'tailwindcss'" }))
|
||||
}
|
||||
}
|
||||
|
||||
// Insert individual imports if not all directives are present
|
||||
else if (utilitiesNode !== null) {
|
||||
if (!utilitiesImportNode) {
|
||||
root.prepend(
|
||||
new AtRule({ name: 'import', params: "'tailwindcss/utilities' layer(utilities)" }),
|
||||
)
|
||||
}
|
||||
} else if (baseNode !== null) {
|
||||
if (!preflightImportNode) {
|
||||
root.prepend(new AtRule({ name: 'import', params: "'tailwindcss/preflight' layer(base)" }))
|
||||
}
|
||||
if (!themeImportNode) {
|
||||
root.prepend(new AtRule({ name: 'import', params: "'tailwindcss/theme' layer(theme)" }))
|
||||
}
|
||||
}
|
||||
|
||||
// Insert `@layer …;` at the top when the order in the CSS was different
|
||||
// from the default.
|
||||
{
|
||||
// Determine if the order is different from the default.
|
||||
let sortedLayerOrder = layerOrder.toSorted((a, z) => {
|
||||
return DEFAULT_LAYER_ORDER.indexOf(a) - DEFAULT_LAYER_ORDER.indexOf(z)
|
||||
})
|
||||
|
||||
if (layerOrder.some((layer, index) => layer !== sortedLayerOrder[index])) {
|
||||
// Create a new `@layer` rule with the sorted order.
|
||||
let newLayerOrder = DEFAULT_LAYER_ORDER.toSorted((a, z) => {
|
||||
return layerOrder.indexOf(a) - layerOrder.indexOf(z)
|
||||
})
|
||||
root.prepend({ name: 'layer', params: newLayerOrder.join(', ') })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
postcssPlugin: '@tailwindcss/upgrade/migrate-tailwind-directives',
|
||||
Once: migrate,
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ import fs from 'node:fs/promises'
|
|||
import path from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
import { migrateAtApply } from './codemods/migrate-at-apply'
|
||||
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
|
||||
|
||||
export async function migrateContents(contents: string, file?: string) {
|
||||
return postcss()
|
||||
.use(migrateAtApply())
|
||||
.use(migrateTailwindDirectives())
|
||||
.process(contents, { from: file })
|
||||
.then((result) => result.css)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue