From e326b54df43459bb48d5fedf921c234ae9c3f895 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 31 Mar 2025 17:35:23 +0200 Subject: [PATCH 01/34] add semver dependency --- packages/@tailwindcss-upgrade/package.json | 8 ++-- pnpm-lock.yaml | 45 ++++++++++++---------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index 495499020786..075feff64b38 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -40,13 +40,15 @@ "postcss-import": "^16.1.0", "postcss-selector-parser": "^7.1.0", "prettier": "catalog:", + "semver": "^7.7.1", + "tailwindcss": "workspace:*", "tree-sitter": "^0.22.4", - "tree-sitter-typescript": "^0.23.2", - "tailwindcss": "workspace:*" + "tree-sitter-typescript": "^0.23.2" }, "devDependencies": { "@types/braces": "^3.0.5", "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3" + "@types/postcss-import": "^14.0.3", + "@types/semver": "^7.7.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0f83beec49..e537ce9f6c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,6 +388,9 @@ importers: prettier: specifier: 'catalog:' version: 3.5.0 + semver: + specifier: ^7.7.1 + version: 7.7.1 tailwindcss: specifier: workspace:* version: link:../tailwindcss @@ -407,6 +410,9 @@ importers: '@types/postcss-import': specifier: ^14.0.3 version: 14.0.3 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 packages/@tailwindcss-vite: dependencies: @@ -2057,7 +2063,6 @@ packages: '@parcel/watcher-darwin-arm64@2.5.1': resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.5.0': @@ -2069,7 +2074,6 @@ packages: '@parcel/watcher-darwin-x64@2.5.1': resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.5.0': @@ -2117,7 +2121,6 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.5.0': @@ -2129,7 +2132,6 @@ packages: '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.5.0': @@ -2141,7 +2143,6 @@ packages: '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.5.0': @@ -2153,7 +2154,6 @@ packages: '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-wasm@2.5.0': @@ -2195,7 +2195,6 @@ packages: '@parcel/watcher-win32-x64@2.5.1': resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [win32] '@parcel/watcher@2.5.0': @@ -2385,6 +2384,9 @@ packages: '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/ws@8.5.12': resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} @@ -2615,7 +2617,6 @@ packages: bun@1.2.8: resolution: {integrity: sha512-X8r9UuXAruvpE37u/JVfvJI8KRdFf9hUdTLw00DMScnBe7Xerawd/VvmFVT9Y/NrmXDAdDp0Dm6N6bulZYTGvA==} - cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -5973,6 +5974,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/semver@7.7.0': {} + '@types/ws@8.5.12': dependencies: '@types/node': 20.14.13 @@ -6783,7 +6786,7 @@ snapshots: eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.1(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.37.2(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.0.0(eslint@9.24.0(jiti@2.4.2)) @@ -6803,7 +6806,7 @@ snapshots: eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.1(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.37.2(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.0.0(eslint@9.24.0(jiti@2.4.2)) @@ -6828,13 +6831,13 @@ snapshots: debug: 4.4.0 enhanced-resolve: 5.18.1 eslint: 9.24.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) fast-glob: 3.3.3 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -6847,20 +6850,20 @@ snapshots: debug: 4.4.0 enhanced-resolve: 5.18.1 eslint: 9.24.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) fast-glob: 3.3.3 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -6871,7 +6874,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -6882,7 +6885,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.24.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -6893,7 +6896,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -6911,7 +6914,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.24.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -6922,7 +6925,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7314,7 +7317,7 @@ snapshots: is-bun-module@1.2.1: dependencies: - semver: 7.6.3 + semver: 7.7.1 is-callable@1.2.7: {} From 83bf84581b2f5d1b616dbfa4bae863ae9c196908 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 1 Apr 2025 17:22:07 +0200 Subject: [PATCH 02/34] do not require Tailwind CSS v3 projects anymore --- packages/@tailwindcss-upgrade/src/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 75bd47b523a4..ffcbc8df1d98 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -20,7 +20,6 @@ import { Stylesheet } from './stylesheet' import { args, type Arg } from './utils/args' import { isRepoDirty } from './utils/git' import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts' -import { getPackageVersion } from './utils/package-version' import { pkg } from './utils/packages' import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer' @@ -59,15 +58,6 @@ async function run() { } } - // Require an installed `tailwindcss` version < 4 - let tailwindVersion = await getPackageVersion('tailwindcss', base) - if (tailwindVersion && Number(tailwindVersion.split('.')[0]) !== 3) { - error( - `Tailwind CSS v${tailwindVersion} found. The migration tool can only be run on v3 projects.`, - ) - process.exit(1) - } - { // Stylesheet migrations From 2879f4ef6317476d7f8d5744a69a524b13166ee1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 11:32:12 +0200 Subject: [PATCH 03/34] add `version` related helpers --- .../src/utils/package-version.ts | 12 ++++++++++ .../@tailwindcss-upgrade/src/utils/version.ts | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/src/utils/version.ts diff --git a/packages/@tailwindcss-upgrade/src/utils/package-version.ts b/packages/@tailwindcss-upgrade/src/utils/package-version.ts index 833ccadf29c9..64a1e7ed1f30 100644 --- a/packages/@tailwindcss-upgrade/src/utils/package-version.ts +++ b/packages/@tailwindcss-upgrade/src/utils/package-version.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import fs from 'node:fs/promises' import { resolveJsId } from './resolve' @@ -15,3 +16,14 @@ export async function getPackageVersion(pkg: string, base: string): Promise=${version}.0.0 <${version + 1}.0.0`) +} + +let cache = new DefaultMap((base) => { + let tailwindVersion = getPackageVersionSync('tailwindcss', base) + if (!tailwindVersion) throw new Error('Tailwind CSS is not installed') + return tailwindVersion +}) + +function installedTailwindVersion(base = process.cwd()): string { + return cache.get(base) +} From ba315ec1ddc98acb0a96b35a05ce94290ad144c1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 11:31:24 +0200 Subject: [PATCH 04/34] conditionally apply migrations based on version number --- .../src/codemods/template/migrate-legacy-classes.ts | 10 ++++++++++ .../template/migrate-simple-legacy-classes.ts | 12 ++++++++++-- .../src/codemods/template/migrate-variant-order.ts | 10 ++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts index 8f3835c8f955..bb872c0be07a 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts @@ -5,6 +5,7 @@ import type { Candidate } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import * as version from '../../utils/version' import { printCandidate } from './candidates' import { isSafeMigration } from './is-safe-migration' @@ -75,6 +76,15 @@ export async function migrateLegacyClasses( end: number }, ): Promise { + // These migrations are only safe when migrating from v3 to v4. + // + // Migrating from `rounded` to `rounded-sm` once is fine (v3 -> v4). But if we + // migrate again (v4 -> v4), then `rounded-sm` would be migrated to + // `rounded-xs` which is incorrect because we already migrated this. + if (!version.isMajor(3)) { + return rawCandidate + } + let defaultDesignSystem = await DESIGN_SYSTEMS.get(__dirname) function* migrate(rawCandidate: string) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts index dd6599152c7f..1f50c295c468 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts @@ -1,10 +1,11 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import * as version from '../../utils/version' import { printCandidate } from './candidates' // Classes that used to exist in Tailwind CSS v3, but do not exist in Tailwind // CSS v4 anymore. -const LEGACY_CLASS_MAP = { +const LEGACY_CLASS_MAP: Record = { 'overflow-ellipsis': 'text-ellipsis', 'flex-grow': 'grow', @@ -14,8 +15,15 @@ const LEGACY_CLASS_MAP = { 'decoration-clone': 'box-decoration-clone', 'decoration-slice': 'box-decoration-slice', +} - 'outline-none': 'outline-hidden', +// `outline-none` in v3 has the same meaning as `outline-hidden` in v4. However, +// `outline-none` in v4 _also_ exists but has a different meaning. +// +// We can only migrate `outline-none` to `outline-hidden` if we are migrating a +// v3 project to v4. +if (version.isMajor(3)) { + LEGACY_CLASS_MAP['outline-none'] = 'outline-hidden' } let seenDesignSystems = new WeakSet() diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts index ab18b6b78476..76feb2174756 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts @@ -2,6 +2,7 @@ import { walk, type AstNode } from '../../../../tailwindcss/src/ast' import { type Variant } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import * as version from '../../utils/version' import { printCandidate } from './candidates' export function migrateVariantOrder( @@ -9,6 +10,15 @@ export function migrateVariantOrder( _userConfig: Config, rawCandidate: string, ): string { + // This migration is only needed for Tailwind CSS v3 + // + // Changing the variant order when migrating from v3 to v4 is fine, but + // migrating v4 to v4 would make it unsafe because the variant order would + // flip-flop every time you run the migration. + if (!version.isMajor(3)) { + return rawCandidate + } + for (let candidate of designSystem.parseCandidate(rawCandidate)) { if (candidate.variants.length <= 1) { continue From 0f97d3e02c956a012adb290f23798e58816d9531 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 11:59:35 +0200 Subject: [PATCH 05/34] only link stylesheets to JS config files when migrating Tailwind CSS v3 projects --- packages/@tailwindcss-upgrade/src/index.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index ffcbc8df1d98..ff4a464376e7 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -22,6 +22,7 @@ import { isRepoDirty } from './utils/git' import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts' import { pkg } from './utils/packages' import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer' +import * as version from './utils/version' const options = { '--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' }, @@ -98,14 +99,17 @@ async function run() { error(`${e?.message ?? e}`, { prefix: '↳ ' }) } - // Ensure stylesheets are linked to configs - try { - await linkConfigsToStylesheets(stylesheets, { - configPath: flags['--config'], - base, - }) - } catch (e: any) { - error(`${e?.message ?? e}`, { prefix: '↳ ' }) + // Ensure stylesheets are linked to configs. But this is only necessary when + // migrating from v3 to v4. + if (version.isMajor(3)) { + try { + await linkConfigsToStylesheets(stylesheets, { + configPath: flags['--config'], + base, + }) + } catch (e: any) { + error(`${e?.message ?? e}`, { prefix: '↳ ' }) + } } // Migrate js config files, linked to stylesheets From 6bef56eae01f6177f92350d798086e1f359f5345 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 12:01:07 +0200 Subject: [PATCH 06/34] only split stylesheets when migrating Tailwind CSS v3 projects --- packages/@tailwindcss-upgrade/src/index.ts | 48 +++++++++++----------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index ff4a464376e7..1ba606571235 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -210,31 +210,33 @@ async function run() { ) // Split up stylesheets (as needed) - try { - await splitStylesheets(stylesheets) - } catch (e: any) { - error(`${e?.message ?? e}`, { prefix: '↳ ' }) - } + if (version.isMajor(3)) { + try { + await splitStylesheets(stylesheets) + } catch (e: any) { + error(`${e?.message ?? e}`, { prefix: '↳ ' }) + } - // Cleanup `@import "…" layer(utilities)` - for (let sheet of stylesheets) { - for (let importRule of sheet.importRules) { - if (!importRule.raws.tailwind_injected_layer) continue - let importedSheet = stylesheets.find( - (sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id, - ) - if (!importedSheet) continue - - // Only remove the `layer(…)` next to the import if any of the children - // contain `@utility`. Otherwise `@utility` will not be top-level. - if ( - !importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility') - ) { - continue - } + // Cleanup `@import "…" layer(utilities)` + for (let sheet of stylesheets) { + for (let importRule of sheet.importRules) { + if (!importRule.raws.tailwind_injected_layer) continue + let importedSheet = stylesheets.find( + (sheet) => sheet.id === importRule.raws.tailwind_destination_sheet_id, + ) + if (!importedSheet) continue + + // Only remove the `layer(…)` next to the import if any of the children + // contain `@utility`. Otherwise `@utility` will not be top-level. + if ( + !importedSheet.containsRule((node) => node.type === 'atrule' && node.name === 'utility') + ) { + continue + } - // Make sure to remove the `layer(…)` from the `@import` at-rule - importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim() + // Make sure to remove the `layer(…)` from the `@import` at-rule + importRule.params = importRule.params.replace(/ layer\([^)]+\)/, '').trim() + } } } From 986092a936f9c87382ddc3e743005c16a13f3f92 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 12:01:36 +0200 Subject: [PATCH 07/34] only migrate PostCSS config when migrating Tailwind CSS v3 projects --- packages/@tailwindcss-upgrade/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 1ba606571235..3c5b51f12c88 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -257,7 +257,7 @@ async function run() { } } - { + if (version.isMajor(3)) { // PostCSS config migration await migratePostCSSConfig(base) } From cfaeb5530138f8b4b3e15cf7bbbdb59405641fa0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 11:56:23 +0200 Subject: [PATCH 08/34] only migrate JS files if they have linked config paths Once we are in v4, it could be that you have `@config` still, and maybe we are able to migrate this stylesheet. But if no `@config` exists, then there is no need to migrate the JS configuration file This is just for printing the header. --- packages/@tailwindcss-upgrade/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 3c5b51f12c88..24684adb4cbc 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -113,7 +113,7 @@ async function run() { } // Migrate js config files, linked to stylesheets - if (stylesheets.some((sheet) => sheet.isTailwindRoot)) { + if (stylesheets.some((sheet) => sheet.isTailwindRoot && sheet.linkedConfigPath)) { info('Migrating JavaScript configuration files…') } let configBySheet = new Map>>() From 2f6e36883585296dda49e8fbdd343d2b58b784f1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 15:15:12 +0200 Subject: [PATCH 09/34] make `jsConfigMigration` nullable --- .../src/codemods/css/migrate-config.ts | 2 +- .../@tailwindcss-upgrade/src/codemods/css/migrate.ts | 2 +- packages/@tailwindcss-upgrade/src/index.ts | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts index 3149c2725e69..4dc80a83b09d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts @@ -11,7 +11,7 @@ export function migrateConfig( { configFilePath, jsConfigMigration, - }: { configFilePath: string; jsConfigMigration: JSConfigMigration }, + }: { configFilePath: string; jsConfigMigration: JSConfigMigration | null }, ): Plugin { function migrate() { if (!sheet.isTailwindRoot) return diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts index ab77bab9f120..33722bb25757 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -19,7 +19,7 @@ export interface MigrateOptions { designSystem: DesignSystem userConfig: Config configFilePath: string - jsConfigMigration: JSConfigMigration + jsConfigMigration: JSConfigMigration | null } export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 24684adb4cbc..c97069907f36 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -123,6 +123,7 @@ async function run() { >() for (let sheet of stylesheets) { if (!sheet.isTailwindRoot) continue + if (!sheet.linkedConfigPath) continue let config = await prepareConfig(sheet.linkedConfigPath, { base }) configBySheet.set(sheet, config) @@ -147,7 +148,7 @@ async function run() { } } - // Migrate source files, linked to config files + // Migrate source files if (configBySheet.size > 0) { info('Migrating templates…') } @@ -190,19 +191,22 @@ async function run() { stylesheets.map(async (sheet) => { try { let config = configBySheet.get(sheet)! - let jsConfigMigration = jsConfigMigrationBySheet.get(sheet)! + let jsConfigMigration = jsConfigMigrationBySheet.get(sheet) if (!config) { for (let parent of sheet.ancestors()) { if (parent.isTailwindRoot) { config ??= configBySheet.get(parent)! - jsConfigMigration ??= jsConfigMigrationBySheet.get(parent)! + jsConfigMigration ??= jsConfigMigrationBySheet.get(parent) break } } } - await migrateStylesheet(sheet, { ...config, jsConfigMigration }) + await migrateStylesheet(sheet, { + ...config, + jsConfigMigration: jsConfigMigration ?? null, + }) } catch (e: any) { error(`${e?.message ?? e} in ${highlight(relative(sheet.file!, base))}`, { prefix: '↳ ' }) } From f78f582e21e06c33b9aba9398c34e828f37d77a4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 15:20:41 +0200 Subject: [PATCH 10/34] change order This swaps the "migrate stylesheets" and "migrate source files" steps. This will make things easier _if_ we don't even need to migrate JS config files and can rely on the CSS file itself. This will happen if you run the upgrade tool in Tailwind CSS v4 projects. --- packages/@tailwindcss-upgrade/src/index.ts | 70 +++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index c97069907f36..ebd5841c7862 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -148,41 +148,6 @@ async function run() { } } - // Migrate source files - if (configBySheet.size > 0) { - info('Migrating templates…') - } - { - // Template migrations - for (let config of configBySheet.values()) { - let set = new Set() - for (let globEntry of config.sources.flatMap((entry) => hoistStaticGlobParts(entry))) { - let files = await globby([globEntry.pattern], { - absolute: true, - gitignore: true, - cwd: globEntry.base, - }) - - for (let file of files) { - set.add(file) - } - } - - let files = Array.from(set) - files.sort() - - // Migrate each file - await Promise.allSettled( - files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)), - ) - - success( - `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, - { prefix: '↳ ' }, - ) - } - } - // Migrate each CSS file if (stylesheets.length > 0) { info('Migrating stylesheets…') @@ -259,6 +224,41 @@ async function run() { success(`Migrated stylesheet: ${highlight(relative(sheet.file, base))}`, { prefix: '↳ ' }) } } + + // Migrate source files + if (configBySheet.size > 0) { + info('Migrating templates…') + } + { + // Template migrations + for (let config of configBySheet.values()) { + let set = new Set() + for (let globEntry of config.sources.flatMap((entry) => hoistStaticGlobParts(entry))) { + let files = await globby([globEntry.pattern], { + absolute: true, + gitignore: true, + cwd: globEntry.base, + }) + + for (let file of files) { + set.add(file) + } + } + + let files = Array.from(set) + files.sort() + + // Migrate each file + await Promise.allSettled( + files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)), + ) + + success( + `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, + { prefix: '↳ ' }, + ) + } + } } if (version.isMajor(3)) { From b6089e45aef3e8fec55caed5cca02174d151fe12 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 15:43:39 +0200 Subject: [PATCH 11/34] bail on empty config We will improve this in a future commit so we can still migrate `@apply` for example in Tailwind CSS v4 -> v4 migrations. --- packages/@tailwindcss-upgrade/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index ebd5841c7862..7b9a445a29dd 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -155,7 +155,7 @@ async function run() { await Promise.all( stylesheets.map(async (sheet) => { try { - let config = configBySheet.get(sheet)! + let config = configBySheet.get(sheet) let jsConfigMigration = jsConfigMigrationBySheet.get(sheet) if (!config) { @@ -168,6 +168,8 @@ async function run() { } } + if (!config) return + await migrateStylesheet(sheet, { ...config, jsConfigMigration: jsConfigMigration ?? null, From a2836e53b50823f0642afe6916b6cdb3653b9b7a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 15:57:24 +0200 Subject: [PATCH 12/34] make `UserConfig` nullable --- .../src/codemods/css/migrate-at-apply.ts | 2 +- .../src/codemods/css/migrate-media-screen.ts | 2 +- .../src/codemods/css/migrate-preflight.ts | 2 +- packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts | 2 +- .../template/migrate-arbitrary-value-to-bare-value.ts | 2 +- .../codemods/template/migrate-automatic-var-injection.ts | 2 +- .../src/codemods/template/migrate-bg-gradient.ts | 2 +- .../template/migrate-handle-empty-arbitrary-values.ts | 2 +- .../src/codemods/template/migrate-important.ts | 2 +- .../codemods/template/migrate-legacy-arbitrary-values.ts | 2 +- .../src/codemods/template/migrate-legacy-classes.ts | 2 +- .../src/codemods/template/migrate-max-width-screen.ts | 2 +- .../template/migrate-modernize-arbitrary-values.ts | 2 +- .../src/codemods/template/migrate-prefix.ts | 5 ++++- .../codemods/template/migrate-simple-legacy-classes.ts | 2 +- .../src/codemods/template/migrate-theme-to-var.ts | 2 +- .../src/codemods/template/migrate-variant-order.ts | 2 +- .../@tailwindcss-upgrade/src/codemods/template/migrate.ts | 8 ++++---- 18 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts index ffcb12619479..752f240c8267 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts @@ -9,7 +9,7 @@ export function migrateAtApply({ userConfig, }: { designSystem: DesignSystem - userConfig: Config + userConfig: Config | null }): Plugin { function migrate(atRule: AtRule) { let utilities = atRule.params.split(/(\s+)/) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts index 57cc20eeb45b..2310ec723240 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts @@ -10,7 +10,7 @@ export function migrateMediaScreen({ userConfig, }: { designSystem?: DesignSystem - userConfig?: Config + userConfig?: Config | null } = {}): Plugin { function migrate(root: Root) { if (!designSystem || !userConfig) return diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts index 32172069100d..373ddf1039f3 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts @@ -35,7 +35,7 @@ export function migratePreflight({ userConfig, }: { designSystem: DesignSystem - userConfig?: Config + userConfig?: Config | null }): Plugin { // @ts-expect-error let defaultBorderColor = userConfig?.theme?.borderColor?.DEFAULT diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts index 33722bb25757..cfeb917b161f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -17,7 +17,7 @@ import { migrateVariantsDirective } from './migrate-variants-directive' export interface MigrateOptions { newPrefix: string | null designSystem: DesignSystem - userConfig: Config + userConfig: Config | null configFilePath: string jsConfigMigration: JSConfigMigration | null } diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts index 6b01bb7437e6..fd26a9a041cb 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.ts @@ -7,7 +7,7 @@ import { printCandidate } from './candidates' export function migrateArbitraryValueToBareValue( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of parseCandidate(rawCandidate, designSystem)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts index dfb702ed8657..9fd1aaaf7594 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts @@ -6,7 +6,7 @@ import { printCandidate } from './candidates' export function migrateAutomaticVarInjection( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts index a056adae6dbf..2a16524a2b1c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-bg-gradient.ts @@ -6,7 +6,7 @@ const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] export function migrateBgGradient( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of designSystem.parseCandidate(rawCandidate)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts index 0c4fddb11bc9..0058814d9a3c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.ts @@ -3,7 +3,7 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system' export function migrateEmptyArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { // We can parse the candidate, nothing to do diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts index 576557fb98d5..34b6d4ef2f01 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts @@ -18,7 +18,7 @@ import { isSafeMigration } from './is-safe-migration' // flex! md:block! export function migrateImportant( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, location?: { contents: string diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts index b2bd44d07a06..784480521082 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts @@ -6,7 +6,7 @@ import { printCandidate } from './candidates' export function migrateLegacyArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of parseCandidate(rawCandidate, designSystem)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts index bb872c0be07a..2a231bb3daa7 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts @@ -68,7 +68,7 @@ const DESIGN_SYSTEMS = new DefaultMap((base) => { export async function migrateLegacyClasses( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, location?: { contents: string diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts index 92fec5099546..f7e04dbf194d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-max-width-screen.ts @@ -4,7 +4,7 @@ import { printCandidate } from './candidates' export function migrateMaxWidthScreen( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of designSystem.parseCandidate(rawCandidate)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index f0eec7485a29..fbdecf285096 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -15,7 +15,7 @@ function memcpy(target: T, source: U) export function migrateModernizeArbitraryValues( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { for (let candidate of parseCandidate(rawCandidate, designSystem)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts index 95bb4c69c3ee..3e55dddf06d5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts @@ -2,16 +2,19 @@ import { parseCandidate, type Candidate } from '../../../../tailwindcss/src/cand import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { segment } from '../../../../tailwindcss/src/utils/segment' +import * as version from '../../utils/version' import { printCandidate } from './candidates' let seenDesignSystems = new WeakSet() export function migratePrefix( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, ): string { if (!designSystem.theme.prefix) return rawCandidate + if (!userConfig) return rawCandidate + if (!version.isMajor(3)) return rawCandidate if (!seenDesignSystems.has(designSystem)) { designSystem.utilities.functional('group', () => null) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts index 1f50c295c468..f3bb60a0499b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts @@ -30,7 +30,7 @@ let seenDesignSystems = new WeakSet() export function migrateSimpleLegacyClasses( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { // Prepare design system with the unknown legacy classes diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts index 3abb4c96fcc4..6e99d6f9482d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-theme-to-var.ts @@ -21,7 +21,7 @@ export const enum Convert { export function migrateThemeToVar( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { let convert = createConverter(designSystem) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts index 76feb2174756..f9da9ecc28ec 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts @@ -7,7 +7,7 @@ import { printCandidate } from './candidates' export function migrateVariantOrder( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, rawCandidate: string, ): string { // This migration is only needed for Tailwind CSS v3 diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index c18e2f25e6a6..99b2a4d97ed0 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -20,7 +20,7 @@ import { migrateVariantOrder } from './migrate-variant-order' export type Migration = ( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, location?: { contents: string @@ -47,7 +47,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [ export async function migrateCandidate( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, // Location is only set when migrating a candidate from a source file location?: { @@ -64,7 +64,7 @@ export async function migrateCandidate( export default async function migrateContents( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, contents: string, extension: string, ): Promise { @@ -93,7 +93,7 @@ export default async function migrateContents( return spliceChangesIntoString(contents, changes) } -export async function migrate(designSystem: DesignSystem, userConfig: Config, file: string) { +export async function migrate(designSystem: DesignSystem, userConfig: Config | null, file: string) { let fullPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file) let contents = await fs.readFile(fullPath, 'utf-8') From c52201f6e91f18ad8a3ca096c31bbb49f795c3f8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 16:20:11 +0200 Subject: [PATCH 13/34] migrate source files based on Tailwind root stylesheets --- packages/@tailwindcss-upgrade/src/index.ts | 69 +++++++++++++------ .../@tailwindcss-upgrade/src/stylesheet.ts | 21 ++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 7b9a445a29dd..67e152467519 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import { Scanner } from '@tailwindcss/oxide' import { globby } from 'globby' import fs from 'node:fs/promises' import path from 'node:path' @@ -19,7 +20,6 @@ import { help } from './commands/help' import { Stylesheet } from './stylesheet' import { args, type Arg } from './utils/args' import { isRepoDirty } from './utils/git' -import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts' import { pkg } from './utils/packages' import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer' import * as version from './utils/version' @@ -227,38 +227,67 @@ async function run() { } } + let tailwindRootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot && sheet.file) + // Migrate source files - if (configBySheet.size > 0) { + if (tailwindRootStylesheets.length > 0) { info('Migrating templates…') } { + let seenFiles = new Set() + // Template migrations - for (let config of configBySheet.values()) { - let set = new Set() - for (let globEntry of config.sources.flatMap((entry) => hoistStaticGlobParts(entry))) { - let files = await globby([globEntry.pattern], { - absolute: true, - gitignore: true, - cwd: globEntry.base, - }) + for (let sheet of tailwindRootStylesheets) { + let compiler = await sheet.compiler() + if (!compiler) continue + let designSystem = await sheet.designSystem() + if (!designSystem) continue + + // Figure out the source files to migrate + let sources = (() => { + // Disable auto source detection + if (compiler.root === 'none') { + return [] + } - for (let file of files) { - set.add(file) + // No root specified, use the base directory + if (compiler.root === null) { + return [{ base, pattern: '**/*', negated: false }] } - } - let files = Array.from(set) - files.sort() + // Use the specified root + return [{ ...compiler.root, negated: false }] + })().concat(compiler.sources) + + let config = configBySheet.get(sheet) + let scanner = new Scanner({ sources }) + let filesToMigrate = [] + for (let file of scanner.files) { + if (seenFiles.has(file)) continue + seenFiles.add(file) + filesToMigrate.push(file) + } // Migrate each file await Promise.allSettled( - files.map((file) => migrateTemplate(config.designSystem, config.userConfig, file)), + filesToMigrate.map((file) => + migrateTemplate(designSystem, config?.userConfig ?? null, file), + ), ) - success( - `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, - { prefix: '↳ ' }, - ) + if (config?.configFilePath) { + success( + `Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`, + { prefix: '↳ ' }, + ) + } else { + success( + `Migrated templates for: ${highlight(relative(sheet.file ?? '', base))}`, + { + prefix: '↳ ', + }, + ) + } } } } diff --git a/packages/@tailwindcss-upgrade/src/stylesheet.ts b/packages/@tailwindcss-upgrade/src/stylesheet.ts index 092d9414b569..42cd1f3c6210 100644 --- a/packages/@tailwindcss-upgrade/src/stylesheet.ts +++ b/packages/@tailwindcss-upgrade/src/stylesheet.ts @@ -1,8 +1,10 @@ +import { __unstable__loadDesignSystem, compileAst } from '@tailwindcss/node' import * as fsSync from 'node:fs' import * as fs from 'node:fs/promises' import * as path from 'node:path' import * as util from 'node:util' import * as postcss from 'postcss' +import { postCssAstToCssAst } from '../../@tailwindcss-postcss/src/ast' export type StylesheetId = string @@ -263,6 +265,25 @@ export class Stylesheet { return false } + async compiler(): Promise> | null> { + if (!this.isTailwindRoot) return null + if (!this.file) return null + + return compileAst(postCssAstToCssAst(this.root), { + base: path.dirname(this.file), + onDependency() {}, + }) + } + + async designSystem(): Promise> | null> { + if (!this.isTailwindRoot) return null + if (!this.file) return null + + return __unstable__loadDesignSystem(this.root.toString(), { + base: path.dirname(this.file), + }) + } + [util.inspect.custom]() { return { ...this, From eacaabfb3c33548208a2d947cffe9a3b38ff16d4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 16:20:44 +0200 Subject: [PATCH 14/34] remove unused `hoistStaticGlobParts` We will be using the `Scanner` from `@tailwindcss/oxide` instead. Since we already migrated the JS config files and linked the potential diverging `content` array to `@source` directives it means we can rely on the scanner. --- .../src/utils/hoist-static-glob-parts.test.ts | 48 ----------- .../src/utils/hoist-static-glob-parts.ts | 82 ------------------- 2 files changed, 130 deletions(-) delete mode 100644 packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts delete mode 100644 packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts diff --git a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts b/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts deleted file mode 100644 index 1e6d8b1e861e..000000000000 --- a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, it } from 'vitest' -import { hoistStaticGlobParts } from './hoist-static-glob-parts' - -it.each([ - // A basic glob - [ - { base: '/projects/project-a', pattern: './src/**/*.html' }, - [{ base: '/projects/project-a/src', pattern: '**/*.html' }], - ], - - // A glob pointing to a folder should result in `**/*` - [ - { base: '/projects/project-a', pattern: './src' }, - [{ base: '/projects/project-a/src', pattern: '**/*' }], - ], - - // A glob pointing to a file, should result in the file as the pattern - [ - { base: '/projects/project-a', pattern: './src/index.html' }, - [{ base: '/projects/project-a/src', pattern: 'index.html' }], - ], - - // A glob going up a directory, should result in the new directory as the base - [ - { base: '/projects/project-a', pattern: '../project-b/src/**/*.html' }, - [{ base: '/projects/project-b/src', pattern: '**/*.html' }], - ], - - // A glob with curlies, should be expanded to multiple globs - [ - { base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.html' }, - [ - { base: '/projects/project-b/src', pattern: '**/*.html' }, - { base: '/projects/project-c/src', pattern: '**/*.html' }, - ], - ], - [ - { base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.{js,html}' }, - [ - { base: '/projects/project-b/src', pattern: '**/*.js' }, - { base: '/projects/project-b/src', pattern: '**/*.html' }, - { base: '/projects/project-c/src', pattern: '**/*.js' }, - { base: '/projects/project-c/src', pattern: '**/*.html' }, - ], - ], -])('should hoist the static parts of the glob: %s', (input, output) => { - expect(hoistStaticGlobParts(input)).toEqual(output) -}) diff --git a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts b/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts deleted file mode 100644 index 18d58799857b..000000000000 --- a/packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { normalizePath } from '@tailwindcss/node' -import braces from 'braces' -import path from 'node:path' - -interface GlobEntry { - base: string - pattern: string -} - -export function hoistStaticGlobParts(entry: GlobEntry): GlobEntry[] { - return braces(entry.pattern, { expand: true }).map((pattern) => { - let clone = { ...entry } - let [staticPart, dynamicPart] = splitPattern(pattern) - - // Move static part into the `base`. - let absolutePosixPath = normalizePath(entry.base) - - if (staticPart !== null) { - clone.base = path.posix.join(absolutePosixPath, staticPart) - } else { - clone.base = absolutePosixPath - } - - // Move dynamic part into the `pattern`. - if (dynamicPart === null) { - clone.pattern = '**/*' - } else { - clone.pattern = dynamicPart - } - - // If the pattern looks like a file, move the file name from the `base` to - // the `pattern`. - let file = path.basename(clone.base) - if (file.includes('.')) { - clone.pattern = file - clone.base = path.dirname(clone.base) - } - - return clone - }) -} - -// Split a glob pattern into a `static` and `dynamic` part. -// -// Assumption: we assume that all globs are expanded, which means that the only -// dynamic parts are using `*`. -// -// E.g.: -// Original input: `../project-b/**/*.{html,js}` -// Expanded input: `../project-b/**/*.html` & `../project-b/**/*.js` -// Split on first input: ("../project-b", "**/*.html") -// Split on second input: ("../project-b", "**/*.js") -function splitPattern(pattern: string): [staticPart: string | null, dynamicPart: string | null] { - // No dynamic parts, so we can just return the input as-is. - if (!pattern.includes('*')) { - return [pattern, null] - } - - let lastSlashPosition: number | null = null - - for (let i = 0; i < pattern.length; i++) { - let c = pattern[i] - if (c === '/') { - lastSlashPosition = i - } - - if (c === '*' || c === '!') { - break - } - } - - // Very first character is a `*`, therefore there is no static part, only a - // dynamic part. - if (lastSlashPosition === null) { - return [null, pattern] - } - - let staticPart = pattern.slice(0, lastSlashPosition).trim() - let dynamicPart = pattern.slice(lastSlashPosition + 1).trim() - - return [staticPart || null, dynamicPart || null] -} From 9be7647e92ae1d0e9dc430092b189120d766f869 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:15:04 +0200 Subject: [PATCH 15/34] make `configFilePath` also nullable --- .../src/codemods/css/migrate-config.ts | 3 ++- .../src/codemods/css/migrate.ts | 2 +- packages/@tailwindcss-upgrade/src/index.ts | 20 ++++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts index 4dc80a83b09d..2004d096903d 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts @@ -11,10 +11,11 @@ export function migrateConfig( { configFilePath, jsConfigMigration, - }: { configFilePath: string; jsConfigMigration: JSConfigMigration | null }, + }: { configFilePath: string | null; jsConfigMigration: JSConfigMigration | null }, ): Plugin { function migrate() { if (!sheet.isTailwindRoot) return + if (!configFilePath) return let alreadyInjected = ALREADY_INJECTED.get(sheet) if (alreadyInjected && alreadyInjected.includes(configFilePath)) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts index cfeb917b161f..238ddb7f4d13 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -18,7 +18,7 @@ export interface MigrateOptions { newPrefix: string | null designSystem: DesignSystem userConfig: Config | null - configFilePath: string + configFilePath: string | null jsConfigMigration: JSConfigMigration | null } diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 67e152467519..8341e74d7019 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -156,23 +156,33 @@ async function run() { stylesheets.map(async (sheet) => { try { let config = configBySheet.get(sheet) - let jsConfigMigration = jsConfigMigrationBySheet.get(sheet) + let jsConfigMigration = jsConfigMigrationBySheet.get(sheet) ?? null if (!config) { for (let parent of sheet.ancestors()) { if (parent.isTailwindRoot) { config ??= configBySheet.get(parent)! - jsConfigMigration ??= jsConfigMigrationBySheet.get(parent) + jsConfigMigration ??= jsConfigMigrationBySheet.get(parent) ?? null break } } } - if (!config) return + let designSystem = config?.designSystem ?? (await sheet.designSystem()) + if (!designSystem) { + return + } + + let newPrefix = config?.newPrefix ?? null + let userConfig = config?.userConfig ?? null + let configFilePath = config?.configFilePath ?? null await migrateStylesheet(sheet, { - ...config, - jsConfigMigration: jsConfigMigration ?? null, + newPrefix, + designSystem, + userConfig, + configFilePath, + jsConfigMigration, }) } catch (e: any) { error(`${e?.message ?? e} in ${highlight(relative(sheet.file!, base))}`, { prefix: '↳ ' }) From bc93e2ce162cc3e5bcdb2653be8672f4a31908e7 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:15:20 +0200 Subject: [PATCH 16/34] do not migrate `preflight` in non-v3 projects --- .../src/codemods/css/migrate-preflight.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts index 373ddf1039f3..4454ba116c62 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts @@ -5,6 +5,7 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path' import * as ValueParser from '../../../../tailwindcss/src/value-parser' +import * as version from '../../utils/version' // Defaults in v4 const DEFAULT_BORDER_COLOR = 'currentcolor' @@ -46,6 +47,10 @@ export function migratePreflight({ } function migrate(root: Root) { + // CSS for backwards compatibility with v3 should only injected in v3 + // projects and not v4 projects. + if (!version.isMajor(3)) return + let isTailwindRoot = false root.walkAtRules('import', (node) => { if ( From 51b220078a689c944c5810417301e913d273e0aa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:19:49 +0200 Subject: [PATCH 17/34] run prettier --- .../tailwindcss/src/compat/apply-config-to-theme.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index 8caec195e607..3a748fd8b306 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -116,10 +116,7 @@ test('config values can be merged into the theme', () => { { '--line-height': '3rem' }, ]) expect(theme.resolve('2xl', ['--text'])).toEqual('2rem') - expect(theme.resolveWith('2xl', ['--text'], ['--line-height'])).toEqual([ - '2rem', - {}, - ]) + expect(theme.resolveWith('2xl', ['--text'], ['--line-height'])).toEqual(['2rem', {}]) expect(theme.resolve('super-wide', ['--tracking'])).toEqual('0.25em') expect(theme.resolve('super-loose', ['--leading'])).toEqual('3') expect(theme.resolve('1/2', ['--width'])).toEqual('60%') From 6a492dad3b3e1f44ca634277235c32e55f088c1f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:23:56 +0200 Subject: [PATCH 18/34] upgrade `tailwindcss` after we migrated the stylesheets --- packages/@tailwindcss-upgrade/src/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 8341e74d7019..150bc6fa221c 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -237,6 +237,13 @@ async function run() { } } + info('Updating dependencies…') + try { + // Upgrade Tailwind CSS + await pkg(base).add(['tailwindcss@latest']) + success(`Updated package: ${highlight('tailwindcss')}`, { prefix: '↳ ' }) + } catch {} + let tailwindRootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot && sheet.file) // Migrate source files @@ -313,12 +320,6 @@ async function run() { await migratePrettierPlugin(base) } - try { - // Upgrade Tailwind CSS - await pkg(base).add(['tailwindcss@latest']) - success(`Updated package: ${highlight('tailwindcss')}`, { prefix: '↳ ' }) - } catch {} - // Run all cleanup functions because we completed the migration await Promise.allSettled(cleanup.map((fn) => fn())) From c5ec584a88e814af63f6a6f1034d8f04b4eb01a9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:26:09 +0200 Subject: [PATCH 19/34] remove test that requires Tailwind CSS v3 --- integrations/upgrade/index.test.ts | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index fd379e20a937..97c9a0798711 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2595,40 +2595,6 @@ test( }, ) -test( - 'requires Tailwind v3 before attempting an upgrade', - { - fs: { - 'package.json': json` - { - "dependencies": { - "tailwindcss": "workspace:^", - "@tailwindcss/upgrade": "workspace:^" - } - } - `, - 'tailwind.config.ts': js` export default {} `, - 'src/index.html': html` -
- `, - 'src/index.css': css` - @tailwind base; - @tailwind components; - @tailwind utilities; - `, - }, - }, - async ({ exec, expect }) => { - let output = await exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => - e.toString(), - ) - - expect(output).toMatch( - /Tailwind CSS v.* found. The migration tool can only be run on v3 projects./, - ) - }, -) - test( `upgrades opacity namespace values to percentages`, { From 71f346128545c872a557a22230e42d5f5d41d147 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 17:48:53 +0200 Subject: [PATCH 20/34] make the `DesignSystem` fully nullable The `DesignSystem` was already nullable (or well, optional) but that wasn't well reflected in the types. This would otherwise break this test: - `tailwindcss/integrations/upgrade/index.test.ts` - `migrate utility files imported by multiple roots` --- .../src/codemods/css/migrate-at-apply.ts | 4 +++- .../src/codemods/css/migrate-media-screen.ts | 2 +- .../src/codemods/css/migrate-preflight.ts | 3 ++- .../src/codemods/css/migrate-theme-to-var.ts | 2 +- .../src/codemods/css/migrate.ts | 2 +- packages/@tailwindcss-upgrade/src/index.ts | 19 +++++-------------- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts index 752f240c8267..69f92688dff5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts @@ -8,7 +8,7 @@ export function migrateAtApply({ designSystem, userConfig, }: { - designSystem: DesignSystem + designSystem: DesignSystem | null userConfig: Config | null }): Plugin { function migrate(atRule: AtRule) { @@ -35,6 +35,8 @@ export function migrateAtApply({ }) return async () => { + if (!designSystem) return + // If we have a valid designSystem and config setup, we can run all // candidate migrations on each utility params = await Promise.all( diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts index 2310ec723240..f4526209d89a 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts @@ -9,7 +9,7 @@ export function migrateMediaScreen({ designSystem, userConfig, }: { - designSystem?: DesignSystem + designSystem?: DesignSystem | null userConfig?: Config | null } = {}): Plugin { function migrate(root: Root) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts index 4454ba116c62..0085a2fd9f8e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts @@ -35,13 +35,14 @@ export function migratePreflight({ designSystem, userConfig, }: { - designSystem: DesignSystem + designSystem: DesignSystem | null userConfig?: Config | null }): Plugin { // @ts-expect-error let defaultBorderColor = userConfig?.theme?.borderColor?.DEFAULT function canResolveThemeValue(path: string) { + if (!designSystem) return false let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const return Boolean(designSystem.theme.get([variable])) } diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts index d724620acd76..8285def1e432 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts @@ -5,7 +5,7 @@ import { Convert, createConverter } from '../template/migrate-theme-to-var' export function migrateThemeToVar({ designSystem, }: { - designSystem?: DesignSystem + designSystem?: DesignSystem | null } = {}): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/migrate-theme-to-var', diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts index 238ddb7f4d13..cb91508513fe 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -16,7 +16,7 @@ import { migrateVariantsDirective } from './migrate-variants-directive' export interface MigrateOptions { newPrefix: string | null - designSystem: DesignSystem + designSystem: DesignSystem | null userConfig: Config | null configFilePath: string | null jsConfigMigration: JSConfigMigration | null diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 150bc6fa221c..1c679518f234 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -123,7 +123,7 @@ async function run() { >() for (let sheet of stylesheets) { if (!sheet.isTailwindRoot) continue - if (!sheet.linkedConfigPath) continue + if (!version.isMajor(3) && !sheet.linkedConfigPath) continue let config = await prepareConfig(sheet.linkedConfigPath, { base }) configBySheet.set(sheet, config) @@ -168,20 +168,11 @@ async function run() { } } - let designSystem = config?.designSystem ?? (await sheet.designSystem()) - if (!designSystem) { - return - } - - let newPrefix = config?.newPrefix ?? null - let userConfig = config?.userConfig ?? null - let configFilePath = config?.configFilePath ?? null - await migrateStylesheet(sheet, { - newPrefix, - designSystem, - userConfig, - configFilePath, + newPrefix: config?.newPrefix ?? null, + designSystem: config?.designSystem ?? (await sheet.designSystem()), + userConfig: config?.userConfig ?? null, + configFilePath: config?.configFilePath ?? null, jsConfigMigration, }) } catch (e: any) { From 6ffa7cb3cb9231944aaaf6423d35e331eadb0e13 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 19:29:15 +0200 Subject: [PATCH 21/34] mock version in local tests --- .../src/codemods/css/migrate-at-apply.test.ts | 4 +++- .../src/codemods/css/migrate-preflight.test.ts | 4 +++- ...grate-handle-empty-arbitrary-values.test.ts | 4 +++- .../template/migrate-legacy-classes.test.ts | 4 +++- .../migrate-modernize-arbitrary-values.test.ts | 4 +++- .../codemods/template/migrate-prefix.test.ts | 4 +++- .../migrate-simple-legacy-classes.test.ts | 4 +++- .../template/migrate-simple-legacy-classes.ts | 18 +++++++++--------- .../template/migrate-variant-order.test.ts | 4 +++- .../@tailwindcss-upgrade/src/index.test.ts | 4 +++- 10 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts index d01368a14073..0c72cb7be4cb 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts @@ -1,9 +1,11 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import postcss from 'postcss' -import { expect, it } from 'vitest' +import { expect, it, vi } from 'vitest' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import * as versions from '../../utils/version' import { migrateAtApply } from './migrate-at-apply' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts index 38471a6d89cb..2d8b700411b1 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts @@ -1,10 +1,12 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import postcss from 'postcss' -import { expect, it } from 'vitest' +import { expect, it, vi } from 'vitest' +import * as versions from '../../utils/version' import { formatNodes } from './format-nodes' import { migratePreflight } from './migrate-preflight' import { sortBuckets } from './sort-buckets' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts index 107c901c0a6c..bebd433432a2 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-handle-empty-arbitrary-values.test.ts @@ -1,7 +1,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['group-[]:flex', 'group-[&]:flex'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts index ad589cbee80e..1dbe90426423 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateLegacyClasses } from './migrate-legacy-classes' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['shadow', 'shadow-sm'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index cc4e5a4a5fdc..fb33e5c6d2bf 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -1,8 +1,10 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' import { migrateModernizeArbitraryValues } from './migrate-modernize-arbitrary-values' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ // Arbitrary variants diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts index 04d9282ab7b1..f47e181af83c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migratePrefix } from './migrate-prefix' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) describe('for projects with configured prefix', () => { test.each([ diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts index cd54234efd4d..456671dd76ba 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.test.ts @@ -1,6 +1,8 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateSimpleLegacyClasses } from './migrate-simple-legacy-classes' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) test.each([ ['overflow-ellipsis', 'text-ellipsis'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts index f3bb60a0499b..de5008ad411a 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts @@ -17,15 +17,6 @@ const LEGACY_CLASS_MAP: Record = { 'decoration-slice': 'box-decoration-slice', } -// `outline-none` in v3 has the same meaning as `outline-hidden` in v4. However, -// `outline-none` in v4 _also_ exists but has a different meaning. -// -// We can only migrate `outline-none` to `outline-hidden` if we are migrating a -// v3 project to v4. -if (version.isMajor(3)) { - LEGACY_CLASS_MAP['outline-none'] = 'outline-hidden' -} - let seenDesignSystems = new WeakSet() export function migrateSimpleLegacyClasses( @@ -33,6 +24,15 @@ export function migrateSimpleLegacyClasses( _userConfig: Config | null, rawCandidate: string, ): string { + // `outline-none` in v3 has the same meaning as `outline-hidden` in v4. However, + // `outline-none` in v4 _also_ exists but has a different meaning. + // + // We can only migrate `outline-none` to `outline-hidden` if we are migrating a + // v3 project to v4. + if (version.isMajor(3)) { + LEGACY_CLASS_MAP['outline-none'] = 'outline-hidden' + } + // Prepare design system with the unknown legacy classes if (!seenDesignSystems.has(designSystem)) { for (let old in LEGACY_CLASS_MAP) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts index 61589b8fed5a..1b14d533f87f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.test.ts @@ -1,7 +1,9 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' import { migrateVariantOrder } from './migrate-variant-order' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) let css = dedent diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index b86647c82f24..2eae32f305f1 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -2,10 +2,12 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import path from 'node:path' import postcss from 'postcss' -import { expect, it } from 'vitest' +import { expect, it, vi } from 'vitest' import { formatNodes } from './codemods/css/format-nodes' import { migrateContents } from './codemods/css/migrate' import { sortBuckets } from './codemods/css/sort-buckets' +import * as versions from './utils/version' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent From 4f987fa3cb18525e505aa90694c9c6e167e7aebb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 19:56:21 +0200 Subject: [PATCH 22/34] only migrate `@layer` in v3 projects --- .../src/codemods/css/migrate-at-layer-utilities.test.ts | 4 +++- .../src/codemods/css/migrate-at-layer-utilities.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts index 553b1f5d079a..ba1d069471ea 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts @@ -1,10 +1,12 @@ import dedent from 'dedent' import postcss from 'postcss' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Stylesheet } from '../../stylesheet' +import * as versions from '../../utils/version' import { formatNodes } from './format-nodes' import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' import { sortBuckets } from './sort-buckets' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) const css = dedent diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts index e775e3b6d26b..9b03e9d1658f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts @@ -2,10 +2,16 @@ import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss' import SelectorParser from 'postcss-selector-parser' import { segment } from '../../../../tailwindcss/src/utils/segment' import { Stylesheet } from '../../stylesheet' +import * as version from '../../utils/version' import { walk, WalkAction, walkDepth } from '../../utils/walk' export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { function migrate(atRule: AtRule) { + // Migrating `@layer utilities` to `@utility` is only supported in Tailwind + // CSS v3 projects. Tailwind CSS v4 projects could also have `@layer + // utilities` but those aren't actual utilities. + if (!version.isMajor(3)) return + // Only migrate `@layer utilities` and `@layer components`. if (atRule.params !== 'utilities' && atRule.params !== 'components') return From 1b5346111abc8a79d58bfc6b806b35e65e8c8972 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 20:01:34 +0200 Subject: [PATCH 23/34] add migrations for newly deprecated classes - `bg-{left,right}-{top,bottom}` in favor of `bg-{top,bottom}-{left,right}` - `object-{left,right}-{top,bottom}` in favor of `object-{top,bottom}-{left,right}` --- .../codemods/template/migrate-simple-legacy-classes.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts index de5008ad411a..c87624a200b5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts @@ -15,6 +15,16 @@ const LEGACY_CLASS_MAP: Record = { 'decoration-clone': 'box-decoration-clone', 'decoration-slice': 'box-decoration-slice', + + // Since v4.1.0 + 'bg-left-top': 'bg-top-left', + 'bg-left-bottom': 'bg-bottom-left', + 'bg-right-top': 'bg-top-right', + 'bg-right-bottom': 'bg-bottom-right', + 'object-left-top': 'object-top-left', + 'object-left-bottom': 'object-bottom-left', + 'object-right-top': 'object-top-right', + 'object-right-bottom': 'object-bottom-right', } let seenDesignSystems = new WeakSet() From 6085b47a8dedb4f8b98710271e2a4e902d56575b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 22:16:06 +0200 Subject: [PATCH 24/34] add `:user-valid` and `:user-invalid` arbitrary variant replacements --- .../src/codemods/template/migrate-modernize-arbitrary-values.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index fbdecf285096..1eec292ebabd 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -242,6 +242,8 @@ export function migrateModernizeArbitraryValues( else if (value === ':required') return 'required' else if (value === ':valid') return 'valid' else if (value === ':invalid') return 'invalid' + else if (value === ':user-valid') return 'user-valid' + else if (value === ':user-invalid') return 'user-invalid' else if (value === ':in-range') return 'in-range' else if (value === ':out-of-range') return 'out-of-range' else if (value === ':read-only') return 'read-only' From a6d43094376ba843c255ce3664f119df45fc4692 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 22:17:27 +0200 Subject: [PATCH 25/34] replace arbitrary `@media` variants This will replace variants such as `[@media(pointer:fine)]:flex` to `pointer-fine:flex` --- ...migrate-modernize-arbitrary-values.test.ts | 4 + .../migrate-modernize-arbitrary-values.ts | 78 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index fb33e5c6d2bf..6a3722dd4dcf 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -80,6 +80,10 @@ test.each([ // Attribute selector wrapped in `&:is(…)` ['[&:is([data-visible])]:flex', 'data-visible:flex'], + // Media queries + ['[@media(pointer:fine)]:flex', 'pointer-fine:flex'], + ['[@media_(pointer_:_fine)]:flex', 'pointer-fine:flex'], + // Compound arbitrary variants ['has-[[data-visible]]:flex', 'has-data-visible:flex'], ['has-[&:is([data-visible])]:flex', 'has-data-visible:flex'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index 1eec292ebabd..b2cb864b61ea 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -3,6 +3,7 @@ import { parseCandidate, type Candidate, type Variant } from '../../../../tailwi import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type' +import * as ValueParser from '../../../../tailwindcss/src/value-parser' import { printCandidate } from './candidates' function memcpy(target: T, source: U): U { @@ -146,6 +147,83 @@ export function migrateModernizeArbitraryValues( continue } + // Migrate `@media` variants + // + // E.g.: `[@media(scripting:none)]:` -> `noscript:` + if ( + // Only top-level, so `in-[@media(scripting:none)]` is not supported + parent === null && + // [@media(scripting:none)]:flex + // ^^^^^^^^^^^^^^^^^^^^^^ + ast.nodes[0].nodes[0].type === 'tag' && + ast.nodes[0].nodes[0].value.startsWith('@media') + ) { + // Replace all whitespace such that `@media (scripting: none)` and + // `@media(scripting:none)` are equivalent. + // + // As arbitrary variants that means that these are equivalent: + // - `[@media_(scripting:_none)]:` + // - `[@media(scripting:none)]:` + let parsed = ValueParser.parse(ast.nodes[0].toString().trim().replace('@media', '')) + + // Drop whitespace + ValueParser.walk(parsed, (node, { replaceWith }) => { + // Drop whitespace nodes + if (node.kind === 'separator' && !node.value.trim()) { + replaceWith([]) + } + + // Trim whitespace + else { + node.value = node.value.trim() + } + }) + + if ( + parsed.length === 1 && + parsed[0].kind === 'function' && // `(` and `)` are considered a function + parsed[0].nodes.length === 3 && + parsed[0].nodes[0].kind === 'word' && + parsed[0].nodes[1].kind === 'separator' && + parsed[0].nodes[1].value === ':' && + parsed[0].nodes[2].kind === 'word' + ) { + let key = parsed[0].nodes[0].value + let value = parsed[0].nodes[2].value + let replacement: string | null = null + + if (key === 'prefers-reduced-motion' && value === 'no-preference') + replacement = 'motion-safe' + if (key === 'prefers-reduced-motion' && value === 'reduce') + replacement = 'motion-reduce' + + if (key === 'prefers-contrast' && value === 'more') replacement = 'contrast-more' + if (key === 'prefers-contrast' && value === 'less') replacement = 'contrast-less' + + if (key === 'orientation' && value === 'portrait') replacement = 'portrait' + if (key === 'orientation' && value === 'landscape') replacement = 'landscape' + + if (key === 'forced-colors' && value === 'active') replacement = 'forced-colors' + + if (key === 'inverted-colors' && value === 'inverted') replacement = 'inverted-colors' + + if (key === 'pointer' && value === 'none') replacement = 'pointer-none' + if (key === 'pointer' && value === 'coarse') replacement = 'pointer-coarse' + if (key === 'pointer' && value === 'fine') replacement = 'pointer-fine' + if (key === 'any-pointer' && value === 'none') replacement = 'any-pointer-none' + if (key === 'any-pointer' && value === 'coarse') replacement = 'any-pointer-coarse' + if (key === 'any-pointer' && value === 'fine') replacement = 'any-pointer-fine' + + if (key === 'scripting' && value === 'none') replacement = 'noscript' + + if (replacement) { + changed = true + memcpy(variant, designSystem.parseVariant(replacement)) + } + } + continue + } + let prefixedVariant: Variant | null = null // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` From 5857fd0b7476fdaac59109ef8b0ee1bdeb7afc3d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 18 Apr 2025 22:31:05 +0200 Subject: [PATCH 26/34] =?UTF-8?q?handle=20`[@media=5Fnot(=E2=80=A6)]`=20va?= =?UTF-8?q?riants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/migrate-modernize-arbitrary-values.test.ts | 1 + .../template/migrate-modernize-arbitrary-values.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index 6a3722dd4dcf..d019f8e05ba5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -83,6 +83,7 @@ test.each([ // Media queries ['[@media(pointer:fine)]:flex', 'pointer-fine:flex'], ['[@media_(pointer_:_fine)]:flex', 'pointer-fine:flex'], + ['[@media_not_(pointer_:_fine)]:flex', 'not-pointer-fine:flex'], // Compound arbitrary variants ['has-[[data-visible]]:flex', 'has-data-visible:flex'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index b2cb864b61ea..2ad648e1b684 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -179,6 +179,12 @@ export function migrateModernizeArbitraryValues( } }) + let not = false + if (parsed[0]?.kind === 'word' && parsed[0].value === 'not') { + not = true + parsed.shift() + } + if ( parsed.length === 1 && parsed[0].kind === 'function' && // `(` and `)` are considered a function @@ -218,7 +224,7 @@ export function migrateModernizeArbitraryValues( if (replacement) { changed = true - memcpy(variant, designSystem.parseVariant(replacement)) + memcpy(variant, designSystem.parseVariant(`${not ? 'not-' : ''}${replacement}`)) } } continue From ba4cab71e52c39d5595489279450982ff3731a65 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 19 Apr 2025 00:03:36 +0200 Subject: [PATCH 27/34] handle `@media` with single argument (e.g.: `@media print`) --- .../migrate-modernize-arbitrary-values.test.ts | 2 ++ .../migrate-modernize-arbitrary-values.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index d019f8e05ba5..842d627f94b1 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -84,6 +84,8 @@ test.each([ ['[@media(pointer:fine)]:flex', 'pointer-fine:flex'], ['[@media_(pointer_:_fine)]:flex', 'pointer-fine:flex'], ['[@media_not_(pointer_:_fine)]:flex', 'not-pointer-fine:flex'], + ['[@media_print]:flex', 'print:flex'], + ['[@media_not_print]:flex', 'not-print:flex'], // Compound arbitrary variants ['has-[[data-visible]]:flex', 'has-data-visible:flex'], diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index 2ad648e1b684..87328430b3ac 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -185,6 +185,23 @@ export function migrateModernizeArbitraryValues( parsed.shift() } + // Single keyword at-rules. + // + // E.g.: `[@media_print]:` -< `@media print` -> `print:` + if (parsed.length === 1 && parsed[0].kind === 'word') { + let key = parsed[0].value + let replacement: string | null = null + if (key === 'print') replacement = 'print' + + if (replacement) { + changed = true + memcpy(variant, designSystem.parseVariant(`${not ? 'not-' : ''}${replacement}`)) + } + } + + // Key/value at-rules. + // + // E.g.: `[@media(scripting:none)]:` -> `scripting:` if ( parsed.length === 1 && parsed[0].kind === 'function' && // `(` and `)` are considered a function From 0e61002614b372c5f451a5d2f2d3f8943deebae3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 19 Apr 2025 01:52:17 +0200 Subject: [PATCH 28/34] ensure we re-print the candidate in case it didn't change When printing a candidate, we do some optimizations already, such as: - `bg-[var(--foo)]` -> `bg-(--foo)` - `bg-[rgb(0,_0,_0)]` -> `bg-[rgb(0,0,0)]` Consistency in your project will reduce the file size. We parse and reprint the candidate if nothing changed during migrations because we don't have dedicated migrations for them. So (a lot) of these classes weren't fully updated to the v4 flavor of the classes. Luckily re-parsing the candidate is fast because we are re-using the design system which means that we have a cached version of the candidate. --- .../src/codemods/template/migrate.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index 99b2a4d97ed0..a80cb2499320 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -1,9 +1,10 @@ import fs from 'node:fs/promises' import path, { extname } from 'node:path' +import { parseCandidate } from '../../../../tailwindcss/src/candidate' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string' -import { extractRawCandidates } from './candidates' +import { extractRawCandidates, printCandidate } from './candidates' import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value' import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection' import { migrateBgGradient } from './migrate-bg-gradient' @@ -56,9 +57,22 @@ export async function migrateCandidate( end: number }, ): Promise { + let original = rawCandidate for (let migration of DEFAULT_MIGRATIONS) { rawCandidate = await migration(designSystem, userConfig, rawCandidate, location) } + + // If nothing changed, let's parse it again and re-print it. This will migrate + // pretty print candidates to the new format. If it did change, we already had + // to re-print it. + // + // E.g.: `bg-red-500/[var(--my-opacity)]` -> `bg-red-500/(--my-opacity)` + if (rawCandidate === original) { + for (let candidate of parseCandidate(rawCandidate, designSystem)) { + return printCandidate(designSystem, candidate) + } + } + return rawCandidate } From 37323c6ed788efe511865878c6fc02f63bf7e1e5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 19 Apr 2025 01:22:48 +0200 Subject: [PATCH 29/34] add tests to ensure upgrade tool runs on v4 and is idempotent --- integrations/upgrade/index.test.ts | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 97c9a0798711..7f25c8e35b6b 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2776,6 +2776,123 @@ test( }, ) +test( + 'upgrades are idempotent, and can run on v4 projects', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + + .foo { + @apply !bg-[var(--my-color)] rounded; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + let before = await fs.dumpFiles('./src/**/*.{css,html}') + expect(before).toMatchInlineSnapshot(` + " + --- ./src/index.html --- +
+ + --- ./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); + } + } + + .foo { + @apply bg-(--my-color)! rounded-sm; + } + " + `) + + // Commit the changes + await exec('git add .') + await exec('git commit -m "upgrade"') + + // Run the upgrade again + let output = await exec('npx @tailwindcss/upgrade') + expect(output).toContain('No changes were made to your repository') + + let after = await fs.dumpFiles('./src/**/*.{css,html}') + expect(after).toMatchInlineSnapshot(` + " + --- ./src/index.html --- +
+ + --- ./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); + } + } + + .foo { + @apply bg-(--my-color)! rounded-sm; + } + " + `) + + // Ensure the file system is in the same state + expect(before).toEqual(after) + }, +) + function withBOM(text: string): string { return '\uFEFF' + text } From aadc926fb91728a12223e08c6f098f48f0bc5f2d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 19 Apr 2025 01:27:54 +0200 Subject: [PATCH 30/34] add test to ensure upgrade runs on v4 projects And actually applies changes where it can. It will also ignore unsafe migrations such as variant order and `rounded` -> `rounded-sm`. --- integrations/upgrade/index.test.ts | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 7f25c8e35b6b..8e82e68b1c86 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2893,6 +2893,68 @@ test( }, ) +test( + 'upgrades run on v4 projects', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^4", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'src/index.html': html` + +
+ + +
+
+ + +
+ `, + 'src/input.css': css` + @import 'tailwindcss'; + + .foo { + @apply !bg-[var(--my-color)]; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- ./src/index.html --- + +
+ + +
+
+ + +
+ + --- ./src/input.css --- + @import 'tailwindcss'; + + .foo { + @apply bg-(--my-color)!; + } + " + `) + }, +) + function withBOM(text: string): string { return '\uFEFF' + text } From d20902f5625421473da726dd19f2b15b3ee14c0a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sat, 19 Apr 2025 02:48:35 +0200 Subject: [PATCH 31/34] only commit changes in dirty git repo --- integrations/upgrade/index.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 8e82e68b1c86..c8bbc85c2e2f 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1,3 +1,4 @@ +import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git' import { candidate, css, html, js, json, test, ts } from '../utils' test( @@ -2848,8 +2849,10 @@ test( `) // Commit the changes - await exec('git add .') - await exec('git commit -m "upgrade"') + if (isRepoDirty()) { + await exec('git add .') + await exec('git commit -m "upgrade"') + } // Run the upgrade again let output = await exec('npx @tailwindcss/upgrade') From 3d8eed94fdd8f285587b5429d4ccc22922bdaaf2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 22 Apr 2025 16:56:56 +0200 Subject: [PATCH 32/34] adjust comment --- .../codemods/template/migrate-modernize-arbitrary-values.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts index 87328430b3ac..c3325f2ced8e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts @@ -151,7 +151,8 @@ export function migrateModernizeArbitraryValues( // // E.g.: `[@media(scripting:none)]:` -> `noscript:` if ( - // Only top-level, so `in-[@media(scripting:none)]` is not supported + // Only top-level, so something like `in-[@media(scripting:none)]` + // (which is not valid anyway) is not supported parent === null && // [@media(scripting:none)]:flex // ^^^^^^^^^^^^^^^^^^^^^^ From fff3cc062d3d0560fd50570b4a0fdf02796a2630 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 22 Apr 2025 16:57:33 +0200 Subject: [PATCH 33/34] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12cc32392814..d47f7cfd2b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Ensure `@tailwindcss/upgrade` runs on Tailwind CSS v4 projects ([#17717](https://github.com/tailwindlabs/tailwindcss/pull/17717)) + ### Fixed - Don't scan `.geojson` files for classes by default ([#17700](https://github.com/tailwindlabs/tailwindcss/pull/17700)) From 6b5935c4fed71756d1a04f190eb9c503a8bb9fa9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 22 Apr 2025 17:01:59 +0200 Subject: [PATCH 34/34] add new migration examples to upgrade test --- integrations/upgrade/index.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index c8bbc85c2e2f..b52031b3275c 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2920,7 +2920,9 @@ test(
-
+
`, 'src/input.css': css` @import 'tailwindcss'; @@ -2945,7 +2947,9 @@ test(
-
+
--- ./src/input.css --- @import 'tailwindcss';