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 https://github.com/tailwindlabs/tailwindcss/pull/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]
This commit is contained in:
parent
b497e1eaf3
commit
9d00662e10
7 changed files with 290 additions and 1 deletions
4
.github/workflows/integration-tests.yml
vendored
4
.github/workflows/integration-tests.yml
vendored
|
|
@ -53,6 +53,10 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967))
|
||||
- Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979))
|
||||
- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988))
|
||||
- Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028))
|
||||
|
||||
## [4.1.13] - 2025-09-03
|
||||
|
||||
|
|
|
|||
227
integrations/upgrade/upgrade-errors.test.ts
Normal file
227
integrations/upgrade/upgrade-errors.test.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { stripVTControlCharacters } from 'node:util'
|
||||
// @ts-expect-error This path does exist
|
||||
import { version } from '../../packages/tailwindcss/package.json'
|
||||
import { css, html, js, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'upgrades half-upgraded v3 project to v4 (pnpm)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="!flex">Hello World</div>
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Ensure we are in a git repo
|
||||
await exec('git init')
|
||||
await exec('git add --all')
|
||||
await exec('git commit -m "before migration"')
|
||||
|
||||
// Fully upgrade to v4
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
// Undo all changes to the current repo. This will bring the repo back to a
|
||||
// v3 state, but the `node_modules` will now have v4 installed.
|
||||
await exec('git reset --hard HEAD')
|
||||
|
||||
// Re-running the upgrade should result in an error
|
||||
return expect(() => {
|
||||
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
// Replacing the current version with a hardcoded `v4` to make it stable
|
||||
// when we release new minor/patch versions.
|
||||
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
|
||||
})
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Command failed: npx @tailwindcss/upgrade
|
||||
≈ tailwindcss v4.0.0
|
||||
|
||||
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
|
||||
|
||||
│ ↳ Version mismatch
|
||||
│
|
||||
│ \`\`\`diff
|
||||
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
|
||||
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
|
||||
│ \`\`\`
|
||||
│
|
||||
│ Make sure to run \`pnpm install\`, and try again.
|
||||
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrades half-upgraded v3 project to v4 (bun)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "workspace:^",
|
||||
"bun": "^1.0.0"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="!flex">Hello World</div>
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `bun` to install dependencies
|
||||
await exec('rm ./pnpm-lock.yaml')
|
||||
await exec('npx bun install')
|
||||
|
||||
// Ensure we are in a git repo
|
||||
await exec('git init')
|
||||
await exec('git add --all')
|
||||
await exec('git commit -m "before migration"')
|
||||
|
||||
// Fully upgrade to v4
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
// Undo all changes to the current repo. This will bring the repo back to a
|
||||
// v3 state, but the `node_modules` will now have v4 installed.
|
||||
await exec('git reset --hard HEAD')
|
||||
|
||||
// Re-running the upgrade should result in an error
|
||||
return expect(() => {
|
||||
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
// Replacing the current version with a hardcoded `v4` to make it stable
|
||||
// when we release new minor/patch versions.
|
||||
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
|
||||
})
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Command failed: npx @tailwindcss/upgrade
|
||||
≈ tailwindcss v4.0.0
|
||||
|
||||
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
|
||||
|
||||
│ ↳ Version mismatch
|
||||
│
|
||||
│ \`\`\`diff
|
||||
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
|
||||
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
|
||||
│ \`\`\`
|
||||
│
|
||||
│ Make sure to run \`bun install\`, and try again.
|
||||
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrades half-upgraded v3 project to v4 (npm)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
}
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="!flex">Hello World</div>
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `bun` to install dependencies
|
||||
await exec('rm ./pnpm-lock.yaml')
|
||||
await exec('rm -rf ./node_modules')
|
||||
await exec('npm install')
|
||||
|
||||
// Ensure we are in a git repo
|
||||
await exec('git init')
|
||||
await exec('git add --all')
|
||||
await exec('git commit -m "before migration"')
|
||||
|
||||
// Fully upgrade to v4
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
// Undo all changes to the current repo. This will bring the repo back to a
|
||||
// v3 state, but the `node_modules` will now have v4 installed.
|
||||
await exec('git reset --hard HEAD')
|
||||
|
||||
// Re-running the upgrade should result in an error
|
||||
return expect(() => {
|
||||
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
// Replacing the current version with a hardcoded `v4` to make it stable
|
||||
// when we release new minor/patch versions.
|
||||
return Promise.reject(stripVTControlCharacters(e.message.replaceAll(version, '4.0.0')))
|
||||
})
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Command failed: npx @tailwindcss/upgrade
|
||||
≈ tailwindcss v4.0.0
|
||||
|
||||
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
|
||||
|
||||
│ ↳ Version mismatch
|
||||
│
|
||||
│ \`\`\`diff
|
||||
│ - "tailwindcss": "^3" (expected version in package.json / lockfile)
|
||||
│ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`)
|
||||
│ \`\`\`
|
||||
│
|
||||
│ Make sure to run \`npm install\`, and try again.
|
||||
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ import { Scanner } from '@tailwindcss/oxide'
|
|||
import { globby } from 'globby'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import pc from 'picocolors'
|
||||
import postcss from 'postcss'
|
||||
import { migrateJsConfig } from './codemods/config/migrate-js-config'
|
||||
import { migratePostCSSConfig } from './codemods/config/migrate-postcss'
|
||||
|
|
@ -62,6 +63,27 @@ async function run() {
|
|||
prefix: '↳ ',
|
||||
})
|
||||
|
||||
if (version.installedTailwindVersion(base) !== version.expectedTailwindVersion(base)) {
|
||||
let pkgManager = await pkg(base).manager()
|
||||
|
||||
error(
|
||||
[
|
||||
'Version mismatch',
|
||||
'',
|
||||
pc.dim('```diff'),
|
||||
`${pc.red('-')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.expectedTailwindVersion(base))}${pc.dim('"')}`}`} (expected version in package.json / lockfile)`,
|
||||
`${pc.green('+')} ${`${pc.dim('"tailwindcss":')} ${`${pc.dim('"')}${pc.blue(version.installedTailwindVersion(base))}${pc.dim('"')}`}`} (installed version in \`node_modules\`)`,
|
||||
pc.dim('```'),
|
||||
'',
|
||||
`Make sure to run ${highlight(`${pkgManager} install`)}, and try again.`,
|
||||
].join('\n'),
|
||||
{
|
||||
prefix: '↳ ',
|
||||
},
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
{
|
||||
// Stylesheet migrations
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ const manifests = new DefaultMap((base) => {
|
|||
|
||||
export function pkg(base: string) {
|
||||
return {
|
||||
async manager() {
|
||||
return await packageManagerForBase.get(base)
|
||||
},
|
||||
async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') {
|
||||
let packageManager = await packageManagerForBase.get(base)
|
||||
let args = packages.slice()
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function wordWrap(text: string, width: number): string[] {
|
|||
// Handle text with newlines by maintaining the newlines, then splitting
|
||||
// each line separately.
|
||||
if (text.includes('\n')) {
|
||||
return text.split('\n').flatMap((line) => wordWrap(line, width))
|
||||
return text.split('\n').flatMap((line) => (line ? wordWrap(line, width) : ['']))
|
||||
}
|
||||
|
||||
let words = text.split(' ')
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { execSync } from 'node:child_process'
|
||||
import semver from 'semver'
|
||||
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
|
||||
import { getPackageVersionSync } from './package-version'
|
||||
|
|
@ -29,3 +30,34 @@ let cache = new DefaultMap((base) => {
|
|||
export function installedTailwindVersion(base = process.cwd()): string {
|
||||
return cache.get(base)
|
||||
}
|
||||
|
||||
let expectedCache = new DefaultMap((base) => {
|
||||
try {
|
||||
// This will report a problem if the package.json/package-lock.json
|
||||
// mismatches with the installed version in node_modules.
|
||||
//
|
||||
// Also tested this with Bun and PNPM, both seem to work fine.
|
||||
execSync('npm ls tailwindcss --json', { cwd: base, stdio: 'pipe' })
|
||||
return installedTailwindVersion(base)
|
||||
} catch (_e) {
|
||||
try {
|
||||
let e = _e as { stdout: Buffer }
|
||||
let data = JSON.parse(e.stdout.toString())
|
||||
|
||||
return (
|
||||
// Could be a sub-dependency issue, but we are only interested in
|
||||
// the top-level version mismatch.
|
||||
/"(.*?)" from the root project/.exec(data.dependencies.tailwindcss.invalid)?.[1] ??
|
||||
// Fallback to the installed version
|
||||
installedTailwindVersion(base)
|
||||
)
|
||||
} catch {
|
||||
// We don't know how to verify, so let's just return the installed
|
||||
// version to not block the user.
|
||||
return installedTailwindVersion(base)
|
||||
}
|
||||
}
|
||||
})
|
||||
export function expectedTailwindVersion(base = process.cwd()): string {
|
||||
return expectedCache.get(base)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue