From 30aea29f7286d23d6aad6e5cb63d3f91fc001945 Mon Sep 17 00:00:00 2001 From: Zhiqi Zhang Date: Tue, 29 Sep 2026 07:20:52 +0000 Subject: [PATCH 1/3] fix(html): resolve percent-encoded srcset urls A srcset url cannot contain literal spaces, so a file named `foo bar.png` has to be written as `./foo%20bar.png`. That encoded candidate was resolved as-is, failed to match a file, and was then run through `encodeURIPath` a second time, producing `./foo%2520bar.png` and skipping the asset. Resolve the decoded url instead, like the src branch already does since #6199. --- .../fixtures/html-srcset/asset space.png | Bin 0 -> 70 bytes .../__tests__/fixtures/html-srcset/index.html | 9 ++++ packages/vite/src/node/__tests__/html.spec.ts | 40 +++++++++++++++++- packages/vite/src/node/plugins/html.ts | 2 +- playground/assets/__tests__/assets.spec.ts | 8 ++++ playground/assets/index.html | 8 ++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png create mode 100644 packages/vite/src/node/__tests__/fixtures/html-srcset/index.html diff --git a/packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png b/packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png new file mode 100644 index 0000000000000000000000000000000000000000..613754cfaf74a7a2d86984231479d5671731f18a GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8HA{D|jgU}|SSG{)78&qol`;+0KfSU_W%F@ literal 0 HcmV?d00001 diff --git a/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html b/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html new file mode 100644 index 00000000000000..1c216f1cb5c99c --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/vite/src/node/__tests__/html.spec.ts b/packages/vite/src/node/__tests__/html.spec.ts index 906a20fbfe6e40..8204fc4ad43f2d 100644 --- a/packages/vite/src/node/__tests__/html.spec.ts +++ b/packages/vite/src/node/__tests__/html.spec.ts @@ -1,5 +1,7 @@ -import type { OutputBundle, OutputChunk } from 'rolldown' +import { resolve } from 'node:path' +import type { OutputAsset, OutputBundle, OutputChunk } from 'rolldown' import { describe, expect, test } from 'vitest' +import { build } from '../build' import { getCssFilesForChunk } from '../plugins/html' function createChunk( @@ -240,3 +242,39 @@ describe('getCssFilesForChunk', () => { ]) }) }) + +describe('srcset urls in index.html', () => { + const dirname = import.meta.dirname + + async function buildFixtureHtml(): Promise { + const outputs = await build({ + root: resolve(dirname, 'fixtures/html-srcset'), + configFile: false, + logLevel: 'silent', + build: { + write: false, + assetsInlineLimit: 0, + }, + }) + const html = outputs.output.find( + (output) => output.type === 'asset' && output.fileName.endsWith('.html'), + ) as OutputAsset + return html.source.toString() + } + + test('resolves percent-encoded srcset candidates against the decoded path', async () => { + const html = await buildFixtureHtml() + + // `./asset%20space.png` resolves to `asset space.png` and is emitted + // with a hash, re-encoded once in the attribute + expect(html).toMatch(/\/assets\/asset%20space-[-\w]+\.png 1x/) + expect(html).toContain('imagesrcset="/assets/asset%20space-') + expect(html).not.toContain('%25') + }) + + test('keeps an unresolvable percent-encoded candidate as written', async () => { + const html = await buildFixtureHtml() + + expect(html).toContain('./missing%20asset.png 2x') + }) +}) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index c09d5067086c37..10e0158b1363fc 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -653,7 +653,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { !isExcludedUrl(decodedUrl) ) { const result = await processAssetUrl( - url, + decodedUrl, getLinkShouldInline(node, attr.attributes), ) return result !== decodedUrl diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index 318d32158b0080..f638f64d56d366 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -672,6 +672,14 @@ describe.runIf(isBuild)('encodeURI', () => { const img = await page.$('.encodeURI') expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/) }) + + test('img srcset with encodeURI', async () => { + const img = await page.$('.encodeURI-srcset') + const srcset = await img.getAttribute('srcset') + srcset.split(', ').forEach((candidate) => { + expect(candidate).toMatch(/^data:image\/png;base64,\S+ \dx$/) + }) + }) }) test('new URL(..., import.meta.url)', async () => { diff --git a/playground/assets/index.html b/playground/assets/index.html index 601863db0dcb34..368c03bb2611ac 100644 --- a/playground/assets/index.html +++ b/playground/assets/index.html @@ -220,6 +220,14 @@

encodeURI for the address

class="encodeURI" src="./nested/%E3%83%86%E3%82%B9%E3%83%88-%E6%B8%AC%E8%A9%A6-white%20space.png" /> +

Image Src Set

From f04e502d895918bae9e7ed4fb286c7a4e9219b9e Mon Sep 17 00:00:00 2001 From: Zhiqi Zhang Date: Tue, 29 Sep 2026 07:38:26 +0000 Subject: [PATCH 2/3] ci: re-run From a52a5f6b573bbfe6fb7a5367c6c24d95095e0cf2 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Wed, 30 Sep 2026 11:23:33 +0900 Subject: [PATCH 3/3] test: simplify the tests --- .../fixtures/html-srcset/asset space.png | Bin 70 -> 0 bytes .../__tests__/fixtures/html-srcset/index.html | 9 ---- packages/vite/src/node/__tests__/html.spec.ts | 40 +----------------- playground/assets/__tests__/assets.spec.ts | 2 +- playground/assets/index.html | 4 -- 5 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png delete mode 100644 packages/vite/src/node/__tests__/fixtures/html-srcset/index.html diff --git a/packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png b/packages/vite/src/node/__tests__/fixtures/html-srcset/asset space.png deleted file mode 100644 index 613754cfaf74a7a2d86984231479d5671731f18a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8HA{D|jgU}|SSG{)78&qol`;+0KfSU_W%F@ diff --git a/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html b/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html deleted file mode 100644 index 1c216f1cb5c99c..00000000000000 --- a/packages/vite/src/node/__tests__/fixtures/html-srcset/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/packages/vite/src/node/__tests__/html.spec.ts b/packages/vite/src/node/__tests__/html.spec.ts index 8204fc4ad43f2d..906a20fbfe6e40 100644 --- a/packages/vite/src/node/__tests__/html.spec.ts +++ b/packages/vite/src/node/__tests__/html.spec.ts @@ -1,7 +1,5 @@ -import { resolve } from 'node:path' -import type { OutputAsset, OutputBundle, OutputChunk } from 'rolldown' +import type { OutputBundle, OutputChunk } from 'rolldown' import { describe, expect, test } from 'vitest' -import { build } from '../build' import { getCssFilesForChunk } from '../plugins/html' function createChunk( @@ -242,39 +240,3 @@ describe('getCssFilesForChunk', () => { ]) }) }) - -describe('srcset urls in index.html', () => { - const dirname = import.meta.dirname - - async function buildFixtureHtml(): Promise { - const outputs = await build({ - root: resolve(dirname, 'fixtures/html-srcset'), - configFile: false, - logLevel: 'silent', - build: { - write: false, - assetsInlineLimit: 0, - }, - }) - const html = outputs.output.find( - (output) => output.type === 'asset' && output.fileName.endsWith('.html'), - ) as OutputAsset - return html.source.toString() - } - - test('resolves percent-encoded srcset candidates against the decoded path', async () => { - const html = await buildFixtureHtml() - - // `./asset%20space.png` resolves to `asset space.png` and is emitted - // with a hash, re-encoded once in the attribute - expect(html).toMatch(/\/assets\/asset%20space-[-\w]+\.png 1x/) - expect(html).toContain('imagesrcset="/assets/asset%20space-') - expect(html).not.toContain('%25') - }) - - test('keeps an unresolvable percent-encoded candidate as written', async () => { - const html = await buildFixtureHtml() - - expect(html).toContain('./missing%20asset.png 2x') - }) -}) diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index f638f64d56d366..c3ceb5cf90d888 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -674,7 +674,7 @@ describe.runIf(isBuild)('encodeURI', () => { }) test('img srcset with encodeURI', async () => { - const img = await page.$('.encodeURI-srcset') + const img = await page.$('.encodeURI') const srcset = await img.getAttribute('srcset') srcset.split(', ').forEach((candidate) => { expect(candidate).toMatch(/^data:image\/png;base64,\S+ \dx$/) diff --git a/playground/assets/index.html b/playground/assets/index.html index 368c03bb2611ac..32c4f58d617d5e 100644 --- a/playground/assets/index.html +++ b/playground/assets/index.html @@ -219,10 +219,6 @@

encodeURI for the address

-