Skip to content
Open
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
22 changes: 22 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ bitflags::bitflags! {
/// `IS_UNUSED` for its own reasons (an unused TypeScript import, a macro
/// import), and those records must never be resolved.
const IS_BARREL_DEFERRED = 1 << 18;

/// The file was found only after the `?query#fragment` of `original_path` was removed.
const RESOLVED_WITHOUT_URL_SUFFIX = 1 << 19;
}
}

impl ImportRecord {
/// Where the `?query#fragment` of a URL starts. It can also be part of a file name (`./C#/logo.png`).
#[inline]
pub fn url_suffix_start(specifier: &[u8]) -> Option<usize> {
bun_core::strings::index_of_any(specifier, b"?#")
}

/// The `?query#fragment` that was removed from `original_path` to find the file, or empty.
pub fn removed_url_suffix(&self) -> &'static [u8] {
if !self.flags.contains(Flags::RESOLVED_WITHOUT_URL_SUFFIX) {
return b"";
}
match Self::url_suffix_start(self.original_path) {
Some(i) => &self.original_path[i..],
None => b"",
}
}
}

Expand Down
29 changes: 27 additions & 2 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub use api::JSBundler::FileMap;
pub struct PendingImport {
pub(crate) to_source_index: Index,
pub(crate) import_record_index: u32,
/// See `ImportRecordFlags::RESOLVED_WITHOUT_URL_SUFFIX`.
pub(crate) resolved_without_url_suffix: bool,
}

pub struct BundleV2<'a> {
Expand Down Expand Up @@ -3024,6 +3026,10 @@ pub mod bv2_impl {
[import_record.importer_source_index as usize]
.as_mut_slice()[import_record.import_record_index as usize];
record.source_index = source_index;
record.flags.set(
bun_ast::ImportRecordFlags::RESOLVED_WITHOUT_URL_SUFFIX,
resolve_result.flags.removed_url_suffix(),
);
}
}

Expand Down Expand Up @@ -5264,6 +5270,10 @@ pub mod bv2_impl {
}
jsc_api::JSBundler::ResolveValue::Success(result) => {
let mut out_source_index: Option<Index> = None;
// An onResolve result cannot say what the plugin removed, so the suffix always goes back.
let resolved_without_url_suffix = resolve.import_record.kind.is_from_css()
&& ImportRecord::url_suffix_start(&resolve.import_record.specifier)
.is_some();
// SAFETY: `result.{path,namespace}` are `Box<[u8]>`. Each arm below
// either moves both boxes into `this.free_list` before it stores
// `path` (`!found_existing`, external import), or drops them and
Expand Down Expand Up @@ -5460,12 +5470,17 @@ pub mod bv2_impl {
let _ = entry.value_ptr.push(PendingImport {
to_source_index: source_index,
import_record_index: resolve.import_record.import_record_index,
resolved_without_url_suffix,
});
} else {
let import_record: &mut ImportRecord = &mut source_import_records
.as_mut_slice()
[resolve.import_record.import_record_index as usize];
import_record.source_index = source_index;
import_record.flags.set(
bun_ast::ImportRecordFlags::RESOLVED_WITHOUT_URL_SUFFIX,
resolved_without_url_suffix,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
this.schedule_barrel_imports_after_plugin_resolve(
resolve.import_record.importer_source_index,
);
Expand Down Expand Up @@ -7027,6 +7042,11 @@ pub mod bv2_impl {
continue;
}

import_record.flags.set(
bun_ast::ImportRecordFlags::RESOLVED_WITHOUT_URL_SUFFIX,
resolve_result.flags.removed_url_suffix(),
);

// borrowck — `Result.path()` returns `Option<&mut Path>`, which
// would lock the whole struct while the loop body still needs to
// read other `resolve_result` fields (`.flags`, `.path_pair`,
Expand Down Expand Up @@ -7426,8 +7446,13 @@ pub mod bv2_impl {
if save_import_record_source_index
|| input_file_loaders[to_assign.to_source_index.get() as usize].is_css()
{
import_records.as_mut_slice()[to_assign.import_record_index as usize]
.source_index = to_assign.to_source_index;
let record = &mut import_records.as_mut_slice()
[to_assign.import_record_index as usize];
record.source_index = to_assign.to_source_index;
record.flags.set(
bun_ast::ImportRecordFlags::RESOLVED_WITHOUT_URL_SUFFIX,
to_assign.resolved_without_url_suffix,
);
}
}
drop(value);
Expand Down
5 changes: 1 addition & 4 deletions src/css/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,7 @@ impl<'a> Printer<'a> {
// 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"",
}
record.removed_url_suffix()
} else {
b""
};
Expand Down
8 changes: 5 additions & 3 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1384,8 +1384,7 @@ impl<'a> Resolver<'a> {
if matches!(tmp, ResultUnion::NotFound) && kind.is_from_css() {
'try_without_suffix: {
// If resolution failed, try again with the URL query and/or hash removed
let maybe_suffix = strings::index_of_any(import_path, b"?#");
let Some(suffix) = maybe_suffix else {
let Some(suffix) = ast::ImportRecord::url_suffix_start(import_path) else {
break 'try_without_suffix;
};
if suffix < 1 {
Comment thread
robobun marked this conversation as resolved.
Expand All @@ -1398,7 +1397,7 @@ impl<'a> Resolver<'a> {
bstr::BStr::new(&import_path[suffix..])
));
}
let result2 = self.resolve_without_symlinks(
let mut result2 = self.resolve_without_symlinks(
source_dir_normalized,
&import_path[0..suffix],
kind,
Expand All @@ -1407,6 +1406,9 @@ impl<'a> Resolver<'a> {
if matches!(result2, ResultUnion::NotFound) {
break 'try_without_suffix;
}
if let ResultUnion::Success(result) = &mut result2 {
result.flags.insert(ResultFlags::REMOVED_URL_SUFFIX);
}
tmp = result2;
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/resolver/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ bitflags::bitflags! {
const IS_STANDALONE_MODULE = 1 << 2;
// This is true when the package was loaded from within the node_modules directory.
const IS_FROM_NODE_MODULES = 1 << 3;
/// The import path (a CSS or HTML URL) resolved only after its `?query#fragment` was removed.
const REMOVED_URL_SUFFIX = 1 << 4;
const EMIT_DECORATOR_METADATA = 1 << 5;
const EXPERIMENTAL_DECORATORS = 1 << 6;
/// tsconfig `"useDefineForClassFields": false` was set explicitly.
Expand Down Expand Up @@ -183,6 +185,10 @@ impl ResultFlags {
self.contains(Self::IS_STANDALONE_MODULE)
}
#[inline]
pub fn removed_url_suffix(self) -> bool {
self.contains(Self::REMOVED_URL_SUFFIX)
}
#[inline]
pub(crate) fn is_from_node_modules(self) -> bool {
self.contains(Self::IS_FROM_NODE_MODULES)
}
Expand Down
33 changes: 33 additions & 0 deletions test/bake/dev/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,39 @@ devTest("asset referenced in css", {
await dev.fetch(extractCssUrl(backgroundImage)).expectFile(imageFixtures.bun2);
},
});
// A literal `#` in a directory name is part of the path. Only a suffix that was
// removed to find the file goes back on the rewritten url().
devTest("asset under a directory with a literal # referenced in css", {
files: {
"index.html": emptyHtmlFile({
styles: ["styles.css"],
}),
"styles.css": `
.a { background-image: url("./C#/big.svg"); }
.b { mask: url(./big.svg#frag); }
.c { background-image: url("./big.svg?v=3#frag"); }
.d { background-image: url("./C#/bun.png"); }
.e { mask: url(./bun.png#frag); }
`,
"C#/big.svg": Buffer.alloc(128 * 1024 + 1, "A"),
"big.svg": Buffer.alloc(128 * 1024 + 1, "Z"),
"C#/bun.png": imageFixtures.bun,
"bun.png": imageFixtures.bun2,
},
async test(dev) {
const stylesheetHref = (await (await dev.fetch("/")).text()).match(/<link rel="stylesheet"[^>]*href="([^"]+)"/)![1];
const stylesheet = await (await dev.fetch(stylesheetHref)).text();
const urls = stylesheet.match(/url\([^)]*\)/g)!.map(url => url.replace(/\/_bun\/asset\/[0-9a-f]+/, "<asset>"));
const png = (image: Buffer) => "data:image/png;base64," + image.toString("base64");
expect(urls).toEqual([
`url("<asset>.svg")`,
`url("<asset>.svg#frag")`,
`url("<asset>.svg?v=3#frag")`,
`url("${png(imageFixtures.bun)}")`,
`url("${png(imageFixtures.bun2)}#frag")`,
]);
},
});
devTest("syntax error crash", {
files: {
"styles.css": `
Expand Down
Loading
Loading