Skip to content

css: preserve url() query/fragment suffix when rewriting asset references - #33685

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/ee75cae4/css-url-fragment-suffix
Jul 8, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/ee75cae4/css-url-fragment-suffix

Conversation

@robobun

@robobun robobun commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

A CSS url() with a fragment or query, e.g. url(sprites.svg#icon) or url(file.svg?v=3#c), silently lost the #icon / ?v=3#c part when the referenced asset was rewritten during bundling. This happened on both rewrite paths:

  • the file-loader copy path (url(./sprites-<hash>.svg))
  • the data-URL inline path (url(data:image/svg+xml;base64,...))

The fragment is load-bearing in CSS: SVG sprite / <view> / paint-server references and mask / clip-path / filter element references are all written url(file.svg#id). After bun build they pointed at the whole document instead of the fragment, so the mask/clip/paint silently resolved to nothing.

Repro

import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = mkdtempSync(join(tmpdir(), "css-"));
writeFileSync(join(dir, "big.svg"), `<svg xmlns="http://www.w3.org/2000/svg"><circle id="icon" r="4"/><!--${"x".repeat(140_000)}--></svg>`);
writeFileSync(join(dir, "small.svg"), `<svg xmlns="http://www.w3.org/2000/svg"><mask id="m1"/></svg>`);
writeFileSync(join(dir, "e.css"),
`.a { mask: url(./big.svg#icon) }
.b { clip-path: url("./big.svg?v=3#c") }
.c { mask: url(./small.svg#m1) }
`);
const b = await Bun.build({ entrypoints: [join(dir, "e.css")], outdir: join(dir, "out") });
console.log(await b.outputs.find(o => o.path.endsWith(".css")).text());

Before:

.a { mask: url("./big-67xdwqqh.svg"); }               /* #icon gone */
.b { clip-path: url("./big-67xdwqqh.svg"); }          /* ?v=3#c gone */
.c { mask: url("data:image/svg+xml;base64,..."); }    /* #m1 gone */

After:

.a { mask: url("./big-67xdwqqh.svg#icon"); }
.b { clip-path: url("./big-67xdwqqh.svg?v=3#c"); }
.c { mask: url("data:image/svg+xml;base64,...#m1"); }

Cause

The resolver strips the ?# suffix to find the file on disk (src/resolver/resolver.rs:1464) but the stripped suffix is never stored or re-appended. The CSS printer's get_import_record_url then emits the rewritten path or data URL keyed only by source_index, which has no per-reference suffix.

Fix

The original specifier (with suffix) is already preserved on the import record as original_path before resolution overwrites path. When the CSS printer emits a rewritten url() reference for an ImportKind::Url record, it now re-appends the suffix from original_path:

  • file-loader path (copied asset, real HTTP URL): the full ?query#fragment is kept.
  • data-URL inline path: only the #fragment is kept. A ?query appended to a base64 data: URL lands in the body passed to forgiving-base64-decode (the Fetch data-URL processor excludes only the fragment when re-serializing) and fails on ?, so the query is dropped there. A query on an inlined asset is meaningless anyway since there is no server to interpret it.

External references are unchanged since they keep their original text.

How did you verify your code works?

Three new itBundled cases in test/bundler/esbuild/css.test.ts:

  • css/URLQueryFragmentPreservedFileLoader: file-loader path with #fragment, ?query#fragment, no suffix (unchanged), and a custom-property --bg: url(...) case
  • css/URLQueryFragmentPreservedDataURL: data-URL inline path with #fragment, ?query (dropped), ?query#fragment (only #fragment kept), and no suffix
  • css/URLQueryFragmentPreservedMinified: minified output path

All three fail on main and pass with the fix. The rest of test/bundler/esbuild/css.test.ts (56 tests) and test/bundler/css/ (168 tests) continue to pass.

…nces

A CSS url() with a fragment like url(sprites.svg#icon) lost the #icon
part when the referenced asset was rewritten to a hashed path or inlined
as a data: URL. The resolver strips the ?query/#fragment suffix to find
the file on disk, but nothing re-appended it to the rewritten reference,
so SVG sprite / mask / clip-path / paint-server references silently
pointed at the whole document instead of the fragment.

The original specifier (with suffix) is already preserved on the import
record as original_path. When the CSS printer emits a rewritten url()
reference (data: URL or file-loader unique key), it now re-appends any
?/# suffix found in original_path. External references are unchanged
since they keep their original text.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2b52c9e7-e1b2-4e62-9944-786d6459c0c5

📥 Commits

Reviewing files that changed from the base of the PR and between 3f5d816 and 2ef395d.

📒 Files selected for processing (2)
  • src/css/printer.rs
  • test/bundler/esbuild/css.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:33 PM PT - Jul 7th, 2026

❌ @robobun, your commit 2ef395d has 1 failures in Build #69985 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33685

That installs a local version of the PR into your bun-33685 executable, so you can run:

bun-33685 --bun

@github-actions github-actions Bot added the claude label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

No open issues were found that this PR would fix.

🤖 Generated with Claude Code

Comment thread src/css/printer.rs
Appending a ?query to a base64 data: URL places it in the body passed
to forgiving-base64-decode (the Fetch data-URL processor excludes only
the fragment when re-serializing), which fails on '?'. Keep only the
#fragment on the inline path; the file-loader path still gets the full
?query#fragment since that becomes a real HTTP URL.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the data-URL ?query concern from my earlier review is addressed in 2ef395d, and the fix is now scoped correctly per path.

Extended reasoning...

Overview

This PR fixes CSS url() references losing their ?query/#fragment suffix when the bundler rewrites the asset path. The change is confined to get_import_record_url() in src/css/printer.rs (~30 lines): when an ImportKind::Url record has a valid source_index, the suffix is recovered from record.original_path and re-appended — the full ?query#fragment for the file-loader (unique-key) path, and only the #fragment for the inlined data-URL path. Concatenation goes through the printer's arena (alloc_slice_fill_copy), matching the file's existing allocation pattern; nothing outlives the arena. Three new itBundled cases in test/bundler/esbuild/css.test.ts cover file-loader, data-URL (including ?v=2 dropped and ?v=2#m1 → #m1), and minified output.

Security risks

None. This is output serialization of already-resolved import records; no new untrusted-input parsing, no filesystem/network access, no auth or crypto surface.

Level of scrutiny

Medium — bundler output correctness affects every CSS user, but the change is narrowly gated (record.kind == ImportKind::Url + source_index.is_valid()), so @import and external URLs are untouched, and the no-suffix fast path returns the original slice unchanged. The one subtle interaction (query strings on base64 data: URLs) was raised in my earlier review and fixed exactly as suggested; the thread is resolved.

Other factors

The tests exercise the full variant matrix (fragment / query+fragment / none / custom property / minified / data-URL query-drop) with exact-output assertions, and the PR description confirms they fail on main and the rest of the CSS bundler suite still passes. Behavior now matches esbuild. No open reviewer comments remain.

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

CI is green on every lane that ran tests (278 jobs passed, including all test-bun shards on debian x64-asan, windows x64/x64-baseline/aarch64). The only hard failures are two :darwin: 26 aarch64 - test-bun shards, both with:

Error: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'

on agent darwin-aarch64-26.5.1-1. This hit four times in a row across builds 69972 and 69985 on the same host before any tests ran, so it is an agent/infra problem and not related to this change. The remaining annotation entries are tagged flaky (bake HMR timing on win-aarch64, an npm tarball download failure, bun-install-registry.test.ts on win-aarch64) and an Azure agent creation failure.

This should be ready for a maintainer to look at.

@Jarred-Sumner
Jarred-Sumner merged commit 3ef9c08 into main Jul 8, 2026
75 of 78 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/ee75cae4/css-url-fragment-suffix branch July 8, 2026 06:10
robobun added a commit that referenced this pull request Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants