Skip to content

fix(css): rewrite urls on the live postcss root (fix #23348) - #23358

Open
chanmilee-fe wants to merge 2 commits into
vitejs:mainfrom
chanmilee-fe:fix/23348-postcss-live-root
Open

fix(css): rewrite urls on the live postcss root (fix #23348)#23358
chanmilee-fe wants to merge 2 commits into
vitejs:mainfrom
chanmilee-fe:fix/23348-postcss-live-root

Conversation

@chanmilee-fe

Copy link
Copy Markdown

Since #22983 the url rewriter runs in OnceExit.
Plugins like postcss-lightningcss and @tailwindcss/vite don't mutate the root there, they reassign result.root.
PostCSS keeps passing the old root to the OnceExit hooks that run after them, so we were rewriting a tree that gets thrown away:
the emitted CSS keeps the source-relative url() and the asset is never emitted at all.

Walking result.root instead fixes it. I considered going back to Once, but that reintroduces what #22983 fixed. Reading the live root covers both cases.

fixes #23348

#23349 was an earlier attempt at the same bug and landed on the same idea, but it was closed without review. Two things I hit while going down the same path:

  • it assigns result.root back to the root parameter, which fails typecheck.
    result.root is Document | Root while the parameter is Root.
  • its test only fails in build. The fixture sits at the playground root, so in serve the un-rewritten relative url resolves to the expected path anyway and the test passes with the bug still present.

So here the rewriter reads result.root directly, and the fixture lives in a subdirectory. The test adds a postcss plugin to playground/css that swaps the root at OnceExit, and it fails in both serve and build without the patch.


I used Claude Code for this. It wrote the patch and the test. I picked the issue, reviewed the diff, and decided what went in.

Two things from that review are worth calling out, since they're why the diff looks like this. The first patch reassigned the root parameter, which is what #23349 does and what its Lint job fails on, so it reads result.root directly instead.
And the first version of the test passed with the bug still in place:
getComputedStyle hands back an absolute url, so the raw ./replaced-bg.png never appears and the assertion matched either way. That's why the fixture sits in a subdirectory now and the assertion looks for the emitted asset hash.

Before trusting any of it I ran the test with the patch reverted in both serve and build, and checked that the existing OnceExit test from #22983 still passes, since breaking that was the main risk.

chanmilee-fe and others added 2 commits August 25, 2026 15:00
Some PostCSS plugins hand back a freshly parsed tree by reassigning
`result.root` at OnceExit instead of mutating the root in place
(postcss-lightningcss, @tailwindcss/vite). PostCSS keeps passing the
pre-reassignment root to the OnceExit hooks that run after it, so ever
since vitejs#22983 moved the url rewriter from Once to OnceExit it has been
walking a tree that never reaches the output. The emitted CSS kept the
source-relative url() and the referenced asset was never emitted.

Walk `result.root` instead. Moving back to Once would reintroduce the
bug vitejs#22983 fixed, whereas reading the live root keeps both cases working.

fix vitejs#23348

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vite 8.2.x leaves CSS asset URLs stale after PostCSS transforms

1 participant