Prevent @tailwindcss/upgrade from rewriting files inside node_modules when run from a workspace subpackage (#20329)
Fixes #20328. ## What changed When `@tailwindcss/upgrade` is invoked from a subpackage of a workspace (e.g. `pnpm --filter …` or `cd packages/foo && pnpm exec upgrade`), it traverses `../../node_modules/…` and applies its v3 → v4 migration transform (`@tailwind utilities;` → `@import 'tailwindcss/utilities' layer(utilities);`) to files inside the installed `tailwindcss` package itself — a self-referential import that later detonates as an infinite CSS-resolution loop and reads like a "cache corruption" bug (matches the report in #19726 / #20328). Root cause: `globby`'s `isGitIgnored` only walks `.gitignore` files at or below its `cwd`. When invoked from a subpackage, the workspace-root `.gitignore` (which almost always lists `node_modules`) is never consulted, and `../../node_modules/…` paths come back as **not** ignored. The fix anchors `isGitIgnored` at the git repository root instead of the subpackage: - New helper: `gitRoot(cwd)` in `src/utils/git.ts` — thin wrapper over `git rev-parse --show-toplevel`. - `analyze(stylesheets, { base })` in `src/codemods/css/analyze.ts` now calls `isGitIgnored({ cwd: gitRoot(base) ?? base })`. Falls back to the previous behavior when not inside a git repository or when `git` is unavailable. ## Test Added an integration test that reproduces the exact scenario: a pnpm workspace with a subpackage that imports `tailwindcss/utilities.css`, upgrade run from the subpackage, then asserts `packages/css/node_modules/tailwindcss/utilities.css` still holds the pristine `@tailwind utilities;` directive. Verified the test fails on `main` and passes with this change. The 415 existing upgrade unit tests still pass. Two npm-related snapshot failures in `upgrade-errors.test.ts` (`half-upgraded v3 project to v4 (bun|npm)`) pre-exist this change — they're about newer npm's `npm notice run` output that isn't in the snapshots, unrelated to what this PR touches. --- Edit by @RobinMalfait: going to run on all [ci-all] so we can make sure it works on Windows as well. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
e48c5e8047
commit
a854b35bcf
5 changed files with 74 additions and 3 deletions
|
|
@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `<length>` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
|
||||
- Lazily load `@parcel/watcher` when using the `--watch` flag in `@tailwindcss/cli`, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/issues/20325))
|
||||
- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#19767](https://github.com/tailwindlabs/tailwindcss/issues/19767), [#19768](https://github.com/tailwindlabs/tailwindcss/issues/19768))
|
||||
- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20328](https://github.com/tailwindlabs/tailwindcss/issues/20328))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
|
|
|
|||
|
|
@ -3371,6 +3371,61 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20328
|
||||
test(
|
||||
'ignored files should not be touched when upgrading from a nested directory',
|
||||
{
|
||||
fs: {
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- packages/*
|
||||
`,
|
||||
'package.json': json`
|
||||
{
|
||||
"name": "root",
|
||||
"private": true
|
||||
}
|
||||
`,
|
||||
'packages/css/package.json': json`
|
||||
{
|
||||
"name": "css-pkg",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'packages/css/src/input.css': css`
|
||||
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
|
||||
`,
|
||||
'packages/css/src/index.html': html`<div class="flex text-red-500">Hi</div>`,
|
||||
|
||||
// Ensure files/folders ignored by a `.gitignore` in the root take effect
|
||||
// when executing the upgrade tool from a sub-package.
|
||||
'.gitignore': txt`
|
||||
node_modules/
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('git init', { cwd: root })
|
||||
|
||||
await exec('pnpm exec upgrade --force', {
|
||||
cwd: path.join(root, 'packages/css'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('packages/css/node_modules/tailwindcss/utilities.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- packages/css/node_modules/tailwindcss/utilities.css ---
|
||||
@tailwind utilities;
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrade <style> blocks carefully',
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import postcss, { type Result } from 'postcss'
|
|||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
import { Stylesheet, type StylesheetConnection } from '../../stylesheet'
|
||||
import { gitRoot } from '../../utils/git'
|
||||
import { error, highlight, relative } from '../../utils/renderer'
|
||||
import { resolveCssId } from '../../utils/resolve'
|
||||
|
||||
export async function analyze(stylesheets: Stylesheet[]) {
|
||||
let isIgnored = await isGitIgnored()
|
||||
export async function analyze(stylesheets: Stylesheet[], { base }: { base: string }) {
|
||||
// Use the project's git root, instead of the `base` to ensure all
|
||||
// `.gitignore` rules are being used.
|
||||
let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base })
|
||||
let processingQueue: (() => Promise<Result>)[] = []
|
||||
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
|
||||
// We don't want to process ignored files (like node_modules)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ async function run() {
|
|||
|
||||
// Analyze the stylesheets
|
||||
try {
|
||||
await analyzeStylesheets(stylesheets)
|
||||
await analyzeStylesheets(stylesheets, { base })
|
||||
} catch (e: any) {
|
||||
error(`${e?.message ?? e}`, { prefix: '↳ ' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
import { execSync } from 'node:child_process'
|
||||
|
||||
export function gitRoot(cwd?: string): string | null {
|
||||
try {
|
||||
return execSync('git rev-parse --show-toplevel', {
|
||||
encoding: 'utf-8',
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isRepoDirty(cwd?: string) {
|
||||
try {
|
||||
let stdout = execSync('git status --porcelain', { encoding: 'utf-8', cwd })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue