diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 6830215563f5fa..c3ecac653ecea2 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -2104,9 +2104,15 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{ return { postcssPlugin: 'vite-url-rewrite', - OnceExit(root) { + OnceExit(_root, { result }) { const promises: Promise[] = [] - root.walkDecls((declaration) => { + // walk `result.root` rather than the `root` argument: a plugin running + // earlier (e.g. postcss-lightningcss, @tailwindcss/vite) may hand back a + // freshly parsed tree by reassigning `result.root`, and PostCSS keeps + // passing the pre-reassignment root to the OnceExit hooks that run after + // it. Rewriting urls on that discarded tree leaves the emitted CSS + // pointing at the original source-relative paths. + result.root.walkDecls((declaration) => { const importer = declaration.source?.input.file if (!importer) { opts.logger.warnOnce( diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index aa0fe2e4914c59..7d4a1f56e8d751 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -12,3 +12,20 @@ test('postcss plugin that injects url() at OnceExit', async () => { isBundled ? /base64/ : '/injected-source/injected-bg.png', ) }) + +// a plugin that reassigns `result.root` (like postcss-lightningcss or +// @tailwindcss/vite) discards the tree Vite's url rewriter walked, so the +// rewritten urls must be read back off the live root +// (https://github.com/vitejs/vite/issues/23348) +test('postcss plugin that replaces the root at OnceExit', async () => { + await page.goto(viteTestUrl) + const replaced = await page.waitForSelector('.replace-root') + // the raw `./replaced-bg.png` must not survive: it is relative to the + // source file, not to wherever the emitted stylesheet ends up. The hash + // (bundled) and the source directory (served) are what prove it was rebased + expect(await getBg(replaced)).toMatch( + isBundled + ? /\/replaced-bg-[-\w]+\.png/ + : '/replace-root-source/replaced-bg.png', + ) +}) diff --git a/playground/css/index.html b/playground/css/index.html index 4231a85e8722d6..a1e8df99059f41 100644 --- a/playground/css/index.html +++ b/playground/css/index.html @@ -27,6 +27,10 @@

CSS

PostCSS plugin injecting at OnceExit: this should have a background image

+

+ PostCSS plugin replacing the root at OnceExit: this should have a background + image +

SASS: This should be orange

diff --git a/playground/css/main.js b/playground/css/main.js index f278820595ca1b..6ea9f10ae89008 100644 --- a/playground/css/main.js +++ b/playground/css/main.js @@ -7,6 +7,7 @@ import './less-plugin.less' import './stylus.styl' import './manual-chunk.css' import './postcss-inject-url.css' +import './postcss-replace-root.css' import urlCss from './url-imported.css?url' appendLinkStylesheet(urlCss) diff --git a/playground/css/postcss-replace-root.css b/playground/css/postcss-replace-root.css new file mode 100644 index 00000000000000..6459aa716c8281 --- /dev/null +++ b/playground/css/postcss-replace-root.css @@ -0,0 +1 @@ +@replace-root; diff --git a/playground/css/postcss.config.js b/playground/css/postcss.config.js index 8198b58495166a..6a87dd906f457b 100644 --- a/playground/css/postcss.config.js +++ b/playground/css/postcss.config.js @@ -11,6 +11,7 @@ export default { testSourceInput, testInjectUrl, testInjectUrlOnceExit, + testReplaceRoot, ], } @@ -116,3 +117,31 @@ function testInjectUrlOnceExit() { } } testInjectUrlOnceExit.postcss = true + +/** + * A plugin for testing url() rewriting when a plugin replaces the whole tree + * at OnceExit by reassigning `result.root` (like postcss-lightningcss or + * @tailwindcss/vite do). + */ +function testReplaceRoot() { + return { + postcssPlugin: 'replace-root', + OnceExit(root, { result, postcss }) { + if (!root.some((node) => node.name === 'replace-root')) return + // re-parse from scratch and hand back a brand new tree instead of + // mutating `root`, the way postcss-lightningcss and @tailwindcss/vite + // return their own output. `from` points at the source file so the + // relative url has something to be rebased against. + result.root = postcss.parse( + '.replace-root { background: url(./replaced-bg.png) }', + { + from: path.join( + import.meta.dirname, + 'replace-root-source/replaced.css', + ), + }, + ) + }, + } +} +testReplaceRoot.postcss = true diff --git a/playground/css/replace-root-source/replaced-bg.png b/playground/css/replace-root-source/replaced-bg.png new file mode 100644 index 00000000000000..eb9f67ae59d62f Binary files /dev/null and b/playground/css/replace-root-source/replaced-bg.png differ