mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Sort upgraded CSS (#14866)
During the migration process, a lot of changes to the CSS file happen.
Some parts are converted, some parts are deleted and some new CSS is
added.
To make sure we are generating a sensible and good looking CSS file, we
will sort the final CSS and pretty print it.
The order we came up with looks like this:
```css
/* Imports */
@import "tailwindcss";
@import "../other.css";
/* Configuration */
@config "../path/to/tailwindcss.config.js";
@plugin "my-plugin-1";
@plugin "my-plugin-2";
@source "./foo/**/*.ts";
@source "./bar/**/*.ts";
@variant foo {}
@variant bar {}
@theme {}
/* Border compatibility CSS */
@layer base {}
/* Utilities */
@utility foo {}
@utility bar {}
/* Rest of your own CSS if any */
```
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
26638af3ef
commit
462308d8d7
@ -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,44 +1497,11 @@ test(
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
@config "../tailwind.config.ts";
|
||||
|
||||
--- ./src/root.3.css ---
|
||||
/* Inject missing @config above first @theme */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@config '../tailwind.config.ts';
|
||||
|
||||
@variant hocus (&:hover, &:focus);
|
||||
@ -1537,10 +1514,45 @@ test(
|
||||
--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
|
||||
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/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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
|
||||
@ -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('tw-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(`@tw-bucket user { @tw-format .bar { .foo { color: red; } } }`))
|
||||
.toMatchInlineSnapshot(`
|
||||
"@tw-format .bar {
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { parse, type ChildNode, type Plugin, type Root } from 'postcss'
|
||||
import { format } from 'prettier'
|
||||
import { walk, WalkAction } from '../utils/walk'
|
||||
import postcss, { type ChildNode, type Plugin, type Root } from 'postcss'
|
||||
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.
|
||||
@ -8,26 +14,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 === 'tw-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 === 'tw-bucket') {
|
||||
// Remove the `@tw-bucket` wrapping, and use the contents directly.
|
||||
return node
|
||||
.toString()
|
||||
.trim()
|
||||
.replace(/@tw-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 === 'tw-bucket' && node.params === 'user') {
|
||||
output.push(contents)
|
||||
continue
|
||||
}
|
||||
|
||||
// Format buckets
|
||||
if (node.type === 'atrule' && node.name === 'tw-bucket') {
|
||||
output.push(await format(contents, FORMAT_OPTIONS))
|
||||
continue
|
||||
}
|
||||
|
||||
// Format any other nodes
|
||||
node.replaceWith(
|
||||
postcss.parse(
|
||||
`${node.raws.before ?? ''}${(await format(contents, FORMAT_OPTIONS)).trim()}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
root.removeAll()
|
||||
root.append(
|
||||
postcss.parse(
|
||||
output
|
||||
.map((bucket) => bucket.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n\n'),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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,13 +778,7 @@ describe('comments', () => {
|
||||
/* After */
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Above */
|
||||
.before {
|
||||
/* Inside */
|
||||
}
|
||||
/* After */
|
||||
|
||||
/* Tailwind Utilities: */
|
||||
"/* Tailwind Utilities: */
|
||||
@utility no-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
/* Second comment */
|
||||
@ -799,6 +795,12 @@ describe('comments', () => {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Above */
|
||||
.before {
|
||||
/* Inside */
|
||||
}
|
||||
/* After */
|
||||
|
||||
/* Above */
|
||||
.after {
|
||||
/* Inside */
|
||||
@ -925,14 +927,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 +976,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 +998,17 @@ describe('layered stylesheets', () => {
|
||||
/* Utility #3 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
#main {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
#secondary {
|
||||
color: red;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -191,8 +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.tailwind_pretty = true
|
||||
clone.raws.before = `${clone.raws.before ?? ''}\n\n`
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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@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
|
||||
@ -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 {
|
||||
|
||||
@ -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<Stylesheet, string[]>()
|
||||
|
||||
@ -39,14 +38,14 @@ export function migrateConfig(
|
||||
})
|
||||
|
||||
let css = '\n\n'
|
||||
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`
|
||||
}
|
||||
if (jsConfigMigration.sources.length > 0) {
|
||||
css = css + '\n'
|
||||
}
|
||||
css += '}\n'
|
||||
|
||||
css += '\n@tw-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' // @tw-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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -122,15 +124,15 @@ it('should migrate rules above the `@tailwind base` directive in an `@layer base
|
||||
* License header
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
161
packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts
Normal file
161
packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts
Normal file
@ -0,0 +1,161 @@
|
||||
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<string, AtRule>((name) => {
|
||||
let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] })
|
||||
root.append(bucket)
|
||||
return bucket
|
||||
})
|
||||
|
||||
// Seed the buckets with existing buckets
|
||||
root.walkAtRules('tw-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 === 'tw-bucket') {
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
// Comments belong to the bucket of the nearest node, which is typically
|
||||
// in the "next" bucket.
|
||||
if (node.type === 'comment') {
|
||||
// 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
|
||||
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`
|
||||
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 `@tw-bucket` with the same name together
|
||||
let firstBuckets = new Map<string, AtRule>()
|
||||
root.walkAtRules('tw-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 `@tw-bucket`
|
||||
root.walkAtRules('tw-bucket', (node) => {
|
||||
if (!node.nodes?.length) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// 4. Sort the `@tw-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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -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;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -102,7 +102,8 @@ async function migrateTheme(
|
||||
)
|
||||
|
||||
let prevSectionKey = ''
|
||||
let css = `@theme {`
|
||||
let css = '\n@tw-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' // @tw-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@tw-bucket variant {\n@variant dark (${variant});\n}\n`
|
||||
}
|
||||
|
||||
// Returns a string identifier used to section theme declarations
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -15,11 +15,14 @@ interface Walkable<T> {
|
||||
|
||||
// Custom walk implementation where we can skip going into nodes when we don't
|
||||
// need to process them.
|
||||
export function walk<T>(rule: Walkable<T>, cb: (rule: T) => void | WalkAction): undefined | false {
|
||||
export function walk<T>(
|
||||
rule: Walkable<T>,
|
||||
cb: (rule: T, idx: number, parent: Walkable<T>) => 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user