Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dc2eb48
runtime: attach ModuleInfo to ESM transpiles so type-only re-exports …
robobun Jul 25, 2026
3a14030
Skip ModuleInfo when the parser emitted errors
robobun Jul 25, 2026
5a122c8
Address review: features_hash, cache-hit test, pin silenced-typo beha…
robobun Jul 25, 2026
f4fd985
test: give each cache-hit variant its own cache dir
robobun Jul 25, 2026
7148b79
test: inspector breakpoints must resolve in runtime-transpiled ESM
robobun Aug 10, 2026
9e60282
Shorten transpiler cache version note
robobun Aug 10, 2026
b2ee4c0
Share the ModuleInfo decision between the VM and the transpiler worker
robobun Aug 10, 2026
05c547f
Make the ModuleInfo escape hatch process-wide so the cache key can ha…
robobun Aug 10, 2026
61db16a
Pin WebKit to the oven-sh/WebKit#345 preview build
robobun Aug 10, 2026
0d8bb62
fix requiring esm graphs with a shared dependency
alii Aug 10, 2026
0de84ef
record namespaced plugin specifiers as printed
alii Aug 10, 2026
03606ce
free the module record when a load is abandoned
alii Aug 10, 2026
24a3936
only intern exported names into the module record
alii Aug 10, 2026
93f7c54
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 10, 2026
26f99d1
Release the rest of a transpile batch when the VM terminates mid-batch
robobun Aug 10, 2026
875178f
bump webkit
alii Aug 10, 2026
2886915
free the module record once a module is linked
alii Aug 10, 2026
66dc26b
Merge branch 'main' into farm/2014f036/runtime-module-info-typescript…
alii Aug 11, 2026
7ffa0b4
Emit local exports last and in name order in the module record
robobun Aug 11, 2026
e184bf8
Point the inspector test at the merged WebKit change and shorten the …
robobun Aug 11, 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
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "447082ab6897278727b44e1ba3c326ae6e1504c3";
export const WEBKIT_VERSION = "autobuild-preview-pr-345-b7c69395";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
2 changes: 2 additions & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ pub mod feature_flag {
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG, "BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, "BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE, "BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE", {});
// Escape hatch for the #7384 fix: never attach ModuleInfo to runtime ESM.
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_RUNTIME_MODULE_INFO, "BUN_FEATURE_FLAG_DISABLE_RUNTIME_MODULE_INFO", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE, "BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO", {});
Expand Down
2 changes: 1 addition & 1 deletion src/bundler/analyze_transpiled_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl ModuleInfoDeserialized {
/// # Safety
/// `this` must have been produced by [`Self::create`] (heap box) or by
/// [`ModuleInfoExt::into_deserialized`].
pub(crate) unsafe fn deinit(this: *mut ModuleInfoDeserialized) {
pub unsafe fn deinit(this: *mut ModuleInfoDeserialized) {
// SAFETY: caller contract — see fn doc above.
unsafe {
match (*this).owner {
Expand Down
67 changes: 44 additions & 23 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,10 @@ pub mod analyze_transpiled_module {
self.record_kinds[idx] = RecordKind::ImportInfoSingleTypeScript;
}
}
// Build-time indexes only; the runtime keeps this struct alive for the
// SourceProvider's lifetime, so drop them now.
Comment thread
robobun marked this conversation as resolved.
Outdated
self.strings_map = HashMap::default();
self.exported_names = HashMap::default();
self.finalized = true;
Ok(())
}
Expand Down Expand Up @@ -4731,8 +4735,8 @@ pub(crate) mod __gated_printer {
// reshaped for borrowck — fetch name before borrowing module_info.
let local_name = self.name_for_symbol(b.r#ref);
if let Some(mi) = self.module_info() {
let name_id = mi.str(local_name);
if tlm.is_export {
let name_id = mi.str(local_name);
mi.add_export_info_local(name_id, name_id);
}
}
Expand Down Expand Up @@ -4846,8 +4850,9 @@ pub(crate) mod __gated_printer {
{
if Self::MAY_HAVE_MODULE_INFO {
if let Some(mi) = self.module_info() {
let name_id = mi.str(str.slice8());
if tlm.is_export {
let name_id =
mi.str(str.slice8());
mi.add_export_info_local(
name_id, name_id,
);
Expand Down Expand Up @@ -4881,8 +4886,8 @@ pub(crate) mod __gated_printer {
// reshaped for borrowck — bump access first.
let str8 = str.slice(self.bump);
if let Some(mi) = self.module_info() {
let name_id = mi.str(str8);
if tlm.is_export {
let name_id = mi.str(str8);
mi.add_export_info_local(
name_id, name_id,
);
Expand Down Expand Up @@ -4995,8 +5000,8 @@ pub(crate) mod __gated_printer {

if Self::MAY_HAVE_MODULE_INFO {
if let Some(mi) = self.module_info() {
let name_id = mi.str(local_name);
if s.func.flags.contains(G::FnFlags::IsExport) {
let name_id = mi.str(local_name);
mi.add_export_info_local(name_id, name_id);
}
}
Expand Down Expand Up @@ -5026,8 +5031,8 @@ pub(crate) mod __gated_printer {

if Self::MAY_HAVE_MODULE_INFO {
if let Some(mi) = self.module_info() {
let name_id = mi.str(name_str);
if s.is_export {
let name_id = mi.str(name_str);
mi.add_export_info_local(name_id, name_id);
}
}
Expand Down Expand Up @@ -5174,15 +5179,17 @@ pub(crate) mod __gated_printer {
self.print_whitespacer(ws!(b"from "));
}

let irp = &self.import_record(s.import_record_index as usize).path.text;
let irp = Self::printed_import_record_path(
self.import_record(s.import_record_index as usize),
);
self.print_import_record_path(
self.import_record(s.import_record_index as usize),
);
self.print_semicolon_after_statement();

if Self::MAY_HAVE_MODULE_INFO {
if let Some(mi) = self.module_info() {
let irp_id = mi.str(irp);
let irp_id = mi.str(&irp);
mi.request_module(
irp_id,
analyze_transpiled_module::FetchParameters::None,
Expand Down Expand Up @@ -5358,7 +5365,7 @@ pub(crate) mod __gated_printer {
}

self.print_whitespacer(ws!(b"} from "));
let irp = &import_record.path.text;
let irp = Self::printed_import_record_path(import_record);
self.print_import_record_path(import_record);
self.print_semicolon_after_statement();

Expand All @@ -5367,7 +5374,7 @@ pub(crate) mod __gated_printer {
// `name_for_symbol` (which needs `&mut self`) can run between uses.
let irp_id = {
let mi = self.module_info().expect("infallible: module_info enabled");
let id = mi.str(irp);
let id = mi.str(&irp);
mi.request_module(id, analyze_transpiled_module::FetchParameters::None);
id
};
Expand Down Expand Up @@ -5880,11 +5887,11 @@ pub(crate) mod __gated_printer {
// reshaped for borrowck — `module_info()` borrows `&mut self`,
// so we re-borrow it between `name_for_symbol` calls instead of holding
// a single long-lived `mi` across the whole block. `irp_id` is Copy.
let import_record_path = &record.path.text;
let import_record_path = Self::printed_import_record_path(record);
use analyze_transpiled_module::FetchParameters as FP;
let (irp_id, fetch_parameters) = {
let mi = self.module_info().expect("infallible: module_info enabled");
let irp_id = mi.str(import_record_path);
let irp_id = mi.str(&import_record_path);
let fetch_parameters: FP = if IS_BUN_PLATFORM {
if let Some(loader) = record.loader {
use bun_ast::Loader;
Expand Down Expand Up @@ -6060,27 +6067,41 @@ pub(crate) mod __gated_printer {
Ok(())
}

fn prints_namespace_in_path(import_record: &ImportRecord) -> bool {
import_record
.flags
.contains(ImportRecordFlags::PRINT_NAMESPACE_IN_PATH)
&& !import_record.path.is_file()
}

/// The module specifier exactly as `print_import_record_path` writes it,
/// so the ModuleInfo record names the same module JSC will request.
Comment thread
robobun marked this conversation as resolved.
fn printed_import_record_path(import_record: &ImportRecord) -> std::borrow::Cow<'_, [u8]> {
if Self::prints_namespace_in_path(import_record) {
let path = &import_record.path;
let mut out = Vec::with_capacity(path.namespace.len() + 1 + path.text.len());
out.extend_from_slice(path.namespace);
out.push(b':');
out.extend_from_slice(path.text);
std::borrow::Cow::Owned(out)
} else {
std::borrow::Cow::Borrowed(import_record.path.text)
}
}

pub(crate) fn print_import_record_path(&mut self, import_record: &ImportRecord) {
if IS_JSON {
unreachable!();
}

let quote = best_quote_char_for_string(import_record.path.text, false);
if import_record
.flags
.contains(ImportRecordFlags::PRINT_NAMESPACE_IN_PATH)
&& !import_record.path.is_file()
{
self.print(quote);
self.print(quote);
if Self::prints_namespace_in_path(import_record) {
self.print_string_characters_utf8(import_record.path.namespace, quote);
self.print(b":");
self.print_string_characters_utf8(import_record.path.text, quote);
self.print(quote);
} else {
self.print(quote);
self.print_string_characters_utf8(import_record.path.text, quote);
self.print(quote);
}
self.print_string_characters_utf8(import_record.path.text, quote);
self.print(quote);
}

#[inline]
Expand Down
18 changes: 14 additions & 4 deletions src/jsc/ResolvedSource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ impl Default for ResolvedSource {
// the raw `ResolvedSource` for FFI is `into_ffi()` (consumes, forgets). If the
// owner is dropped instead, every contained `BunString` is `deref()`d.
//
// The `module_info` pointer (a `Box<ModuleInfoDeserialized>` leaked via
// `heap::into_raw`) is intentionally NOT freed here — its ownership protocol
// is separate (C++ calls `Bun__free_module_info` on success; on Rust-side drop
// it would still leak today, tracked separately).
// `module_info` (a `Box<ModuleInfoDeserialized>` leaked via `heap::into_raw`)
// follows the same rule: `into_ffi()` hands it to C++ (adopted by
// `Zig::SourceProvider::create`, or freed by `ResolvedSourceCodeHolder`), and a
// Rust-side drop frees it here.
Comment thread
robobun marked this conversation as resolved.
// ──────────────────────────────────────────────────────────────────────────
#[repr(transparent)]
#[derive(Default)]
Expand Down Expand Up @@ -142,5 +142,15 @@ impl Drop for OwnedResolvedSource {
self.0.specifier.deref();
self.0.source_url.deref();
self.0.bytecode_origin_path.deref();
if !self.0.module_info.is_null() {
// SAFETY: non-null `module_info` is always the `heap::into_raw` of a
// `Box<ModuleInfoDeserialized>` that nothing else has adopted yet.
unsafe {
bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized::deinit(
self.0.module_info.cast(),
)
};
self.0.module_info = core::ptr::null_mut();
}
}
}
8 changes: 7 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ bun_core::declare_scope!(cache, visible);
/// Version 25: Every ModuleInfo record carries a trailing FetchParameters slot
/// so ImportEntry/ExportEntry/StarExportEntry moduleRequestType matches JSC's
/// after WebKit 90b2ecf79ae3 keyed m_loadedModules on (specifier, type).
const EXPECTED_VERSION: u32 = 25;
/// Version 26: ModuleInfo is written for every runtime ESM transpile, not only
/// under --isolate; older entries have an empty esm_record (#7384).
Comment thread
robobun marked this conversation as resolved.
const EXPECTED_VERSION: u32 = 26;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down Expand Up @@ -962,6 +964,10 @@ impl RuntimeTranspilerCache {

let mut features_hasher = Wyhash::init(SEED);
parser_options.hash_for_runtime_transpiler(&mut features_hasher, used_jsx);
// Decides whether the entry carries an esm_record.
features_hasher.update(&[u8::from(
crate::virtual_machine::VirtualMachine::use_module_info_for_esm(),
)]);
self.features_hash = Some(features_hasher.final_());

self.entry = match Self::from_file(
Expand Down
56 changes: 38 additions & 18 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,23 @@
}
// SAFETY: a live job popped from the intrusive queue; this thread
// owns it now (its worker-thread part finished before `close()`).
unsafe {
(*job).promise.deinit();
(*job).reset_for_pool();
self.store.put(job);
}
unsafe { self.release_job(job) };
}
}

/// Release a popped job without running its completion: drops the
/// transpiled source (with its ModuleInfo), log and module promise, and
/// recycles the slot.
///
/// # Safety
/// `job` was popped from `self.queue` on the JS thread and nothing else
/// references it.
Comment thread
robobun marked this conversation as resolved.
unsafe fn release_job(&mut self, job: *mut TranspilerJob) {
// SAFETY: per fn contract.
unsafe {
(*job).promise.deinit();
(*job).reset_for_pool();
self.store.put(job);
}
}

Expand All @@ -276,17 +288,26 @@
if let Err(err) = unsafe { (*first).run_from_js_thread() } {
global.report_uncaught_exception_from_error(err);
}
let mut terminated = false;
loop {
let job = iter.next();
if job.is_null() {
break;
}
// if there are more, we need to drain the microtasks from the previous run
// SAFETY: `event_loop` is the VM's live event-loop self-pointer.
if unsafe { (*event_loop.as_ptr()).drain_microtasks_with_global(global, jsc_vm) }
.is_err()
if !terminated
&& unsafe { (*event_loop.as_ptr()).drain_microtasks_with_global(global, jsc_vm) }

Check failure on line 300 in src/jsc/RuntimeTranspilerStore.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

unsafe block missing a safety comment
.is_err()
{
return;
terminated = true;
}
if terminated {
// The rest of the batch is already off the queue, so teardown's
// `release_queued_jobs_for_teardown` would never see it.
// SAFETY: `job` is a live job popped from the intrusive queue.
unsafe { self.release_job(job) };
continue;
}
// SAFETY: `job` is a live job popped from the intrusive queue.
if let Err(err) = unsafe { (*job).run_from_js_thread() } {
Expand Down Expand Up @@ -484,8 +505,8 @@

impl TranspilerJob {
/// Kept as a private inherent fn (not `impl Drop`) because the
/// slot is recycled into the HiveArray via `store.put(this)`. Only caller is
/// `run_from_js_thread`.
/// slot is recycled into the HiveArray via `store.put(this)`. Callers are
/// `run_from_js_thread` and `RuntimeTranspilerStore::release_job`.
Comment thread
robobun marked this conversation as resolved.
///
/// Note: `HiveArrayFallback::put` runs `drop_in_place` on the slot (see
/// hive_array.rs note), so the Drop-carrying fields — `OwnedString` ×2,
Expand Down Expand Up @@ -972,12 +993,7 @@
}
}

// SAFETY: leaf scalar field read; see `vm` note above. Inlined
// `VirtualMachine::use_isolation_source_provider_cache` to avoid forming
// `&VirtualMachine`.
let use_isolation_source_provider_cache = unsafe { (*vm).test_isolation_enabled }
&& !bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE::get()
.unwrap_or(false);
let use_module_info_for_esm = VirtualMachine::use_module_info_for_esm();

if let Some(entry_ptr) = cache.entry.take() {
// SAFETY: `entry` was boxed by `JSC_PARSER_CACHE_VTABLE.get` from a
Expand All @@ -1000,7 +1016,7 @@
dump_source_string(vm, specifier, entry.output_code.byte_slice());
}

let module_info: *mut c_void = if use_isolation_source_provider_cache
let module_info: *mut c_void = if use_module_info_for_esm
&& entry.metadata.module_type != CacheModuleType::Cjs
&& !entry.esm_record.is_empty()
{
Expand Down Expand Up @@ -1116,10 +1132,14 @@

let is_commonjs_module = parse_result.ast.has_commonjs_export_names
|| parse_result.ast.exports_kind == ExportsKind::Cjs;
// `!log.has_errors()`: a duplicate-export or similar parser error leaves
// an AST whose ModuleInfo would mask the real syntax error. Fall back to
// JSC's analyze (mirrors the sync path's `log.errors > 0` bail).
Comment thread
robobun marked this conversation as resolved.
let mut module_info: Option<Box<analyze_transpiled_module::ModuleInfo>> =
if use_isolation_source_provider_cache
if use_module_info_for_esm
&& !is_commonjs_module
&& loader.is_java_script_like()
&& !log.has_errors()
{
Some(analyze_transpiled_module::ModuleInfo::create(
loader.is_type_script(),
Expand Down
8 changes: 8 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4801,6 +4801,14 @@ impl VirtualMachine {
.unwrap_or(false)
}

/// Attach `ModuleInfo` to runtime-transpiled ESM so JSC builds the module
/// record from Bun's output (keeps TypeScript type-only re-exports linkable,
/// #7384). Process-wide so `RuntimeTranspilerCache` can hash it into its key.
Comment thread
robobun marked this conversation as resolved.
pub fn use_module_info_for_esm() -> bool {
!bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_RUNTIME_MODULE_INFO::get()
.unwrap_or(false)
}

/// Resets entry-point state and re-loads `entry_path` for the test runner, returning the load promise.
pub(crate) fn reload_entry_point_for_test_runner(
&mut self,
Expand Down
Loading
Loading