Skip to content

@tailwindcss/upgrade rewrites files in node_modules when run from a pnpm workspace subpackage #20328

Description

@benface

Summary

When @tailwindcss/upgrade is run from a subpackage of a pnpm workspace, it traverses ../../node_modules/… and rewrites files inside the installed tailwindcss package in place — applying its v3 → v4 migration transform (@tailwind utilities;@import 'tailwindcss/utilities' layer(utilities);) to node_modules/tailwindcss/utilities.css (and, in a flat-project variant, to index.css).

This looks like the tail of #19726 — the fix landed there (ignore gitignored files) works for the simple flat-project case, but doesn't cover the workspace-subpackage variant where the tool reaches node_modules through ../../. In our case (a pnpm-workspace v4 project), running the tool as a canonicalizer left node_modules/tailwindcss/utilities.css self-referentially importing itself, which detonated much later as an infinite CSS-resolution loop and read like a "cache corruption" problem.

Reproduction

Repo: https://github.com/benface/tailwindcss-upgrade-node-modules-repro

git clone https://github.com/benface/tailwindcss-upgrade-node-modules-repro
cd tailwindcss-upgrade-node-modules-repro
pnpm install
cd packages/css
pnpm dlx @tailwindcss/upgrade

The tool's own output confirms the write:

│ ↳ Migrated stylesheet: `../../node_modules/.pnpm/tailwindcss@4.3.2/node_modules/tailwindcss/utilities.css`
│ ↳ Updated package: `tailwindcss`

Diff against a pristine copy from the tarball:

1c1
< @tailwind utilities;
---
> @import 'tailwindcss/utilities' layer(utilities);

node_modules is gitignored at the repo root (and even at the subpackage level, tested separately — same result).

Counter-tests (where the existing fix works correctly)

For contrast, both of these leave node_modules untouched:

  • Flat project (no workspace), node_modules in .gitignore, @import 'tailwindcss', run from project root
  • Same, using explicit-path imports (@import 'tailwindcss/utilities.css' layer(utilities) source(none))

The pnpm-workspace-subpackage layout is the specific gap.

Why this matters

  1. node_modules shouldn't be a write target. The files are shared across projects via the pnpm store, are immutable install artifacts elsewhere, and are out of scope for git status, so the corruption is invisible until it detonates.
  2. The rewrite creates a self-referential import — the mutated utilities.css imports itself under a layer(...) wrapper, causing an infinite CSS-resolution loop.
  3. Symptoms look unrelated to the upgrade run — the user hits infinite-recursion errors much later, from any command that touches Tailwind, with a clean git status.
  4. Recovery requires nuking node_modules + reinstalling (and on Bun, purging the install cache too, per Exceeded maximum recursion depth while resolving tailwindcss/utilities #19726). Not obvious from the symptoms.

Suggested guard

Skip any file whose resolved absolute path falls under a node_modules/ segment, regardless of how it was reached (traversal via ../../, symlink resolution, or @import resolution). The existing gitignore-aware filter probably just needs its input paths canonicalized before the check.

Environment

  • macOS (arm64), Node 24, pnpm 11.12.0
  • tailwindcss@4.3.2
  • @tailwindcss/upgrade@4.3.2 (via pnpm dlx)

Happy to test a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions