diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index b504c1c697c8c5..79d1146765889f 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -381,7 +381,11 @@ export function cssPlugin(config: ResolvedConfig): Plugin { if (encodePublicUrlsInCSS(config)) { return [publicFileToBuiltUrl(decodedUrl, config), undefined] } else { - return [joinUrlSegments(config.base, decodedUrl), undefined] + const base = joinUrlSegments( + config.server.origin ?? '', + config.base, + ) + return [joinUrlSegments(base, decodedUrl), undefined] } } const [id, fragment] = decodedUrl.split('#') diff --git a/playground/backend-integration/__tests__/backend-integration.spec.ts b/playground/backend-integration/__tests__/backend-integration.spec.ts index 7f470d06b08c11..801046700fed8b 100644 --- a/playground/backend-integration/__tests__/backend-integration.spec.ts +++ b/playground/backend-integration/__tests__/backend-integration.spec.ts @@ -4,6 +4,7 @@ import { browserLogs, editFile, getColor, + getCssRuleBg, isBuild, isServe, listAssets, @@ -120,6 +121,20 @@ describe.runIf(isServe)('serve', () => { expect(await link.getAttribute('href')).toContain('/dev/global.css?t=') }) + test('server.origin is applied to non-public CSS url()', async () => { + const bg = await getCssRuleBg('.outside-root--aliased') + expect(bg).toContain( + `http://localhost:${ports['backend-integration']}/dev/`, + ) + }) + + test('server.origin is applied to public CSS url()', async () => { + const bg = await getCssRuleBg('.public-asset') + expect(bg).toContain( + `http://localhost:${ports['backend-integration']}/dev/icon.png`, + ) + }) + test('CSS dependencies are tracked for HMR', async () => { const el = await page.$('h1') await untilBrowserLogAfter( diff --git a/playground/backend-integration/frontend/entrypoints/index.html b/playground/backend-integration/frontend/entrypoints/index.html index bc75dbc4b898e9..bda9a390db0800 100644 --- a/playground/backend-integration/frontend/entrypoints/index.html +++ b/playground/backend-integration/frontend/entrypoints/index.html @@ -25,6 +25,10 @@

CSS Asset References

Background URL with Relative Path:
+
  • + Background URL with Public Asset: +
    +
  • CSS imported from JS

    diff --git a/playground/backend-integration/frontend/entrypoints/public/icon.png b/playground/backend-integration/frontend/entrypoints/public/icon.png new file mode 100644 index 00000000000000..4388bfdca3d4d7 Binary files /dev/null and b/playground/backend-integration/frontend/entrypoints/public/icon.png differ diff --git a/playground/backend-integration/frontend/styles/background.css b/playground/backend-integration/frontend/styles/background.css index a6cc929f859c52..64e53b814b2669 100644 --- a/playground/backend-integration/frontend/styles/background.css +++ b/playground/backend-integration/frontend/styles/background.css @@ -13,3 +13,7 @@ .outside-root--relative { background-image: url('../images/logo.png'); } + +.public-asset { + background-image: url('/icon.png'); +} diff --git a/playground/test-utils.ts b/playground/test-utils.ts index 5e084e636d61d6..34df6142d3f52d 100644 --- a/playground/test-utils.ts +++ b/playground/test-utils.ts @@ -121,6 +121,25 @@ export async function getBg( return el.evaluate((el) => getComputedStyle(el as Element).backgroundImage) } +/** + * Unlike `getBg`, this function returns the raw value of the `background-image` CSS property. + * + * `getBg` returns the resolved value, which has the hostname and port prepended due to `computedStyle` call. + */ +export async function getCssRuleBg(selector: string): Promise { + return page.evaluate((sel) => { + for (const sheet of document.styleSheets) { + try { + for (const rule of sheet.cssRules) { + if (rule instanceof CSSStyleRule && rule.selectorText === sel) { + return rule.style.backgroundImage + } + } + } catch (_e) {} + } + }, selector) +} + export async function getBgColor( el: string | ElementHandle | Locator, ): Promise {