Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
15 changes: 7 additions & 8 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,12 +972,7 @@ impl TranspilerJob {
}
}

// 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 +995,7 @@ impl TranspilerJob {
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 +1111,14 @@ impl TranspilerJob {

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
19 changes: 7 additions & 12 deletions src/jsc/bindings/BunAnalyzeTranspiledModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
#include "ZigSourceProvider.h"
#include "ZigGlobalObject.h"
#include "headers-handwritten.h"
#include "IsolatedModuleCache.h"
#include "BunAnalyzeTranspiledModule.h"

// ref: JSModuleLoader.cpp
Expand Down Expand Up @@ -170,20 +169,16 @@ extern "C" EncodedJSValue Bun__analyzeTranspiledModule(JSGlobalObject* globalObj

auto provider = static_cast<Zig::SourceProvider*>(sourceCode.provider());

if (provider->m_resolvedSource.module_info == nullptr) {
dataLog("[note] module_info is null for module: ", moduleKey.utf8(), "\n");
RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("module_info is null")))));
}
// module_info stays on the provider until ~SourceProvider: JSC analyzes the
// same JSSourceCode more than once (require(esm) sync replay re-issues
// makeModule on an already-fetched entry; --isolate reuses providers across
// globals), and every call must produce the same record.
Comment thread
robobun marked this conversation as resolved.
Outdated
ASSERT_WITH_MESSAGE(provider->m_resolvedSource.module_info, "BunTranspiledModule provider without module_info: %s", moduleKey.utf8().data());
if (provider->m_resolvedSource.module_info == nullptr) [[unlikely]]
RELEASE_AND_RETURN(scope, fallbackParse(globalObject, moduleKey, sourceCode, promise));

auto* moduleInfo = static_cast<bun_ModuleInfoDeserialized*>(provider->m_resolvedSource.module_info);
auto moduleRecord = zig__ModuleInfoDeserialized__toJSModuleRecord(globalObject, vm, moduleKey, sourceCode, moduleInfo);
// Under --isolate the same SourceProvider is reused across globals via the
// IsolatedModuleCache, so module_info must remain alive on the provider;
// ~SourceProvider frees it. Otherwise, free now.
if (!Bun::IsolatedModuleCache::canUse(vm, uncheckedDowncast<Zig::GlobalObject>(globalObject)->bunVM())) {
zig__ModuleInfoDeserialized__deinit(moduleInfo);
provider->m_resolvedSource.module_info = nullptr;
}
if (moduleRecord == nullptr) {
RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("parseFromSourceCode failed")))));
}
Expand Down
16 changes: 13 additions & 3 deletions src/jsc/bindings/ModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <JavaScriptCore/JSInternalFieldObjectImpl.h>

#include "ZigSourceProvider.h"
#include "BunAnalyzeTranspiledModule.h"

#include <JavaScriptCore/JSSourceCode.h>
#include <JavaScriptCore/JSString.h>
Expand Down Expand Up @@ -57,9 +58,18 @@ class ResolvedSourceCodeHolder {

~ResolvedSourceCodeHolder()
{
if (res->success && res->result.value.source_code.tag == BunStringTag::WTFStringImpl && res->result.value.needsDeref) {
res->result.value.needsDeref = false;
res->result.value.source_code.impl.wtf->deref();
if (!res->success)
return;
auto& value = res->result.value;
if (value.source_code.tag == BunStringTag::WTFStringImpl && value.needsDeref) {
value.needsDeref = false;
value.source_code.impl.wtf->deref();
}
// Non-null only if no SourceProvider adopted it (early return before
// Zig::SourceProvider::create).
Comment thread
robobun marked this conversation as resolved.
if (value.module_info) {
zig__ModuleInfoDeserialized__deinit(static_cast<bun_ModuleInfoDeserialized*>(value.module_info));
value.module_info = nullptr;
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/ZigSourceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ Ref<SourceProvider> SourceProvider::create(
};

auto provider = getProvider();
// The provider now owns module_info (freed in ~SourceProvider); clear the
// caller's copy so ResolvedSourceCodeHolder does not free it again.
Comment thread
robobun marked this conversation as resolved.
resolvedSource.module_info = nullptr;

if (shouldGenerateCodeCoverage) {
ByteRangeMapping__generate(Bun::toString(provider->sourceURL()), Bun::toString(provider->source().toStringWithoutCopying()), provider->asID());
Expand Down
Loading
Loading