Improve @tailwindcss/upgrade and pnpm workspaces support (#18065)

This PR fixes an issue where an error such as:

<img width="1702" alt="image"
src="https://github.com/user-attachments/assets/4e6f75c7-3182-4497-939e-96cff08c55ae"
/>

Will be thrown during the upgrade process. This can happen when you are
using `pnpm` and your CSS file includes a `@import "tailwindcss";`. In
this scenario, `tailwindcss` will be loaded from a shared `.pnpm` folder
outside of the current working directory.

In this case, we are also not interested in migrating _that_ file, but
we also don't want the upgrade process to just crash.

I didn't see an option to ignore errors like this, so wrapped it in a
try/catch instead.

It also fixes another issue where if you are using a pnpm workspace and
run the upgrade tool from the root, then it throws you an error that you
cannot add dependencies to the workspace root unless `-w` or
`--workspace-root` flags are passed.

For this, we disable the check entirely using the
`--ignore-workspace-root-check` flag. If we always used the
`--workspace-root` flag, then the dependencies would always be added to
the root, regardless of where you are running the script from which is
not what we want.

## Test plan

Before:

<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/78246876-3eb6-4539-a557-d3d366f1b3a3"
/>

After:

<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/a65e4421-d7c5-4d83-b35d-934708543e25"
/>

Before:
 
<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/53772661-2c4a-4212-84d9-a556a0ad320f"
/>

After:

<img width="1816" alt="image"
src="https://github.com/user-attachments/assets/5bfaf20e-34b8-44fd-9b59-e72d36738879"
/>
This commit is contained in:
Robin Malfait 2025-05-19 12:47:08 +02:00 committed by GitHub
parent c7d368b3c4
commit 71fb9cdf59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 185 additions and 20 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Upgrade: Do not migrate declarations that look like candidates in `<style>` blocks ([#18057](https://github.com/tailwindlabs/tailwindcss/pull/18057))
- Upgrade: Improve `pnpm` workspaces support ([#18065](https://github.com/tailwindlabs/tailwindcss/pull/18065))
## [4.1.7] - 2025-05-15

View File

@ -1,5 +1,6 @@
import path from 'node:path'
import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git'
import { candidate, css, html, js, json, test, ts } from '../utils'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
test(
'error when no CSS file with @tailwind is used',
@ -2967,6 +2968,155 @@ test(
},
)
test(
'upgrades can run in a pnpm workspace',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': json`
{
"dependencies": {
"tailwindcss": "^4"
},
"devDependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'project-a/src/index.html': html`
<!-- Migrating 'ring', 'rounded' and 'outline-none' are unsafe in v4 -> v4 migrations -->
<div class="ring rounded outline"></div>
<!-- Variant order is also unsafe to change in v4 projects -->
<div class="file:hover:flex *:hover:flex"></div>
<div class="hover:file:flex hover:*:flex"></div>
<!-- These are safe to migrate: -->
<div
class="!flex bg-red-500/[var(--my-opacity)] [@media(pointer:fine)]:flex bg-right-bottom object-left-top"
></div>
`,
'project-a/src/input.css': css`
@import 'tailwindcss';
.foo {
@apply !bg-[var(--my-color)];
}
`,
},
},
async ({ root, exec, fs, expect }) => {
let stdout = await exec('npx @tailwindcss/upgrade', {
cwd: path.join(root, 'project-a'),
})
expect(/Path .*? is not in cwd/.test(stdout)).toBe(false)
expect(await fs.dumpFiles('./project-a/src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- ./project-a/src/index.html ---
<!-- Migrating 'ring', 'rounded' and 'outline-none' are unsafe in v4 -> v4 migrations -->
<div class="ring rounded outline"></div>
<!-- Variant order is also unsafe to change in v4 projects -->
<div class="file:hover:flex *:hover:flex"></div>
<div class="hover:file:flex hover:*:flex"></div>
<!-- These are safe to migrate: -->
<div
class="flex! bg-red-500/(--my-opacity) pointer-fine:flex bg-bottom-right object-top-left"
></div>
--- ./project-a/src/input.css ---
@import 'tailwindcss';
.foo {
@apply bg-(--my-color)!;
}
"
`)
},
)
test(
'upgrades can run in a pnpm workspace root',
{
fs: {
'pnpm-workspace.yaml': yaml`
#
packages:
- .
`,
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4"
},
"devDependencies": {
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/index.html': html`
<!-- Migrating 'ring', 'rounded' and 'outline-none' are unsafe in v4 -> v4 migrations -->
<div class="ring rounded outline"></div>
<!-- Variant order is also unsafe to change in v4 projects -->
<div class="file:hover:flex *:hover:flex"></div>
<div class="hover:file:flex hover:*:flex"></div>
<!-- These are safe to migrate: -->
<div
class="!flex bg-red-500/[var(--my-opacity)] [@media(pointer:fine)]:flex bg-right-bottom object-left-top"
></div>
`,
'src/input.css': css`
@import 'tailwindcss';
.foo {
@apply !bg-[var(--my-color)];
}
`,
},
},
async ({ exec, fs, expect }) => {
let stdout = await exec('npx @tailwindcss/upgrade')
expect(stdout).not.toContain(
'Running this command will add the dependency to the workspace root',
)
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<!-- Migrating 'ring', 'rounded' and 'outline-none' are unsafe in v4 -> v4 migrations -->
<div class="ring rounded outline"></div>
<!-- Variant order is also unsafe to change in v4 projects -->
<div class="file:hover:flex *:hover:flex"></div>
<div class="hover:file:flex hover:*:flex"></div>
<!-- These are safe to migrate: -->
<div
class="flex! bg-red-500/(--my-opacity) pointer-fine:flex bg-bottom-right object-top-left"
></div>
--- ./src/input.css ---
@import 'tailwindcss';
.foo {
@apply bg-(--my-color)!;
}
"
`)
},
)
test(
'upgrade <style> blocks carefully',
{
@ -2980,21 +3130,21 @@ test(
}
`,
'src/index.vue': html`
<template
<template>
<div class="!flex"></div>
</template>
<style>
@reference "./input.css";
@reference "./input.css";
.foo {
@apply !bg-red-500;
}
.foo {
@apply !bg-red-500;
}
.bar {
/* Do not upgrade the key: */
flex-shrink: 0;
}
.bar {
/* Do not upgrade the key: */
flex-shrink: 0;
}
</style>
`,
'src/input.css': css`
@ -3016,21 +3166,21 @@ test(
expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(`
"
--- ./src/index.vue ---
<template
<template>
<div class="flex!"></div>
</template>
<style>
@reference "./input.css";
@reference "./input.css";
.foo {
@apply !bg-red-500;
}
.foo {
@apply !bg-red-500;
}
.bar {
/* Do not upgrade the key: */
flex-shrink: 0;
}
.bar {
/* Do not upgrade the key: */
flex-shrink: 0;
}
</style>
--- ./src/input.css ---

View File

@ -12,7 +12,14 @@ export async function analyze(stylesheets: Stylesheet[]) {
let processingQueue: (() => Promise<Result>)[] = []
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
// We don't want to process ignored files (like node_modules)
if (isIgnored(file)) {
try {
if (isIgnored(file)) {
return null
}
} catch {
// If the file is not part of the current working directory (which can
// happen if you import `tailwindcss` and it's loading a shared file from
// pnpm) then this will throw.
return null
}

View File

@ -31,6 +31,13 @@ export function pkg(base: string) {
args.push(SAVE_DEV[packageManager] || SAVE_DEV.default)
}
// Allow running the `pnpm` command in the workspace root without
// erroring. Can't just use `--workspace-root` because that will force
// install dependencies in the workspace root.
if (packageManager === 'pnpm') {
args.push('--ignore-workspace-root-check')
}
let command = `${packageManager} add ${args.join(' ')}`
try {
return await exec(command, { cwd: base })