diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0148dd9836b4..b84ba214049c 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c3ce0903ba..2740ac27b481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/upgrade/upgrade-errors.test.ts b/integrations/upgrade/upgrade-errors.test.ts new file mode 100644 index 000000000000..13e37aa99101 --- /dev/null +++ b/integrations/upgrade/upgrade-errors.test.ts @@ -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` +
Hello World
+ `, + '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` +
Hello World
+ `, + '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` +
Hello World
+ `, + '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. + + " + `) + }, +) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 4c50330846cf..5cfd4acb4398 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/utils/packages.ts b/packages/@tailwindcss-upgrade/src/utils/packages.ts index 65f823bec7e0..461a6983a86d 100644 --- a/packages/@tailwindcss-upgrade/src/utils/packages.ts +++ b/packages/@tailwindcss-upgrade/src/utils/packages.ts @@ -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() diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts index 38bd9826a0ab..c9a9678014d7 100644 --- a/packages/@tailwindcss-upgrade/src/utils/renderer.ts +++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts @@ -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(' ') diff --git a/packages/@tailwindcss-upgrade/src/utils/version.ts b/packages/@tailwindcss-upgrade/src/utils/version.ts index 6dab20a3546e..27bc44f2ba1f 100644 --- a/packages/@tailwindcss-upgrade/src/utils/version.ts +++ b/packages/@tailwindcss-upgrade/src/utils/version.ts @@ -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) +}