diff --git a/CHANGELOG.md b/CHANGELOG.md index f02aa1fc4d95..464ccfae7cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556)) - Add `color-scheme` utilities ([#14567](https://github.com/tailwindlabs/tailwindcss/pull/14567)) - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) +- _Experimental_: Migrate `@apply` utilities with the template codemods ([#14574](https://github.com/tailwindlabs/tailwindcss/pull/14574)) - _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537])) - _Experimental_: Add template codemods for migrating prefixes ([#14557](https://github.com/tailwindlabs/tailwindcss/pull/14557])) - _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526)) diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 7d17299aa8d1..881f40010a78 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -69,6 +69,10 @@ test( @tailwind base; @tailwind components; @tailwind utilities; + + .btn { + @apply !tw__rounded-md tw__px-2 tw__py-1 tw__bg-blue-500 tw__text-white; + } `, }, }, @@ -83,7 +87,15 @@ test( `, ) - await fs.expectFileToContain('src/input.css', css`@import 'tailwindcss' prefix(tw);`) + await fs.expectFileToContain('src/input.css', css` @import 'tailwindcss' prefix(tw); `) + await fs.expectFileToContain( + 'src/input.css', + css` + .btn { + @apply tw:rounded-md! tw:px-2 tw:py-1 tw:bg-blue-500 tw:text-white; + } + `, + ) }, ) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts index f270472d1e65..dc19bbae9c49 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts @@ -1,3 +1,4 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' import dedent from 'dedent' import postcss from 'postcss' import { expect, it } from 'vitest' @@ -5,7 +6,7 @@ import { migrateAtApply } from './migrate-at-apply' const css = dedent -function migrate(input: string) { +function migrateWithoutConfig(input: string) { return postcss() .use(migrateAtApply()) .process(input, { from: expect.getState().testPath }) @@ -14,7 +15,7 @@ function migrate(input: string) { it('should not migrate `@apply`, when there are no issues', async () => { expect( - await migrate(css` + await migrateWithoutConfig(css` .foo { @apply flex flex-col items-center; } @@ -28,7 +29,7 @@ it('should not migrate `@apply`, when there are no issues', async () => { it('should append `!` to each utility, when using `!important`', async () => { expect( - await migrate(css` + await migrateWithoutConfig(css` .foo { @apply flex flex-col !important; } @@ -43,7 +44,7 @@ it('should append `!` to each utility, when using `!important`', async () => { // TODO: Handle SCSS syntax it.skip('should append `!` to each utility, when using `#{!important}`', async () => { expect( - await migrate(css` + await migrateWithoutConfig(css` .foo { @apply flex flex-col #{!important}; } @@ -57,7 +58,7 @@ it.skip('should append `!` to each utility, when using `#{!important}`', async ( it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => { expect( - await migrate(css` + await migrateWithoutConfig(css` .foo { @apply !flex flex-col! hover:!items-start items-center; } @@ -68,3 +69,36 @@ it('should move the legacy `!` prefix, to the new `!` postfix notation', async ( }" `) }) + +it('should apply all candidate migration when migrating with a config', async () => { + async function migrateWithConfig(input: string) { + return postcss() + .use( + migrateAtApply({ + designSystem: await __unstable__loadDesignSystem( + css` + @import 'tailwindcss' prefix(tw); + `, + { base: __dirname }, + ), + userConfig: { + prefix: 'tw_', + }, + }), + ) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) + } + + expect( + await migrateWithConfig(css` + .foo { + @apply !tw_flex [color:--my-color]; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply tw:flex! tw:[color:var(--my-color)]; + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts index ebea30e96d6a..487039f48b1b 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts @@ -1,7 +1,13 @@ import type { AtRule, Plugin } from 'postcss' +import type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' import { segment } from '../../../tailwindcss/src/utils/segment' +import { migrateCandidate } from '../template/migrate' -export function migrateAtApply(): Plugin { +export function migrateAtApply({ + designSystem, + userConfig, +}: { designSystem?: DesignSystem; userConfig?: Config } = {}): Plugin { function migrate(atRule: AtRule) { let utilities = atRule.params.split(/(\s+)/) let important = @@ -30,6 +36,12 @@ export function migrateAtApply(): Plugin { return [...variants, utility].join(':') }) + // If we have a valid designSystem and config setup, we can run all + // candidate migrations on each utility + if (designSystem && userConfig) { + params = params.map((param) => migrateCandidate(designSystem, userConfig, param)) + } + atRule.params = params.join('').trim() } diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 1bc83ff1617a..573c13f48cd6 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -112,7 +112,11 @@ async function run() { // Migrate each file await Promise.allSettled( files.map((file) => - migrateStylesheet(file, { newPrefix: parsedConfig?.newPrefix ?? undefined }), + migrateStylesheet(file, { + newPrefix: parsedConfig?.newPrefix ?? undefined, + designSystem: parsedConfig?.designSystem, + userConfig: parsedConfig?.userConfig, + }), ), ) diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index e4b0aa36ab5a..680e7c67b04c 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -1,6 +1,8 @@ import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' +import type { Config } from 'tailwindcss' +import type { DesignSystem } from '../../tailwindcss/src/design-system' import { formatNodes } from './codemods/format-nodes' import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' @@ -9,11 +11,13 @@ import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directive export interface MigrateOptions { newPrefix?: string + designSystem?: DesignSystem + userConfig?: Config } export async function migrateContents(contents: string, options: MigrateOptions, file?: string) { return postcss() - .use(migrateAtApply()) + .use(migrateAtApply(options)) .use(migrateAtLayerUtilities()) .use(migrateMissingLayers()) .use(migrateTailwindDirectives(options)) diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts index 738fd5ac4adc..9fce6aaeeb59 100644 --- a/packages/@tailwindcss-upgrade/src/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts @@ -14,11 +14,28 @@ export type Migration = ( rawCandidate: string, ) => string +export const DEFAULT_MIGRATIONS: Migration[] = [ + prefix, + important, + automaticVarInjection, + bgGradient, +] + +export function migrateCandidate( + designSystem: DesignSystem, + userConfig: Config, + rawCandidate: string, +): string { + for (let migration of DEFAULT_MIGRATIONS) { + rawCandidate = migration(designSystem, userConfig, rawCandidate) + } + return rawCandidate +} + export default async function migrateContents( designSystem: DesignSystem, userConfig: Config, contents: string, - migrations: Migration[] = [prefix, important, automaticVarInjection, bgGradient], ): Promise { let candidates = await extractRawCandidates(contents) @@ -27,17 +44,10 @@ export default async function migrateContents( let output = contents for (let { rawCandidate, start, end } of candidates) { - let needsMigration = false - for (let migration of migrations) { - let candidate = migration(designSystem, userConfig, rawCandidate) - if (rawCandidate !== candidate) { - rawCandidate = candidate - needsMigration = true - } - } + let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate) - if (needsMigration) { - output = replaceCandidateInContent(output, rawCandidate, start, end) + if (migratedCandidate !== rawCandidate) { + output = replaceCandidateInContent(output, migratedCandidate, start, end) } }