diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81c92200cdf5..515473f78728 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))
+- _Upgrade (experimental)_: Don't migrate important modifiers that are actually logical negations (e.g. `let foo = !border` to `let foo = border!`) ([#14737](https://github.com/tailwindlabs/tailwindcss/pull/14737))
### Changed
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index cf068b6e614b..22e4ace9cd34 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -72,6 +72,11 @@ test(
@tailwind components;
@tailwind utilities;
`,
+ 'src/test.js': ts`
+ export default {
+ shouldNotUse: !border.shouldUse,
+ }
+ `,
'node_modules/my-external-lib/src/template.html': html`
Hello world!
@@ -82,7 +87,7 @@ test(
async ({ exec, fs }) => {
await exec('npx @tailwindcss/upgrade')
- expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
+ expect(await fs.dumpFiles('src/**/*.{css,js}')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@@ -134,6 +139,11 @@ test(
}
}
}
+
+ --- src/test.js ---
+ export default {
+ shouldNotUse: !border.shouldUse,
+ }
"
`)
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
index 4a53f8d4aa8b..64f098b9bde4 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts
@@ -15,5 +15,39 @@ test.each([
base: __dirname,
})
- expect(important(designSystem, {}, candidate)).toEqual(result)
+ expect(
+ important(designSystem, {}, candidate, {
+ contents: `"${candidate}"`,
+ start: 1,
+ end: candidate.length + 1,
+ }),
+ ).toEqual(result)
+})
+
+test('does not match false positives', async () => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ expect(
+ important(designSystem, {}, '!border', {
+ contents: `let notBorder = !border\n`,
+ start: 16,
+ end: 16 + '!border'.length,
+ }),
+ ).toEqual('!border')
+})
+
+test('does not match false positives with spaces at the end of the line', async () => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ expect(
+ important(designSystem, {}, '!border', {
+ contents: `let notBorder = !border \n`,
+ start: 16,
+ end: 16 + '!border'.length,
+ }),
+ ).toEqual('!border')
})
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
index 69dbdba853f3..4547905291ae 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts
@@ -19,9 +19,50 @@ export function important(
designSystem: DesignSystem,
_userConfig: Config,
rawCandidate: string,
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
): string {
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
+ // The important migration is one of the most broad migrations with a high
+ // potential of matching false positives since `!` is a valid character in
+ // most programming languages. Since v4 is technically backward compatible
+ // with v3 in that it can read `!` in the front of the utility too, we err
+ // on the side of caution and only migrate candidates that we are certain
+ // are inside of a string.
+ if (location) {
+ let isQuoteBeforeCandidate = false
+ for (let i = location.start - 1; i >= 0; i--) {
+ let char = location.contents.at(i)!
+ if (char === '\n') {
+ break
+ }
+ if (isQuote(char)) {
+ isQuoteBeforeCandidate = true
+ break
+ }
+ }
+
+ let isQuoteAfterCandidate = false
+ for (let i = location.end; i < location.contents.length; i++) {
+ let char = location.contents.at(i)!
+ if (char === '\n') {
+ break
+ }
+ if (isQuote(char)) {
+ isQuoteAfterCandidate = true
+ break
+ }
+ }
+
+ if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
+ continue
+ }
+ }
+
// The printCandidate function will already put the exclamation mark in
// the right place, so we just need to mark this candidate as requiring a
// migration.
@@ -31,3 +72,14 @@ export function important(
return rawCandidate
}
+
+function isQuote(char: string) {
+ switch (char) {
+ case '"':
+ case "'":
+ case '`':
+ return true
+ default:
+ return false
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts
index c6aa0a11cdef..5e2d02eab52b 100644
--- a/packages/@tailwindcss-upgrade/src/template/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts
@@ -17,6 +17,11 @@ export type Migration = (
designSystem: DesignSystem,
userConfig: Config,
rawCandidate: string,
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
) => string
export const DEFAULT_MIGRATIONS: Migration[] = [
@@ -34,9 +39,15 @@ export function migrateCandidate(
designSystem: DesignSystem,
userConfig: Config,
rawCandidate: string,
+ // Location is only set when migrating a candidate from a source file
+ location?: {
+ contents: string
+ start: number
+ end: number
+ },
): string {
for (let migration of DEFAULT_MIGRATIONS) {
- rawCandidate = migration(designSystem, userConfig, rawCandidate)
+ rawCandidate = migration(designSystem, userConfig, rawCandidate, location)
}
return rawCandidate
}
@@ -52,7 +63,11 @@ export default async function migrateContents(
let changes: StringChange[] = []
for (let { rawCandidate, start, end } of candidates) {
- let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate)
+ let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate, {
+ contents,
+ start,
+ end,
+ })
if (migratedCandidate === rawCandidate) {
continue