Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate at-apply utilites with template migrations #14574

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 variant order ([#14524](https://github.com/tailwindlabs/tailwindcss/pull/14524]))
- _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]))
Expand Down
14 changes: 13 additions & 1 deletion integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`,
},
},
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so beautiful 🥹

}
`,
)
},
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
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 })
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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};
}
Expand All @@ -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;
}
Expand All @@ -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 () => {
philipp-spiess marked this conversation as resolved.
Show resolved Hide resolved
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] tw_bg-gradient-to-t;
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply tw:flex! tw:[color:var(--my-color)] tw:bg-linear-to-t;
}"
`)
})
14 changes: 13 additions & 1 deletion packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down Expand Up @@ -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))
}
Comment on lines +39 to +43
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably consider making a JS config file input required. This way we can simplify this decision here and don't have a case where @apply is migrated differently. Any thoughts on this @RobinMalfait?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I think a better way is to try to figure out where the config file lives (similar to how Tailwind CSS v3 just finds the config file if it's a known file name + extension in the root). If you have a custom file (aka, we can't resolve one) then the --config option should be required.

Another tricky thing, but will be more related to the stuff I'm working on with @thecrypticace (where we handle stylesheets as graphs to figure out which file imports what), is the fact that in theory you can have multiple roots with different settings. Which means that @apply in one file could require tw: prefixes, and the other file might not require prefixes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense! Adding a task


atRule.params = params.join('').trim()
}

Expand Down
6 changes: 5 additions & 1 deletion packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
),
)

Expand Down
6 changes: 5 additions & 1 deletion packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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))
Expand Down
33 changes: 22 additions & 11 deletions packages/@tailwindcss-upgrade/src/template/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,29 @@ export type Migration = (
rawCandidate: string,
) => string

export const DEFAULT_MIGRATIONS: Migration[] = [
prefix,
important,
automaticVarInjection,
bgGradient,
variantOrder,
]

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, bgGradient, automaticVarInjection, variantOrder],
): Promise<string> {
let candidates = await extractRawCandidates(contents)

Expand All @@ -28,17 +46,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)
}
}

Expand Down