Skip to content

Commit 47d6a02

Browse files
Ensure first class inside className in React is migrated (#19031)
### 1. Handling React className migration The PR fixes an issue when migrating React components to tailwind v4 with the migration tool, that the first class after `className="` is ignored. For example, when migrating ```JSX <div className="shadow"></div> ``` `shadow` will not be migrated to `shadow-sm` . This is because in `is-safe-migration.ts`, it tests the line before candidate with regex `/(?<!:?class)=['"]$/`. This basically skips the migration for anything like `foo="shadow"`, with only exception for Vue (eg. `class="shadow"`). The PR changes the regex from ```regex /(?<!:?class)=['"]$/ ```` to ```regex /(?<!:?class|className)=['"]$/ ``` which essentially adds a new exception specifically for React's `className="shadow"` case. ### 2. Removing redundant rules Besides, I found that several other rules in `CONDITIONAL_TEMPLATE_SYNTAX` being redundant since they are already covered by the rule above, so I removed them. If we prefer the previous explicit approach, I can revert it. ## Test plan <!-- Explain how you tested your changes. Include the exact commands that you used to verify the change works and include screenshots/screen recordings of the update behavior in the browser if applicable. --> Tests added for both the Vue and React classes to prevent false negative cases. --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent 9d00662 commit 47d6a02

File tree

3 files changed

+30
-7
lines changed

3 files changed

+30
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979))
2929
- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988))
3030
- Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028))
31+
- Upgrade: Ensure first class inside className in React is migrated ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
32+
- Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
3133

3234
## [4.1.13] - 2025-09-03
3335

packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,27 @@ describe('is-safe-migration', async () => {
112112
}),
113113
).toEqual(candidate)
114114
})
115+
116+
test.each([
117+
// Vue classes
118+
[`<div class="shadow"></div>`, 'shadow', 'shadow-sm'],
119+
[`<div :class="{ shadow: true }"></div>`, 'shadow', 'shadow-sm'],
120+
[`<div enter-class="shadow"></div>`, 'shadow', 'shadow-sm'],
121+
[`<div :enter-class="{ shadow: true }"></div>`, 'shadow', 'shadow-sm'],
122+
123+
// React classes
124+
[`<div className="shadow"></div>`, 'shadow', 'shadow-sm'],
125+
[`<div enterClassName="shadow"></div>`, 'shadow', 'shadow-sm'],
126+
127+
// Preact-style
128+
[`<div enterClass="shadow"></div>`, 'shadow', 'shadow-sm'],
129+
])('replaces classes in valid positions #%#', async (example, candidate, expected) => {
130+
expect(
131+
await migrateCandidate(designSystem, {}, candidate, {
132+
contents: example,
133+
start: example.indexOf(candidate),
134+
end: example.indexOf(candidate) + candidate.length,
135+
}),
136+
).toEqual(expected)
137+
})
115138
})

packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,16 @@ import * as version from '../../utils/version'
55

66
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
77
const CONDITIONAL_TEMPLATE_SYNTAX = [
8-
// Vue
9-
/v-else-if=['"]$/,
10-
/v-if=['"]$/,
11-
/v-show=['"]$/,
12-
/(?<!:?class)=['"]$/,
8+
// Skip any generic attributes like `xxx="shadow"`,
9+
// including Vue conditions like `v-if="something && shadow"`
10+
// and Alpine conditions like `x-if="shadow"`,
11+
// but allow Vue and React classes
12+
/(?<!:?class|className)=['"]$/i,
1313

1414
// JavaScript / TypeScript
1515
/addEventListener\(['"`]$/,
1616

1717
// Alpine
18-
/x-if=['"]$/,
19-
/x-show=['"]$/,
2018
/wire:[^\s]*?$/,
2119

2220
// shadcn/ui variants

0 commit comments

Comments
 (0)