Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f6ae01c
compile: resolve every spelling of an embedded Worker entry point aga…
dylan-conway Aug 27, 2026
dceb99b
bound the .js rewrite by the buffer; test: module imports, plus an ab…
dylan-conway Aug 27, 2026
54a781a
resolve embedded modules in the resolver, not in web_worker; give emb…
dylan-conway Aug 27, 2026
24a04e6
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
e9f6c8a
clippy: redundant closure
dylan-conway Aug 27, 2026
6e354cf
Merge branch 'claude/compile-worker-resolution' of github.com:oven-sh…
dylan-conway Aug 27, 2026
aefd861
only ask the resolver about the embedded graph for relative or embedd…
dylan-conway Aug 27, 2026
352c71f
StandaloneModuleGraph::find returns the graph's 'static name (no inte…
dylan-conway Aug 27, 2026
1cf6ecc
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
7da4862
resolve_embedded(): the one rule for mapping a specifier to an embedd…
dylan-conway Aug 27, 2026
07b5998
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
8527399
review: StandaloneModuleGraph::resolve() as a provided trait method (…
dylan-conway Aug 27, 2026
b70170f
Merge branch 'claude/compile-worker-resolution' of github.com:oven-sh…
dylan-conway Aug 27, 2026
4f667dc
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
8b180a7
tests: nested dirs, precedence vs the cwd, canonical resolved names, …
dylan-conway Aug 27, 2026
56b1fa2
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
58f54a5
test: Buffer.alloc for the long specifier
dylan-conway Aug 27, 2026
628cc87
a specifier with a trailing separator names no embedded module (the W…
dylan-conway Aug 27, 2026
db817ad
test: onerror on every awaited Worker
dylan-conway Aug 27, 2026
16597a1
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
33e07d2
Merge remote-tracking branch 'origin/main' into claude/compile-worker…
dylan-conway Aug 27, 2026
423537c
spell the trailing-separator check without an index
dylan-conway Aug 27, 2026
0a9dbd1
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 27, 2026
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
8 changes: 3 additions & 5 deletions src/jsc/ResolvedSource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ pub struct ResolvedSource {
pub bytecode_cache: Bytecode,
/// `Zig::SourceProvider` takes it (nulling the field).
pub module_info: Option<Box<ModuleInfoDeserialized>>,
/// The file path used as the source origin for bytecode cache validation.
/// JSC validates bytecode by checking if the origin URL matches exactly what
/// was used at build time. If empty, the origin is derived from source_url.
/// This is converted to a file:// URL on the C++ side.
pub bytecode_origin_path: BunString,
/// The file path whose `file://` URL is this module's source origin (what `import()` resolves against and what a
/// bytecode cache is validated against). Empty: derived from `source_url` (a builtin gets a `builtin://` origin).
pub origin_path: BunString,
}

/// `ResolvedSource.bytecode_cache`: C++ sees `{ uint8_t* ptr; size_t len; bool owned; }`.
Expand Down
11 changes: 3 additions & 8 deletions src/jsc/bindings/ZigSourceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,10 @@ Ref<SourceProvider> SourceProvider::create(
shouldGenerateCodeCoverage = BunTest__shouldGenerateCodeCoverage(&sourceURLBunString);
}

// Compute source origin: use explicit bytecode_origin_path if provided, otherwise derive from source_url.
// bytecode_origin_path is used for bytecode cache validation where the origin must match
// exactly what was used at build time.
const auto getSourceOrigin = [&]() -> SourceOrigin {
auto bytecodeOriginPath = resolvedSource.bytecode_origin_path.transferToWTFString();
if (!bytecodeOriginPath.isNull() && !bytecodeOriginPath.isEmpty()) {
// Convert file path to file:// URL (same as build time)
return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(bytecodeOriginPath));
}
auto originPath = resolvedSource.origin_path.transferToWTFString();
if (!originPath.isEmpty())
return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(originPath));
return toSourceOrigin(sourceURLString, isBuiltin);
};

Expand Down
8 changes: 4 additions & 4 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ typedef struct ResolvedSource {
bool bytecode_cache_persistent;
// Owned; Zig::SourceProvider takes it (nulling the field).
bun_ModuleInfoDeserialized* module_info;
// File path used as source origin for bytecode cache validation.
// Converted to file:// URL. If empty, origin is derived from source_url.
BunString bytecode_origin_path;
// File path whose file:// URL is the source origin (what import() resolves against, what a bytecode cache is
// validated against). If empty, origin is derived from source_url.
BunString origin_path;
} ResolvedSource;
static_assert(sizeof(ResolvedSource) == 136, "ResolvedSource layout is mirrored in src/jsc/ResolvedSource.rs");
inline constexpr uint32_t ResolvedSourceTagPackageJSONTypeModule = 1;
Expand All @@ -163,7 +163,7 @@ struct ErrorableResolvedSource {
return;
result.value.source_code.deref();
result.value.source_url.deref();
result.value.bytecode_origin_path.deref();
result.value.origin_path.deref();
if (result.value.bytecode_cache_owned && result.value.bytecode_cache)
ResolvedSource__freeBytecode(result.value.bytecode_cache);
if (result.value.module_info)
Expand Down
77 changes: 8 additions & 69 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,75 +1289,14 @@ unsafe fn resolve_entry_point_specifier<'s>(
error_message: &mut BunString,
log: &mut bun_ast::Log,
) -> Option<&'s [u8]> {
// SAFETY: per fn contract; read-only field.
if let Some(graph) = unsafe { (*parent).standalone_module_graph } {
if graph.find(str).is_some() {
return Some(str);
}

// Since `bun build --compile` renames files to `.js` by default, we
// need to do the reverse of our file extension mapping.
//
// new Worker("./foo") -> new Worker("./foo.js")
// new Worker("./foo.ts") -> new Worker("./foo.js")
// new Worker("./foo.jsx") -> new Worker("./foo.js")
// new Worker("./foo.mjs") -> new Worker("./foo.js")
// new Worker("./foo.mts") -> new Worker("./foo.js")
// new Worker("./foo.cjs") -> new Worker("./foo.js")
// new Worker("./foo.cts") -> new Worker("./foo.js")
// new Worker("./foo.tsx") -> new Worker("./foo.js")
//
if str.starts_with(b"./") || str.starts_with(b"../") {
'try_from_extension: {
let mut pathbuf = bun_paths::path_buffer_pool::get();
let base_path = graph.base_public_path_with_default_suffix();
let base = bun_paths::resolve_path::join_abs_string_buf::<bun_paths::platform::Loose>(
base_path,
&mut pathbuf[..],
&[str],
);
let base_len = base.len();
let extname_len = bun_paths::extension(base).len();
// `extname` cannot be held as a sub-slice of `pathbuf` while
// writing into `pathbuf` — compare
// by re-slicing after dropping the mutable borrow.
let extname = &pathbuf[base_len - extname_len..base_len];

// ./foo -> ./foo.js
if extname.is_empty() {
pathbuf[base_len..base_len + 3].copy_from_slice(b".js");
if let Some(js_file) = graph.find(&pathbuf[0..base_len + 3]) {
return Some(js_file);
}
break 'try_from_extension;
}

// ./foo.ts -> ./foo.js
if extname == b".ts" {
pathbuf[base_len - 3..base_len].copy_from_slice(b".js");
if let Some(js_file) = graph.find(&pathbuf[0..base_len]) {
return Some(js_file);
}
break 'try_from_extension;
}

if extname.len() == 4 {
const EXTS: [&[u8]; 6] = [b".tsx", b".jsx", b".mjs", b".mts", b".cts", b".cjs"];
for ext in EXTS {
if extname == ext {
let js_len = b".js".len();
pathbuf[base_len - ext.len()..base_len - ext.len() + js_len]
.copy_from_slice(b".js");
let as_js = &pathbuf[0..base_len - ext.len() + js_len];
if let Some(js_file) = graph.find(as_js) {
return Some(js_file);
}
break 'try_from_extension;
}
}
}
}
}
// In a `bun build --compile` executable, a relative specifier names an embedded entry point (relative to the
// embedded root) before it names a file on disk, and an absolute one may be an embedded path in either syntax
// (`new URL("./w.ts", import.meta.url)`).
// SAFETY: per fn contract; `standalone_module_graph` is a read-only field.
if let Some(graph) = unsafe { (*parent).standalone_module_graph }
&& let Some(name) = graph.resolve(graph.base_public_path_with_default_suffix(), str)
{
return Some(name);
}

// A `data:` URL is the module itself (the loader decodes it); it never names
Expand Down
48 changes: 10 additions & 38 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,6 @@ pub struct Bufs {
pub(crate) remap_path_trailing_slash: PathBuffer,
pub(crate) path_in_global_disk_cache: PathBuffer,
pub(crate) abs_to_rel: PathBuffer,
pub(crate) import_path_for_standalone_module_graph: PathBuffer,

#[cfg(windows)]
pub(crate) win32_normalized_dir_info_cache: [u8; MAX_PATH_BYTES * 2],
Expand Down Expand Up @@ -1296,54 +1295,27 @@ impl<'a> Resolver<'a> {
let mut source_dir_resolver = bun_paths::PosixToWinNormalizer::default();
let source_dir_normalized: &[u8] = 'brk: {
if let Some(graph) = self.standalone_module_graph {
if ::bun_options_types::standalone_path::is_bun_standalone_file_path(import_path) {
if graph.find_assume_standalone_path(import_path).is_some() {
let specifier_is_embedded_path =
::bun_options_types::standalone_path::is_bun_standalone_file_path(import_path);
if specifier_is_embedded_path
|| ::bun_options_types::standalone_path::is_bun_standalone_file_path(source_dir)
{
if let Some(file_name) = graph.resolve(source_dir, import_path) {
self.extension_order = original_order;
return ResultUnion::Success(Result {
import_kind: kind,
path_pair: PathPair {
primary: Path::init(import_path),
primary: Path::init(file_name),
secondary: None,
},
module_type: options::ModuleType::Esm,
flags: ResultFlags::IS_STANDALONE_MODULE,
..Default::default()
});
}

self.extension_order = original_order;
return ResultUnion::NotFound;
} else if ::bun_options_types::standalone_path::is_bun_standalone_file_path(
source_dir,
) {
if import_path.len() > 2 && is_dot_slash(&import_path[0..2]) {
let buf = bufs!(import_path_for_standalone_module_graph);
let joined = bun_paths::join_abs_string_buf(
source_dir,
buf,
&[import_path],
bun_paths::Platform::Loose,
);

// Support relative paths in the graph
if let Some(file_name) = graph.find_assume_standalone_path(joined) {
// Intern: trait borrows into the graph; `Path::init`
// needs `'static` (DirnameStore-backed).
let file_name = Fs::file_system::DirnameStore::instance()
.append_slice(file_name)
.expect("unreachable");
self.extension_order = original_order;
return ResultUnion::Success(Result {
import_kind: kind,
path_pair: PathPair {
primary: Path::init(file_name),
secondary: None,
},
module_type: options::ModuleType::Esm,
flags: ResultFlags::IS_STANDALONE_MODULE,
..Default::default()
});
}
if specifier_is_embedded_path {
self.extension_order = original_order;
return ResultUnion::NotFound;
}
break 'brk Fs::FileSystem::instance().top_level_dir;
}
Expand Down
65 changes: 60 additions & 5 deletions src/resolver/standalone_module_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,70 @@
pub trait StandaloneModuleGraph: Send + Sync {
/// Look up `name` (already known to be under the standalone virtual root)
/// and return the embedded file's canonical name slice if present.
fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&[u8]>;
fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&'static [u8]>;
/// Whether the embedded file at `name` carries a serialized ES module record (`module_info`).
fn has_module_info(&self, _name: &[u8]) -> bool {
false
}
/// Look up `name` (any path — checks the standalone virtual-root prefix
/// first) and return the embedded file's canonical name slice if present.
/// Spec `StandaloneModuleGraph.find`.
fn find(&self, name: &[u8]) -> Option<&[u8]>;
/// The embedded module `specifier` names when imported from `source_dir`: an absolute embedded path (in either
/// path syntax), or a `./` / `../` specifier joined onto `source_dir`, looked up as spelled and then -- since every
/// entry point is embedded under a `.js` name -- under the `.js` name for a source extension or no extension
/// (`./w.ts` -> `/$bunfs/root/w.js`). Returns the graph's own name for the module, which is what the module
/// loader keys on; `None` for anything else (bare specifiers, other absolute paths, misses).
fn resolve(&self, source_dir: &[u8], specifier: &[u8]) -> Option<&'static [u8]> {
let is_relative = matches!(specifier, [b'.', s, ..] | [b'.', b'.', s, ..] if bun_paths::is_sep_native(*s));
let is_embedded_path =
bun_options_types::standalone_path::is_bun_standalone_file_path(specifier);
if (!is_relative && !is_embedded_path)
|| specifier
.last()
.is_some_and(|&c| bun_paths::is_sep_native(c))
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return None;
}
let mut buf = bun_paths::path_buffer_pool::get();
let path_len = if is_embedded_path {
if specifier.len() > buf.len() {
return None;
}
buf[..specifier.len()].copy_from_slice(specifier);
specifier.len()
} else {
bun_paths::resolve_path::join_abs_string_buf_checked::<bun_paths::platform::Loose>(
source_dir,
&mut buf[..],
&[specifier],
)?
.len()
};
if let Some(name) = self.find_assume_standalone_path(&buf[..path_len]) {
return Some(name);
}
// Entry points are embedded under a `.js` name whatever the (case-insensitive) source extension was.
let extension = bun_paths::extension(&buf[..path_len]);
let extension_len = extension.len();
let is_source_extension = extension.is_empty()
|| [
b"ts".as_slice(),
b"tsx",
b"jsx",
b"mjs",
b"mts",
b"cjs",
b"cts",
]
.iter()
.any(|source| extension[1..].eq_ignore_ascii_case(source));
if !is_source_extension {
return None;
}
let stem_len = path_len - extension_len;
if stem_len + 3 > buf.len() {
return None;
}
buf[stem_len..stem_len + 3].copy_from_slice(b".js");
self.find_assume_standalone_path(&buf[..stem_len + 3])
}
/// `StandaloneModuleGraph.base_public_path_with_default_suffix` — the
/// virtual-root prefix used for embedded modules (e.g. `/$bunfs/root/`).
/// Baked-in `'static` constant; surfaced here so low-tier callers
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3788,7 +3788,13 @@ export default db;
return Some(ResolvedSource {
source_code: file.to_wtf_string(),
source_url: specifier.clone(),
bytecode_origin_path: bun_core::String::from_bytes(file.bytecode_origin_path),
// An embedded file is served through the builtin-module path but is a file: its origin is its own path
// (or, with --bytecode, the path the cache was generated under, which must match exactly).
origin_path: if file.bytecode_origin_path.is_empty() {
specifier.clone()
} else {
bun_core::String::from_bytes(file.bytecode_origin_path)
},
bytecode_cache: Bytecode::persistent(bytecode),
source_code_hash: file.source_hash,
module_info: if !module_info.is_empty() {
Expand Down
6 changes: 1 addition & 5 deletions src/standalone_graph/StandaloneModuleGraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,14 +309,10 @@ impl bun_resolver::StandaloneModuleGraph for StandaloneModuleGraph {
self.find_ref(name)
.is_some_and(|file| !file.module_info.is_empty())
}
fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&[u8]> {
fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&'static [u8]> {
self.lookup_file(name).map(|f| f.name)
}

fn find(&self, name: &[u8]) -> Option<&[u8]> {
self.find_ref(name).map(|f| f.name)
}

fn base_public_path_with_default_suffix(&self) -> &'static [u8] {
BASE_PUBLIC_PATH_WITH_DEFAULT_SUFFIX.as_bytes()
}
Expand Down
Loading
Loading