Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/css/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,16 +412,42 @@ impl<'a> Printer<'a> {
};
let record = &import_info.import_records[import_record_idx as usize];
if record.source_index.is_valid() {
// It has an inlined url for CSS
// A `url()`'s `?query`/`#fragment` (e.g. `url(sprites.svg#icon)`) is
// stripped by the resolver to find the file; re-append it to the
// rewritten reference so the fragment still addresses the element.
let suffix: &[u8] = if record.kind == bun_ast::ImportKind::Url {
match bun_core::strings::index_of_any(record.original_path, b"?#") {
Some(i) => &record.original_path[i..],
None => b"",
}
} else {
b""
};
let arena = self.arena;
let with_suffix = |url: &'a [u8], suffix: &[u8]| -> &'a [u8] {
if suffix.is_empty() {
return url;
}
let buf = arena.alloc_slice_fill_copy(url.len() + suffix.len(), 0u8);
buf[..url.len()].copy_from_slice(url);
buf[url.len()..].copy_from_slice(suffix);
buf
};
// It has an inlined data: URL for CSS. A `?query` here lands in the
// base64 body and fails decoding, so keep only the `#fragment`.
let urls_for_css = import_info.ast_urls_for_css[record.source_index.get() as usize];
if !urls_for_css.is_empty() {
return Ok(urls_for_css);
let fragment: &[u8] = match bun_core::strings::index_of_char(suffix, b'#') {
Some(i) => &suffix[i as usize..],
None => b"",
};
return Ok(with_suffix(urls_for_css, fragment));
}
Comment thread
robobun marked this conversation as resolved.
// It is a chunk URL
// It is a chunk URL (copied asset): keep the full `?query#fragment`.
let unique_key_for_additional_file =
import_info.ast_unique_key_for_additional_file[record.source_index.get() as usize];
if !unique_key_for_additional_file.is_empty() {
return Ok(unique_key_for_additional_file);
return Ok(with_suffix(unique_key_for_additional_file, suffix));
}
}
// External URL stays as-is
Expand Down
83 changes: 83 additions & 0 deletions test/bundler/esbuild/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,89 @@ b {
},
});

// A url()'s `?query` / `#fragment` suffix must survive path rewriting so that
// SVG fragment references like `url(sprites.svg#icon)` keep addressing the
// element inside the asset. esbuild also preserves these.
itBundled("css/URLQueryFragmentPreservedFileLoader", {
files: {
"/entry.css": /* css */ `
.a { mask: url(./sprites.svg#icon) }
.b { clip-path: url("./sprites.svg?v=3#c") }
.c { background: url(./sprites.svg) }
.d { --bg: url("./sprites.svg#custom") }
`,
"/sprites.svg": Buffer.alloc(128 * 1024 + 1, "Z").toString(),
},
loader: {
".svg": "file",
},
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `
/* entry.css */
.a {
mask: url("./sprites-mrrzcz3w.svg#icon");
}
.b {
clip-path: url("./sprites-mrrzcz3w.svg?v=3#c");
}
.c {
background: url("./sprites-mrrzcz3w.svg");
}
.d {
--bg: url("./sprites-mrrzcz3w.svg#custom");
}
`);
},
});
itBundled("css/URLQueryFragmentPreservedDataURL", {
files: {
"/entry.css": /* css */ `
.a { mask: url(./small.svg#m1) }
.b { background: url("./small.svg?v=2") }
.c { mask: url(./small.svg?v=2#m1) }
.d { background: url(./small.svg) }
`,
"/small.svg": `<svg xmlns="http://www.w3.org/2000/svg"><mask id="m1"/></svg>`,
},
outdir: "/out",
onAfterBundle(api) {
// A `?query` appended to a base64 data: URL lands in the body and fails
// decoding, so only the `#fragment` is kept on the inline path.
api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `
/* entry.css */
.a {
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxtYXNrIGlkPSJtMSIvPjwvc3ZnPg==#m1");
}
.b {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxtYXNrIGlkPSJtMSIvPjwvc3ZnPg==");
}
.c {
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxtYXNrIGlkPSJtMSIvPjwvc3ZnPg==#m1");
}
.d {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxtYXNrIGlkPSJtMSIvPjwvc3ZnPg==");
}
`);
},
});
itBundled("css/URLQueryFragmentPreservedMinified", {
files: {
"/entry.css": /* css */ `
.a { mask: url(./sprites.svg#icon) }
`,
"/sprites.svg": Buffer.alloc(128 * 1024 + 1, "Z").toString(),
},
loader: {
".svg": "file",
},
minifyWhitespace: true,
outdir: "/out",
onAfterBundle(api) {
api.expectFile("/out/entry.css").toEqualIgnoringWhitespace(/* css */ `.a{mask:url(./sprites-mrrzcz3w.svg#icon)}`);
},
});

itBundled("css/IgnoreURLsInAtRulePrelude", {
// GENERATED
files: {
Expand Down
Loading