From 6c8755c22f65f103fe25967cc1dc6c63ccf78088 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 3 Nov 2024 14:33:12 +0100 Subject: [PATCH 1/9] bucket sorting --- integrations/upgrade/index.test.ts | 45 +++-- integrations/upgrade/js-config.test.ts | 157 +++++++++--------- .../src/codemods/format-nodes.test.ts | 22 ++- .../src/codemods/format-nodes.ts | 74 +++++++-- .../migrate-at-layer-utilities.test.ts | 70 ++++---- .../codemods/migrate-at-layer-utilities.ts | 1 - .../migrate-border-compatibility.test.ts | 30 ++-- .../codemods/migrate-border-compatibility.ts | 30 +--- .../src/codemods/migrate-config.ts | 36 +--- .../src/codemods/migrate-media-screen.test.ts | 2 + .../codemods/migrate-missing-layers.test.ts | 20 ++- .../migrate-tailwind-directives.test.ts | 6 +- .../codemods/migrate-tailwind-directives.ts | 3 - .../src/codemods/migrate-theme-to-var.test.ts | 2 + .../migrate-variants-directive.test.ts | 2 + .../src/codemods/sort-buckets.ts | 142 ++++++++++++++++ .../@tailwindcss-upgrade/src/index.test.ts | 32 ++-- packages/@tailwindcss-upgrade/src/index.ts | 3 +- .../src/migrate-js-config.ts | 10 +- packages/@tailwindcss-upgrade/src/migrate.ts | 7 +- .../@tailwindcss-upgrade/src/utils/walk.ts | 9 +- 21 files changed, 442 insertions(+), 261 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 1489243a68b1..9784f57c98b7 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -398,7 +398,6 @@ test( If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - @layer base { *, ::after, @@ -1002,6 +1001,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -1193,6 +1193,7 @@ test( --- ./src/a.1.utilities.1.css --- @import './a.1.utilities.utilities.css'; + @utility foo-from-a { color: red; } @@ -1214,12 +1215,14 @@ test( --- ./src/b.1.css --- @import './b.1.components.css'; + @utility bar-from-b { color: red; } --- ./src/c.1.css --- @import './c.2.css' layer(utilities); + .baz-from-c { color: green; } @@ -1229,12 +1232,14 @@ test( --- ./src/c.2.css --- @import './c.3.css'; + #baz { --keep: me; } --- ./src/c.2.utilities.css --- @import './c.3.utilities.css'; + @utility baz-from-import { color: yellow; } @@ -1417,6 +1422,8 @@ test( /* Inject missing @config */ @import 'tailwindcss'; + @config '../tailwind.config.ts'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -1434,6 +1441,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -1449,12 +1457,13 @@ test( border-width: 0; } } - @config '../tailwind.config.ts'; --- ./src/root.2.css --- /* Already contains @config */ @import 'tailwindcss'; + @config "../tailwind.config.ts"; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -1472,6 +1481,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -1487,12 +1497,23 @@ test( border-width: 0; } } - @config "../tailwind.config.ts"; --- ./src/root.3.css --- /* Inject missing @config above first @theme */ @import 'tailwindcss'; + @config '../tailwind.config.ts'; + + @variant hocus (&:hover, &:focus); + + @theme { + --color-red-500: #f00; + } + + @theme { + --color-blue-500: #00f; + } + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -1510,6 +1531,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -1525,22 +1547,12 @@ test( border-width: 0; } } - @config '../tailwind.config.ts'; - - @variant hocus (&:hover, &:focus); - - @theme { - --color-red-500: #f00; - } - - @theme { - --color-blue-500: #00f; - } --- ./src/root.4.css --- /* Inject missing @config due to nested imports with tailwind imports */ @import './root.4/base.css'; @import './root.4/utilities.css'; + @config '../tailwind.config.ts'; --- ./src/root.5.css --- @@ -1591,6 +1603,8 @@ test( /* Inject missing @config in this file, due to full import */ @import 'tailwindcss'; + @config '../../tailwind.config.ts'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -1608,6 +1622,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -1623,7 +1638,6 @@ test( border-width: 0; } } - @config '../../tailwind.config.ts'; " `) }, @@ -1681,6 +1695,7 @@ test( border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 12f52494bcc1..bee8044cbad7 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -144,40 +144,6 @@ test( --- src/input.css --- @import 'tailwindcss'; - /* - The default border color has changed to \`currentColor\` in Tailwind CSS v4, - so we've added these compatibility styles to make sure everything still - looks the same as it did with Tailwind CSS v3. - - If we ever want to remove these styles, we need to add an explicit border - color utility to any element that depends on these defaults. - */ - @layer base { - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentColor); - } - } - - /* - Form elements have a 1px border by default in Tailwind CSS v4, so we've - added these compatibility styles to make sure everything still looks the - same as it did with Tailwind CSS v3. - - If we ever want to remove these styles, we need to add \`border-0\` to - any form elements that shouldn't have a border. - */ - @layer base { - input:where(:not([type='button'], [type='reset'], [type='submit'])), - select, - textarea { - border-width: 0; - } - } - @source '../node_modules/my-external-lib/**/*.{html}'; @variant dark (&:where(.dark, .dark *)); @@ -274,6 +240,40 @@ test( } } + /* + The default border color has changed to \`currentColor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } + } + + /* + Form elements have a 1px border by default in Tailwind CSS v4, so we've + added these compatibility styles to make sure everything still looks the + same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add \`border-0\` to + any form elements that shouldn't have a border. + */ + @layer base { + input:where(:not([type='button'], [type='reset'], [type='submit'])), + select, + textarea { + border-width: 0; + } + } + --- src/test.js --- export default { 'shouldNotMigrate': !border.test + '', @@ -346,6 +346,24 @@ test( --- src/input.css --- @import 'tailwindcss'; + @plugin '@tailwindcss/typography'; + @plugin '../custom-plugin' { + is-null: null; + is-true: true; + is-false: false; + is-int: 1234567; + is-float: 1.35; + is-sci: 0.0000135; + is-str-null: 'null'; + is-str-true: 'true'; + is-str-false: 'false'; + is-str-int: '1234567'; + is-str-float: '1.35'; + is-str-sci: '1.35e-5'; + is-arr: 'foo', 'bar'; + is-arr-mixed: null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'; + } + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -379,24 +397,6 @@ test( border-width: 0; } } - - @plugin '@tailwindcss/typography'; - @plugin '../custom-plugin' { - is-null: null; - is-true: true; - is-false: false; - is-int: 1234567; - is-float: 1.35; - is-sci: 0.0000135; - is-str-null: 'null'; - is-str-true: 'true'; - is-str-false: 'false'; - is-str-int: '1234567'; - is-str-float: '1.35'; - is-str-sci: '1.35e-5'; - is-arr: 'foo', 'bar'; - is-arr-mixed: null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'; - } " `) @@ -447,6 +447,20 @@ test( --- src/input.css --- @import 'tailwindcss'; + @theme { + --color-gray-50: oklch(0.985 0 0); + --color-gray-100: oklch(0.97 0 0); + --color-gray-200: oklch(0.922 0 0); + --color-gray-300: oklch(0.87 0 0); + --color-gray-400: oklch(0.708 0 0); + --color-gray-500: oklch(0.556 0 0); + --color-gray-600: oklch(0.439 0 0); + --color-gray-700: oklch(0.371 0 0); + --color-gray-800: oklch(0.269 0 0); + --color-gray-900: oklch(0.205 0 0); + --color-gray-950: oklch(0.145 0 0); + } + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -480,20 +494,6 @@ test( border-width: 0; } } - - @theme { - --color-gray-50: oklch(0.985 0 0); - --color-gray-100: oklch(0.97 0 0); - --color-gray-200: oklch(0.922 0 0); - --color-gray-300: oklch(0.87 0 0); - --color-gray-400: oklch(0.708 0 0); - --color-gray-500: oklch(0.556 0 0); - --color-gray-600: oklch(0.439 0 0); - --color-gray-700: oklch(0.371 0 0); - --color-gray-800: oklch(0.269 0 0); - --color-gray-900: oklch(0.205 0 0); - --color-gray-950: oklch(0.145 0 0); - } " `) @@ -548,6 +548,8 @@ test( --- src/input.css --- @import 'tailwindcss'; + @config '../tailwind.config.ts'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -581,7 +583,6 @@ test( border-width: 0; } } - @config '../tailwind.config.ts'; " `) @@ -640,6 +641,8 @@ test( --- src/input.css --- @import 'tailwindcss'; + @config '../tailwind.config.ts'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -673,7 +676,6 @@ test( border-width: 0; } } - @config '../tailwind.config.ts'; " `) @@ -728,6 +730,8 @@ test( --- src/input.css --- @import 'tailwindcss'; + @config '../tailwind.config.ts'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -761,7 +765,6 @@ test( border-width: 0; } } - @config '../tailwind.config.ts'; " `) @@ -852,6 +855,10 @@ test( --- project-a/src/input.css --- @import 'tailwindcss'; + @theme { + --color-primary: red; + } + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -886,13 +893,13 @@ test( } } - @theme { - --color-primary: red; - } - --- project-b/src/input.css --- @import 'tailwindcss'; + @theme { + --color-primary: blue; + } + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -926,10 +933,6 @@ test( border-width: 0; } } - - @theme { - --color-primary: blue; - } " `) }, diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts index 517c517c0cad..116aa090dccd 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts @@ -1,12 +1,13 @@ import postcss, { type Plugin } from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' +import { sortBuckets } from './sort-buckets' function markPretty(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/mark-pretty', OnceExit(root) { - root.walkAtRules('utility', (atRule) => { + root.walkAtRules('format', (atRule) => { atRule.raws.tailwind_pretty = true }) }, @@ -16,16 +17,14 @@ function markPretty(): Plugin { function migrate(input: string) { return postcss() .use(markPretty()) + .use(sortBuckets()) .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(` +it('should format PostCSS nodes', async () => { + expect(await migrate(`@utility .foo { .foo { color: red; } }`)).toMatchInlineSnapshot(` "@utility .foo { .foo { color: red; @@ -33,3 +32,14 @@ it('should format PostCSS nodes that are marked with tailwind_pretty', async () }" `) }) + +it('should format PostCSS nodes in the `user` bucket', async () => { + expect(await migrate(`@bucket user { @format .bar { .foo { color: red; } } }`)) + .toMatchInlineSnapshot(` + "@format .bar { + .foo { + color: red; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts index b545d6009e72..fb2f4a63ca28 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts @@ -1,6 +1,6 @@ -import { parse, type ChildNode, type Plugin, type Root } from 'postcss' +import postcss, { type ChildNode, type Plugin, type Root } from 'postcss' import { format } from 'prettier' -import { walk, WalkAction } from '../utils/walk' +import { walk } 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. @@ -8,26 +8,66 @@ 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) { + walk(root, (child, _idx, parent) => { + // Always print semicolons after at-rules + if (child.type === 'atrule') { + child.raws.semicolon = true + } + + if (child.type === 'atrule' && child.name === 'bucket') { nodesToFormat.push(child) - return WalkAction.Skip + } else if (child.raws.tailwind_pretty) { + // @ts-expect-error We might not have a parent + child.parent ??= parent + nodesToFormat.unshift(child) } }) + let output: string[] = [] + // Format the nodes - await Promise.all( - nodesToFormat.map(async (node) => { - node.replaceWith( - parse( - await format(node.toString(), { - parser: 'css', - semi: true, - singleQuote: true, - }), - ), - ) - }), + for (let node of nodesToFormat) { + let contents = (() => { + if (node.type === 'atrule' && node.name === 'bucket') { + // Remove the `@bucket` wrapping, and use the contents directly. + return node + .toString() + .trim() + .replace(/@bucket(.*?){([\s\S]*)}/, '$2') + } + + return node.toString() + })() + + // Do not format the user bucket to ensure we keep the user's formatting + // intact. + if (node.type === 'atrule' && node.name === 'bucket' && node.params === 'user') { + output.push(contents) + continue + } + + // Format buckets + if (node.type === 'atrule' && node.name === 'bucket') { + output.push(await format(contents, { parser: 'css', semi: true, singleQuote: true })) + continue + } + + // Format any other nodes + node.replaceWith( + postcss.parse( + `${node.raws.before ?? ''}${(await format(contents, { parser: 'css', semi: true, singleQuote: true })).trim()}`, + ), + ) + } + + root.removeAll() + root.append( + postcss.parse( + output + .map((bucket) => bucket.trim()) + .filter(Boolean) + .join('\n\n'), + ), ) } 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 d8134cf48421..1b2afae18621 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 @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest' import { Stylesheet } from '../stylesheet' import { formatNodes } from './format-nodes' import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' +import { sortBuckets } from './sort-buckets' const css = dedent @@ -33,6 +34,7 @@ async function migrate( return postcss() .use(migrateAtLayerUtilities(stylesheet)) + .use(sortBuckets()) .use(formatNodes()) .process(stylesheet.root!, { from: expect.getState().testPath }) .then((result) => result.css) @@ -145,7 +147,18 @@ it('should leave non-class utilities alone', async () => { } `), ).toMatchInlineSnapshot(` - "@layer utilities { + "@utility foo { + /* 2. */ + /* 2.1. */ + color: red; + /* 2.2. */ + .bar { + /* 2.2.1. */ + font-weight: bold; + } + } + + @layer utilities { /* 1. */ #before { /* 1.1. */ @@ -167,17 +180,6 @@ it('should leave non-class utilities alone', async () => { font-weight: bold; } } - } - - @utility foo { - /* 2. */ - /* 2.1. */ - color: red; - /* 2.2. */ - .bar { - /* 2.2.1. */ - font-weight: bold; - } }" `) }) @@ -776,11 +778,7 @@ describe('comments', () => { /* After */ `), ).toMatchInlineSnapshot(` - "/* Above */ - .before { - /* Inside */ - } - /* After */ + "/* After */ /* Tailwind Utilities: */ @utility no-scrollbar { @@ -799,6 +797,11 @@ describe('comments', () => { scrollbar-width: none; /* Firefox */ } + /* Above */ + .before { + /* Inside */ + } + /* Above */ .after { /* Inside */ @@ -925,14 +928,13 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - " - #main { + "@utility foo { + /* Utility #1 */ + /* Declarations: */ color: red; } - @utility foo { - /* Utility #1 */ - /* Declarations: */ + #main { color: red; }" `) @@ -975,18 +977,7 @@ describe('layered stylesheets', () => { layers: ['utilities'], }), ).toMatchInlineSnapshot(` - "@layer utilities { - - #main { - color: red; - } - } - - #secondary { - color: red; - } - - @utility foo { + "@utility foo { @layer utilities { @layer utilities { /* Utility #1 */ @@ -1008,6 +999,17 @@ describe('layered stylesheets', () => { /* Utility #3 */ /* Declarations: */ color: red; + } + + @layer utilities { + + #main { + color: red; + } + } + + #secondary { + color: red; }" `) }) 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 e274dc202155..8dbff28163c4 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -192,7 +192,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { clone.params = cls // Mark the node as pretty so that it gets formatted by Prettier later. - clone.raws.tailwind_pretty = true clone.raws.before = `${clone.raws.before ?? ''}\n\n` } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.test.ts index 65bbaac3dbc0..fae0db2b37de 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.test.ts @@ -4,6 +4,7 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateBorderCompatibility } from './migrate-border-compatibility' +import { sortBuckets } from './sort-buckets' const css = dedent @@ -17,6 +18,7 @@ async function migrate(input: string) { return postcss() .use(migrateBorderCompatibility({ designSystem })) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -95,6 +97,7 @@ it('should add the compatibility CSS after the last `@import`', async () => { border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -156,6 +159,7 @@ it('should add the compatibility CSS after the last import, even if a body-less border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -200,9 +204,6 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai @variant foo { } - @utility bar { - } - /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -211,7 +212,6 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - @layer base { *, ::after, @@ -238,12 +238,15 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai } } - @layer base { + @utility bar { } @utility baz { } + @layer base { + } + @layer base { }" `) @@ -275,9 +278,6 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai @variant foo { } - @utility bar { - } - /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -286,7 +286,6 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - @layer base { *, ::after, @@ -313,12 +312,15 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai } } - @layer base { + @utility bar { } @utility baz { } + @layer base { + } + @layer base { }" `) @@ -349,10 +351,10 @@ it('should not add the backwards compatibility CSS when no `@import "tailwindcss @utility bar { } - @layer base { + @utility baz { } - @utility baz { + @layer base { } @layer base { @@ -389,10 +391,10 @@ it('should not add the backwards compatibility CSS when another `@import "tailwi @utility bar { } - @layer base { + @utility baz { } - @utility baz { + @layer base { } @layer base { diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts index f6e7b1e38246..f8e791fc2e46 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts @@ -1,5 +1,5 @@ import dedent from 'dedent' -import postcss, { AtRule, type Plugin, type Root } from 'postcss' +import postcss, { type Plugin, type Root } from 'postcss' import type { Config } from 'tailwindcss' import { keyPathToCssProperty } from '../../../tailwindcss/src/compat/apply-config-to-theme' import type { DesignSystem } from '../../../tailwindcss/src/design-system' @@ -77,19 +77,6 @@ export function migrateBorderCompatibility({ if (!isTailwindRoot) return - let targetNode = null as AtRule | null - - root.walkAtRules((node) => { - if (node.name === 'import') { - targetNode = node - } else if (node.name === 'layer' && node.params === 'base') { - targetNode = node - return false - } - }) - - if (!targetNode) return - // Figure out the compatibility CSS to inject let compatibilityCssString = '' if (defaultBorderColor !== DEFAULT_BORDER_COLOR) { @@ -98,6 +85,7 @@ export function migrateBorderCompatibility({ } compatibilityCssString += BORDER_WIDTH_COMPATIBILITY_CSS + compatibilityCssString = `\n@bucket compatibility {\n${compatibilityCssString}\n}\n` let compatibilityCss = postcss.parse(compatibilityCssString) // Replace the `theme(…)` with v3 values if we can't resolve the theme @@ -129,19 +117,7 @@ export function migrateBorderCompatibility({ }) // Inject the compatibility CSS - if (targetNode.name === 'import') { - targetNode.after(compatibilityCss) - - let next = targetNode.next() - if (next) next.raws.before = '\n\n' - } else { - let rawsBefore = compatibilityCss.last?.raws.before - - targetNode.before(compatibilityCss) - - let prev = targetNode.prev() - if (prev) prev.raws.before = rawsBefore - } + root.append(compatibilityCss) } return { diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index b6a43eec2f1b..3013e875a363 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -3,7 +3,6 @@ import postcss, { AtRule, type Plugin, Root } from 'postcss' import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path' import type { JSConfigMigration } from '../migrate-js-config' import type { Stylesheet } from '../stylesheet' -import { walk, WalkAction } from '../utils/walk' const ALREADY_INJECTED = new WeakMap() @@ -39,14 +38,14 @@ export function migrateConfig( }) let css = '\n\n' + css += '\n@bucket source {' for (let source of jsConfigMigration.sources) { let absolute = path.resolve(source.base, source.pattern) css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` } - if (jsConfigMigration.sources.length > 0) { - css = css + '\n' - } + css += '}\n' + css += '\n@bucket plugin {\n' for (let plugin of jsConfigMigration.plugins) { let relative = plugin.path[0] === '.' @@ -71,37 +70,16 @@ export function migrateConfig( css += ` ${property}: ${cssValue};\n` } - css += '}\n' + css += '}\n' // @plugin } } - if (jsConfigMigration.plugins.length > 0) { - css = css + '\n' - } + css += '}\n' // @bucket cssConfig.append(postcss.parse(css + jsConfigMigration.css)) } - // Inject the `@config` directive after the last `@import` or at the - // top of the file if no `@import` rules are present - let locationNode = null as AtRule | null - - walk(root, (node) => { - if (node.type === 'atrule' && node.name === 'import') { - locationNode = node - } - - return WalkAction.Skip - }) - - for (let node of cssConfig?.nodes ?? []) { - node.raws.tailwind_pretty = true - } - - if (!locationNode) { - root.prepend(cssConfig.nodes) - } else if (locationNode.name === 'import') { - locationNode.after(cssConfig.nodes) - } + // Inject the `@config` directive + root.append(cssConfig.nodes) } function migrate(root: Root) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts index d0d2312076bb..f4afd15fa268 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts @@ -5,6 +5,7 @@ import { expect, it } from 'vitest' import type { UserConfig } from '../../../tailwindcss/src/compat/config/types' import { formatNodes } from './format-nodes' import { migrateMediaScreen } from './migrate-media-screen' +import { sortBuckets } from './sort-buckets' const css = dedent @@ -18,6 +19,7 @@ async function migrate(input: string, userConfig: UserConfig = {}) { userConfig, }), ) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) 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 d2782863a731..eee531be3ec0 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts @@ -3,12 +3,14 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateMissingLayers } from './migrate-missing-layers' +import { sortBuckets } from './sort-buckets' const css = dedent function migrate(input: string) { return postcss() .use(migrateMissingLayers()) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -118,6 +120,10 @@ it('should migrate rules above the `@tailwind base` directive in an `@layer base "@charset "UTF-8"; @layer foo, bar, baz; + @tailwind base; + @tailwind components; + @tailwind utilities; + /**! * License header */ @@ -126,11 +132,7 @@ it('should migrate rules above the `@tailwind base` directive in an `@layer base html { color: red; } - } - - @tailwind base; - @tailwind components; - @tailwind utilities;" + }" `) }) @@ -159,13 +161,15 @@ it('should migrate rules between tailwind directives', async () => { ).toMatchInlineSnapshot(` "@tailwind base; + @tailwind components; + + @tailwind utilities; + @layer base { .base { } } - @tailwind components; - @layer components { .component-a { } @@ -173,8 +177,6 @@ it('should migrate rules between tailwind directives', async () => { } } - @tailwind utilities; - .utility-a { } .utility-b { 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 7c74b1ed6e9c..61e88bef12bf 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts @@ -3,12 +3,14 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateTailwindDirectives } from './migrate-tailwind-directives' +import { sortBuckets } from './sort-buckets' const css = dedent function migrate(input: string, options: { newPrefix: string | null } = { newPrefix: null }) { return postcss() .use(migrateTailwindDirectives(options)) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) @@ -412,7 +414,7 @@ it('should replace `@responsive` with its children', async () => { `), ).toMatchInlineSnapshot(` ".foo { - color: red; - }" + color: red; + }" `) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts index d3d74a65e386..c33f9ca01c27 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts @@ -58,9 +58,6 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null }) // Replace Tailwind CSS v2 directives that still worked in v3. else if (node.name === 'responsive') { if (node.nodes) { - for (let child of node.nodes) { - child.raws.tailwind_pretty = true - } node.replaceWith(node.nodes) } else { node.remove() diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-theme-to-var.test.ts index 2bd35ce8e8b7..ce3940bc907e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-theme-to-var.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-theme-to-var.test.ts @@ -4,6 +4,7 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateThemeToVar } from './migrate-theme-to-var' +import { sortBuckets } from './sort-buckets' const css = dedent @@ -16,6 +17,7 @@ async function migrate(input: string) { }), }), ) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts index e18a161e6539..3521d34f7039 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts @@ -3,12 +3,14 @@ import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './format-nodes' import { migrateVariantsDirective } from './migrate-variants-directive' +import { sortBuckets } from './sort-buckets' const css = dedent function migrate(input: string) { return postcss() .use(migrateVariantsDirective()) + .use(sortBuckets()) .use(formatNodes()) .process(input, { from: expect.getState().testPath }) .then((result) => result.css) diff --git a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts new file mode 100644 index 000000000000..d5ea90ebc840 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts @@ -0,0 +1,142 @@ +import postcss, { type AtRule, type ChildNode, type Comment, type Plugin, type Root } from 'postcss' +import { DefaultMap } from '../../../tailwindcss/src/utils/default-map' +import { walk, WalkAction } from '../utils/walk' + +const BUCKET_ORDER = [ + // Imports + 'import', // @import + + // Configuration + 'config', // @config + 'plugin', // @plugin + 'source', // @source + 'variant', // @variant + 'theme', // @theme + + // Styles + 'compatibility', // @layer base with compatibility CSS + 'utility', // @utility + + // User CSS + 'user', +] + +export function sortBuckets(): Plugin { + async function migrate(root: Root) { + // 1. Move items that are not in a bucket, into a bucket + { + let comments: Comment[] = [] + + let buckets = new DefaultMap((name) => { + let bucket = postcss.atRule({ name: 'bucket', params: name, nodes: [] }) + root.append(bucket) + return bucket + }) + + // Seed the buckets with existing buckets + root.walkAtRules('bucket', (node) => { + buckets.set(node.params, node) + }) + + let lastLayer = 'user' + function injectInto(name: string, ...nodes: ChildNode[]) { + lastLayer = name + buckets.get(name).nodes?.push(...comments.splice(0), ...nodes) + } + + walk(root, (node) => { + // Already in a bucket, skip it + if (node.type === 'atrule' && node.name === 'bucket') { + return WalkAction.Skip + } + + // Comments belong to the "next" bucket. If a bucket contains comments + // at the end, they belong to the next bucket. + if (node.type === 'comment') { + comments.push(node) + return + } + + // Known at-rules + else if (node.type === 'atrule' && node.name === 'utility') { + injectInto('utility', node) + } else if (node.type === 'atrule' && node.name === 'variant') { + injectInto('variant', node) + } else if (node.type === 'atrule' && node.name === 'source') { + injectInto('source', node) + } else if (node.type === 'atrule' && node.name === 'theme') { + injectInto('theme', node) + } else if (node.type === 'atrule' && node.name === 'config') { + injectInto('config', node) + } else if (node.type === 'atrule' && node.name === 'plugin') { + injectInto('plugin', node) + } + + // Imports bucket, which also contains the `@charset` and body-less `@layer` + else if ( + (node.type === 'atrule' && node.name === 'layer' && !node.nodes) || // @layer foo, bar; + (node.type === 'atrule' && node.name === 'import') || + (node.type === 'atrule' && node.name === 'charset') || // @charset "UTF-8"; + (node.type === 'atrule' && node.name === 'tailwind') + ) { + injectInto('import', node) + } + + // User CSS + else if (node.type === 'rule' || node.type === 'atrule') { + injectInto('user', node) + } + + // Fallback + else { + injectInto('user', node) + } + + return WalkAction.Skip + }) + + if (comments.length > 0) { + injectInto(lastLayer) + } + } + + // 2. Merge `@bucket` with the same name together + let firstBuckets = new Map() + root.walkAtRules('bucket', (node) => { + let firstBucket = firstBuckets.get(node.params) + if (!firstBucket) { + firstBuckets.set(node.params, node) + return + } + + if (node.nodes) { + firstBucket.append(...node.nodes) + } + }) + + // 3. Remove empty `@bucket` + root.walkAtRules('bucket', (node) => { + if (!node.nodes?.length) { + node.remove() + } + }) + + // 4. Sort the `@bucket` themselves + { + let sorted = Array.from(firstBuckets.values()).sort((a, z) => { + let aIndex = BUCKET_ORDER.indexOf(a.params) + let zIndex = BUCKET_ORDER.indexOf(z.params) + return aIndex - zIndex + }) + + // Re-inject the sorted buckets + root.removeAll() + root.append(sorted) + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/sort-buckets', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index f3c06d61a604..519508297594 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -4,6 +4,7 @@ import path from 'node:path' import postcss from 'postcss' import { expect, it } from 'vitest' import { formatNodes } from './codemods/format-nodes' +import { sortBuckets } from './codemods/sort-buckets' import { migrateContents } from './migrate' const css = dedent @@ -25,7 +26,7 @@ let config = { function migrate(input: string, config: any) { return migrateContents(input, config, expect.getState().testPath) - .then((result) => postcss([formatNodes()]).process(result.root, result.opts)) + .then((result) => postcss([sortBuckets(), formatNodes()]).process(result.root, result.opts)) .then((result) => result.css) } @@ -103,7 +104,6 @@ it('should migrate a stylesheet', async () => { If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - @layer base { *, ::after, @@ -130,6 +130,14 @@ it('should migrate a stylesheet', async () => { } } + @utility b { + z-index: 2; + } + + @utility e { + z-index: 5; + } + @layer base { html { overflow: hidden; @@ -142,10 +150,6 @@ it('should migrate a stylesheet', async () => { } } - @utility b { - z-index: 2; - } - @layer components { .c { z-index: 3; @@ -156,10 +160,6 @@ it('should migrate a stylesheet', async () => { .d { z-index: 4; } - } - - @utility e { - z-index: 5; }" `) }) @@ -200,6 +200,7 @@ it('should migrate a stylesheet (with imports)', async () => { border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -239,6 +240,7 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in @layer foo, bar, baz; /**! My license comment */ @import 'tailwindcss'; + /* The default border color has changed to \`currentColor\` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still @@ -256,6 +258,7 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in border-color: var(--color-gray-200, currentColor); } } + /* Form elements have a 1px border by default in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the @@ -271,6 +274,7 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in border-width: 0; } } + @layer base { html { color: red; @@ -296,12 +300,12 @@ it('should keep CSS as-is before existing `@layer` at-rules', async () => { config, ), ).toMatchInlineSnapshot(` - ".foo { - color: blue; + "@utility bar { + color: red; } - @utility bar { - color: red; + .foo { + color: blue; }" `) }) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index a059c7461395..b2c9df5cc966 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -5,6 +5,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' import { formatNodes } from './codemods/format-nodes' +import { sortBuckets } from './codemods/sort-buckets' import { help } from './commands/help' import { analyze as analyzeStylesheets, @@ -223,7 +224,7 @@ async function run() { // Format nodes for (let sheet of stylesheets) { - await postcss([formatNodes()]).process(sheet.root!, { from: sheet.file! }) + await postcss([sortBuckets(), formatNodes()]).process(sheet.root!, { from: sheet.file! }) } // Write all files to disk diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 047a6bfb680b..87b3193650c1 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -102,7 +102,8 @@ async function migrateTheme( ) let prevSectionKey = '' - let css = `@theme {` + let css = '\n@bucket theme {\n' + css += `\n@theme {\n` let containsThemeKeys = false for (let [key, value] of themeableValues(resolvedConfig.theme)) { if (typeof value !== 'string' && typeof value !== 'number') { @@ -143,7 +144,10 @@ async function migrateTheme( return null } - return css + '}\n' + css += '}\n' // @theme + css += '}\n' // @bucket + + return css } function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { @@ -155,7 +159,7 @@ function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { if (variant === '') { return '' } - return `@variant dark (${variant});\n` + return `\n@bucket variant {\n@variant dark (${variant});\n}\n` } // Returns a string identifier used to section theme declarations diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 14c9ec1588b3..e75dd3328d65 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -419,11 +419,7 @@ export async function split(stylesheets: Stylesheet[]) { } } - let utilities = postcss.root({ - raws: { - tailwind_pretty: true, - }, - }) + let utilities = postcss.root() walk(sheet.root, (node) => { if (node.type !== 'atrule') return @@ -511,7 +507,6 @@ export async function split(stylesheets: Stylesheet[]) { let newImport = node.clone({ params: `${quote}${newFile}${quote}`, raws: { - after: '\n\n', tailwind_injected_layer: node.raws.tailwind_injected_layer, tailwind_original_params: `${quote}${id}${quote}`, tailwind_destination_sheet_id: utilityDestination.id, diff --git a/packages/@tailwindcss-upgrade/src/utils/walk.ts b/packages/@tailwindcss-upgrade/src/utils/walk.ts index 7a86b7ae533d..4f34b13a09a4 100644 --- a/packages/@tailwindcss-upgrade/src/utils/walk.ts +++ b/packages/@tailwindcss-upgrade/src/utils/walk.ts @@ -15,11 +15,14 @@ interface Walkable { // 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 { +export function walk( + rule: Walkable, + cb: (rule: T, idx: number, parent: Walkable) => void | WalkAction, +): undefined | false { let result: undefined | false = undefined - rule.each?.((node) => { - let action = cb(node) ?? WalkAction.Continue + rule.each?.((node, idx) => { + let action = cb(node, idx, rule) ?? WalkAction.Continue if (action === WalkAction.Stop) { result = false return result From b32b8a4be1e7fd322d7ea56ef3bd5f199f02dfcd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 5 Nov 2024 01:06:45 +0100 Subject: [PATCH 2/9] connect dangling comments to correct "bucket" --- .../migrate-at-layer-utilities.test.ts | 5 ++- .../codemods/migrate-missing-layers.test.ts | 8 ++--- .../src/codemods/sort-buckets.ts | 34 ++++++++++++++++--- 3 files changed, 36 insertions(+), 11 deletions(-) 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 1b2afae18621..17aeb857722e 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 @@ -778,9 +778,7 @@ describe('comments', () => { /* After */ `), ).toMatchInlineSnapshot(` - "/* After */ - - /* Tailwind Utilities: */ + "/* Tailwind Utilities: */ @utility no-scrollbar { /* Chrome, Safari and Opera */ /* Second comment */ @@ -801,6 +799,7 @@ describe('comments', () => { .before { /* Inside */ } + /* After */ /* Above */ .after { 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 eee531be3ec0..307702b16c34 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.test.ts @@ -120,14 +120,14 @@ it('should migrate rules above the `@tailwind base` directive in an `@layer base "@charset "UTF-8"; @layer foo, bar, baz; - @tailwind base; - @tailwind components; - @tailwind utilities; - /**! * License header */ + @tailwind base; + @tailwind components; + @tailwind utilities; + @layer base { html { color: red; diff --git a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts index d5ea90ebc840..0ab4c8c92a76 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts @@ -50,11 +50,26 @@ export function sortBuckets(): Plugin { return WalkAction.Skip } - // Comments belong to the "next" bucket. If a bucket contains comments - // at the end, they belong to the next bucket. + // Comments belong to the bucket of the nearest node, which is typically + // in the "next" bucket. if (node.type === 'comment') { - comments.push(node) - return + // We already have comments, which means that we already have nodes + // that belong in the next bucket, so we should move the current + // comment into the next bucket as well. + if (comments.length > 0) { + comments.push(node) + return + } + + // Figure out the closest node to the comment + let prevDistance = distance(node.prev(), node) ?? Infinity + let nextDistance = distance(node, node.next()) ?? Infinity + + if (prevDistance < nextDistance) { + buckets.get(lastLayer).nodes?.push(node) + } else { + comments.push(node) + } } // Known at-rules @@ -140,3 +155,14 @@ export function sortBuckets(): Plugin { OnceExit: migrate, } } + +function distance(before?: ChildNode, after?: ChildNode): number | null { + if (!before || !after) return null + if (!before.source || !after.source) return null + if (!before.source.start || !after.source.start) return null + if (!before.source.end || !after.source.end) return null + + // Compare end of Before, to start of After + let d = Math.abs(before.source.end.line - after.source.start.line) + return d +} From 0bfd73663dbf0839840a896bd733782cb20dd369 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 5 Nov 2024 10:55:41 +0100 Subject: [PATCH 3/9] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e1e15394b3..d7fc02919358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) +- _Upgrade (experimental)_: Sort upgraded CSS ([#14866](https://github.com/tailwindlabs/tailwindcss/pull/14866)) ### Changed From 149c34612009e12d98645a64dfe6bb474ff492e6 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 6 Nov 2024 13:40:03 +0100 Subject: [PATCH 4/9] format user nodes in `@responsive` Otherwise the wrong indentation is being used. --- .../src/codemods/migrate-tailwind-directives.test.ts | 4 ++-- .../src/codemods/migrate-tailwind-directives.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) 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 61e88bef12bf..fdf480b3ef3d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts @@ -414,7 +414,7 @@ it('should replace `@responsive` with its children', async () => { `), ).toMatchInlineSnapshot(` ".foo { - color: red; - }" + color: red; + }" `) }) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts index c33f9ca01c27..d3d74a65e386 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts @@ -58,6 +58,9 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null }) // Replace Tailwind CSS v2 directives that still worked in v3. else if (node.name === 'responsive') { if (node.nodes) { + for (let child of node.nodes) { + child.raws.tailwind_pretty = true + } node.replaceWith(node.nodes) } else { node.remove() From 155d57d3f35c33f018aeeebf36588b07e92c30ea Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 6 Nov 2024 17:39:28 +0100 Subject: [PATCH 5/9] use a loop instead of separate if-else-statements --- .../src/codemods/sort-buckets.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts index 0ab4c8c92a76..13dc8a670f5b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts @@ -73,18 +73,11 @@ export function sortBuckets(): Plugin { } // Known at-rules - else if (node.type === 'atrule' && node.name === 'utility') { - injectInto('utility', node) - } else if (node.type === 'atrule' && node.name === 'variant') { - injectInto('variant', node) - } else if (node.type === 'atrule' && node.name === 'source') { - injectInto('source', node) - } else if (node.type === 'atrule' && node.name === 'theme') { - injectInto('theme', node) - } else if (node.type === 'atrule' && node.name === 'config') { - injectInto('config', node) - } else if (node.type === 'atrule' && node.name === 'plugin') { - injectInto('plugin', node) + else if ( + node.type === 'atrule' && + ['config', 'plugin', 'source', 'theme', 'utility', 'variant'].includes(node.name) + ) { + injectInto(node.name, node) } // Imports bucket, which also contains the `@charset` and body-less `@layer` From 4d2a2262e7e7243249e45a8ab5a9940a2e308916 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 6 Nov 2024 17:56:44 +0100 Subject: [PATCH 6/9] rename `@bucket` to `@tw-bucket` Since this is purely an internal API, using a `@tw-` prefix reduces the chances of colliding with real user CSS. --- .../src/codemods/format-nodes.test.ts | 6 +++--- .../src/codemods/format-nodes.ts | 12 ++++++------ .../src/codemods/migrate-border-compatibility.ts | 2 +- .../src/codemods/migrate-config.ts | 6 +++--- .../src/codemods/sort-buckets.ts | 16 ++++++++-------- .../src/migrate-js-config.ts | 6 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts index 116aa090dccd..9d6393a02b82 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.test.ts @@ -7,7 +7,7 @@ function markPretty(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/mark-pretty', OnceExit(root) { - root.walkAtRules('format', (atRule) => { + root.walkAtRules('tw-format', (atRule) => { atRule.raws.tailwind_pretty = true }) }, @@ -34,9 +34,9 @@ it('should format PostCSS nodes', async () => { }) it('should format PostCSS nodes in the `user` bucket', async () => { - expect(await migrate(`@bucket user { @format .bar { .foo { color: red; } } }`)) + expect(await migrate(`@tw-bucket user { @tw-format .bar { .foo { color: red; } } }`)) .toMatchInlineSnapshot(` - "@format .bar { + "@tw-format .bar { .foo { color: red; } diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts index fb2f4a63ca28..6261930dd321 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts @@ -14,7 +14,7 @@ export function formatNodes(): Plugin { child.raws.semicolon = true } - if (child.type === 'atrule' && child.name === 'bucket') { + if (child.type === 'atrule' && child.name === 'tw-bucket') { nodesToFormat.push(child) } else if (child.raws.tailwind_pretty) { // @ts-expect-error We might not have a parent @@ -28,12 +28,12 @@ export function formatNodes(): Plugin { // Format the nodes for (let node of nodesToFormat) { let contents = (() => { - if (node.type === 'atrule' && node.name === 'bucket') { - // Remove the `@bucket` wrapping, and use the contents directly. + if (node.type === 'atrule' && node.name === 'tw-bucket') { + // Remove the `@tw-bucket` wrapping, and use the contents directly. return node .toString() .trim() - .replace(/@bucket(.*?){([\s\S]*)}/, '$2') + .replace(/@tw-bucket(.*?){([\s\S]*)}/, '$2') } return node.toString() @@ -41,13 +41,13 @@ export function formatNodes(): Plugin { // Do not format the user bucket to ensure we keep the user's formatting // intact. - if (node.type === 'atrule' && node.name === 'bucket' && node.params === 'user') { + if (node.type === 'atrule' && node.name === 'tw-bucket' && node.params === 'user') { output.push(contents) continue } // Format buckets - if (node.type === 'atrule' && node.name === 'bucket') { + if (node.type === 'atrule' && node.name === 'tw-bucket') { output.push(await format(contents, { parser: 'css', semi: true, singleQuote: true })) continue } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts index f8e791fc2e46..a3165ae77915 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-border-compatibility.ts @@ -85,7 +85,7 @@ export function migrateBorderCompatibility({ } compatibilityCssString += BORDER_WIDTH_COMPATIBILITY_CSS - compatibilityCssString = `\n@bucket compatibility {\n${compatibilityCssString}\n}\n` + compatibilityCssString = `\n@tw-bucket compatibility {\n${compatibilityCssString}\n}\n` let compatibilityCss = postcss.parse(compatibilityCssString) // Replace the `theme(…)` with v3 values if we can't resolve the theme diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 3013e875a363..d5557ec997bd 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -38,14 +38,14 @@ export function migrateConfig( }) let css = '\n\n' - css += '\n@bucket source {' + css += '\n@tw-bucket source {' for (let source of jsConfigMigration.sources) { let absolute = path.resolve(source.base, source.pattern) css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` } css += '}\n' - css += '\n@bucket plugin {\n' + css += '\n@tw-bucket plugin {\n' for (let plugin of jsConfigMigration.plugins) { let relative = plugin.path[0] === '.' @@ -73,7 +73,7 @@ export function migrateConfig( css += '}\n' // @plugin } } - css += '}\n' // @bucket + css += '}\n' // @tw-bucket cssConfig.append(postcss.parse(css + jsConfigMigration.css)) } diff --git a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts index 13dc8a670f5b..b99badeb01b3 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts @@ -28,13 +28,13 @@ export function sortBuckets(): Plugin { let comments: Comment[] = [] let buckets = new DefaultMap((name) => { - let bucket = postcss.atRule({ name: 'bucket', params: name, nodes: [] }) + let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] }) root.append(bucket) return bucket }) // Seed the buckets with existing buckets - root.walkAtRules('bucket', (node) => { + root.walkAtRules('tw-bucket', (node) => { buckets.set(node.params, node) }) @@ -46,7 +46,7 @@ export function sortBuckets(): Plugin { walk(root, (node) => { // Already in a bucket, skip it - if (node.type === 'atrule' && node.name === 'bucket') { + if (node.type === 'atrule' && node.name === 'tw-bucket') { return WalkAction.Skip } @@ -108,9 +108,9 @@ export function sortBuckets(): Plugin { } } - // 2. Merge `@bucket` with the same name together + // 2. Merge `@tw-bucket` with the same name together let firstBuckets = new Map() - root.walkAtRules('bucket', (node) => { + root.walkAtRules('tw-bucket', (node) => { let firstBucket = firstBuckets.get(node.params) if (!firstBucket) { firstBuckets.set(node.params, node) @@ -122,14 +122,14 @@ export function sortBuckets(): Plugin { } }) - // 3. Remove empty `@bucket` - root.walkAtRules('bucket', (node) => { + // 3. Remove empty `@tw-bucket` + root.walkAtRules('tw-bucket', (node) => { if (!node.nodes?.length) { node.remove() } }) - // 4. Sort the `@bucket` themselves + // 4. Sort the `@tw-bucket` themselves { let sorted = Array.from(firstBuckets.values()).sort((a, z) => { let aIndex = BUCKET_ORDER.indexOf(a.params) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 87b3193650c1..be33b726bb4e 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -102,7 +102,7 @@ async function migrateTheme( ) let prevSectionKey = '' - let css = '\n@bucket theme {\n' + let css = '\n@tw-bucket theme {\n' css += `\n@theme {\n` let containsThemeKeys = false for (let [key, value] of themeableValues(resolvedConfig.theme)) { @@ -145,7 +145,7 @@ async function migrateTheme( } css += '}\n' // @theme - css += '}\n' // @bucket + css += '}\n' // @tw-bucket return css } @@ -159,7 +159,7 @@ function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { if (variant === '') { return '' } - return `\n@bucket variant {\n@variant dark (${variant});\n}\n` + return `\n@tw-bucket variant {\n@variant dark (${variant});\n}\n` } // Returns a string identifier used to section theme declarations From 798871b6b7105351697c218d6d6d38ecae2afba7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Nov 2024 12:57:24 +0100 Subject: [PATCH 7/9] Update packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts Co-authored-by: Philipp Spiess --- .../src/codemods/migrate-at-layer-utilities.ts | 1 - 1 file changed, 1 deletion(-) 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 8dbff28163c4..f0f11d3d66cb 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -191,7 +191,6 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { clone.name = 'utility' clone.params = cls - // Mark the node as pretty so that it gets formatted by Prettier later. clone.raws.before = `${clone.raws.before ?? ''}\n\n` } From 459ca601a6209206bd790b62db682b677a4f1e7b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Nov 2024 12:57:07 +0100 Subject: [PATCH 8/9] re-use format options --- .../src/codemods/format-nodes.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts index 6261930dd321..a5c98050576d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts @@ -1,7 +1,13 @@ import postcss, { type ChildNode, type Plugin, type Root } from 'postcss' -import { format } from 'prettier' +import { format, type Options } from 'prettier' import { walk } from '../utils/walk' +const FORMAT_OPTIONS: Options = { + parser: 'css', + semi: true, + singleQuote: true, +} + // 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 { @@ -48,14 +54,14 @@ export function formatNodes(): Plugin { // Format buckets if (node.type === 'atrule' && node.name === 'tw-bucket') { - output.push(await format(contents, { parser: 'css', semi: true, singleQuote: true })) + output.push(await format(contents, FORMAT_OPTIONS)) continue } // Format any other nodes node.replaceWith( postcss.parse( - `${node.raws.before ?? ''}${(await format(contents, { parser: 'css', semi: true, singleQuote: true })).trim()}`, + `${node.raws.before ?? ''}${(await format(contents, FORMAT_OPTIONS)).trim()}`, ), ) } From 50e48408beb07b3c08f408ab2c31ff2ce999c593 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 7 Nov 2024 12:59:04 +0100 Subject: [PATCH 9/9] drop changelog entry --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7fc02919358..a9e1e15394b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure adjacent rules are merged together after handling nesting when generating optimized CSS ([#14873](https://github.com/tailwindlabs/tailwindcss/pull/14873)) - _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830)) - _Upgrade (experimental)_: Remove whitespace around `,` separator when print arbitrary values ([#14838](https://github.com/tailwindlabs/tailwindcss/pull/14838)) -- _Upgrade (experimental)_: Sort upgraded CSS ([#14866](https://github.com/tailwindlabs/tailwindcss/pull/14866)) ### Changed