Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSS codemod for missing @layer #14504

Merged
merged 13 commits into from
Sep 24, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add new `shadow-initial` and `inset-shadow-initial` utilities for resetting shadow colors ([#14468](https://github.com/tailwindlabs/tailwindcss/pull/14468))
- Add `field-sizing-*` utilities ([#14469](https://github.com/tailwindlabs/tailwindcss/pull/14469))
- Include gradient color properties in color transitions ([#14489](https://github.com/tailwindlabs/tailwindcss/pull/14489))
- _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411))
- _Experimental_: Add CSS codemods for `@apply` ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14434))
- _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411), [#14504](https://github.com/tailwindlabs/tailwindcss/pull/14504))
- _Experimental_: Add CSS codemods for migrating `@layer utilities` and `@layer components` ([#14455](https://github.com/tailwindlabs/tailwindcss/pull/14455))

### Fixed
Expand Down
32 changes: 31 additions & 1 deletion integrations/cli/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,45 @@ test(
`,
'src/index.css': css`
@tailwind base;

html {
color: #333;
}

@tailwind components;

.btn {
color: red;
}

@tailwind utilities;
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')

await fs.expectFileToContain('src/index.css', css` @import 'tailwindcss'; `)
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
await fs.expectFileToContain(
'src/index.css',
css`
@layer base {
html {
color: #333;
}
}
`,
)
await fs.expectFileToContain(
'src/index.css',
css`
@layer components {
.btn {
color: red;
}
}
`,
)
},
)

Expand Down
35 changes: 35 additions & 0 deletions packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import postcss, { type Plugin } from 'postcss'
import { expect, it } from 'vitest'
import { formatNodes } from './format-nodes'

function markPretty(): Plugin {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick plugin to mark the nodes with tailwind_pretty

return {
postcssPlugin: '@tailwindcss/upgrade/mark-pretty',
OnceExit(root) {
root.walkAtRules('utility', (atRule) => {
atRule.raws.tailwind_pretty = true
})
},
}
}

function migrate(input: string) {
return postcss()
.use(markPretty())
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}

it('should format PostCSS nodes that are marked with tailwind_pretty', async () => {
expect(
await migrate(`
@utility .foo { .foo { color: red; } }`),
).toMatchInlineSnapshot(`
"@utility .foo {
.foo {
color: red;
}
}"
`)
})
30 changes: 30 additions & 0 deletions packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { parse, type ChildNode, type Plugin, type Root } from 'postcss'
import { format } from 'prettier'
import { walk, WalkAction } from '../utils/walk'

// Prettier is used to generate cleaner output, but it's only used on the nodes
// that were marked as `pretty` during the migration.
export function formatNodes(): Plugin {
async function migrate(root: Root) {
// Find the nodes to format
let nodesToFormat: ChildNode[] = []
walk(root, (child) => {
if (child.raws.tailwind_pretty) {
nodesToFormat.push(child)
return WalkAction.Skip
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the custom walk implementation so that we can skip going inside of the node, once we found a node with tailwind_pretty.

}
})

// Format the nodes
await Promise.all(
nodesToFormat.map(async (node) => {
node.replaceWith(parse(await format(node.toString(), { parser: 'css', semi: true })))
}),
)
}

return {
postcssPlugin: '@tailwindcss/upgrade/format-nodes',
OnceExit: migrate,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export function migrateAtApply(): Plugin {
let params = utilities.map((part) => {
// Keep whitespace
if (part.trim() === '') return part

let variants = segment(part, ':')
let utility = variants.pop()!

Expand All @@ -36,8 +35,8 @@ export function migrateAtApply(): Plugin {

return {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
AtRule: {
apply: migrate,
OnceExit(root) {
root.walkAtRules('apply', migrate)
},
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import dedent from 'dedent'
import postcss from 'postcss'
import { describe, expect, it } from 'vitest'
import { formatNodes } from './format-nodes'
import { migrateAtLayerUtilities } from './migrate-at-layer-utilities'

const css = dedent

function migrate(input: string) {
return postcss()
.use(migrateAtLayerUtilities())
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,16 @@
import { AtRule, parse, Rule, type ChildNode, type Comment, type Plugin } from 'postcss'
import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss'
import SelectorParser from 'postcss-selector-parser'
import { format } from 'prettier'
import { segment } from '../../../tailwindcss/src/utils/segment'

enum WalkAction {
// Continue walking the tree. Default behavior.
Continue,

// Skip walking into the current node.
Skip,

// Stop walking the tree entirely.
Stop,
}

interface Walkable<T> {
each(cb: (node: T, index: number) => void): void
}

// Custom walk implementation where we can skip going into nodes when we don't
// need to process them.
function walk<T>(rule: Walkable<T>, cb: (rule: T) => void | WalkAction): undefined | false {
let result: undefined | false = undefined

rule.each?.((node) => {
let action = cb(node) ?? WalkAction.Continue
if (action === WalkAction.Stop) {
result = false
return result
}
if (action !== WalkAction.Skip) {
result = walk(node as Walkable<T>, cb)
return result
}
})

return result
}

// Depth first walk reversal implementation.
function walkDepth<T>(rule: Walkable<T>, cb: (rule: T) => void) {
rule?.each?.((node) => {
walkDepth(node as Walkable<T>, cb)
cb(node)
})
}
import { walk, WalkAction, walkDepth } from '../utils/walk'

export function migrateAtLayerUtilities(): Plugin {
function migrate(atRule: AtRule) {
// Only migrate `@layer utilities` and `@layer components`.
if (atRule.params !== 'utilities' && atRule.params !== 'components') return

// If the `@layer utilities` contains CSS that should not be turned into an
// `@utility` at-rule, then we have to keep it around (including the
// `@layer utilities` wrapper). To prevent this from being processed over
// and over again, we mark it as seen and bail early.
if (atRule.raws.seen) return

// Keep rules that should not be turned into utilities as is. This will
// include rules with element or ID selectors.
let defaultsAtRule = atRule.clone({ raws: { seen: true } })
let defaultsAtRule = atRule.clone()

// Clone each rule with multiple selectors into their own rule with a single
// selector.
Expand Down Expand Up @@ -312,32 +263,12 @@ export function migrateAtLayerUtilities(): Plugin {

return {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities',
OnceExit: async (root) => {
OnceExit: (root) => {
// Migrate `@layer utilities` and `@layer components` into `@utility`.
// Using this instead of the visitor API in case we want to use
// postcss-nesting in the future.
root.walkAtRules('layer', migrate)

// Prettier is used to generate cleaner output, but it's only used on the
// nodes that were marked as `pretty` during the migration.
{
// Find the nodes to format
let nodesToFormat: ChildNode[] = []
walk(root, (child) => {
if (child.raws.tailwind_pretty) {
nodesToFormat.push(child)
return WalkAction.Skip
}
})

// Format the nodes
await Promise.all(
nodesToFormat.map(async (node) => {
node.replaceWith(parse(await format(node.toString(), { parser: 'css', semi: true })))
}),
)
}

// Merge `@utility <name>` with the same name into a single rule. This can
// happen when the same classes is used in multiple `@layer utilities`
// blocks.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
import { formatNodes } from './format-nodes'
import { migrateMissingLayers } from './migrate-missing-layers'

const css = dedent

function migrate(input: string) {
return postcss()
.use(migrateMissingLayers())
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}

it('should migrate rules between tailwind directives', async () => {
expect(
await migrate(css`
@tailwind base;

.base {
}

@tailwind components;

.component-a {
}
.component-b {
}

@tailwind utilities;

.utility-a {
}
.utility-b {
}
`),
).toMatchInlineSnapshot(`
"@tailwind base;

@layer base {
.base {
}
}

@tailwind components;

@layer components {
.component-a {
}
.component-b {
}
}

@tailwind utilities;

@layer utilities {
.utility-a {
}
.utility-b {
}
}"
`)
})
Loading