Skip to content

Improve @tailwindcss/upgrade and pnpm workspaces support #18065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 19, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
188 changes: 169 additions & 19 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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',
{
Expand All @@ -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`
Expand All @@ -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 ---
Expand Down
9 changes: 8 additions & 1 deletion packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
7 changes: 7 additions & 0 deletions packages/@tailwindcss-upgrade/src/utils/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down