Skip to content

Commit 9d00662

Browse files
authored
Show version mismatch when running upgrade tool (#19028)
This PR fixes an issue where sometimes people try to run the upgrade tool, reset the changes and then try again. If this happens, then the `package.json` and/or your lock file will point to the old Tailwind CSS v3 version, but the actual installed version will be v4. This will also cause the upgrade tool to now upgrade from v4 to v4, which is not what most people want if they were trying to upgrade from v3 to v4. This in turn will cause some issues because now we won't try to migrate the config file, or v3-specific classes that also exist in v4 but are only safe to upgrade from v3 to v4. This PR uses `npm ls tailwindcss` to determine the actual installed version. This command already errors if there is a mismatch between the installed version and the version in `package.json` or the lock file. This also happens to work in pnpm and bun projects (added integration tests for these). If for whatever reason we can't determine the expected version, we fall back to the old behavior of just upgrading. In this scenario, the changes introduced in #19026 will at least give you a hint of what version was actually installed. ### Test plan 1. Tested it in a v3 project where I performed the following steps: 1. Run the upgrade tool in full (`npx tailwindcss-upgrade`) 2. Reset the changes (`git reset --hard && git clean -df`) 1. Run the upgrade tool again This resulted in the following output: <img width="1059" height="683" alt="image" src="https://github.com/user-attachments/assets/1d2ea2d1-b602-4631-958f-cc21eb8a633f" /> 2. Added some integration tests to make sure this also works in pnpm, bun and normal npm projects. [ci-all]
1 parent b497e1e commit 9d00662

File tree

7 files changed

+290
-1
lines changed

7 files changed

+290
-1
lines changed

.github/workflows/integration-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ jobs:
5353
- uses: actions/checkout@v4
5454
- uses: pnpm/action-setup@v4
5555

56+
- run: |
57+
git config --global user.name "github-actions[bot]"
58+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
59+
5660
- name: Use Node.js ${{ matrix.node-version }}
5761
uses: actions/setup-node@v4
5862
with:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967))
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))
30+
- Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028))
3031

3132
## [4.1.13] - 2025-09-03
3233

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { stripVTControlCharacters } from 'node:util'
2+
// @ts-expect-error This path does exist
3+
import { version } from '../../packages/tailwindcss/package.json'
4+
import { css, html, js, json, test } from '../utils'
5+
6+
test(
7+
'upgrades half-upgraded v3 project to v4 (pnpm)',
8+
{
9+
fs: {
10+
'package.json': json`
11+
{
12+
"dependencies": {
13+
"tailwindcss": "^3",
14+
"@tailwindcss/upgrade": "workspace:^"
15+
},
16+
"devDependencies": {
17+
"@tailwindcss/cli": "workspace:^"
18+
}
19+
}
20+
`,
21+
'tailwind.config.js': js`
22+
/** @type {import('tailwindcss').Config} */
23+
module.exports = {
24+
content: ['./src/**/*.{html,js}'],
25+
}
26+
`,
27+
'src/index.html': html`
28+
<div class="!flex">Hello World</div>
29+
`,
30+
'src/input.css': css`
31+
@tailwind base;
32+
@tailwind components;
33+
@tailwind utilities;
34+
`,
35+
},
36+
},
37+
async ({ exec, expect }) => {
38+
// Ensure we are in a git repo
39+
await exec('git init')
40+
await exec('git add --all')
41+
await exec('git commit -m "before migration"')
42+
43+
// Fully upgrade to v4
44+
await exec('npx @tailwindcss/upgrade')
45+
46+
// Undo all changes to the current repo. This will bring the repo back to a
47+
// v3 state, but the `node_modules` will now have v4 installed.
48+
await exec('git reset --hard HEAD')
49+
50+
// Re-running the upgrade should result in an error
51+
return expect(() => {
52+
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
53+
// Replacing the current version with a hardcoded `v4` to make it stable
54+
// when we release new minor/patch versions.
55+
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
56+
})
57+
}).rejects.toThrowErrorMatchingInlineSnapshot(`
58+
"Command failed: npx @tailwindcss/upgrade
59+
≈ tailwindcss v4.0.0
60+
61+
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
62+
63+
│ ↳ Version mismatch
64+
65+
│ \`\`\`diff
66+
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
67+
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
68+
│ \`\`\`
69+
70+
│ Make sure to run \`pnpm install\`, and try again.
71+
72+
"
73+
`)
74+
},
75+
)
76+
77+
test(
78+
'upgrades half-upgraded v3 project to v4 (bun)',
79+
{
80+
fs: {
81+
'package.json': json`
82+
{
83+
"dependencies": {
84+
"tailwindcss": "^3",
85+
"@tailwindcss/upgrade": "workspace:^"
86+
},
87+
"devDependencies": {
88+
"@tailwindcss/cli": "workspace:^",
89+
"bun": "^1.0.0"
90+
}
91+
}
92+
`,
93+
'tailwind.config.js': js`
94+
/** @type {import('tailwindcss').Config} */
95+
module.exports = {
96+
content: ['./src/**/*.{html,js}'],
97+
}
98+
`,
99+
'src/index.html': html`
100+
<div class="!flex">Hello World</div>
101+
`,
102+
'src/input.css': css`
103+
@tailwind base;
104+
@tailwind components;
105+
@tailwind utilities;
106+
`,
107+
},
108+
},
109+
async ({ exec, expect }) => {
110+
// Use `bun` to install dependencies
111+
await exec('rm ./pnpm-lock.yaml')
112+
await exec('npx bun install')
113+
114+
// Ensure we are in a git repo
115+
await exec('git init')
116+
await exec('git add --all')
117+
await exec('git commit -m "before migration"')
118+
119+
// Fully upgrade to v4
120+
await exec('npx @tailwindcss/upgrade')
121+
122+
// Undo all changes to the current repo. This will bring the repo back to a
123+
// v3 state, but the `node_modules` will now have v4 installed.
124+
await exec('git reset --hard HEAD')
125+
126+
// Re-running the upgrade should result in an error
127+
return expect(() => {
128+
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
129+
// Replacing the current version with a hardcoded `v4` to make it stable
130+
// when we release new minor/patch versions.
131+
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
132+
})
133+
}).rejects.toThrowErrorMatchingInlineSnapshot(`
134+
"Command failed: npx @tailwindcss/upgrade
135+
≈ tailwindcss v4.0.0
136+
137+
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
138+
139+
│ ↳ Version mismatch
140+
141+
│ \`\`\`diff
142+
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
143+
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
144+
│ \`\`\`
145+
146+
│ Make sure to run \`bun install\`, and try again.
147+
148+
"
149+
`)
150+
},
151+
)
152+
153+
test(
154+
'upgrades half-upgraded v3 project to v4 (npm)',
155+
{
156+
fs: {
157+
'package.json': json`
158+
{
159+
"dependencies": {
160+
"tailwindcss": "^3",
161+
"@tailwindcss/upgrade": "workspace:^"
162+
},
163+
"devDependencies": {
164+
"@tailwindcss/cli": "workspace:^"
165+
}
166+
}
167+
`,
168+
'tailwind.config.js': js`
169+
/** @type {import('tailwindcss').Config} */
170+
module.exports = {
171+
content: ['./src/**/*.{html,js}'],
172+
}
173+
`,
174+
'src/index.html': html`
175+
<div class="!flex">Hello World</div>
176+
`,
177+
'src/input.css': css`
178+
@tailwind base;
179+
@tailwind components;
180+
@tailwind utilities;
181+
`,
182+
},
183+
},
184+
async ({ exec, expect }) => {
185+
// Use `bun` to install dependencies
186+
await exec('rm ./pnpm-lock.yaml')
187+
await exec('rm -rf ./node_modules')
188+
await exec('npm install')
189+
190+
// Ensure we are in a git repo
191+
await exec('git init')
192+
await exec('git add --all')
193+
await exec('git commit -m "before migration"')
194+
195+
// Fully upgrade to v4
196+
await exec('npx @tailwindcss/upgrade')
197+
198+
// Undo all changes to the current repo. This will bring the repo back to a
199+
// v3 state, but the `node_modules` will now have v4 installed.
200+
await exec('git reset --hard HEAD')
201+
202+
// Re-running the upgrade should result in an error
203+
return expect(() => {
204+
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
205+
// Replacing the current version with a hardcoded `v4` to make it stable
206+
// when we release new minor/patch versions.
207+
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
208+
})
209+
}).rejects.toThrowErrorMatchingInlineSnapshot(`
210+
"Command failed: npx @tailwindcss/upgrade
211+
≈ tailwindcss v4.0.0
212+
213+
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
214+
215+
│ ↳ Version mismatch
216+
217+
│ \`\`\`diff
218+
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
219+
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
220+
│ \`\`\`
221+
222+
│ Make sure to run \`npm install\`, and try again.
223+
224+
"
225+
`)
226+
},
227+
)

packages/@tailwindcss-upgrade/src/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Scanner } from '@tailwindcss/oxide'
44
import { globby } from 'globby'
55
import fs from 'node:fs/promises'
66
import path from 'node:path'
7+
import pc from 'picocolors'
78
import postcss from 'postcss'
89
import { migrateJsConfig } from './codemods/config/migrate-js-config'
910
import { migratePostCSSConfig } from './codemods/config/migrate-postcss'
@@ -62,6 +63,27 @@ async function run() {
6263
prefix: '↳ ',
6364
})
6465

66+
if (version.installedTailwindVersion(base) !== version.expectedTailwindVersion(base)) {
67+
let pkgManager = await pkg(base).manager()
68+
69+
error(
70+
[
71+
'Version mismatch',
72+
'',
73+
pc.dim('```diff'),
74+
`${pc.red('-')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.expectedTailwindVersion(base))}${pc.dim('"')}`}`} (expected version in package.json / lockfile)`,
75+
`${pc.green('+')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.installedTailwindVersion(base))}${pc.dim('"')}`}`} (installed version in \`node_modules\`)`,
76+
pc.dim('```'),
77+
'',
78+
`Make sure to run ${highlight(`${pkgManager} install`)}, and try again.`,
79+
].join('\n'),
80+
{
81+
prefix: '↳ ',
82+
},
83+
)
84+
process.exit(1)
85+
}
86+
6587
{
6688
// Stylesheet migrations
6789

packages/@tailwindcss-upgrade/src/utils/packages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const manifests = new DefaultMap((base) => {
2424

2525
export function pkg(base: string) {
2626
return {
27+
async manager() {
28+
return await packageManagerForBase.get(base)
29+
},
2730
async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') {
2831
let packageManager = await packageManagerForBase.get(base)
2932
let args = packages.slice()

packages/@tailwindcss-upgrade/src/utils/renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function wordWrap(text: string, width: number): string[] {
4444
// Handle text with newlines by maintaining the newlines, then splitting
4545
// each line separately.
4646
if (text.includes('\n')) {
47-
return text.split('\n').flatMap((line) => wordWrap(line, width))
47+
return text.split('\n').flatMap((line) => (line ? wordWrap(line, width) : ['']))
4848
}
4949

5050
let words = text.split(' ')

packages/@tailwindcss-upgrade/src/utils/version.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { execSync } from 'node:child_process'
12
import semver from 'semver'
23
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
34
import { getPackageVersionSync } from './package-version'
@@ -29,3 +30,34 @@ let cache = new DefaultMap((base) => {
2930
export function installedTailwindVersion(base = process.cwd()): string {
3031
return cache.get(base)
3132
}
33+
34+
let expectedCache = new DefaultMap((base) => {
35+
try {
36+
// This will report a problem if the package.json/package-lock.json
37+
// mismatches with the installed version in node_modules.
38+
//
39+
// Also tested this with Bun and PNPM, both seem to work fine.
40+
execSync('npm ls tailwindcss --json', { cwd: base, stdio: 'pipe' })
41+
return installedTailwindVersion(base)
42+
} catch (_e) {
43+
try {
44+
let e = _e as { stdout: Buffer }
45+
let data = JSON.parse(e.stdout.toString())
46+
47+
return (
48+
// Could be a sub-dependency issue, but we are only interested in
49+
// the top-level version mismatch.
50+
/"(.*?)" from the root project/.exec(data.dependencies.tailwindcss.invalid)?.[1] ??
51+
// Fallback to the installed version
52+
installedTailwindVersion(base)
53+
)
54+
} catch {
55+
// We don't know how to verify, so let's just return the installed
56+
// version to not block the user.
57+
return installedTailwindVersion(base)
58+
}
59+
}
60+
})
61+
export function expectedTailwindVersion(base = process.cwd()): string {
62+
return expectedCache.get(base)
63+
}

0 commit comments

Comments
 (0)