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)) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index fd379e20a937..b52031b3275c 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( @@ -2595,40 +2596,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`, { @@ -2810,6 +2777,191 @@ 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 + if (isRepoDirty()) { + 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) + }, +) + +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 } 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/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-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts index ffcb12619479..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,8 +8,8 @@ export function migrateAtApply({ designSystem, userConfig, }: { - designSystem: DesignSystem - userConfig: Config + designSystem: DesignSystem | null + userConfig: Config | null }): Plugin { function migrate(atRule: AtRule) { let utilities = atRule.params.split(/(\s+)/) @@ -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-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 diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts index 3149c2725e69..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 }, + }: { 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-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts index 57cc20eeb45b..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,8 +9,8 @@ export function migrateMediaScreen({ designSystem, userConfig, }: { - designSystem?: DesignSystem - userConfig?: Config + designSystem?: DesignSystem | null + userConfig?: Config | null } = {}): Plugin { function migrate(root: Root) { if (!designSystem || !userConfig) return 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/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts index 32172069100d..0085a2fd9f8e 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' @@ -34,18 +35,23 @@ export function migratePreflight({ designSystem, userConfig, }: { - designSystem: DesignSystem - userConfig?: Config + 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])) } 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 ( 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 ab77bab9f120..cb91508513fe 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -16,10 +16,10 @@ import { migrateVariantsDirective } from './migrate-variants-directive' export interface MigrateOptions { newPrefix: string | null - designSystem: DesignSystem - userConfig: Config - configFilePath: string - jsConfigMigration: JSConfigMigration + designSystem: DesignSystem | null + userConfig: Config | null + configFilePath: string | null + jsConfigMigration: JSConfigMigration | null } export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { 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.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-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.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-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts index 8f3835c8f955..2a231bb3daa7 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' @@ -67,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 @@ -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-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.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index cc4e5a4a5fdc..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 @@ -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 @@ -78,6 +80,13 @@ 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'], + ['[@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'], ['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 f0eec7485a29..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 @@ -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 { @@ -15,7 +16,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)) { @@ -146,6 +147,107 @@ export function migrateModernizeArbitraryValues( continue } + // Migrate `@media` variants + // + // E.g.: `[@media(scripting:none)]:` -> `noscript:` + if ( + // Only top-level, so something like `in-[@media(scripting:none)]` + // (which is not valid anyway) 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() + } + }) + + let not = false + if (parsed[0]?.kind === 'word' && parsed[0].value === 'not') { + not = true + 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 + 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(`${not ? 'not-' : ''}${replacement}`)) + } + } + continue + } + let prefixedVariant: Variant | null = null // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` @@ -242,6 +344,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' 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-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.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 dd6599152c7f..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 @@ -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', @@ -15,16 +16,33 @@ const LEGACY_CLASS_MAP = { 'decoration-clone': 'box-decoration-clone', 'decoration-slice': 'box-decoration-slice', - 'outline-none': 'outline-hidden', + // 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() export function migrateSimpleLegacyClasses( designSystem: DesignSystem, - _userConfig: Config, + _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-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.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/codemods/template/migrate-variant-order.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts index ab18b6b78476..f9da9ecc28ec 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-variant-order.ts @@ -2,13 +2,23 @@ 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( designSystem: DesignSystem, - _userConfig: Config, + _userConfig: Config | null, 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 diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index c18e2f25e6a6..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' @@ -20,7 +21,7 @@ import { migrateVariantOrder } from './migrate-variant-order' export type Migration = ( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, rawCandidate: string, location?: { contents: string @@ -47,7 +48,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?: { @@ -56,15 +57,28 @@ 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 } export default async function migrateContents( designSystem: DesignSystem, - userConfig: Config, + userConfig: Config | null, contents: string, extension: string, ): Promise { @@ -93,7 +107,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') 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 diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 75bd47b523a4..1c679518f234 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,10 +20,9 @@ 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 { getPackageVersion } from './utils/package-version' 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' }, @@ -59,15 +59,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 @@ -108,18 +99,21 @@ 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 - if (stylesheets.some((sheet) => sheet.isTailwindRoot)) { + if (stylesheets.some((sheet) => sheet.isTailwindRoot && sheet.linkedConfigPath)) { info('Migrating JavaScript configuration files…') } let configBySheet = new Map>>() @@ -129,6 +123,7 @@ async function run() { >() for (let sheet of stylesheets) { if (!sheet.isTailwindRoot) continue + if (!version.isMajor(3) && !sheet.linkedConfigPath) continue let config = await prepareConfig(sheet.linkedConfigPath, { base }) configBySheet.set(sheet, config) @@ -153,41 +148,6 @@ async function run() { } } - // Migrate source files, linked to config 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…') @@ -195,20 +155,26 @@ async function run() { await Promise.all( stylesheets.map(async (sheet) => { try { - let config = configBySheet.get(sheet)! - let jsConfigMigration = jsConfigMigrationBySheet.get(sheet)! + let config = configBySheet.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 } } } - await migrateStylesheet(sheet, { ...config, jsConfigMigration }) + await migrateStylesheet(sheet, { + newPrefix: config?.newPrefix ?? null, + designSystem: config?.designSystem ?? (await sheet.designSystem()), + userConfig: config?.userConfig ?? null, + configFilePath: config?.configFilePath ?? null, + jsConfigMigration, + }) } catch (e: any) { error(`${e?.message ?? e} in ${highlight(relative(sheet.file!, base))}`, { prefix: '↳ ' }) } @@ -216,31 +182,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() + } } } @@ -259,9 +227,80 @@ async function run() { success(`Migrated stylesheet: ${highlight(relative(sheet.file, base))}`, { prefix: '↳ ' }) } } + + 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 + if (tailwindRootStylesheets.length > 0) { + info('Migrating templates…') + } + { + let seenFiles = new Set() + + // Template migrations + 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 [] + } + + // No root specified, use the base directory + if (compiler.root === null) { + return [{ base, pattern: '**/*', negated: false }] + } + + // 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( + filesToMigrate.map((file) => + migrateTemplate(designSystem, config?.userConfig ?? null, file), + ), + ) + + 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: '↳ ', + }, + ) + } + } + } } - { + if (version.isMajor(3)) { // PostCSS config migration await migratePostCSSConfig(base) } @@ -272,12 +311,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())) 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, 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] -} 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) +} 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%') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d571aeafffd..88f88d2287b3 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.1.2': resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + '@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: {}