Skip to content
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
4 changes: 4 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
227 changes: 227 additions & 0 deletions integrations/upgrade/upgrade-errors.test.ts
Original file line number Diff line number Diff line change
@@ -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.

"
`)
},
)
22 changes: 22 additions & 0 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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: '↳ ',
},
)
Comment on lines +69 to +83
Copy link
Member Author

@RobinMalfait RobinMalfait Sep 29, 2025

Choose a reason for hiding this comment

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

Open to suggestions here!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think i would say "Tailwind CSS version mismatch" but other than that this seems fine to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

aside: I don't think it needs to change since tailwindcss is also prevent in the diff so if you don't wanna change it that's fine imo. 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is a good change if we want to show more issues. But I used the '↳' as well because it's sort of a continuation of the previous Tailwind CSS version

process.exit(1)
}

{
// Stylesheet migrations

Expand Down
3 changes: 3 additions & 0 deletions packages/@tailwindcss-upgrade/src/utils/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/utils/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ')
Expand Down
32 changes: 32 additions & 0 deletions packages/@tailwindcss-upgrade/src/utils/version.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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