From e046272f9e1157f131f73986c6c56267e9de0f17 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 24 Aug 2026 18:38:25 +0500 Subject: [PATCH] fix(css): read the live root in the OnceExit url rewriter --- packages/vite/src/node/plugins/css.ts | 8 +++++++- playground/css/__tests__/css.spec.ts | 10 ++++++++++ playground/css/index.html | 4 ++++ playground/css/main.js | 1 + playground/css/postcss.config.js | 24 ++++++++++++++++++++++++ playground/css/replace-root.css | 1 + 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 playground/css/replace-root.css diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 6830215563f5fa..5a254e2a163616 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -2104,7 +2104,13 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{ return { postcssPlugin: 'vite-url-rewrite', - OnceExit(root) { + OnceExit(root, { result }) { + // a plugin earlier in the pipeline (e.g. postcss-lightningcss) may + // reassign `result.root` to a freshly parsed tree from its own + // OnceExit hook. PostCSS keeps passing the pre-reassignment `root` to + // subsequently run OnceExit hooks, so read the live root off `result` + // instead of trusting the argument. + root = result.root const promises: Promise[] = [] root.walkDecls((declaration) => { const importer = declaration.source?.input.file diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index aa0fe2e4914c59..3e4d23c2ae796b 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -12,3 +12,13 @@ test('postcss plugin that injects url() at OnceExit', async () => { isBundled ? /base64/ : '/injected-source/injected-bg.png', ) }) + +// a plugin that reassigns `result.root` at OnceExit (like postcss-lightningcss +// does) instead of mutating the existing root in place +test('postcss plugin that reassigns result.root at OnceExit', async () => { + await page.goto(viteTestUrl) + const imported = await page.waitForSelector('.replace-root-once-exit') + expect(await getBg(imported)).toMatch( + isBundled ? /base64/ : '/injected-source/injected-bg.png', + ) +}) diff --git a/playground/css/index.html b/playground/css/index.html index 4231a85e8722d6..f8fcc8df036e74 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 result.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..3a52c26e018e60 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 './replace-root.css' import urlCss from './url-imported.css?url' appendLinkStylesheet(urlCss) diff --git a/playground/css/postcss.config.js b/playground/css/postcss.config.js index 8198b58495166a..9bccb2505f1e5b 100644 --- a/playground/css/postcss.config.js +++ b/playground/css/postcss.config.js @@ -11,6 +11,7 @@ export default { testSourceInput, testInjectUrl, testInjectUrlOnceExit, + testReplaceRootOnceExit, ], } @@ -116,3 +117,26 @@ function testInjectUrlOnceExit() { } } testInjectUrlOnceExit.postcss = true + +/** + * A plugin for testing url() rewriting when a plugin reassigns `result.root` + * to a freshly parsed tree at OnceExit instead of mutating the existing root + * in place (this is what postcss-lightningcss does) + */ +function testReplaceRootOnceExit() { + return { + postcssPlugin: 'replace-root-once-exit', + OnceExit(root, { result, postcss }) { + if (!root.source?.input.file?.endsWith('replace-root.css')) return + root.walkAtRules('replace-root-once-exit', (atRule) => { + atRule.replaceWith( + '.replace-root-once-exit { background: url("./injected-source/injected-bg.png") }', + ) + }) + result.root = postcss.parse(root.toString(), { + from: root.source.input.file, + }) + }, + } +} +testReplaceRootOnceExit.postcss = true diff --git a/playground/css/replace-root.css b/playground/css/replace-root.css new file mode 100644 index 00000000000000..19bc5c3962095d --- /dev/null +++ b/playground/css/replace-root.css @@ -0,0 +1 @@ +@replace-root-once-exit;