dev-proxy: a stylesheet's url() is a URL

Two bugs, one line apart, both silent.

The `url(/…)` rule only matched an UNQUOTED target, so `url("/fonts/x.woff2")` — the form every
generator emits — went through untouched. And it only ran on HTML, so a linked stylesheet got the
narrow `/_bun/` rule and nothing else.

Together that meant a site's own @font-face rules kept pointing at `/fonts/…`, which inside the frame
is the platform's root. Every face 404'd and the preview rendered in a system fallback: nothing looked
broken, it just looked like a different site than the one being previewed.

JavaScript keeps the narrow rule — an absolute path in JS may not be a URL. In CSS it always is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 06:42:14 +01:00
co-authored by Claude Opus 5
parent 9f243f569f
commit ab2336516f
3 changed files with 79 additions and 6 deletions
+40 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'bun:test';
import { proxyOverrideScript, rewriteAssetPaths, rewriteHtml } from './html-rewrite';
import { proxyOverrideScript, rewriteAssetPaths, rewriteCss, rewriteHtml } from './html-rewrite';
const BASE = '/api/dev-proxy/3001';
const attrs = (html: string) => [...html.matchAll(/(?:src|href)="([^"]+)"/g)].map((m) => m[1]!);
@@ -212,3 +212,42 @@ describe('the injected location reporter', () => {
expect(r.posted).toHaveLength(2);
});
});
describe('stylesheets', () => {
test('a quoted url is prefixed — the form every generator emits', () => {
expect(rewriteCss('@font-face{src:url("/fonts/Averta.woff2") format("woff2")}', BASE)).toContain(
`url("${BASE}/fonts/Averta.woff2")`,
);
});
test('single quotes and bare urls too', () => {
expect(rewriteCss("a{background:url('/bg.png')}", BASE)).toContain(`url('${BASE}/bg.png')`);
expect(rewriteCss('a{background:url(/bg.png)}', BASE)).toContain(`url(${BASE}/bg.png)`);
});
test('the quote style is preserved, not normalised', () => {
expect(rewriteCss('a{background:url("/bg.png")}', BASE)).not.toContain("url('");
});
test('a url that is already absolute is left alone', () => {
const external = 'a{background:url("https://cdn.example.com/bg.png")}';
expect(rewriteCss(external, BASE)).toBe(external);
const protocolRelative = 'a{background:url("//cdn.example.com/bg.png")}';
expect(rewriteCss(protocolRelative, BASE)).toBe(protocolRelative);
});
test('a relative url is left alone — it already resolves against the stylesheet', () => {
const relative = 'a{background:url("./bg.png")}';
expect(rewriteCss(relative, BASE)).toBe(relative);
});
test('a stylesheet still gets the /_bun/ rule', () => {
expect(rewriteCss('@import "/_bun/x.css";', BASE)).toContain(`${BASE}/_bun/x.css`);
});
test('inline CSS in HTML gets the same treatment, quotes included', () => {
expect(rewriteHtml('<div style=\'background:url("/a.png")\'></div>', BASE)).toContain(
`url("${BASE}/a.png")`,
);
});
});