From 0cc1c3eb125abb2d30b164b95d2fdcdb01970532 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 14:30:28 +0200 Subject: [PATCH 01/13] move `walk` utils in their own file --- .../codemods/migrate-at-layer-utilities.ts | 46 +------------------ .../@tailwindcss-upgrade/src/utils/walk.ts | 42 +++++++++++++++++ 2 files changed, 44 insertions(+), 44 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/utils/walk.ts diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts index 2bc0c9601cbf..799af418790d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -1,50 +1,8 @@ -import { AtRule, parse, Rule, type ChildNode, type Comment, type Plugin } from 'postcss' +import { parse, type AtRule, type ChildNode, 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 { - 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(rule: Walkable, 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, cb) - return result - } - }) - - return result -} - -// Depth first walk reversal implementation. -function walkDepth(rule: Walkable, cb: (rule: T) => void) { - rule?.each?.((node) => { - walkDepth(node as Walkable, cb) - cb(node) - }) -} +import { walk, WalkAction, walkDepth } from '../utils/walk' export function migrateAtLayerUtilities(): Plugin { function migrate(atRule: AtRule) { diff --git a/packages/@tailwindcss-upgrade/src/utils/walk.ts b/packages/@tailwindcss-upgrade/src/utils/walk.ts new file mode 100644 index 000000000000..7a86b7ae533d --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/walk.ts @@ -0,0 +1,42 @@ +export enum WalkAction { + // Continue walking the tree. Default behavior. + Continue, + + // Skip walking into the current node. + Skip, + + // Stop walking the tree entirely. + Stop, +} + +interface Walkable { + 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. +export function walk(rule: Walkable, 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, cb) + return result + } + }) + + return result +} + +// Depth first walk reversal implementation. +export function walkDepth(rule: Walkable, cb: (rule: T) => void) { + rule?.each?.((node) => { + walkDepth(node as Walkable, cb) + cb(node) + }) +} From 3458c24e0c5edcbbed848104ef787b11d75f0bd1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 14:49:01 +0200 Subject: [PATCH 02/13] use `OnceExit` instead of the visitor API This way we can ensure that all migrations run in order as expected. --- .../@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts | 5 ++--- .../src/codemods/migrate-at-layer-utilities.ts | 8 +------- .../src/codemods/migrate-tailwind-directives.ts | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts index 41ff03ec3323..ebea30e96d6a 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts @@ -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()! @@ -36,8 +35,8 @@ export function migrateAtApply(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply', - AtRule: { - apply: migrate, + OnceExit(root) { + root.walkAtRules('apply', migrate) }, } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts index 799af418790d..93b502369303 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -9,15 +9,9 @@ export function migrateAtLayerUtilities(): Plugin { // 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. diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts index cb47714a0197..5bb4a34e2349 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts @@ -86,6 +86,6 @@ export function migrateTailwindDirectives(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/migrate-tailwind-directives', - Once: migrate, + OnceExit: migrate, } } From bf1d0868bbf990779fda3d8d40d7f9f1dcde3b1b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 14:31:10 +0200 Subject: [PATCH 03/13] add separate step for formatting nodes This allows us to re-use the logic, the only thing we have to do is mark a node with `raws.tailwind_pretty = true` --- .../src/codemods/format-nodes.test.ts | 35 +++++++++++++++++++ .../src/codemods/format-nodes.ts | 30 ++++++++++++++++ .../migrate-at-layer-utilities.test.ts | 1 + .../codemods/migrate-at-layer-utilities.ts | 25 ++----------- .../migrate-tailwind-directives.test.ts | 2 ++ 5 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts new file mode 100644 index 000000000000..2764474621a0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts @@ -0,0 +1,35 @@ +import postcss, { type Plugin } from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' + +function markPretty(): Plugin { + 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; + } + } " + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts new file mode 100644 index 000000000000..e90e0e2637ca --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts @@ -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 + } + }) + + // 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, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts index a493639f2ea3..7c85eabb9986 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts @@ -8,6 +8,7 @@ const css = dedent function migrate(input: string) { return postcss() .use(migrateAtLayerUtilities()) + .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts index 93b502369303..c1e383ef5356 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -1,6 +1,5 @@ -import { parse, type AtRule, type ChildNode, type Comment, type Plugin, type Rule } 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' import { walk, WalkAction, walkDepth } from '../utils/walk' @@ -264,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 ` with the same name into a single rule. This can // happen when the same classes is used in multiple `@layer utilities` // blocks. diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts index f59fef01e848..9f4ae095501d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts @@ -1,6 +1,7 @@ import dedent from 'dedent' import postcss from 'postcss' import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' import { migrateTailwindDirectives } from './migrate-tailwind-directives' const css = dedent @@ -8,6 +9,7 @@ const css = dedent function migrate(input: string) { return postcss() .use(migrateTailwindDirectives()) + .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) } From c3445b0fcecb276588abb2584e1674d9ca80e7b2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 15:10:29 +0200 Subject: [PATCH 04/13] add separate step that hoists imports We will convert `@tailwind` directives in one step to `@import` at-rules. These could now exist in the middle of the CSS. This step hoists them to the top in a separate pass. --- .../src/codemods/hoist-imports.test.ts | 138 ++++++++++++++++++ .../src/codemods/hoist-imports.ts | 63 ++++++++ 2 files changed, 201 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts diff --git a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts new file mode 100644 index 000000000000..355f838b22b7 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts @@ -0,0 +1,138 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { hoistImports } from './hoist-imports' + +const css = dedent + +function migrate(input: string) { + return postcss() + .use(hoistImports()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should keep imports as-is if they are in the correct spot', async () => { + expect( + await migrate(css` + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities';" + `) +}) + +it('should hoist imports to the top', async () => { + expect( + await migrate(css` + @import 'tailwindcss/base'; + .a { + } + @import 'tailwindcss/components'; + .b { + } + @import 'tailwindcss/utilities'; + .c { + } + `), + ).toMatchInlineSnapshot(` + " + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + .a { + } + .b { + } + .c { + }" + `) +}) + +it('should keep @charset at the top', async () => { + expect( + await migrate(css` + @charset 'utf-8'; + @import 'tailwindcss/base'; + .a { + } + @import 'tailwindcss/components'; + .b { + } + @import 'tailwindcss/utilities'; + .c { + } + `), + ).toMatchInlineSnapshot(` + "@charset 'utf-8'; + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + .a { + } + .b { + } + .c { + }" + `) +}) + +it('should keep the empty @layer at the top', async () => { + expect( + await migrate(css` + @layer foo, bar, baz; + @import 'tailwindcss/base'; + .a { + } + @import 'tailwindcss/components'; + .b { + } + @import 'tailwindcss/utilities'; + .c { + } + `), + ).toMatchInlineSnapshot(` + "@layer foo, bar, baz; + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + .a { + } + .b { + } + .c { + }" + `) +}) + +it('should keep comments above the imports', async () => { + expect( + await migrate(css` + /* Imports: */ + @import 'tailwindcss/base'; + .a { + } + @import 'tailwindcss/components'; + .b { + } + @import 'tailwindcss/utilities'; + .c { + } + `), + ).toMatchInlineSnapshot(` + "/* Imports: */ + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + .a { + } + .b { + } + .c { + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts new file mode 100644 index 000000000000..413594a76746 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts @@ -0,0 +1,63 @@ +import { type AtRule, type ChildNode, type Plugin, type Root } from 'postcss' + +export function hoistImports(): Plugin { + function migrate(root: Root) { + let imports: AtRule[] = [] + + // Track the node where we want to insert the imports after. + let after: ChildNode | null = null as ChildNode | null + + // Whether a node that is not an import exists before an import. + // Except for `@charset` and `@layer foo, bar, baz;`. + let seenNonImport = false + + // Whether we should hoist the imports. + let shouldHoist = false + + root.each((node) => { + // Track the `@import` at-rules, themselves. + if (node.type === 'atrule' && node.name === 'import') { + // Once we've seen a non-import node, we should hoist the imports. + if (seenNonImport) { + shouldHoist = true + } + + imports.push(node) + } + + // The `@charset` at-rule is allowed to exist before the imports. + else if (node.type === 'atrule' && node.name === 'charset') { + after = node + } + + // The `@layer` at-rule without a body is allowed to exist before the + // imports to define the layer order. + else if (node.type === 'atrule' && node.name === 'layer' && !node.nodes) { + after = node + } + + // Comments are allowed to exist before the imports. + else if (node.type === 'comment' && imports.length === 0) { + after = node + } + + // Once we see a non-import node, we should hoist the imports. + else { + seenNonImport = true + } + }) + + if (shouldHoist) { + if (after !== null) { + after.after(imports) + } else { + root.prepend(imports) + } + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/hoist-imports', + OnceExit: migrate, + } +} From 58b8f8d79084ba3c17b02d0329c89eaf485e4b75 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 23 Sep 2024 14:54:48 +0200 Subject: [PATCH 05/13] implement missing layer migration --- .../codemods/migrate-missing-layers.test.ts | 65 +++++++++++ .../src/codemods/migrate-missing-layers.ts | 110 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts new file mode 100644 index 000000000000..630ae3af9597 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts @@ -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 tw 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 { + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts new file mode 100644 index 000000000000..3c1818ff24a4 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts @@ -0,0 +1,110 @@ +import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss' + +export function migrateMissingLayers(): Plugin { + function migrate(root: Root) { + let lastLayer = '' + let bucket: ChildNode[] = [] + let buckets: [layer: string, bucket: typeof bucket][] = [] + + root.each((node) => { + if (node.type === 'atrule') { + // Known Tailwind directives that should not be inside a layer. + if (node.name === 'theme' || node.name === 'utility') { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + return + } + + // Base + if ( + (node.name === 'tailwind' && node.params === 'base') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + lastLayer = 'base' + return + } + + // Components + if ( + (node.name === 'tailwind' && node.params === 'components') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/components["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + lastLayer = 'components' + return + } + + // Utilities + if ( + (node.name === 'tailwind' && node.params === 'utilities') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + lastLayer = 'utilities' + return + } + + // Already in a layer + if (node.name === 'layer') { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + return + } + + // Add layer to `@import` at-rules + if (node.name === 'import') { + if (!node.params.includes('layer(')) { + node.params += ` layer(${lastLayer})` + } + + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + return + } + } + + // Track the node + if (lastLayer !== '') { + if (bucket.push(node) !== 1) { + node.remove() + } + } + }) + + // Add the last bucket if it's not empty + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + // Wrap each bucket in an `@layer` at-rule + for (let [layerName, nodes] of buckets) { + let layerNode = new AtRule({ + name: 'layer', + params: layerName, + nodes, + raws: { + tailwind_pretty: true, + }, + }) + nodes[0].replaceWith(layerNode) + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-missing-layers', + OnceExit: migrate, + } +} From 4be746c15d89e757d4d130e47037175980bd925e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 15:17:22 +0200 Subject: [PATCH 06/13] add generic migration test --- .../@tailwindcss-upgrade/src/index.test.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index a043092d9ae6..ee155875fa7b 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -26,3 +26,86 @@ it('should print the input as-is', async () => { }" `) }) + +it('should migrate a stylesheet', async () => { + expect( + await migrateContents(css` + @import 'tailwindcss/base'; + @import './my-base.css'; + + html { + overflow: hidden; + } + + @import 'tailwindcss/components'; + @import './my-components.css'; + + .a { + z-index: 1; + } + + @layer components { + .b { + z-index: 2; + } + } + + .c { + z-index: 3; + } + + @import 'tailwindcss/utilities'; + @import './my-utilities.css'; + + .d { + z-index: 4; + } + + @layer utilities { + .e { + z-index: 5; + } + } + `), + ).toMatchInlineSnapshot(` + " + + @import 'tailwindcss'; + + @import './my-base.css' layer(base); + @import './my-components.css' layer(components); + @import './my-utilities.css' layer(utilities); + + @layer base { + html { + overflow: hidden; + } + } + + @layer components { + .a { + z-index: 1; + } + } + + @utility b { + z-index: 2; + } + + @layer components { + .c { + z-index: 3; + } + } + + @layer utilities { + .d { + z-index: 4; + } + } + + @utility e { + z-index: 5; + }" + `) +}) From 7cfc018541357cf4d862138f166bc1df4bf5784c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 15:08:39 +0200 Subject: [PATCH 07/13] use new migration plugins --- .../src/codemods/migrate-at-layer-utilities.test.ts | 1 + packages/@tailwindcss-upgrade/src/migrate.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts index 7c85eabb9986..6f1a03301f93 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts @@ -1,6 +1,7 @@ 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 diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 88c54350db5f..e78e72cd82ed 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -1,15 +1,21 @@ import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' +import { formatNodes } from './codemods/format-nodes' +import { hoistImports } from './codemods/hoist-imports' import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' +import { migrateMissingLayers } from './codemods/migrate-missing-layers' import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives' export async function migrateContents(contents: string, file?: string) { return postcss() .use(migrateAtApply()) - .use(migrateTailwindDirectives()) .use(migrateAtLayerUtilities()) + .use(migrateMissingLayers()) + .use(migrateTailwindDirectives()) + .use(hoistImports()) + .use(formatNodes()) .process(contents, { from: file }) .then((result) => result.css) } From 289989179e313bb7c76e302d4c220b0493599c01 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 16:51:28 +0200 Subject: [PATCH 08/13] cleanup trailing whitespace --- .../@tailwindcss-upgrade/src/codemods/format-nodes.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts index 2764474621a0..517c517c0cad 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts @@ -24,12 +24,12 @@ function migrate(input: string) { it('should format PostCSS nodes that are marked with tailwind_pretty', async () => { expect( await migrate(` - @utility .foo { .foo { color: red; } } `), + @utility .foo { .foo { color: red; } }`), ).toMatchInlineSnapshot(` "@utility .foo { .foo { color: red; } - } " + }" `) }) From 2cbb8e79dfffe32c9ec6e822efbb4829a610c360 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 17:00:54 +0200 Subject: [PATCH 09/13] fix test name --- .../src/codemods/migrate-missing-layers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts index 630ae3af9597..c33bcc03e1d3 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts @@ -14,7 +14,7 @@ function migrate(input: string) { .then((result) => result.css) } -it('should migrate rules between tw directives', async () => { +it('should migrate rules between tailwind directives', async () => { expect( await migrate(css` @tailwind base; From 1063c0d68fc48809e9e3241c7ecb85123a03f0f8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 17:03:27 +0200 Subject: [PATCH 10/13] add integration test --- integrations/cli/upgrade.test.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/integrations/cli/upgrade.test.ts b/integrations/cli/upgrade.test.ts index 72b1bf4a8a2f..9629a685eb1e 100644 --- a/integrations/cli/upgrade.test.ts +++ b/integrations/cli/upgrade.test.ts @@ -65,7 +65,17 @@ test( `, 'src/index.css': css` @tailwind base; + + html { + color: #333; + } + @tailwind components; + + .btn { + color: red; + } + @tailwind utilities; `, }, @@ -73,7 +83,27 @@ test( 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; + } + } + `, + ) }, ) From 4813d54cd34afcb4ba968bad0f1a9dc16ba16c0f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 17:56:51 +0200 Subject: [PATCH 11/13] drop hoist imports We don't need this, the migrate directives already takes care of this by prepending at-rules to the top. --- .../src/codemods/hoist-imports.test.ts | 138 ------------------ .../src/codemods/hoist-imports.ts | 63 -------- packages/@tailwindcss-upgrade/src/migrate.ts | 2 - 3 files changed, 203 deletions(-) delete mode 100644 packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts delete mode 100644 packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts diff --git a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts deleted file mode 100644 index 355f838b22b7..000000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import dedent from 'dedent' -import postcss from 'postcss' -import { expect, it } from 'vitest' -import { hoistImports } from './hoist-imports' - -const css = dedent - -function migrate(input: string) { - return postcss() - .use(hoistImports()) - .process(input, { from: expect.getState().testPath }) - .then((result) => result.css) -} - -it('should keep imports as-is if they are in the correct spot', async () => { - expect( - await migrate(css` - @import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities'; - `), - ).toMatchInlineSnapshot(` - "@import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities';" - `) -}) - -it('should hoist imports to the top', async () => { - expect( - await migrate(css` - @import 'tailwindcss/base'; - .a { - } - @import 'tailwindcss/components'; - .b { - } - @import 'tailwindcss/utilities'; - .c { - } - `), - ).toMatchInlineSnapshot(` - " - @import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities'; - .a { - } - .b { - } - .c { - }" - `) -}) - -it('should keep @charset at the top', async () => { - expect( - await migrate(css` - @charset 'utf-8'; - @import 'tailwindcss/base'; - .a { - } - @import 'tailwindcss/components'; - .b { - } - @import 'tailwindcss/utilities'; - .c { - } - `), - ).toMatchInlineSnapshot(` - "@charset 'utf-8'; - @import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities'; - .a { - } - .b { - } - .c { - }" - `) -}) - -it('should keep the empty @layer at the top', async () => { - expect( - await migrate(css` - @layer foo, bar, baz; - @import 'tailwindcss/base'; - .a { - } - @import 'tailwindcss/components'; - .b { - } - @import 'tailwindcss/utilities'; - .c { - } - `), - ).toMatchInlineSnapshot(` - "@layer foo, bar, baz; - @import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities'; - .a { - } - .b { - } - .c { - }" - `) -}) - -it('should keep comments above the imports', async () => { - expect( - await migrate(css` - /* Imports: */ - @import 'tailwindcss/base'; - .a { - } - @import 'tailwindcss/components'; - .b { - } - @import 'tailwindcss/utilities'; - .c { - } - `), - ).toMatchInlineSnapshot(` - "/* Imports: */ - @import 'tailwindcss/base'; - @import 'tailwindcss/components'; - @import 'tailwindcss/utilities'; - .a { - } - .b { - } - .c { - }" - `) -}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts b/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts deleted file mode 100644 index 413594a76746..000000000000 --- a/packages/@tailwindcss-upgrade/src/codemods/hoist-imports.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { type AtRule, type ChildNode, type Plugin, type Root } from 'postcss' - -export function hoistImports(): Plugin { - function migrate(root: Root) { - let imports: AtRule[] = [] - - // Track the node where we want to insert the imports after. - let after: ChildNode | null = null as ChildNode | null - - // Whether a node that is not an import exists before an import. - // Except for `@charset` and `@layer foo, bar, baz;`. - let seenNonImport = false - - // Whether we should hoist the imports. - let shouldHoist = false - - root.each((node) => { - // Track the `@import` at-rules, themselves. - if (node.type === 'atrule' && node.name === 'import') { - // Once we've seen a non-import node, we should hoist the imports. - if (seenNonImport) { - shouldHoist = true - } - - imports.push(node) - } - - // The `@charset` at-rule is allowed to exist before the imports. - else if (node.type === 'atrule' && node.name === 'charset') { - after = node - } - - // The `@layer` at-rule without a body is allowed to exist before the - // imports to define the layer order. - else if (node.type === 'atrule' && node.name === 'layer' && !node.nodes) { - after = node - } - - // Comments are allowed to exist before the imports. - else if (node.type === 'comment' && imports.length === 0) { - after = node - } - - // Once we see a non-import node, we should hoist the imports. - else { - seenNonImport = true - } - }) - - if (shouldHoist) { - if (after !== null) { - after.after(imports) - } else { - root.prepend(imports) - } - } - } - - return { - postcssPlugin: '@tailwindcss/upgrade/hoist-imports', - OnceExit: migrate, - } -} diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index e78e72cd82ed..cfbe50d3dcb3 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -2,7 +2,6 @@ import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' import { formatNodes } from './codemods/format-nodes' -import { hoistImports } from './codemods/hoist-imports' import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' import { migrateMissingLayers } from './codemods/migrate-missing-layers' @@ -14,7 +13,6 @@ export async function migrateContents(contents: string, file?: string) { .use(migrateAtLayerUtilities()) .use(migrateMissingLayers()) .use(migrateTailwindDirectives()) - .use(hoistImports()) .use(formatNodes()) .process(contents, { from: file }) .then((result) => result.css) From 0a1b1e81403690d691d1665aefcc8045249b6ce8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 17:58:40 +0200 Subject: [PATCH 12/13] split test into better examples Instead of crafting a bad input, we can just not do it. --- .../@tailwindcss-upgrade/src/index.test.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index ee155875fa7b..2f4f7475e3f6 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -30,15 +30,13 @@ it('should print the input as-is', async () => { it('should migrate a stylesheet', async () => { expect( await migrateContents(css` - @import 'tailwindcss/base'; - @import './my-base.css'; + @tailwind base; html { overflow: hidden; } - @import 'tailwindcss/components'; - @import './my-components.css'; + @tailwind components; .a { z-index: 1; @@ -54,8 +52,7 @@ it('should migrate a stylesheet', async () => { z-index: 3; } - @import 'tailwindcss/utilities'; - @import './my-utilities.css'; + @tailwind utilities; .d { z-index: 4; @@ -68,13 +65,7 @@ it('should migrate a stylesheet', async () => { } `), ).toMatchInlineSnapshot(` - " - - @import 'tailwindcss'; - - @import './my-base.css' layer(base); - @import './my-components.css' layer(components); - @import './my-utilities.css' layer(utilities); + "@import 'tailwindcss'; @layer base { html { @@ -109,3 +100,21 @@ it('should migrate a stylesheet', async () => { }" `) }) + +it('should migrate a stylesheet (with imports)', async () => { + expect( + await migrateContents(css` + @import 'tailwindcss/base'; + @import './my-base.css'; + @import 'tailwindcss/components'; + @import './my-components.css'; + @import 'tailwindcss/utilities'; + @import './my-utilities.css'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + @import './my-base.css' layer(base); + @import './my-components.css' layer(components); + @import './my-utilities.css' layer(utilities);" + `) +}) From e5e07f98f7693d6308c40b9e2213a6abed072577 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 18:08:56 +0200 Subject: [PATCH 13/13] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6922f5bfd3eb..108fc3a26274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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